对于许多kernels来说,工作组大小的调整会是一种简单有效的方法。这章将会介绍基于工作组大小的基础知识,比如如何获取工作组大小,为什么工作组大小非常重要,同时也会讨论关于最优工作组大小的选择和调整的一般方法。
6.1 获取最大的工作组尺寸
在运行完clBuildProgram后,使用下面的API函数可以查询设备的最大工作组尺寸。
size_t maxWorkGroupSize;
clGetKernelWorkGroupInfo(myKernel,
myDevice,
CL_KERNEL_WORK_GROUP_SIZE,
sizeof(size_t),
&maxWorkGroupSize,
NULL );
在clEnqueueNDRangeKernel中使用的实际工作组尺寸不能超过maxWorkGroupSize。如果应用程序没有指定工作组大小,Adreno OpenCL软件可能会选择最大的工作组尺寸。
6.2 需要的和优先的工作组尺寸
每一个kernel函数都有他需要或者优先的工作组大小。对于需要的工作组大小,OpenCL通过下面方法提供给编译器。
- 使用reqd_work_group_size属性。
作为需求,reqd_work_group_size(X, Y, Z) 属性会传入一个指定的工作组尺寸。如果指定的工作组大小不能满足将会返回一个错误。
比如,如果要求16x16的工作组尺寸:
__kernel __attribute__(( reqd_work_group_size(16, 16, 1) ))
void myKernel( __global float4 *in, __global float4 *out)
{ . . . }
- 使用work_group_size_hint 属性
OpenCL会尝试使用这个指定的尺寸,但是不保证真实的大小与指定的一致。比如,提示使用64x64工作组尺寸:
__kernel __attribute__(( work_group_size_hint (64, 4, 1) ))
void myKernel( __global float4 *in, __global float4 *out)
{ . . . }
在许多情况下,当工作组尺寸严格指定时,编译器不能保证能编译出最优的机器代码。而且,如果片上寄存器不能满足要求的工作组尺寸时,编译器可能会需要将寄存器溢出到系统的RAM内存上。因此,这两种属性并不建议使用,除非必须指定工作组尺寸,kernel才能运行。
注意:为了交叉编译的兼容性,将kernel写成依赖固定工作组的尺寸或者布局,并不是一个好的做好。
6.3 影响工作组最大尺寸的因素
如果没有指定工作组尺寸的属性,一个kernel的最大工作组尺寸依赖以下的几个因素:
- kernel的寄存器使用。一般来说,kernel越复杂,寄存器使用越多,支持的最大的工作组尺寸越小。过多地使用寄存器的原因如下:
- 一个工作项中有过多的工作任务。
- 有控制流
- 高精度的数学函数(比如,没有使用内部函数或者快速数学运算的编译选项-fastmath)
- 本地内存,如果需要分配额外的寄存器暂时存储装载和存储指令中源和目的地址。
- 私有内存,比如为每一个工作组定义了一个数组
- 循环展开
- 内联函数
- 通用寄存器的大小
- Adreno低级系列的GPU可能有更少的寄存器数量。
- n kernel中的栅栏(Barrier)
- 如果一个kernel没有使用栅栏(barrier),在Adreno A4x and A5x系列中,在不用考虑寄存器使用的情况下,工作组最大可以设置为DEVICEMAXIUMUM。
6.4 没有barrier的kernels
以前地,一个workgroup中所有的work item要求在同一时间同时驻留在GPU上。对于大量消耗寄存器的kernel,这将会限制他们的最大工作组尺寸,并将会远远小于设备支持的最大工作组尺寸。
从Adreno A4系列起,不需要考虑寄存器的使用情况,没有barrier的kernel就可以有Adreno支持的最大工作组尺寸,一般是1024。对于这种类型的kernel(没有barrier)来说,因为不需要wave之间进行同步,所以当一个旧的wave执行完毕,新的wave就可以开始执行了。
在某些情况下,拥有最大的workgroup尺寸并不意味着他们有最好的并行性。一个没有barriers的kernel可能会因为太复杂导致只有很少的wave在SP上并行执行,这将会导致性能降低。开发者需要继续优化和减少寄存器使用,不考虑从clGetKernelWorkGroupInfo函数中获取到的最大的workgroup尺寸。
6.5 工作组尺寸的调整
这个部分将会介绍一些在选择最优的工作组尺寸和形状时通用的指导准则。
6.5.1 避免使用默认的工作组大小
如果一个kernel调用没有指定workgroup的尺寸,那么OpenCL会用简单的方法找一个能用的工作组尺寸。开发者必须要要意识到,这种默认的工作组尺寸通常不是最优的。有效的做法是,手动尝试使用不同的工作组大小和维度(2D/3D),然后找出最优的一个。
6.5.2 越大的工作组尺寸,越好的性能?
对于许多kernel来说这是正确的,因为增加工作组尺寸能够允许更多的wave运行在SP上,这样能够更好地隐藏延迟和提升SP的使用。
然而,对于某些kernel来说,增加工作组尺寸可能会导致性能退化。一种情况是,由于不良的数据局部性和访问模式,越大的工作组尺寸将导致越多的cache垃圾。这个数据局部性的问题在使用texture获取时更加严重,因为texture cache比统一的L2 cache要小。最终,决定最优的工作大小和维度的本质是kernel的数据获取。
6.5.3 固定的 vs. 动态的工作组尺寸
为了不同设备之间的性能兼容性,避免假设一个工作组尺寸能够适合所有的设备,避免对workgroup尺寸固定编码。一个指定的工作组大小和维度在一个设备上是最优的,在另一个设备上可能是次优的。因此,给定一个kernel,建议针对kernel能够执行的所有设备统计出不同的workgroup尺寸,然后在运行时对每个设备选出一个最优的。
6.5.1 一维 vs二维 vs三维 workgroup(1D/2D/3D)
kernel的维度可以会影响性能。取决于work item的数据获取方式,在某些情况下,一个2D的kernel可能会在cache上有更好的数据本地性(数据在cache上),导致更好的内存获取和更好的性能。然而在其他情况下,一个2Dkernel比1D会产生更多的cache 垃圾。建议尝试使用不同的维度,从而获取最优的性能。
6.6 关于workgroup的其他话题
6.6.1 全局的work size和填充
OpenCL 1.x 要求一个kernel的全局worksize 必须是workgroup尺寸的倍数。如果应用程序指定的workgroup 尺寸不满足这个条件,那么clEnqueueNDRangeKernel的函数调用将会返回一个错误。在这种情况下,应用程序可以填充全局worksize,保证它是用户指定的workgroup尺寸的倍数。
注意:OpenCL 2.0 取消的这个限制,而且global worksize 并不需要必须是workgroup size的倍数,这种被叫做非归一化的workgroup。
理想情况是,workgroup 尺寸的第一个维度是wave尺寸的倍数(比如说32),这样能充分利用wave的资源。如果不是这种情况,可以考虑填充workgroup的大小来满足这个条件,需要记住,在OpenCL 1.x中,全局的worksize必须填充(保证是workgroup的倍数)。
6.6.2 残酷地寻找
因为workgroup 尺寸选择的复杂性,经验常常是发现最优大小和维度的最好方法。
一种选择是,在程序开始时,使用一个与实际的工作任务相同复杂度(但是一般使用比较简单的任务)的唤醒功能的kernel 去动态的寻找最优的workgroup 尺寸。然后将这个选出来的workgroup尺寸用在实际的kernel中。很多商业的标准检查程序就是使用的这种方法。
6.6.3 在workgroup中避免不均匀的工作负载
一些应用程序可能被写成,在不同工作组中出现不均衡的负载。比如说,基于区域的图像处理的用例中会出现一些区域需要比其他区域多很多处理的情况。这种情况需要避免,因为这会导致性能的不可预测性。另外的,如果单个workgroup任务需要太长时间运行的话,会导致上下文切换变的复杂。
解决这个问题的方法是,使用两个阶段处理策略。第一个阶段可能会收集感兴趣的点和为第二阶段准备数据。工作负载越具有确定性,在不同workgroups中进行均等的分配将会更简单。
6.6.4 工作组的同步
OpenCL 并不能保证workgroup的执行顺序,而且也没有定义一个工作组同步的机制。不建议有需要依赖工作组顺序的程序。
在实际情况下,可以使用atomic 函数或者其他方法,在workgroup之间可以进行有限的同步。比如说,一个应用程序可能分配了一个全局内存对象,这个对象需要被不同的工作组中的workitem更新。一个workgroup可以管理一个由其他workgroup更新的内存对象。通过这种方式,可能会实现有限的工作组同步。