我正在学习如何优化GPU代码。我读到了记忆局部性的重要性。我还看到了GPU卷积的一些tutorials和examples。基于此,我编写并测试了几个自己的内核。令人惊讶的是,我发现最简单的天真的kernell是最快的!?,它比CPU快了不到10倍。 (是的,我通过运行kenrnel 64x分摊上传/下载时间)。
我做错了什么?我希望卷积只是优化GPU的那种操作。如果我能得到100x speed-up on matrix multiplication,为什么卷积这么慢?
性能[CPU滴答/像素](越低越好):编辑: GPU-scanline_async 我在阅读有关async_work_group_copy
我想知道两件事:
通过在256x256像素的浮点阵列上运行内核64x /帧,在my laptop上使用CPU Intel Core i7 6700HQ Skylake和GPU nVidia 960M进行测试。 code full can be seen here。
===========内核代码===========
内核 GPU-Naive 2D global =(256,256)local =(16,16)
__kernel void blur2D_naive(
__global float* I,
__global float* O
){
const int ix = get_global_id (0)+1;
const int iy = get_global_id (1)+1;
const int nx = get_global_size(0)+2;
int i = iy * nx + ix;
// 1.6 ticks/pixel
O[i] =( I[i-nx-1] + I[i-nx] + I[i-nx+1] +
I[i -1] + I[i ] + I[i +1] +
I[i+nx-1] + I[i+nx] + I[i+nx+1] ) * 0.11111111111;
// modified with gaussian mask 4.9 ticks/pixel
//O[i] =( 0.0625*I[i-nx-1] + 0.125*I[i-nx] + 0.0625*I[i-nx+1] +
// 0.125 *I[i -1] + 0.25 *I[i ] + 0.125 *I[i +1] +
// 0.0625*I[i+nx-1] + 0.125*I[i+nx] + 0.0625*I[i+nx+1] );
}
内核 GPU-local 2D global =(256,256)local =(16,16)
#define NBx 18 // tile size including borders [halo] 16+2
#define NBy 18
// seems to be slower than naive method
__kernel void blur2D_local(
__global float* I,
__global float* O
){
__local float L[NBx*NBy];
const int2 iG = (int2)(get_global_id (0)+1 , get_global_id (1)+1 );
const int2 nG = (int2)(get_global_size(0)+2 , get_global_size(1)+2 );
const int2 iL = (int2)(get_local_id (0)+1 , get_local_id (1)+1 );
const int2 nL = (int2)(get_local_size (0)+2 , get_local_size (1)+2 );
const int2 iGR = (int2)(get_group_id (0) , get_group_id (1) );
// copy boundary pixels to local memory
switch( get_local_id(1) ){ // some threads copy one more of boundary (halo) pixels
case 4:
switch( get_local_id(0) ){ // copy corner points
case 0: L[ 0 ] = I[ nG.x* get_group_id(1)*get_local_size(1) + get_group_id(0)*get_local_size(0) ]; break; // upper-left
case 1: L[ NBx-1 ] = I[ nG.x* get_group_id(1)*get_local_size(1) + get_group_id(0)*get_local_size(0)+(NBx-1) ]; break; // upper-right
case 2: L[ (NBy-1)*NBx ] = I[ nG.x*(get_group_id(1)*get_local_size(1)+(NBy-1)) + get_group_id(0)*get_local_size(0) ]; break; // lower-left
case 3: L[ NBy* NBx-1 ] = I[ nG.x*(get_group_id(1)*get_local_size(1)+(NBy-1)) + get_group_id(0)*get_local_size(0)+(NBx-1) ]; break; // lower-rigth
}
// copy border lines
case 0: L[ iL.x ] = I[ nG.x* get_group_id(1)*get_local_size(1) + iG.x ]; break; // top line
case 1: L[ NBx*(NBy-1) + iL.x ] = I[ nG.x*(get_group_id(1)*get_local_size(1)+(NBy-1) ) + iG.x ]; break; // botton line
case 2: L[ NBx*iL.x ] = I[ nG.x*(get_group_id(1)*get_local_size(1)+get_local_id(0) ) + get_group_id(0)*get_local_size(0) ]; break; // left line
case 3: L[ NBx*iL.x + (NBx-1) ] = I[ nG.x*(get_group_id(1)*get_local_size(1)+get_local_id(0) ) + (get_group_id(0)*get_local_size(0)+(NBx-1)) ]; break; // right line
} // each thread coppied at max. 1 border pixels
int ig = iG.y*nG.x + iG.x;
int il = iL.y*nL.x + iL.x;
L[il] = I[ig]; // each thread copy his pixel to local memory
barrier(CLK_LOCAL_MEM_FENCE);
const float renorm = 1.0/9.0;
O[ig] =( L[il-NBx-1] + L[il-NBx] + L[il-NBx+1] +
L[il -1] + L[il ] + L[il +1] +
L[il+NBx-1] + L[il+NBx] + L[il+NBx+1] ) / 9.0;
}
内核 GPU-local_async 2D global =(256,16)local =(16,16)
#define nTiles 16
#define NBx 18
#define NBy 18
#define copy_tile(event,ig0,I,L) { int ig_=ig0; int il_=0; for(int i=0; i<NBy; i++){ event = async_work_group_copy( L+il_, I+ig_, NBx, event ); ig_+=nx; il_+=NBx; } }
// https://streamcomputing.eu/blog/2014-06-19/using-async_work_group_copy-on-2d-data/
__kernel void blur2D_local_async(
__global float* I,
__global float* O
){
const int nx = get_global_size(0)+2;
__local float LI[NBx*NBy*2];
int iL0 = 0;
int iL1 = NBx*NBy;
event_t event = 0;
int ig0 = get_group_id(0)*get_local_size(0);
copy_tile(event,ig0,I,LI);
for( int it=0; it<nTiles; it++ ){
int ig = ig0 + (get_local_id(1)+1)*nx + get_local_id(0)+1;
int il = (get_local_id(1)+1)*NBx + get_local_id(0) + iL0;
ig0 += get_local_size(1)*nx;
event_t event_ = 0;
copy_tile(event_,ig0,I,LI+iL1);
wait_group_events(1, &event);
//barrier(CLK_LOCAL_MEM_FENCE);
O[ig] =( LI[il-NBx] + LI[il-NBx+1] + LI[il-NBx+2] +
LI[il ] + LI[il +1] + LI[il +2] +
LI[il+NBx] + LI[il+NBx+1] + LI[il+NBx+2] ) * 0.11111111111;
int iLtmp=iL0; iL0=iL1; iL1=iLtmp;
event = event_;
}
}
内核 GPU-scanline_private 1D global =(256)local =(32)
__kernel void blur2D_scanline_priv(
int nx, int ny,
__global float* I,
__global float* O
){
int ig = get_global_id(0)+1;
float3 Lm = (float3)( I[ig-1], I[ig], I[ig+1] ); ig += nx;
float3 L0 = (float3)( I[ig-1], I[ig], I[ig+1] );
for(int iy=1; iy<(ny-1); iy++ ){
ig += nx;
float3 Lp= (float3)( I[ig-1], I[ig], I[ig+1] );
O[ig-nx] =
( Lm.x + Lm.y + Lm.z +
L0.x + L0.y + L0.z +
Lp.x + Lp.y + Lp.z ) * 0.11111111111;
Lm=L0; L0=Lp;
}
}
内核 GPU-scanline_async 1D global =(256)local =(32)
#define NB 34
__kernel void blur2D_scanline_async(
int nx, int ny,
__global float* I,
__global float* O
){
__local float L[NB*4];
int i0=0;
int i1=NB;
int i2=NB*2;
int i3=NB*3;
event_t event = 0;
int ig0 = get_group_id(0)*get_local_size(0);
event = async_work_group_copy( L , I+ig0, NB, event ); ig0 += nx;
event = async_work_group_copy( L+NB , I+ig0, NB, event ); ig0 += nx;
event = async_work_group_copy( L+NB*2, I+ig0, NB, event ); ig0 += nx;
const int il = get_local_id(0);
int ig = get_global_id(0)+1;
for(int iy=1; iy<(ny-2); iy++ ){
wait_group_events(1, &event);
event = async_work_group_copy( L+i3, I+ig0, NB, event ); ig0 += nx;
ig += nx;
O[ig] =
( L[i0+il] + L[i0+il+1] + L[i0+il+2] +
L[i1+il] + L[i1+il+1] + L[i1+il+2] +
L[i2+il] + L[i2+il+1] + L[i2+il+2] ) * 0.11111111111;
__local float *Ltmp;
int itmp=i0; i0=i1; i1=i2; i2=i3; i3=itmp;
}
}
内核 CPU-naive
void blur(int nx, int ny, float * I, float * O ){
float renorm = 1.0/9.0;
for(int iy=1;iy<ny-1;iy++){ for(int ix=1;ix<nx-1;ix++){
int i = iy*nx+ix;
O[i] =( I[i-nx-1] + I[i-nx] + I[i-nx+1] +
I[i -1] + I[i ] + I[i +1] +
I[i+nx-1] + I[i+nx] + I[i+nx+1] ) * renorm;
} }
}
答案 0 :(得分:3)
在矩阵乘法中,每个子矩阵(补丁)用于另一个矩阵中所有行中的所有补丁。如果补丁中存在2x2子矩阵,并且如果主矩阵是20x20,则每个子矩阵使用10次进行乘法。 GPU通常使用16x16或32x32大小的补丁,这意味着,对于2kx2k的乘法,每个16x16补丁至少重复使用128次。
MM reuse = 128
并添加子矩阵 - 子矩阵乘法重用,足以将gpu推到极限。
在3x3卷积中,3x3补丁不用于整个扫描线或整个图片。只重复使用其像素。
3x3模板:每个像素由相邻的8个模板重复使用。
5x5模板:每个像素由相邻的24个模板重复使用。
要赶上矩阵乘法,需要
11x11 stencil to have a reuse of 120
也比矩阵乘法更局部,并且应该得到比它更多的gflops但它没有进行相同数量的乘法和加法。
它正在进行9次加法+ 1次乘法。
失去了8次潜在的乘法。将近一半的GFLOPS限制丢失。
您应该尝试异步工作组副本。
矩阵乘法/ 16x16子矩阵)vs卷积(17x17画笔大小):
矩阵:L2重用率随主矩阵大小增加,或L1重用率随子矩阵大小(L1)增加
矩阵:每个工作组增加16 * 16 * 16次+ 16 * 16 * 16次
矩阵:统一线程使用,不使用if-else,重新使用所有本地内存
矩阵:增加补丁大小也会增加子矩阵乘法中立方功率的重复使用(但由于每条线路的补丁较少,因此会减少L2的重复使用,这样可以像平方功率一样重复使用率)
矩阵:本地内存必须至少为2x平铺区域(sub mat-mat mul)
矩阵:可以在私有内存中执行4x4子子乘法(使用每个元素4次),这意味着4x4内存= 64加+ 64 mul
有一个额外的重内核为另一个乘法繁重的内核留出空间,可以同时或异步地在同一个内核中工作。也许如果您使用它进行图像处理,也许您可以添加一些&#34;混合&#34;或者&#34;调整大小&#34;里面的内核让他们一起工作?
Scanline版本正在加载3个元素,执行9添加+ 1 mul然后重复,加载的元素保持3个回合,这意味着它们仅被重复使用3次并且其邻居(x或y directio)可能不会落入邻居线程甚至是邻居工作组。另外3个负载与1个存储不平衡。如果内存带宽为100 GB / s,那么它将使用50GB / s用于加载,15 GB / s用于存储,除非它们来自L1。
您可以使用累加器减少添加/ mul不平衡。
store = (accumulator) * 0.1111111
accumulator+=new vector // 3 adds
accumulator-=old vecotr // 3 adds
所以现在增加了6个+ 1个muls,所以更加平衡如下:1Tflops GPU将增加500Gflops,为muls增加90 Gflops。
Naive版本不使用本地内存,为飞行中的更多波前留出更多空间。本地内存版本实际上打破了L1访问模式,并减少了飞行中的波前。这减少了VALU的占用。
您可以通过在工作组级别而不是线程级别执行扫描线来减少本地内存使用量。我的意思是:
从内存加载:x x x x x x x x x x 扫描线:(从左到右,1-D)a b c d e f g h i j 现在将它用于工作组级别的扫描线:a c c u m u l a t o r(+ new) (从上到下)z x z x z x z x z x( - old)
calculate frontline 1-d scanline: 30 additions for each new row
calculate wide vector 2-d scanline:30*30 additions
each pixel get 1 value instead of adding 3 values
storing: 16x16 multiplications
much less local memory used, more balanced (~8 add 1 mul)
这有1-d扫描线,对于N个周期是单线程,或者对于LogN周期是多线程减少(考虑到计算单元中的足够线程)。