《OpenACC并行编程实战》—— 3.3 计算构件kernels

简介: OpenACC有两个计算构件(Compute Construct):parallel和kernels,用来将循环并行化。两个构件的目标是一样的,但行为有较大的区别。初学者应优先使用简单的kernels构件,熟练以后可以使用parallel构件,众多的子语还能够精细控制并行化方案。

本节书摘来自华章出版社《OpenACC并行编程实战》一 书中的第3章,第3.3节,作者何沧平,更多章节内容可以访问云栖社区“华章计算机”公众号查看。

3.3 计算构件kernels

OpenACC有两个计算构件(Compute Construct):parallel和kernels,用来将循环并行化。两个构件的目标是一样的,但行为有较大的区别。初学者应优先使用简单的kernels构件,熟练以后可以使用parallel构件,众多的子语还能够精细控制并行化方案。
想象这样一些场景:刚接触OpenACC,细节了解不多,想尽快上手体验下效果;虽然了解精细的调优技术,但是要在有限的时间内改造一个大程序,因此不再追求最优性能,希望编译器能够自动选择一些参数,尽快完成任务;程序将要在多种设备上运行,为了在所有设备上都能跑出较好的性能,参数不能写死,需要编译器根据设备环境自动选择合适的参数。此时,kernels构件正合适。
编译器将把kernels构件区域编译成一系列在加速器设备上执行的CUDA kernel,这也是kernels构件名字是复数形式的原因。
本节列出kernels构件的所有子语,但只讲解一部分,剩余子语会在后面陆续讲解。阅读后续章节时,或者编写实际代码时,可以回到此处或2.2节查阅完整的语法,而不必翻遍全书查找零碎的语法。

C和C++中,kernels导语的语法是:

#pragma acc kernels [子语列表] 换行
  结构块

在Fortran中的语法是:

!$acc kernels [子语列表]
  结构块 
!$acc end kernels

这里的子语是下列中的一个:

async[(整数表达式)]
wait[(整数表达式列表)]
num_gangs(整数表达式)
num_workers(整数表达式)
vector_length(整数表达式)
device_type(设备类型列表)
if(条件)
copy(变量列表)
copyin(变量列表)
copyout(变量列表)
create(变量列表)
present(变量列表)
deviceptr(变量列表)
default(none|present)

编译器将kernels区域内的代码分割为一系列的加速器内核(kernel)。通常,每个非嵌套循环成为一个单独的内核。当程序遇到一个kernels构件时,它在设备上按顺序启动这一系列内核。对不同的内核,gang的数量、每个gang包含多少个worker、vector的长度以及三者的组织方式都可能不同。
如果没有使用async子语,kernels区域结束时将有一个隐式障碍,本地线程不再向前执行,直至所有的内核都执行完毕。
用一些小例子说明kernels构件的行为特征。这些例子本身没有应用价值,只为演示语法和程序行为。
3.3.1 构件内有1个循环
例3.5和例3.6中的循环执行一维数组相加操作,很简单,不再描述代码本身的含义。

e33da640a19a968fc995a897fdcfd285942b5f4e 602e78fec1eacea6d3c8ec3d67cbb79a29811859

例3.5只在串行代码上添加第11行这一个导语,告诉编译器将第12~13行的循环并行化,即映射成一个CUDA内核,线程网格维数、线程块维数等参数由编译器自行选择。第15行输出检验信息,正确结果应该是2N。用pgcc编译代码:
$ pgcc -acc -Minfo k1c.c -o k1c.exe
这里的选项-Minfo要求编译器反馈一些编译信息:
main:
    11, Generating copyout(a[:])
        Generating copyin(b[:],c[:])
    12, Loop is parallelizable
        Accelerator kernel generated
        Generating Tesla code
        12, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */

从反馈信息的最后一行可以看出,例3.5第12行的for循环确实被并行化了,编译器自行选择gang模式并行,向量(verctor)长度是128。
为了观察运行时采用的参数,设置PGI编译器的环境变量:
export PGI_ACC_NOTIFY=1
环境变量PGI_ACC_NOTIFY告诉编译器,运行程序时输出CUDA内核的执行配置。运行生成的可执行程序,得到下列运行时反馈信息:

$ ./k1c.exe
launch CUDA kernel  file=C:\cygwin64\home\he\accbook\kernels\k1c.c function=main line=12 device=0 threadid=1 num_gangs=2 num_workers=1 vector_length=128 grid=2 block=128

这里的num_gangs是gang数量,num_workers是worker数量,vector_length是向量长度,这三个是OpenACC的术语。grid和block是CUDA术语,block对应gang,grid与OpenACC术语无确定对应关系。例3.5中的对应关系如图3.1和图3.2。

