将Tensor核心引入标准Fortran

将Tensor核心引入标准Fortran

调优的数学库是从HPC系统提取最终性能的一种简单而可靠的方法。但是,对于寿命长的应用程序或需要在各种平台上运行的应用程序,为每个供应商或库版本调整库调用可能是维护的噩梦。 

可以自动生成对调优数学库的调用的编译器为提供了两全其美的优势:易于移植和终极性能。在本文中,将展示如何在GPU上无缝加速许多标准Fortran数组内在函数和语言构造。nvfortran编译器通过将Fortran语句映射到NVIDIA cuTENSOR库中的可用功能来自动启用此加速,该库是NVIDIA cuTENSOR的先河,提供GPU加速的张量线性代数库,提供张量收缩,缩小和逐元素运算。

轻松过渡到NVIDIA GPU

以下是标准Fortran数组内在函数如何映射到GPU加速的数学库。在最简单的级别上,只需要两个Fortran语句即可利用cuTENSOR库提供的出色性能:

使用cutensorex
...
c = matmul(a,b)

使用cutensorex预定义模块的第一条语句以重载的Fortran内在过程,数组表达式和重载的赋值形式包含cuTENSOR库的接口。编写接口以仅映射位于GPU设备内存中的阵列。在本文的稍后部分,将从OpenACC和CUDA Fortran程序员的角度讨论这意味着什么。定义了这些接口后,包含matmul()内部调用的第二条语句将自动映射到cuTENSOR函数调用。

这些接口通过识别并匹配可以映射到单个cuTENSOR内核调用的几种常用模式来实现延迟执行。在所有情况下,都将调用多个cuTENSOR函数来设置cuTENSOR所需的句柄,描述符数据结构和工作缓冲区。

但是,只有一个内核被启动到GPU。出于性能考虑,将整个语句(包括分配给左侧数组)映射起来很重要。不希望编译器为右侧操作的输入或结果(中间或最终)创建Fortran中常见的临时数组。

支持的标准Fortran操作

cuTENSOR库包含常规的排列和收缩操作。置换的结果可以可选地通过元素函数来操作,并且可选地缩放。

nvfortran编译器可以识别和映射与通用数组语法结合使用的各种Fortran转换内在函数和基本内在函数,并将其映射到cuTENSOR功能。一些更直接的翻译包括:

