GPU上的广义滑动窗口计算

时间:2011-10-05 03:01:04

标签: cuda gpu dot-product sliding-window

这是一些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有效地实现这种滑动窗口计算。有人想给我一些方向吗?干杯!

更新:您可以在下面的答案中看到我在其他用户的帮助下完成优化过程。

4 个答案:

答案 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 YdimsXdimsoutdims相同)。

out[out_indx] += X[X_indx]*Y[Y_indx];

同样,一个非常糟糕的主意。创建一个寄存器变量并使用它执行所有操作。在内核结束时只写一次全局数组。

这些优化是你应该做的第一件事。第二件事是让你XY 3D纹理,因此将缓存对它们的访问。我想,在此之后,CUDA将胜过CPU。

为了进一步优化,您最好阅读CUDA C Best Practices Guide。必须阅读,你会更好地了解如何编写高效的GPU代码(现在你的实现太幼稚了)

答案 2 :(得分:0)

v0.1 - 朴素的实施

这是我第一次尝试这项工作的天真尝试:

__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 ] 

v0.2 - 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 ] 

v0.3 - 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个部分:

  1. 从纹理内存中读取,存储到整个块的共享内存

    __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.
    
  2. #pragma展开您的for循环 这将显着增加您的ILP,并且对于您的恒定循环大小具有更少的分支

  3. 确保您的共享内存访问适当,否则银行冲突会导致您的性能下降。