e7dc2aaf775723a55f777e6fedc9fef55d9ca625

用OpenACC术语来说,长度为256的一维循环分成割成gang-0和gang-1,每个gang只包含一个worker,即worker-0,每个worker里有一个vector,vector长度是128。
1ccf593a41554e406d589b89c90e59c6e2ec39d2

用CUDA术语来说,长度为256的一维循环映射到一个网格上,这个网格包含2个线程块,每个线程块包含128个线程。
例3.5中的N值为256时,编译器选取的向量长度为128。调整N的数值,根据对第12行循环选取的向量长度,可以推测编译器的选取规律,见表3.1。
f8c33e7822e4f667911f1c7a161fdef6e1be814c

从表3.1中可知,向量长度是32的整数倍,最大长度是128。当迭代步数量位于两个向量长度之间时,采用较大的向量长度。表3.1中的32对应CUDA编程模型里的幅(Warp)。此时的设备是英伟达显卡GeForce GT 420M,在其他型号的设备上,向量长度的跳跃间隔和最大长度可能会有所不同。
Fortran版例3.6的不同之处是导语要成对出现,其他跟C版相同。
3.3.2 构件内2个循环
如果kernels构件内包含2个及以上的循环,编译器也会自动并行化。自动选取的向量长度取决于设备本身和循环的迭代次数,请看例3.7和例3.8。
f5e342c88ddff9642bf9951c69e20601bc95e7da a3471a635a27751da2241029f5686d71ee19329b

语法上需要注意,例3.7中第13~16行的两循环用花括号包围起来形成一个结构块,从而第11行的kernels导语能作用到两个循环上。如果没有这两个花括号,kernels导语只对第13~14行上的循环起作用。编译器的反馈信息表明两个循环都被并行化了:
     13, Loop is parallelizable
         Accelerator kernel generated
         Generating Tesla code
         13, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */
     15, Loop is parallelizable
         Accelerator kernel generated
         Generating Tesla code
         15, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */

运行时反馈信息如下:

launch CUDA kernel  file=C:\cygwin64\home\he\accbook\kernels\k2c.c function=main line=13 device=0 threadid=1 num_gangs=8 num_workers=1 vector_length=128 grid=8 block=128
launch CUDA kernel  file=C:\cygwin64\home\he\accbook\kernels\k2c.c function=main line=15 device=0 threadid=1 num_gangs=5 num_workers=1 vector_length=128 grid=5 block=128

由此运行时反馈可知,第13行的循环使用8个gang,每个只包含1个worker,每个worker包含一个vector,vector长度为128。而第15行的循环使用5个gang,其他与第13行相同。这个例子显示了一个重要特征:kernels构件中的不同循环可以使用不同的并行化参数。原因也很容易理解,每个循环映射成一个独立的CUDA内核,每个内核当然可以灵活选择执行配置参数。
3.3.3 构件内二重嵌套循环
例3.9让两个二维数组b和c相加,结果存入数组c,对应元素的相加操作使用kernels构件并行化。

d75ce3b78a03621a169c9aa2635c58ed53933338 6113a68ae9fac92bbd94e8ca474b311fd04d7593

例3.9中第6~8行对数组初始化,第14行输出一个数组元素验证结果的正确性。编译反馈信息表明,二维数组的并行化方案也是二维的。第11行的i循环对应gang剖分,而第12行的j循环映射为gang、vector两层循环。
编译反馈如下:
     11, Loop is parallelizable
     12, Loop is parallelizable
         Accelerator kernel generated
         Generating Tesla code
         11, #pragma acc loop gang /* blockIdx.y */
         12, #pragma acc loop gang, vector(128) /* blockIdx.x threadIdx.x */

运行时反馈如下:

launch CUDA kernel  file=C:\cygwin64\home\he\accbook\kernels\k3c.c function=main line=12 device=0 threadid=1 num_gangs=256 num_workers=1 vector_length=128 grid=2x128 block=128

用OpenACC术语来说,如图3.3,128×256个迭代步被划分成256个gang,每个gang包含1个worker,每个worker包含1个vector,vector长度为128。

661b825a1e6e3360c7134f5603cecafd93801dd8

用CUDA术语来说,如图3.4,128×256个迭代步映射到一个2×128的网格上,共2×128个一维线程块,每个线程块包含128个线程,即blockDim.x的值为128。
Fortran版二重嵌套循环代码的映射关系大致相同,见例3.10。需要注意,为了说明kernels构件的行为,将原本可以使用数组整体操作的运算,改用展开的循环操作。
1b6c8fb50cf94eba22f40a280c1a119f8dbb08f7 a0ef5e086d792c212bbc5ba969efb422064a301e