d =转置(a)
d =函数(transpose(a))
d = alpha * func(转置(a)
 
d =重塑(a,shape = [...])
d =重塑(a,shape = [...],order = [...])
d =函数(reshape(a,...))
d = alpha * func(reshape(a,...))
 
d =点差(a,dim = k,ncopies = n)
d =函数(spread(a,dim = k,ncopies = n))
d = alpha * func(spread(a,dim = k,ncopies = n))

matmul()也可以在cuTENSOR中排列to的输入,并且可以缩放和累加结果。这导致几种可能的组合,例如以下语句:

c = matmul(a,b)
c = c + matmul(a,b)
c = c-matmul(a,b)
c = c + alpha * matmul(a,b)
d = alpha * matmul(a,b)+ beta * c
 
c = matmul(转置(a),b)
c = matmul(reshape(a,shape = [...],order = [...]),b)
c = matmul(a,转置(b))
c = matmul(a,reshape(b,shape = [...],order = [...]))

使用标准Fortran中的NVIDIA Tensor Core

当使用cutensorex模块中包含的用于随机数生成的功能时,利用cuTENSOR和NVIDIA Tensor Core就像下面的代码示例一样容易:

程序主体
      使用cutensorex
      整数,参数:: ni = 5120,nj = 5120,nk = 5120,ntimes = 10
      实数(8),可分配,尺寸(:,:) :: a,b,d
      分配(a(ni,nk),b(nk,nj),d(ni,nj))
      呼叫random_number(a)
      呼叫random_number(b)
      d = 0.0d0
 
      打印*,“裁切器”
      呼叫cpu_time(t1)
      做nt = 1,ntimes
        d = d + matmul(a,b)
      做完
      呼叫cpu_time(t2)
 
      拖鞋= 2.0 * ni * nj * nk
      翻牌=翻牌* n次
      打印*,“ times”,t2,t1,t2-t1
      打印*,“ GFlops”,flops /(t2-t1)/1.e9
      结束程序

matmul()固有调用映射到cuTENSOR调用,无缝地使用张量核尽可能。将在本文后面显示一些性能结果。

使用nvfortran编译程序

可能会问, cutensorex接口仅将GPU设备阵列上的操作映射到cuTENSOR调用时,该程序如何使用cuTENSOR 。答案在于程序的编译方式:

%nvfortran -acc -gpu =托管-cuda -cudalib main.f90

在这里,将程序编译为OpenACC程序,并利用OpenACC托管内存模式,在该模式下,所有可分配阵列都分配在CUDA统一内存中。由于增加了-cuda,使CUDA Fortran扩展为好,数组实质上CUDA Fortran语言-管理型阵列。CUDA Fortran通用接口匹配的一条规则是,当主机和设备接口同时存在时,优选使用设备接口作为托管实际参数。

当声明,分配和使用位于同一程序单元中时,nvfortran编译器提供一些快捷方式。通常,最好使用OpenACC指令来指示编译器传递设备地址,如以下代码示例所示:

!$ acc host_data use_device(a,b,d)
      做nt = 1,ntimes
        d = d + matmul(a,b)
      做完
!$ acc结束host_data

在这种情况下,-cuda不需要编译器选项。

使用CUDA Fortran中的cuTENSOR

对于CUDA Fortran用户而言,cutensorex模块和Fortran转换内在函数成为获取高性能和完全可移植代码的快速途径。使用!@cuf前哨添加由nvfortran CUDA Fortran编译器解释和编译的代码行,或由标准Fortran编译器作为注释忽略的代码行:

      程序主体
!@cuf使用cutensorex
!@cuf使用cudafor
      整数,参数:: ni = 5120,nj = 5120,nk = 5120,ntimes = 10
      实数(8),可分配,尺寸(:,:) :: a,b,d
!@cuf属性(设备):: a,b,d
      分配(a(ni,nk),b(nk,nj),d(ni,nj))
      呼叫random_number(a)
      呼叫random_number(b)
      d = 0.0d0
 
      打印*,“裁切器”
      呼叫cpu_time(t1)
      做nt = 1,ntimes
        d = d + matmul(a,b)
      做完
      呼叫cpu_time(t2)
 
      拖鞋= 2.0 * ni * nj * nk
      翻牌=翻牌* n次
      打印*,“ times”,t2,t1,t2-t1
      打印*,“ GFlops”,flops /(t2-t1)/1.e9
      结束程序

在第6行,使用设备属性声明了数组,将其放置在GPU设备内存中。但是,也可以使用托管属性来声明它。可以编译该程序并将其与以下命令链接:

%nvfortran -Mcudalib main.cuf

实测(8)数据的性能

下面是从前面示例中使用的real(8)(双精度)数据开始的性能观察。可以通过以下几种方法来衡量矩阵乘法性能:

  • 单线程CPU实现
  • 多线程或多核CPU实现
  • 天真编码矩阵乘法使用指令分载
  • matmul()映射到cuTENSOR内在

为了获得最佳的线程CPU性能,请使用基本的线性代数子程序(BLAS)库例程DGEMM。以下命令与先前操作的等效DGEMM调用:

呼叫dgemm('n','n',ni,nj,nk,1.0d0,a,ni,b,nk,1.0d0,d,ni)

为了了解经过调整的库可以通过天真的实现提供什么,使用以下OpenACC循环结构在GPU上运行。循环结构不使用特殊的拼贴或硬件指令。

$ acc内核
      做j = 1,nj
         是否等于1,ni
            做k = 1,nk
               d(i,j)= d(i,j)+ a(i,k)* b(k,j)
            做完
         做完
      做完
$ acc结束内核

表1显示了real(8)在基于双插槽AMD EPYC 7742 Rome CPU的服务器,单个NVIDIA V100和单个NVIDIA A100 GPU的一个NUMA节点上获得的性能。

 

 表1.双插槽AMD EPYC 7742 Rome基于CPU的服务器,单个V100和单个A100 GPU在一个NUMA节点上的real(8)性能。

不仅可以使用matmul()固有函数在V100和A100 GPU上获得自动GPU加速,而且在A100上matmul()到cuTENSOR调用的映射可以使自动使用FP64 Tensor Core。

在real(4)和real(2)数据上测得的性能

可以使用real(4)(单精度)数据并调用SGEMM而不是DGEMM来执行同一组运行。此外,CUDA 11.0 cuTENSOR Fortran包装器可以利用A100 TF32数据类型和Tensor Core。表2显示了这些运行的性能。

 

 表2.双插槽AMD EPYC 7742 Rome基于CPU的服务器,单个V100和单个A100 GPU的一个NUMA节点上的real(4)性能。

为什么停在那里?nvfortran编译器使用real(2)数据类型支持16位浮点格式(FP16)。可以在较早的测试中更改数组的类型,并以半精度运行计时。

Tensor Core操作是在V100上引入的,用于半精度数据,然后在A100 GPU上进行了扩展,以支持TF32和完整的双精度DP64 Tensor Core。尽管nvfortran支持real(2)V100和A100上的Tensor Core,但它不支持real(2)CPU上的完整和优化,标准BLAS库也不支持。在这种情况下,只有比较GPU加速版本的性能才有意义(表3)。

 

 表3.单个V100和单个A100 GPU的real(2)性能。

尽管A100的性能令人印象深刻且代码可完全移植,但它远低于TF32和FP16的峰值。有固定的开销:在每个调用中,创建并销毁cuTENSOR张量描述符并创建收缩计划。还必须查询和管理收缩中使用的工作空间要求,这最终可能会调用cudaMalloccudaFree。如果FP64的开销为5 – 10%,则对于这种大小的问题,TF32的开销将接近25%,FP16的开销将接近35%。

对于需要最终性能的开发人员,nvfortran确实支持HPC SDK中也提供的Fortran cutensor模块中与C cuTENSOR API的Fortran接口。可以自己管理张量描述符,计划和工作区。

结论

在本文中,展示了一些简单的程序以及可以在GPU上自动加速的Fortran内部调用和代码模式的类型。甚至可以通过cuTENSOR自动利用Tensor Core。使用几乎完全是Fortran标准的程序并完全可移植到其编译器和系统的程序,可以在NVIDIA GPU上实现矩阵乘法,矩阵转置,元素数组内在函数以及数组语法的多种组合的近峰性能。 

无法预测使用这些新功能可能会做什么。期待着的反馈和结果。NVIDIA继续添加更多功能,使可以使用标准的Fortran结构以最高的性能对NVIDIA GPU进行编程。

 

posted @ 2021-02-14 08:58  吴建明wujianming  阅读(285)  评论(0编辑  收藏  举报