Cuda GPU上的广义滑动窗口计算

Cuda GPU上的广义滑动窗口计算,cuda,gpu,dot-product,sliding-window,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

下面是一些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)
导入numpy
def滑点(X,Y):
断言X.ndim==Y.ndim==3
iw,ih,id=X.shape
fw,fh,fd=Y形
断言id==fd
断言fw
一般来说,Y在第一和第二维度上总是比X小得多,但在第三维度上是相等的

注意,我们可以用Y和窗口的任何函数替换numpy.dot()。这与卷积有点不同,因为Y只沿着X的第一和第二维度滑动。我正在寻找一种有效的策略,使用CUDA高效地实现这种滑动窗口计算。有人想给我指点方向吗?干杯


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

好吧,下面是一些想法:

执行~640*480次
numpy.dot
迭代,该迭代本身处理6*6*32个元素。并行化点产品几乎不值得:对于GPU来说,192个并行线程是不够的,CUDA的减少是额外的麻烦。因此,在我看来,并行化任务的最佳方法是为每个线程分配一个输出数组元素

关于内存:输出数组将在全局内存中,没有太多选择。对于输入数据,
A
看起来非常适合纹理内存,因为相邻线程访问相邻元素。或者,您可以在共享内存中手动“缓存”它,但在这种情况下,它看起来并没有简单使用纹理的优势。对于
B
,共享内存是不好的,因为它会导致库冲突,因为当您计算点积时,半扭曲中的所有线程都访问相同的B元素(您可以从不同线程中的不同元素开始求和,但这(同样)看起来不太可能)。因此,我们要么选择纹理,要么选择常量。我投票支持常量,因为(a)常量内存适合设备上所有线程访问的数据,(b)不会污染纹理缓存

以上只是我的猜测,要真正实现良好的性能,您最好尝试不同的变体

更新您的幼稚实施

for (int Yi = 0; Yi < Ydims[0]; Yi++ )
同样,这是一个非常糟糕的主意。创建一个寄存器变量并使用它执行所有操作。在内核末尾只向全局数组写入一次

这些优化是您应该做的第一件事。第二件事是让你
X
Y
3D纹理,这样对它们的访问就会被缓存。我想,在这之后,CUDA将超越CPU

对于进一步的优化,您最好阅读。这是必读的,您将更好地了解如何编写高效的GPU代码(现在您的实现太幼稚了)

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];
                }
            }
        }
    }
}

v0.2-
纹理
我希望每个人都能像我一样从中学到很多东西!我听从了@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;
    }
}

v0.3-
纹理

谢谢你的建议

您可能想尝试从存储区中分离出读数和总和

因此,每个内核应该有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.
    
    uu共享块x[Ydims.z][Ydims.y][Ydims.x];
    __共享块y[Ydims.z][Ydims.y][Ydims.x];
    //注意:使每个线程加载k个元素*2,而不是每个线程加载Ydims.X*Y*Z元素
    blockX[k][yj][yi]=。。。
    块状[k][yj][yi]=。。。
    __syncthreads();// 在像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];
    }
    

    在3GHz Phenom II上运行时,GTX470在635x475 2D网格上使用64个线程块,即大约50倍的速度,包括使用可分页主机内存分配的模块加载、设置和内存传输。内核本身的速度大约是Python的100倍,而不包括内存传输和安装开销。请注意,这是一个双精度版本-Python默认使用双精度浮点运算。

    谢谢!尝试您的建议并将每个输出像素映射到单个线程。尚未尝试进行任何内存优化。到目前为止,结果喜忧参半。哇,太棒了!据我所知,内核参数存储在本地内存中,而本地内存是片外的。有什么方法可以将DIMS、Xdims和Ydims输出到片上内存吗?@BrianTheLion Nope,内核参数存储在片上共享内存中,通常与寄存器一样快。您可能会混淆OpenCL'ish本地内存(与CUDA'ish共享内存相同)和CUDA'ish本地内存(实际上只是片外全局内存的一部分)。酷。我现在猜测我的v0.2性能是由于我使用的是1D纹理,因此没有得到2D优化缓存的好处。谢谢!共享内存优化是我今天早上一直在做的事情。我们很快就会知道结果。在你最快的v0.2版本中有很多“低垂的果实”。您当前正在对点积内循环中的每个fmad执行14个整数运算。这是一个巨大的开销,14个iops中至少有12个是冗余的。
    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,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;
        }
    }
    
    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 ] 
    
    __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.
    
    __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];
    }
    
    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