-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
【论文解读】CLTune: A Generic Auto-Tuner for OpenCL Kernels #47
Comments
CLTune作者在CLBlast的文章里并没有谈及较为细致的tune说明,而在这篇CLTune,作者在实验部分以矩阵乘法和二维卷积为例,讲了自己CLTune的工作,在2D卷积和GEMM的实验结果上,都达到甚至超过现今最好性能,实验在NVIDIA/AMD/Intel GPU上进行,且统一为FP32精度。 作者在2016年NVIDIA举办的GPU技术大会上也做了主题为**《Better Than All the Rest: Finding Maximum-Performance GPU Kernels Using Auto-Tuning》**的演讲。下面的内容将结合作者的文章摘要、演讲Slide以及我浅薄的理解。 CLTune与现有很多Tuner工具不同的地方,在其对Kernel的普适性tune支持、易用、支持多种搜索策略(随机/粒子群PSO/模拟退火)且开源。多种搜索策略,尤其是启发式算法,也是由于搜索空间太大而不得不选择的,例如GEMM的搜索空间就达到20万种组合。说到这里,不得不提一下AutoTune的使用场景,也是作者在设计之初考虑的:
此外,CLTune的tuner是C++ API,在使用方式上可以离线或在线集成到项目使用。CLBlast将OpenCL API的调用完全隐藏,如设备初始化/Kernel调用/内存管理等。 |
1. 模板化实现Kernel为有一个直观的描述,下面从一个简单例子 举例1:copy在kernel实现之初,就以类似模板kernel的形式来实现,其模板参数 __kernel
void copy(__global float* in,
__global float* out) {
const int tid = get_global_id(0);
for (int w = 0; w < WPT; ++w) {
out[tid * WPT + w] = in[tid * WPT + w];
}
} 在实际tune过程中,会根据 // Creates a new tuner on device 1 of platform 0
cltune::Tuner tuner(0, 1);
// Kernel: 2048/WPT global and 64 local threads
tuner.AddKernel("copy.cl", "copy", {2048}, {64});
tuner.AddParameter("WPT", {1, 2, 4});
tuner.DivGlobalSize({"WPT"});
// Specifies the input and output host arrays
tuner.AddArgumentInput(in_vector);
tuner.AddArgumentOutput(out_vector);
// Starts the tunning process
tuner.Tune(); 其中传入给 |
举例2:matvec_tiled第二个例子是矩阵mat_a(M行N列)与向量vec_x(N列1行)的乘操作,且结果为向量vec_y(M行1列),属于BLAS level2 routine。其kernel模板化的实现中有一个可调优的参数TS,即tile size,该参数用于对vec_x的分片缓存,即预先放到local memory中,再在与矩阵mat_a计算的时warp内的work item就能共享使用。vec_x的维度是tile_size的整数倍,且vec_x能被分成N/TS个tile size,cl kernel执行一次(即一个线程执),其内部内的for循环会将所有N/TS个tile size的第一个元素保存到对应local mem的tile_x中,供一个warp内的work item共享。 图:matvec_tiled kernel的host端和kernel代码 可以看到这个mat_tiled的外部global work size用到了1维,gid(0)遍历mat_a的M行即[0,M-1),用到local work size的1维,lid(0)遍历分片(tiled size)的尺寸即[0,TS-1),遍历的目的是对vec_x做部分的缓存(local memory),缓存大小即分片大小TS行1列,由于是local memory,因而这些是一个warp内所有work item所共享的,且需要在填充完设置barrier(CLK_LOCAL_MEM_FENCE),用于后续计算vec_y元素的部分结果,即mat_a的1行TS列,与tile_x的TS行1列的尺寸相对应。 将上面的代码画成了示意图: 左侧代码,蓝色的表示对vec_x做local memory的部分,绿色是部分mat_vec和部分vec_x做计算的部分; 右侧示意图,在读左侧代码的绿色内容时,发现对mat_a取元素是列优先的方式。外层for循环每执行一次,会计算TS大小的mat_a和vec_x计算得到一个vec_y元素的部分结果。 Local memory的使用场景是当work item访问相同内容的数据大于2次时,如计算3x3的滤波计算,在滑动窗口步长很小时,两次计算的数据有较多重复,就可用到,减少对去Global memory的频繁加载。 图:Adreno OpenCL内存说明 对于local mem见图:Adreno OpenCL内存使用,能看出其特点是在片上即Shader Processor里,相比global mem有性能优势,特点是一个work group内的所有work item共享。此外,Adreno官方文档有罗列使用要点:
对local mem扯远了,模板kernel的写法由于引入了和local mem有关的参数TS(Tile Size),我们不得不去关注性能相关的使用限制。 归纳从copy和matvec_tiled两个例子中,可以将这个tune的完整步骤归纳为:
但不难看出也存在一些问题,tune场景一般来说分为离线和在线,离线调优的场景如固定设备的安防厂商/IOT厂商/GPU厂商等,花多久的时间都能容忍,但是在线调优的场景如APP开发者,需要兼容适配尽可能多的手机,为了性能最佳,从APP采集到的信息根据机型占有量,离线做当然可以,提前采购该APP占有率最多如80%的机型,分别看GPU型号进行离线适配,将离线调优好的参数加载。 |
2. 搜索过程中的主要耗时但当用户量达到一定规模时,这种方式也可以,但数量太过庞大,可能需要on-line在线方式调优,这就要考虑GPU可用性和兼容性,也要考虑到在线调优的时长。 像上面以宏参数的形式传入调优的各种值,是比较好的,但是每次需要编译,在手机上编译一次入mobilenetv1模型,如骁龙8系列的BuildProgram就要100ms这个数量级,模型更大的情况下OpenCL的Program Build如Yolov3模型则500ms到1秒之间,这还是复用了编译过的Program的情况。 所以在移动端上做on-line tune,可能就需考虑避免二次Build Program的调优,可以尝试将原本的宏参数改为 但搜索空间特别庞大时,即使是离线,考虑调优的时间包括: 图:Profiling flags
表:cl_profiling_info剖析时间信息类型
|
3. 搜索空间的特点搜索过程不是基于一堆已有的性能数据和选项做预测最佳设定,即没有性能数据库,而是基于候选的选项如WPT各种候选值、VW各种候选值等在这些设定下,跑出最好的性能。即使如此,也有一些人为的设定限制,但即使在有这些限制下,搜索空间还是很大,如下图是5个参数下,排列组合且去除不合理设定下仍有3424种组合。 图:直接卷积的实现下的搜索空间 这其中也能发现一些空间上的规律:
由于非线性(且值非常接近)和布尔变量参数值的存在,基于导数、自动微分、无导数来寻找最优值的三种方法也不适用。因而选择启发式、以及随机搜索的方式。其实随机搜索是最简单的策略,其采样并测试随机的组合情况。其执行效率完全取决于搜索空间的形状,如果高性能排列组合的参数在搜索空间里挨得近,那么搜索(到高性能的参数)自然效率就低。 |
4. 矩阵乘法作者介绍了两个例子:2D卷积和矩阵乘法。矩阵乘法介绍的更详细一些,这里我展开一下。 矩阵乘法也是计算密集型算子,且作为2D卷积的实现方式之一,在深度学习和机器学习领域被广泛使用。也是大多数BLAS调优库的重点优化对象。矩阵乘法可以表示为 在调优参数上,为了尽可能粒度能细一些,实现了一个高度可调优的版本,其中调优参数有14个: // Parameters determined by the tuner
// 1. MWG : M维度上的Tile-size,如64, 128
// 2. NWG : N维度上的Tile-size,如64, 128
// 3. KWG : K维度上的Tile-size,如8,16
// 4. MDIMC : M维度上每个workgroup的线程数,如8, 16, 32
// 5. NDIMC : N维度上每个workgroup的线程数,如8, 16, 32
// 6. MDIMA : 矩阵A的Re-shaped tile的M方向长度,reshape tile A的维度为KDIMA * MDIMA
// 7. NDIMB : 矩阵B的Re-shaped tile的N方向长度,reshape tile B的维度为KDIMB * NDIMB
// 8. KWI : KWG循环的展开系数,小于等于KWG
// 9. VWM : 矩阵A和C向量宽度,支持包括1, 2, 4, 8
// 10. VWN : 矩阵B的向量宽度,支持包括1, 2, 4, 8
// 11. STRM : 在M维度上是(1)否(0)使用带步长的线程访问
// 12. STRN : 在N维度上是(1)否(0)使用带步长的线程访问
// 13. SA : 是(1)否(0)使用local/shared内存来对矩阵A做缓存
// 14. SB : 是(1)否(0)使用local/shared内存来对矩阵B做缓存
此外,还有基于上述14个调优参数的辅助参数:
#define MWI (MWG/MDIMC) // 每线程的M维度工作量,即M方向的tile size大小除以M方向的workgroup线程数
#define NWI (NWG/NDIMC) // 每线程的N维度工作量,即N方向的tile size大小除以N方向的workgroup线程数
#define KDIMA ((MDIMC*NDIMC)/(MDIMA)) // 矩阵A的Re-shaped tile的K方向长度,reshape tile A维度为KDIMA * MDIMA
#define KDIMB ((MDIMC*NDIMC)/(NDIMB)) // 矩阵B的Re-shaped tile的K方向长度,reshape tile B维度为KDIMB * NDIMB
#define MWA (MWG/MDIMA) // 每线程在矩阵A的M方向的load总数
#define KWA (KWG/KDIMA) // 每线程在矩阵A的K方向的load总数
#define KWB (KWG/KDIMB) // 每线程在矩阵B的K方向的load总数
#define NWB (NWG/NDIMB) // 每线程在矩阵B的N方向的load总数 作者在其实现中,有10个函数,除
下面结合示意图,来具体说明这14个参数对应的优化点: 图:矩阵乘法和调优参数示意图 4.1 workgroup 2D tile对应上图青色部分,为3个参数,通过三个参数
4.2 thread tile对应上图橘色部分,2个参数。local work size(即workgroup size)在2个维度上分别为 4.3 memory缓存:global->local / global->private是否输入矩阵A或B做大小为2D workgroup tile的local mem缓存,如果不使用则将tile size大小cache到private mem中。因为是A和B两个矩阵,是4种可能,作者因此分别实现了名为 4.4 memory调优:local mem reshape该优化点需确保开启即对A或B使用local mem,在该情况下,决定是否对local mem做reshape操作。遵循对矩阵A\B\C workgroup维度上的要求,即:
不太清楚是指后续做矩阵分块还是什么意思;根据后文的最佳参数值,该值候选值为 4.5 访存调优单个线程在非片上内存访问的步长。实际我在阅读过程中也没太理解做这个的目的,因而贴出原文:
但有一点是可以明确的,访问内存的方式对对性能有极大的影响,最理想的方式则是:一个workgroup内的线程访问连续的内存地址,这可以高效利用GPU L1 Cache。即使是调优LWS,也是提高L2 Cache的利用率(这部分参考ARM Compute Library相关的演讲,其中有提到,最理想的情况下是:不同计算单元复用相同的内存块)。 4.6 访存-level调优:向量宽度通过调整访问内存(即读取和存储)的向量宽度,增加操作数来提升性能。对矩阵A为 4.7 访存-level调优:循环展开对应上图A矩阵红色部分,通过开启或者关闭循环展开系数,来实现编译器级别的动态循环展开。 循环展开可以由程序员完成,也可由编译器自动优化完成。循环展开通过将循环体代码复制多次实现。增大指令调度的空间,减少循环分支指令的开销。循环展开可以更好地实现数据预取技术,这其中加入 下面是该操作的优点和缺点,这部分内容摘自CPU在循环展开时候的特点:
4.8 不同设备上的最佳参数表:不同设备在矩阵乘法上搜索到的最佳参数值 矩阵乘法上,作者在K40m上性能没有拼过cuBLAS的主要原因还是CUDA在汇编级别的优化上做到了减少寄存器压力,移除寄存器bank冲突,其实本质上是拿不到类似CUDA |
6. 搜索策略的经验两种启发式算法:模拟退火和粒子群优化,都有其各自的特点,不同的问题哪一种更合适需要尝试的。 表:作者实验调优的硬件 通过作者的尝试,也发现一些经验:
其实类似的实验经验还有一些,但是都是设备相关的,不具有普适性。总的来说,CLTune提供了在OpenCL Kernel上为每一个硬件设备、以模板化方法实现来调优的思路,将异构计算的通用性思维发扬光大。
|
The text was updated successfully, but these errors were encountered: