分析我的CUDA内核的内存访问合并

时间:2012-12-07 21:48:39

标签: cuda shared

我想通过BS_x读取(BS_X + 1)*(BS_Y + 1)全局内存位置* BS_Y线程将内容移动到共享内存,我开发了以下代码。

int i       = threadIdx.x;
int j       = threadIdx.y;
int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

int index1  = j*BLOCK_SIZE_Y+i;

int i1      = (index1)%(BLOCK_SIZE_X+1);
int j1      = (index1)/(BLOCK_SIZE_Y+1);

int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);

__shared__ double Ezx_h_shared_ext[BLOCK_SIZE_X+1][BLOCK_SIZE_Y+1];     

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];

if ((i2<(BLOCK_SIZE_X+1))&&(j2<(BLOCK_SIZE_Y+1))) 
Ezx_h_shared_ext[i2][j2]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j2)*xdim+(blockIdx.x*BLOCK_SIZE_X+i2)];

根据我的理解,合并是顺序处理的连续内存读取的并行等价物。现在如何检测全局内存访问是否已合并?我注意到从(i1,j1)到(i2,j2)的索引跳转。 提前谢谢。

3 个答案:

答案 0 :(得分:5)

我用手写的合并分析器评估了代码的内存访问。评估显示代码较少利用合并。以下是您可能会觉得有用的合并分析器:

#include <stdio.h>
#include <malloc.h>

typedef struct dim3_t{
    int x;
    int y;
} dim3;


// KERNEL LAUNCH PARAMETERS
#define GRIDDIMX 4
#define GRIDDIMY 4
#define BLOCKDIMX 16
#define BLOCKDIMY 16


// ARCHITECTURE DEPENDENT
// number of threads aggregated for coalescing
#define COALESCINGWIDTH 32
// number of bytes in one coalesced transaction
#define CACHEBLOCKSIZE 128
#define CACHE_BLOCK_ADDR(addr,size)  (addr*size)&(~(CACHEBLOCKSIZE-1))


int main(){
    // fixed dim3 variables
    // grid and block size
    dim3 blockDim,gridDim;
    blockDim.x=BLOCKDIMX;
    blockDim.y=BLOCKDIMY;
    gridDim.x=GRIDDIMX;
    gridDim.y=GRIDDIMY;

    // counters
    int unq_accesses=0;
    int *unq_addr=(int*)malloc(sizeof(int)*COALESCINGWIDTH);
    int total_unq_accesses=0;

    // iter over total number of threads
    // and count the number of memory requests (the coalesced requests)
    int I, II, III;
    for(I=0; I<GRIDDIMX*GRIDDIMY; I++){
        dim3 blockIdx;
        blockIdx.x = I%GRIDDIMX;
        blockIdx.y = I/GRIDDIMX;
        for(II=0; II<BLOCKDIMX*BLOCKDIMY; II++){
            if(II%COALESCINGWIDTH==0){
                // new coalescing bunch
                total_unq_accesses+=unq_accesses;
                unq_accesses=0;
            }
            dim3 threadIdx;
            threadIdx.x=II%BLOCKDIMX;
            threadIdx.y=II/BLOCKDIMX;

            ////////////////////////////////////////////////////////
            // Change this section to evaluate different accesses //
            ////////////////////////////////////////////////////////
            // do your indexing here
            #define BLOCK_SIZE_X BLOCKDIMX
            #define BLOCK_SIZE_Y BLOCKDIMY
            #define xdim 32
            int i       = threadIdx.x;
            int j       = threadIdx.y;
            int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
            int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

            int index1  = j*BLOCK_SIZE_Y+i;

            int i1      = (index1)%(BLOCK_SIZE_X+1);
            int j1      = (index1)/(BLOCK_SIZE_Y+1);

            int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
            int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);
            // calculate the accessed location and offset here
            // change the line "Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];" to
            int addr = (blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1);
            int size = sizeof(double);
            //////////////////////////
            // End of modifications //
            //////////////////////////

            printf("tid (%d,%d) from blockid (%d,%d) accessing to block %d\n",threadIdx.x,threadIdx.y,blockIdx.x,blockIdx.y,CACHE_BLOCK_ADDR(addr,size));
            // check whether it can be merged with existing requests or not
            short merged=0;
            for(III=0; III<unq_accesses; III++){
                if(CACHE_BLOCK_ADDR(addr,size)==CACHE_BLOCK_ADDR(unq_addr[III],size)){
                    merged=1;
                    break;
                }
            }
            if(!merged){
                // new cache block accessed over this coalescing width
                unq_addr[unq_accesses]=CACHE_BLOCK_ADDR(addr,size);
                unq_accesses++;
            }
        }
    }
    printf("%d threads make %d memory transactions\n",GRIDDIMX*GRIDDIMY*BLOCKDIMX*BLOCKDIMY, total_unq_accesses);
}