编译反馈如下:
     14, Loop is parallelizable
     15, Loop is parallelizable
         Accelerator kernel generated
         Generating Tesla code
         14, !$acc loop gang ! blockidx%y
         15, !$acc loop gang, vector(128) ! blockidx%x threadidx%x

运行时反馈如下:

launch CUDA kernel 
file=C:\cygwin64\home\he\accbook\kernels\k3f.f90 function=main line=15 device=0 threadid=1 num_gangs=256 num_workers=1 vector_length=128 grid=1x256 block=128 

因为C数组是按行存储,而Fortran数组是按列存储,所以例3.10第14~15行将列循环放在了外层。编译反馈显示,例3.10的第15行对应数组行循环,例3.9的第11行也对应数组的行循环,它们均被映射到CUDA的y指标方向,这说明编译器还是很智能的。当然,映射关系也要行列对调。
用OpenACC术语来说,如图3.5所示,数组的每列对应1个gang,每个gang包含1个worker,每个worker包含1个vector,vector的长度对应数组的行数,是128。
用CUDA术语来说,如图3.6,数组的每列对应1个一维线程块,每个线程块包含128个线程,即blockDim.x的值为128。

e6fe062483a432db7a752abae7bd7622c0602271

3.3.4 kernels构件内三重嵌套循环

对三重循环,编译器的翻译方式就不一样了。仍然沿用数组相加的例子,稍加修改就得到例3.11和例3.12。

cb6af742754ea948616ba44d6554653249027951 3c3e113e979a544f1223ab1b9a9a06c7e2f91099

例3.11的编译反馈显示,第13~15行的3个循环都被并行化了,但只有第14~15行上的2个循环映射到了线程上:
     13, Loop is parallelizable
     14, Loop is parallelizable
     15, Loop is parallelizable
         Accelerator kernel generated
         Generating Tesla code
         14, #pragma acc loop gang, vector(4) /* blockIdx.y threadIdx.y */
         15, #pragma acc loop gang, vector(32) /* blockIdx.x threadIdx.x */

例3.11的运行时反馈如下:

launch CUDA kernel  file=C:\cygwin64\home\he\accbook\kernels\k4c.c function=main line=15 device=0 threadid=1 num_gangs=8 num_workers=4 vector_length=32 grid=1x8 block=32x4

用OpenACC术语来说,如图3.7,例3.11的第14~15行的32×32个迭代步划分为8个gang,每个gang包含4个worker,每worker包含1个长度为32的vector。虽然对应的数组元素按二维形式组织,但是图3.7中的线程动用了三个并行层次。这是因为OpenACC的三个并行层次gang、worker和vector都是一维,不像CUDA C中的网络、线程块可以按一维、二维、三维形式组织。

64d6df341215f4dd6d5d8bbee35c5146ece92dfb

用CUDA术语来说,如图3.8所示,例3.11的第14~15行的32×32个迭代步划分为8个block,每个block包含32×4个线程。
e1e4b0f0b0e35bb8d1a426ab0c379d8ccb8d2fa7

