调优数学库是从HPC系统中提取最终性能的一种简单而可靠的方法。 然而,对于长期存在的应用程序或需要在各种平台上运行的应用程序来说,为每个供应商或库版本调整库调用可能是一个维护噩梦。
一个编译器可以自动生成对调优的数学库的调用,这给您提供了两个世界中最好的:易于移植和最终性能。 在这篇文章中,我展示了如何无缝地加速GPU上的许多标准Fortran数组intrinsic和语言构造。 nvfortran编译器通过将Fortran语句映射到NVIDIA cu TEN SOR库中可用的函数来自动实现这种加速,这是一种第一种GPU加速的张量线性代数库,提供张量收缩、约简和元素操作。
一个简单的上车到NVIDIA GPU
下面是标准Fortran数组内在函数如何映射到GPU加速的数学库。 在最简单的层次上,只需要两个Fortran语句就可以利用cut TEN SOR库提供的出色性能:
use 卡顿索克斯 ... c = matmul(a,b)
使用的第一个语句 卡顿索克斯 预定义模块以重载Fortran内部过程、数组表达式和重载赋值的形式包含cuTENSOR库的接口。这些接口仅用于映射位于GPU设备内存中的阵列。在本文后面,我将从OpenACC和CUDA Fortran程序员的角度讨论这意味着什么。定义了这些接口后,第二条语句包含 马修() 内在调用自动映射到cuTEN SOR函数调用。
接口通过识别和匹配几种常用模式来实现延迟执行,这些模式可以映射到单个cu TEN SOR内核调用。 在所有情况下,调用多个cu TEN SOR函数来设置cu TEN SOR所需的句柄、描述符数据结构和工作缓冲区。
然而,只有一个内核被启动到GPU上。 由于性能原因,必须映射整个语句,包括左侧数组的赋值。 您不希望编译器为右侧操作的输入或结果(中间或最终)创建临时数组,这在Fortran中很常见。
支持标准Fortran操作
cut TEN SOR库包含一般的置换和收缩操作。 置换的结果可以选择由元素函数操作,也可以选择缩放。
nvfortran编译器可以识别和映射各种Fortran转换intrinsic和元素intrinsic函数,这些函数与通用数组语法相结合,用于cut TEN SOR功能。 一些比较直接的翻译包括以下内容:
d = transpose(a) d = func(transpose(a)) d = alpha * func(transpose(a) d = reshape(a,shape=[...]) d = reshape(a,shape=[...],order=[...]) d = func(reshape(a,...)) d = alpha * func(reshape(a,...)) d = spread(a,dim=k,ncopies=n) d = func(spread(a,dim=k,ncopies=n)) d = alpha * func(spread(a,dim=k,ncopies=n))
的投入 马修() 也可以在CuTEN SOR中置换,结果可以缩放和累积。 这导致了几种可能的组合,例如以下陈述:
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(transpose(a),b) c = matmul(reshape(a,shape=[...],order=[...]),b) c = matmul(a,transpose(b)) c = matmul(a,reshape(b,shape=[...],order=[...]))
使用来自标准Fortran的NVIDIA TensorCores
利用cuTEN SOR和NVIDIA TensorCores可以像下面的代码示例一样容易,当您使用包含在其中的随机数生成特性时 卡顿索克斯 模块:
program main use 卡顿索克斯 integer, parameter :: ni=5120, nj=5120, nk=5120, ntimes=10 真实的(8), allocatable, dimension(:,:) :: a, b, d allocate(a(ni,nk),b(nk,nj),d(ni,nj)) call random_number(a) call random_number(b) d = 0.0d0 print *,"cutensor" call cpu_time(t1) do nt = 1, ntimes d = d + matmul(a,b) end do call cpu_time(t2) flops = 2.0*ni*nj*nk flops = flops*ntimes print *,"times",t2,t1,t2-t1 print *,"GFlops",flops/(t2-t1)/1.e9 end program
The 马修() 内在调用映射到cuTENSOR调用,在可能的情况下无缝地使用Tensor Cores。我将在本文后面展示一些性能结果。
用nvfortran编译程序
你可能会问这个程序是如何使用cuTEN SOR的,当我早些时候说的 cutensorex 接口只将GPU设备阵列上的操作映射到CuTEN SOR调用。 答案在于程序是如何编译的:
% nvfortran -acc -gpu=managed -cuda -cudalib main.f90
在这里,我将程序编译为Open ACC程序,并利用OpenACC管理内存模式,其中所有可分配数组都在CUDA统一内存中分配。 加上了 -cuda 这也支持CUDAFortran扩展,数组本质上是CUDAFortran– 托管数组。 CUDA Fortran通用接口匹配的一个规则是,当主机和设备接口都存在时,对于托管的实际参数更喜欢设备接口。
当声明、分配和使用在同一个程序单元中时,nvfortran编译器提供了一些快捷方式。 一般来说,最好使用OpenACC指令来指示编译器传递设备地址,如下面的代码示例:
!$acc host_data use_device(a, b, d) do nt = 1, ntimes d = d + matmul(a,b) end do !$acc end host_data
在这种情况下 -cuda 不需要编译器选项。
使用CUDAFortran的CuTEN SOR
对于CUDAFortran用户,the cutensorex 模块和Fortran转换本质成为高性能和完全可移植代码的快速路径。 使用这个 !@cuf 哨兵添加由nvfortranCUDAFortran编译器解释和编译的代码行,或被标准Fortran编译器忽略为注释:
program main !@cuf use cutensorex !@cuf use cudafor integer, parameter :: ni=5120, nj=5120, nk=5120, ntimes=10 real(8), allocatable, dimension(:,:) :: a, b, d !@cuf attributes(device) :: a, b, d allocate(a(ni,nk),b(nk,nj),d(ni,nj)) call random_number(a) call random_number(b) d = 0.0d0 print *,"cutensor" call cpu_time(t1) do nt = 1, ntimes d = d + matmul(a,b) end do call cpu_time(t2) flops = 2.0*ni*nj*nk flops = flops*ntimes print *,"times",t2,t1,t2-t1 print *,"GFlops",flops/(t2-t1)/1.e9 end program
在第6行,我用设备属性声明了数组,它将它们放在GPU设备内存中。 但是,它们也可以用托管属性来声明。 本程序可编译并链接如下命令:
% nvfortran -Mcudalib main.cuf
在真实(8)数据上测量的性能
下面是性能,从前面示例中使用的真实(8)(双精度)数据开始。 你可以用几种方式来衡量矩阵乘性能:
单线程CPU实现
多线程或多核CPU实现
朴素编码矩阵乘用指令卸载
The 马修() 内在映射到CuTEN SOR
To get the best threaded-CPU performance, use the basic linear algebra subprogram (BLAS) library routine DGEMM. The equivalent DGEMM call to the earlier operation is the following command:
call dgemm('n','n',ni,nj,nk,1.0d0,a,ni,b,nk,1.0d0,d,ni)
为了了解调优库在天真的实现中可以提供什么,请使用下面的Open ACC循环结构在GPU上运行。 回路结构采用无特殊平铺或硬件指令。
!$acc kernels do j = 1, nj do i = 1, ni do k = 1, nk d(i,j) = d(i,j) + a(i,k) * b(k,j) end do end do end do !$acc end kernels
实施/处理器 | TFLOP |
NVFORTRAN单CPU核上的Matmul | 0.010 |
在64个CPU核心上的MKLDGEMM | 1.674 |
天真开放ACC在V100 | 0.235 |
天真开放ACC在A100 | 0.447 |
NVFORTRAN Matmul on V100 | 6.866 |
A100上的NVFORTRAN Matmul | 一十七点六六 |
您不仅得到自动GPU加速在V100和A100GPU使用 马修() 内在的,但在A100上的映射 马修() 对于cuTensor调用,您可以自动使用FP64TensorCores。
在真实(4)和真实(2)数据上测量的性能
您可以使用相同的运行集执行 真实的(4) (单一精度)数据和调用SGEMM而不是DGEMM。 此外,CUDA11.0cut Tensor Fortran包装器可以利用A100TF32数据类型和TensorCores。 表2显示了这些运行的性能。
实施/处理器 | TFLOP |
NVFORTRAN单CPU核上的Matmul | 0.025 |
在64个CPU核心上的MKLSGEMM | 3.017 |
天真开放ACC在V100 | 0.460 |
天真开放ACC在A100 | 0.946 |
NVFORTRAN Matmul on V100 | 一十点七零六 |
A100上的NVFORTRAN Matmul | 一十四点六二一 |
NVFORTRAN Matmul on A100 using TF32 | 六十点三五八 |
为什么停在那里? nvfortran编译器支持16位浮点格式(FP16 真实的(2) 数据类型。 您可以在前面的测试中更改数组的类型,并在半精度上运行时间。
在V100上引入了半精度数据的TensorCore操作,然后在A100GPU上扩展,以支持TF32和全双精度DP64TensorCores。 而nvfortran支持 真实的(2) 而TensorCores在V100和A100上,它不支持完整和优化 真实的(2) 在CPU上,标准的BLAS库也没有。 在这种情况下,比较GPU加速版本的性能是有意义的(表3)。
实施/处理器 | TFLOP |
天真开放ACC在V100 | 0.490 |
天真开放ACC在A100 | 2.058 |
NVFORTRAN Matmul on V100 | 六十八点二四二 |
A100上的NVFORTRAN Matmul | 九十二点八一 |
虽然A100的性能令人印象深刻,代码是完全可移植的,但对于TF32和FP16来说,它明显低于峰值。 有固定的开销:在每次调用时,创建和销毁cutTEN SOR张量描述符并创建收缩计划。 您还必须查询和管理收缩中使用的工作区需求,这最终可能会调用 古达·马洛克 and 无库达 。 如果开销是5– 对于FP64,这变得更接近25%的TF32和大约35%的FP16,对于这个大小的问题。
对于需要最终性能的开发人员,nvfortran确实直接支持Fortran接口到FortranCutensor模块中的CcuTEN SORAPI,也是在HPCSDK中提供的。 您可以自己管理张量描述符、计划和工作区。
结局推论
在这篇文章中,我展示了一些简单的程序和Fortran intrinsic调用的类型以及可以在GPU上自动加速的代码模式。 他们甚至可以通过cuTEN SOR自动利用TensorCores。 使用几乎完全标准的Fortran和完全可移植到其他编译器和系统的程序,您可以在NVIDIA GPU上实现矩阵乘法、矩阵转置、元素数组本质和数组语法的多个组合上的近峰性能。
不可能预测你可以用这些新特性做些什么或实现什么。 我期待着看到你的反馈和结果。 NVIDIA继续添加更多的特性,允许您使用标准Fortran结构以最大性能编程NVIDIA GPU。
关于作者
关于布伦特·莱克
Brent Leback管理NVIDIA HPC编译器客户支持和高级服务,并与HPC社区一起移植和优化GPU计算应用程序。 他是CUDAFortran编程语言的共同创造者,并继续积极参与新的CUDAFortran功能的设计。 他是开放ACC GPU黑客马拉松的常客,也是CUDA Fortran的专家。
审核编辑 黄昊宇
全部0条评论
快来发表一下你的评论吧 !