代码将针对网格的每个线程运行,并计算合并请求的数量,内存访问合并的度量标准。

要使用分析器,请将代码的索引计算部分粘贴到指定区域,并将内存访问(数组)分解为“地址”和“大小”。我已经为你的代码做了这个,索引是:

int i       = threadIdx.x;
int j       = threadIdx.y;
int idx     = blockIdx.x*BLOCK_SIZE_X + threadIdx.x;
int idy     = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y;

int index1  = j*BLOCK_SIZE_Y+i;

int i1      = (index1)%(BLOCK_SIZE_X+1);
int j1      = (index1)/(BLOCK_SIZE_Y+1);

int i2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1);
int j2      = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1);

并且内存访问是:

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];

分析器报告4096个线程访问4064个缓存块。运行实际网格和块大小的代码并分析合并行为。

答案 1 :(得分:2)

随着GPU的发展,获得合并访问的要求变得越来越不严格。对于早期的GPU架构,您对合并访问的描述比最近的GPU架构更准确。特别是,Fermi(计算能力2.0)显着放宽了要求。在费米及以后,连续访问存储器位置并不重要。相反,焦点已转移到使用尽可能少的内存事务访问内存。在Fermi上,全局内存事务是128字节宽。因此,当warp中的32个线程命中执行加载或存储的指令时,将调度128字节的事务来为warp中的所有线程提供服务。然后,性能取决于需要多少交易。如果所有线程都访问128字节区域内与128字节对齐的值,则需要进行单个事务。如果所有线程都访问不同128字节区域中的值,则需要32个事务。这将是在warp中为单个指令提供服务的最坏情况。

您可以使用其中一个CUDA分析器来确定为请求提供服务所需的事务数的平均值。该数字应尽可能接近1。数字越大意味着您应该看看是否有机会优化内核中的内存访问。

答案 2 :(得分:1)

visual profiler是检查工作的绝佳工具。在功能正确的代码片段之后,然后从可视化分析器中运行它。例如,在Linux上,假设你有一个X会话,只需从终端窗口运行nvvp。然后,您将获得一个向导,该向导将提示您应用程序以及任何命令行参数进行分析。

然后,探查器将对您的应用进行基本运行以收集统计信息。您还可以选择更高级的统计信息收集(需要添加运行),其中一个将是内存利用率统计信息。它会将内存利用率报告为峰值的百分比,并且还会标记其认为低利用率的警告,值得您关注。

如果您的利用率超过50%,那么您的应用可能正在以您期望的方式运行。如果你的数字很少,你可能错过了一些合并细节。它将分别报告内存读取和内存写入的统计信息。要获得100%或接近它,您还需要确保经线的合并读取和写入在128字节边界上对齐。

在这些情况下常见的错误是使用基于threadIdx.y的变量作为最快速变化的索引。在我看来你没有犯这个错误。例如这是shared[threadIdx.x][threadIdx.y]的常见错误,因为这通常是我们在C中考虑它的方式。但是线程首先在x轴上组合在一起,所以我们想要使用shared[threadIdx.y][threadIdx.x]或类似的东西。如果您确实犯了这个错误,那么您的代码仍然可以在功能上正确,但在分析器中您将获得较低的利用率百分比,例如大约12%甚至3%。

如前所述,要达到50%以上且接近100%,您需要确保不仅所有线程请求都相邻,而且它们在128B边界上对齐。由于L1 / L2缓存,这些并不是硬性规定,而是指导方针。缓存可以在某种程度上缓解一些错误。