OpenCL面向存储结构优化的研究

OpenCL是一种面向多设备、多平台的编程语言,被多个设备生产厂家支持,具有良好的兼容性。但是,由于不同设备之间有很大的差异,在不同设备上针对OpenCL的调优手段也大不相同。这里主要针对两款GPU,分别是ARM公司的Mali-T860(以下简称为T860)和NVIDIA公司的GeForce GTX 950M(以下简称为950M),使用不同的内存优化策略,评估1024*1024规模的矩阵相乘的性能(c = a * b)。重点分析了两类的内存优化策略:

  • 矩阵所使用的内存的分配位置。主要包括是在host上分配还是在device上面分配,以及调用clCreateBuffer函数时,采用不同的参数所对应的结果。
  • Kernel函数的优化手段,主要分析OpenCL的local内存的使用对性能的影响。

矩阵内存分配位置对性能的影响

调用clCreateBuffer函数时,有3个flag参数会影响到矩阵的分配位置。

  • CL_MEM_COPY_HOST_PTR。原始的数据在host端。在device端创建cl_mem对象,分配内存,并把数据从host端复制到device端。
  • CL_MEM_USE_HOST_PTR。原始的数据在host端。在device端创建cl_mem对象,使用host端的数据,也有可能会产生从host端复制数据到device端的行为。
  • CL_MEM_ALLOC_HOST_PTR。原始的数据在device端分配。在device端创建cl_mem对象,并且在host可访问的区域为该对象分配内存。也就是说,可以把这样的对象map(调用clEnqueueMapBuffer)到host端,然后从host端访问。

表1和表2给出了两款GPU下,3种不同的内存分配方式下,矩阵运算对应的性能。矩阵运算采用了相同的kernel。

表1 T860下不同内存分配方式的性能(单位: ms)
OpenCL面向存储结构优化的研究_第1张图片
当参数为CL_MEM_USE_HOST_PTR时,矩阵a,b都在host端使用malloc分配,访存时间是指调用clEnqueueReadBuffer把kernel函数的运算结果读回host的过程。参数为CL_MEM_COPY_HOST_PTR的情况类似,a,b也在host端使用malloc分配,同样通过调用clEnqueueReadBuffer把运算结果读回host。
参数为CL_MEM_ALLOC_HOST_PTR的情况则有些特殊,这也是《ARM Mali GPU OpenCL Developer Guide》手册中所推荐的内存使用方式,具有比较好的性能。可以调用以CL_MEM_ALLOC_HOST_PTR为参数的clCreateBuffer函数创建内存,然后使用clEnqueueMapBuffer把内存map到host端,使得host端可访问,clEnqueueUnmapMemObject函数则可以把内存从host端再unmap回device端。因为T860这款嵌入式的GPU并没有local内存,而是device和host采用统一的内存,所以map和unmap操作几乎没有时间代价,这个可以从表1中看出来。表1中的map和unmap的操作主要分为一下4步。

  • 把a和b从device上map到host,然后在host上为这两个矩阵赋初值。
  • 把a和b从host上unmap回device上,以便kernel执行。
  • 把结果c从device上map到host上,方便使用。
  • 把结果再从host上unmap回device上面,以便后续释放空间。

表2 950M下不同内存分配方式的性能(单位: ms)
OpenCL面向存储结构优化的研究_第2张图片
两款GPU内存结构的不同,它们的性能也有较大差别。在950M上,因为GPU的内存与host的内存是独立的,所以map和unmap操作是有代价的。表2中的map和unmap操作内容与表1中的一致。即便只考虑对结果c的map和unmap操作,CL_MEM_ALLOC_HOST_PTR参数所对应的性能也不具备优势。在950M上,性能最优的是CL_MEM_USE_HOST_PTR参数。而在T860上则与之相反,因为map和unmap操作几乎没有代价,所以CL_MEM_ALLOC_HOST_PTR参数所对应的性能最优。

Kernel优化手段对性能的影响

这里主要考虑了使用local变量对性能的影响,比较了前一节所用的基础kernel和local优化后的kernel的性能的差别。
基础kernel的核心代码如图1所示。Kernel对GPU进行而2维划分,每个work item计算结果矩阵中的一个点。两个维度上的work group的大小都设置为8.
Local优化后的kernel核心代码如图2所示。同样把两个维度上的work group的大小都设置为8。每个work item读取矩阵a和b中的一个数据,组成两个8 * 8的sub矩阵,用local变量保存,结果矩阵中的值由sub矩阵计算得出。

__kernel void gemm_origin(__global float *A, __global float *B, __global float *C)
{
  const int g_col = get_global_id(0);
  const int g_row = get_global_id(1); 

  float result = 0.0;
  for(int i = 0; i < AW; i++)
    result += A[g_row * AW + i] * B[i * BW + g_col];
  C[g_row * BW + g_col] = result;
}

图1 基础kernel

__local float aSub[BLK][BLK], bSub[BLK][BLK];
float sum = 0.0f;
const int tiles = AW / BLK;
int idx;

for(idx = 0; idx < tiles; idx++)
{
  aSub[l_row][l_col] = g_row < AH? A[g_row * AW + idx * BLK + l_col]: 0.0f;
  bSub[l_row][l_col] = g_col < BW? B[(idx * BLK + l_row)* BW + g_col]: 0.0f;
  barrier(CLK_LOCAL_MEM_FENCE | CLK_GLOBAL_MEM_FENCE);

  for(int j = 0; j < BLK; j++)
  {
    sum += aSub[l_row][j] * bSub[j][l_col];
  }
  barrier(CLK_LOCAL_MEM_FENCE);
}

图2 local优化的kernel

表3给出了两种优化的性能对比,内存分配采用的都是CL_MEM_USE_HOST_PTR的方式,并且表中只统计核心运行时间,不统计内存复制的时间。从表中可知,T860的local优化不但没有效果,反而性能降低。而950M则有较好的性能加速,这也印证了T860没有local内存的事实。

表3 两种优化的性能对比
OpenCL面向存储结构优化的研究_第3张图片

你可能感兴趣的:(算法)