这是一些Python代码,它在两个3D矩阵X和Y上实现滑动窗口计算。
import numpy
def sliding_dot( X,Y ) :
assert X.ndim == Y.ndim == 3
iw,ih,id = X.shape
fw,fh,fd = Y.shape
assert id == fd
assert fw < iw and fh < ih
ow,oh = iw-fw+1,ih-fh+1
out = numpy.zeros( [ow,oh] )
for x in xrange(ow) :
for y in xrange(oh) :
window = X[x:x+fw,y:y+fh,:]
out[x,y] = numpy.dot( window.flatten(),Y.flatten() )
return out
#################
A_dims = (640,480,32)
B_dims = (6,6,32)
A = numpy.random.rand(*A_dims)
B = numpy.random.rand(*B_dims)
sliding_dot(A,B)
一般来说,Y在第一维和第二维上总是小于X,但在第三维中它们相等。
请注意,我们可以用Y和窗口的任何函数替换numpy.dot()。这与卷积有点不同,因为Y只沿X的第一维和第二维滑动。我正在寻找一种有效的策略,使用CUDA有效地实现这种滑动窗口计算。有人想给我一些方向吗?干杯!
更新:您可以在下面的答案中看到我在其他用户的帮助下完成优化过程。
答案 0 :(得分:4)
尝试设计一个“通用”实现,它可以容纳您可能想要的任何操作,这将是像CUDA这样的架构中的巨大折衷。对于具体的dot产品示例,这是一个典型的简化操作,这是一个非常有用的实现:
__constant__ int ldaX[3];
__constant__ int ldaY[3];
__constant__ int dimX[3];
__constant__ int dimY[3];
template<typename real,int blocksize>
__global__ void sliding_k(const real *X, const real *Y, real *out)
{
__shared__ volatile real buffer[blocksize];
int tid = threadIdx.x;
int gid = blockIdx.x * gridDim.y + blockIdx.y;
real value = (real)0;
int xpos = (blockIdx.y * ldaX[2]) + (blockIdx.x * ldaX[1]);
int ypos = 0;
for(int i=0; i<dimY[0]; i++) {
for(int jk=tid; jk<ldaY[1]; jk+=blocksize) {
value += X[xpos+jk] * Y[ypos+jk];
}
xpos += ldaX[1];
ypos += ldaY[1];
}
buffer[tid] = value;
__syncthreads();
# pragma unroll
for(int i=(tid+32); ((tid<32)&&(i<blocksize)); i+=32)
buffer[tid] += buffer[i];
if (tid < 16) buffer[tid] += buffer[tid + 16];
if (tid < 8) buffer[tid] += buffer[tid + 8];
if (tid < 4) buffer[tid] += buffer[tid + 4];
if (tid < 2) buffer[tid] += buffer[tid + 2];
if (tid == 0) out[gid] = buffer[0] + buffer[1];
}
您可以使用任何类型的缩减运算符替换点乘积使用的浮点乘加/求和运算,并且代码应该可以正常工作。每个窗口计算由单个块执行。有足够的并行工作来证明每个窗口的窗口大小为一个块。这允许合并的全局存储器访问,并且在费米卡上,有大量的L1缓存命中。
这里我只在代码中构建了一个假设,即源数组的第三维和窗口数组是相等的。这允许内部两个循环“融合”到单个操作中,因为它们共享共同的存储器布局。使用改进版本的参考代码在Python中运行测试工具,使用PyCUDA编写的主机代码,我得到了:
In [15]: %timeit -n3 -r3 out2=sliding_cuda(A,B)
3 loops, best of 3: 49.8 ms per loop
In [16]: %timeit -n3 -r3 out=sliding_dot(A,B)
3 loops, best of 3: 2.18 s per loop
In [17]: (numpy.abs(out2-out)/numpy.abs(out)).max()
Out[17]: 4.2921323635558404e-15
使用GTX470在635x475 2D网格上使用64个线程块运行3GHz Phenom II时 - 即。大约50倍的加速,包括使用可分页主机内存分配的模块加载,设置和内存传输。内核本身比Python快约100倍,不包括内存传输和设置开销。请注意,这是一个双精度版本 - 默认情况下,Python使用双精度浮点运算。
答案 1 :(得分:1)
嗯,这里有一些但是:
执行〜{640} 480次numpy.dot
次迭代,它本身处理6 * 6 * 32个元素。并行化点产品几乎不值得:对于GPU来说,192个并行线程是不够的,减少CUDA是另外一个麻烦。因此,IMO,并行化任务的最佳方法是为每个线程分配一个输出数组元素。
现在关于内存:输出数组将在全局内存中,没有太多选择。对于输入数据,A
看起来非常适合纹理内存,因为相邻线程访问相邻元素。或者,您可以手动将其“缓存”在共享内存中,但在这种情况下,它看起来并不比简单地使用纹理更有利。对于B
,共享内存不好,因为它会导致库冲突,因为当你计算点积时,半转换中的所有线程都访问相同的B元素(你可以从不同线程中的不同元素开始求和) ,但(再次)看起来并不乐观。所以选择是纹理还是常量。我投票给常数,因为(a)常量内存适用于设备上所有线程访问的数据,(b)你不会污染纹理缓存。
以上只是我的猜测,为了实现良好的性能,你最好尝试不同的变种......
有关您天真实施的更新
for (int Yi = 0; Yi < Ydims[0]; Yi++ )
在这里,您可以在每次迭代时对全局内存进行处理。这是一个巨大的性能杀手。由于您有3个维度,因此最好将int *Ydims
替换为int3 Ydims
(Xdims
和outdims
相同)。
out[out_indx] += X[X_indx]*Y[Y_indx];
同样,一个非常糟糕的主意。创建一个寄存器变量并使用它执行所有操作。在内核结束时只写一次全局数组。
这些优化是你应该做的第一件事。第二件事是让你X
和Y
3D纹理,因此将缓存对它们的访问。我想,在此之后,CUDA将胜过CPU。
为了进一步优化,您最好阅读CUDA C Best Practices Guide。必须阅读,你会更好地了解如何编写高效的GPU代码(现在你的实现太幼稚了)
答案 2 :(得分:0)
这是我第一次尝试这项工作的天真尝试:
__global__ void sliding_dot(float *out, int *outdims, float *X, int *Xdims, float *Y, int *Ydims )
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int j = threadIdx.y + blockDim.y * blockIdx.y;
int Y_indx = 0;
int X_indx = 0;
if ( i < outdims[0] & j < outdims[1] )
{
int out_indx = j + i*outdims[1];
for (int Yi = 0; Yi < Ydims[0]; Yi++ )
{
for (int Yj = 0; Yj < Ydims[1]; Yj++ )
{
for (int k = 0; k < Ydims[2]; k++ )
{
Y_indx = k + Yj* Ydims[2] + Yi* Ydims[2]*Ydims[1];
X_indx = k + (j+Yj)*Xdims[2] + (i+Yi)*Xdims[2]*Xdims[1];
out[out_indx] += X[X_indx]*Y[Y_indx];
}
}
}
}
}
到目前为止,结果并不理想。对于块大小(32,32,1)和网格尺寸p,q选择使得p * 32> = outdims [0]和q * 32&gt; = outdims [1]:
method=[ sliding_dot ] gputime=[ 7013.280 ] cputime=[ 18.000 ] occupancy=[ 0.667 ]
method=[ sliding_dot ] gputime=[ 6945.184 ] cputime=[ 7.000 ] occupancy=[ 0.667 ]
method=[ sliding_dot ] gputime=[ 6990.816 ] cputime=[ 6.000 ] occupancy=[ 0.667 ]
method=[ sliding_dot ] gputime=[ 6931.648 ] cputime=[ 6.000 ] occupancy=[ 0.667 ]
texture<float,1>
我希望每个人都像我一样从中学到这么多!我遵循了@ aland的建议并获得了相当大的加速:
texture<float,1> X;
texture<float,1> Y;
__global__ void dotconv(float *out, int2 outdims, int3 Xdims, int3 Ydims )
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int j = threadIdx.y + blockDim.y * blockIdx.y;
if ( i < outdims.x & j < outdims.y )
{
int out_indx = j + i*outdims.y;
float total = 0.0f;
int X_indx = 0;
int Y_indx = 0;
for (int Yi=0; Yi<Ydims.x; Yi++ )
{
for (int Yj=0; Yj<Ydims.y; Yj++ )
{
for (int k=0; k<Ydims.z; k++ )
{
Y_indx = k + Yj* Ydims.z + Yi* Ydims.z*Ydims.y;
X_indx = k + (j+Yj)*Xdims.z + (i+Yi)*Xdims.z*Xdims.y;
total += tex1Dfetch(X,X_indx)*tex1Dfetch(Y,Y_indx);
}
}
}
out[out_indx] = total;
}
}
但我们仍然没有像CPU一样快速运行:
method=[ dotconv ] gputime=[ 2224.928 ] cputime=[ 24.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2222.592 ] cputime=[ 7.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2225.216 ] cputime=[ 10.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2222.752 ] cputime=[ 10.000 ] occupancy=[ 0.667 ]
texture<float,3>
texture<float,3,cudaReadModeElementType> X;
texture<float,3,cudaReadModeElementType> Y;
__global__ void dotconv(float *out, int2 outdims, int3 Xdims, int3 Ydims )
{
int i = threadIdx.x + blockDim.x * blockIdx.x;
int j = threadIdx.y + blockDim.y * blockIdx.y;
if ( i < outdims.x & j < outdims.y )
{
int out_indx = j + i*outdims.y;
float total = 0.0f;
for (int Yi=0; Yi<Ydims.x; Yi++ )
{
for (int Yj=0; Yj<Ydims.y; Yj++ )
{
for (int k=0; k<Ydims.z; k++ )
{
total += tex3D(X,k,j+Yj,i+Yi) * tex3D(Y,k,Yj,Yi);
}
}
}
out[out_indx] = total;
}
}
这实际上比v0.2慢一点
method=[ dotconv ] gputime=[ 2403.360 ] cputime=[ 35.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2392.160 ] cputime=[ 15.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2396.448 ] cputime=[ 15.000 ] occupancy=[ 0.667 ]
method=[ dotconv ] gputime=[ 2398.880 ] cputime=[ 16.000 ] occupancy=[ 0.667 ]
感谢您的建议!
答案 3 :(得分:0)
您可能想尝试将您的读数与商店中的金额分开。
所以每个内核应该有3个部分:
从纹理内存中读取,存储到整个块的共享内存
__shared blockX[ Ydims.z ][ Ydims.y ][ Ydims.x ];
__shared blockY[ Ydims.z ][ Ydims.y ][ Ydims.x ];
// NOTE: MAKE EACH THREAD LOAD k ELEMENTs * 2 rather than each thread loading Ydims.X*Y*Z elements
blockX[k][yj][yi] = ...
blockY[k][yj][yi] = ...
__syncthreads(); // <-- critical -- all threads in block must finish
// reading from shared memory before any may use the values.
#pragma
展开您的for
循环
这将显着增加您的ILP,并且对于您的恒定循环大小具有更少的分支
确保您的共享内存访问适当,否则银行冲突会导致您的性能下降。