例3.11第13行的循环怎么并行化的呢?猜想是循环展开,写了32遍二维并行代码。到底怎么并行化的?不知道哇。编译的时候可以用选项nollvm和keepgpu保留下来的中间代码,像这样:
$ pgcc -acc -Minfo -ta=tesla:nollvm,keepgpu k4c.c
得到的中间代码文件k4c.n001.gpu是机器自动生成的,也是给机器看的,不是给人看的,缩排列出如下,想读懂的话要投入很多时间和耐心,读者随意感受下就好。
#include "cuda_runtime.h"
#include "pgi_cuda_runtime.h"
#include "k4c.n001.h"
extern "C" __global__ __launch_bounds__(128) void
main_15_gpu(
    signed char* __restrict__ p2/* c */,
    signed char* __restrict__ p3/* b */,
    signed char* p4/* a */)
{
int _i_1;int i19s;int i21s;int e26;int e28;int e30;int e31;int e33;int j123;
int j121;int j119;int j84;
e30 = ((int)threadIdx.x)*(4);e26 = ((int)blockIdx.x)*(128);
e28 = ((int)threadIdx.y)*(128);i19s = 0;
e31 = (((((int)blockIdx.y)*(512))+(e26))+(e28))+(e30);j123 = 0;
_BB_11: ;
i21s = 0;j121 = 0;
_BB_12: ;
e33 = ((e31)+((i19s)*(128)))+((i21s)*(4));
if( (((-32)+(j121))>=0)) goto _BB_13;
if( (((-32)+(j123))>=0)) goto _BB_13;
if( (((((((int)blockIdx.y)*(4))+((int)threadIdx.y))+(j123))+(-32))>=0)) goto _BB_13;
if( (((((((int)blockIdx.x)*(32))+((int)threadIdx.x))+(j121))+(-32))>=0)) goto _BB_13;
j84 = -32;j119 = 0;
_BB_31: ;
_i_1 = (e33)+(j119);
*( int*)((p4/* a */)+((long long)(_i_1))) = (*( int*)((p2/* c */)+((long long)((((e31)+((i19s)*(128)))+((i21s)*(4)))+(j119)))))+(*( int*)((p3/* b */)+((long long)(_i_1))));
j84 = (j84)+(1);j119 = (j119)+(4096);
if( ((j84)<0)) goto _BB_31;
_BB_13: ;
_i_1 = ((int)gridDim.x)*(32);i21s = (_i_1)+(i21s);j121 = (j121)+(_i_1);
if( (((-32)+(j121))<0)) goto _BB_12;
_i_1 = ((int)gridDim.y)*(4);i19s = (_i_1)+(i19s);j123 = (j123)+(_i_1);
if( (((-32)+(j123))<0)) goto _BB_11;
}
相关实践学习
基于阿里云DeepGPU实例,用AI画唯美国风少女
本实验基于阿里云DeepGPU实例,使用aiacctorch加速stable-diffusion-webui,用AI画唯美国风少女,可提升性能至高至原性能的2.6倍。
相关文章
|
并行计算 异构计算
《OpenACC并行程序设计:性能优化实践指南》一 3.6 第一个并行PIConGPU实现
本节书摘来自华章出版社《OpenACC并行程序设计:性能优化实践指南》一 书中的第3章,第3.6节,作者:[美] 罗布·法伯(Rob Farber),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1511 0
|
并行计算 异构计算
《OpenACC并行程序设计:性能优化实践指南》一 3.10 使用Score-P和Vampir记录OpenACC运行时事件
本节书摘来自华章出版社《OpenACC并行程序设计:性能优化实践指南》一 书中的第3章,第3.10节,作者:[美] 罗布·法伯(Rob Farber),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1484 0
|
程序员 C++
《OpenACC并行程序设计:性能优化实践指南》一 1.2 简单的任务并行示例
本节书摘来自华章出版社《OpenACC并行程序设计:性能优化实践指南》一 书中的第1章,第1.2节,作者:[美] 罗布·法伯(Rob Farber),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
1431 0
|
并行计算 安全 程序员
《OpenACC并行程序设计:性能优化实践指南》一 1.1 简单的数据并行循环
本节书摘来自华章出版社《OpenACC并行程序设计:性能优化实践指南》一 书中的第1章,第1.1节,作者:[美] 罗布·法伯(Rob Farber),更多章节内容可以访问云栖社区“华章计算机”公众号查看。
3118 0
|
并行计算 C++
《并行计算的编程模型》一2.5.2 可移植平台头文件
本节书摘来华章计算机《并行计算的编程模型》一书中的第2章 ,第2.5.2节, [(美)帕万·巴拉吉(Pavan Balaji)编著;张云泉等译,更多章节内容可以访问云栖社区“华章计算机”公众号查看。
770 0
|
Linux C语言
《OpenACC并行编程实战》—— 第3章 OpenACC计算构件 3.1 条件编译
程序的加速效果来自于对计算部分的并行化。本章重点介绍计算并行化所用的3个构件:kernels、loop和parallel,以及几个重要的子语。编译器将串行循环映射成并行线程的方式多种多样,需要仔细观察几种常用循环的并行化方式,掌握映射规律。
2107 0
|
并行计算
《OpenACC并行编程实战》—— 3.5 计算构件parallel
OpenACC中的计算构件有两个,一个是前面介绍的kernels构件,一个就是这里要介绍的parallel构件。两个计算构件的作用都是将循环并行化,但有一些重要区别。本节将结合一些例子详细对比介绍。 parallel这个基本构件开启加速器设备上的并行执行。
2308 0
《OpenACC并行编程实战》—— 2.2 OpenACC 2.5规范
本节列出OpenACC的主要构件、导语,读完本书后可以在此处快速查阅语法,不必到正文中寻找零星的介绍。初次阅读请跳过。
1540 0
|
程序员 C++
OpenACC并行编程实战》—— 3.4 loop构件
kernels构件让编译器自动分析代码,挖掘代码里的并行性,并实施并行化。但是,编译器毕竟只是个软件,不会知道程序员的真实意图。若想更准确高效地指导编译器的并行化工作,程序员可以使用loop导语。该导语能告诉编译器哪些循环需要并行化,以及用什么方式并行化。
2057 0