如何确保编译器将我的负载与全局内存并行化?

时间:2016-09-07 03:22:03

标签: cuda gpu

我写了一个看起来像这样的CUDA内核:

int tIdx = threadIdx.x; // Assume a 1-D thread block and a 1-D grid
int buffNo = 0;
for (int offset=buffSz*blockIdx.x; offset<totalCount; offset+=buffSz*gridDim.x) {
    // Select which "page" we're using on this iteration
    float *buff = &sharedMem[buffNo*buffSz];
    // Load data from global memory
    if (tIdx < nLoadThreads) {
        for (int ii=tIdx; ii<buffSz; ii+=nLoadThreads)
            buff[ii] = globalMem[ii+offset];
    }
    // Wait for shared memory
    __syncthreads();
    // Perform computation
    if (tIdx >= nLoadThreads) {
        // Perform some computation on the contents of buff[]
    }
    // Switch pages
    buffNo ^= 0x01;
}

请注意,循环中只有一个__syncthreads(),因此第一个nLoadThreads线程将开始加载第二次迭代的数据,而其余线程仍在计算第一次迭代的结果

我在考虑为加载和计算分配多少个线程,我推断我只需要一个warp来加载,无论缓冲区大小如何,因为内部for循环包含来自全局内存的独立加载:他们都可以在同一时间飞行。这是一个有效的推理线吗?

然而当我尝试这一点时,我发现(1)增加负载扭曲数会大大提高性能,(2)nvvp中的反汇编表明buff[ii] = globalMem[ii+offset]被编译成了从全局内存加载后跟随2条指令的存储到共享内存,表明编译器没有在这里应用指令级并行。

const__restrict__上的其他限定符(buffglobalMem等)是否有助于确保编译器执行我想要的操作?

我怀疑问题与编译时buffSz未知的事实有关(实际数据是2-D,适当的缓冲区大小取决于矩阵维度)。为了做我想做的事,编译器需要为飞行中的每个LD操作分配一个单独的寄存器,对吧?如果我手动展开循环,编译器会重新排序指令,以便在相应的ST需要访问该寄存器之前有一些LD在飞行中。我尝试了#pragma unroll,但编译器只展开循环而不重新排序指令,所以这没有帮助。我还能做什么?

1 个答案:

答案 0 :(得分:3)

编译器没有机会将存储重新排序到共享内存,而不是来自全局内存的负载,因为__syncthreads()屏障紧随其后。 由于所有线程都必须在屏障处等待,因此使用更多线程进行加载会更快。这意味着更多的全局内存事务可以在任何时候进行,并且每个加载线程都必须减少全局内存延迟。

到目前为止,所有CUDA设备都不支持无序执行,因此加载循环将在每次循环迭代中产生一个全局内存延迟,除非编译器可以在存储之前将其展开并重新排序。

要允许完全展开,需要在编译时知道循环迭代次数。你可以使用talonmies&#39;建议模仿循环行程来实现这一目标。

您也可以使用部分展开。使用#pragma unroll 2注释加载循环将允许编译器发出两个加载,然后每两个循环迭代发出两个存储,从而实现与加倍nLoadThreads类似的效果。可以用更高的数字替换2,但是您将在某个时刻达到飞行中的最大事务数量(使用float2或float4移动以传输具有相同数量事务的更多数据)。此外,很难预测编译器是否更愿意重新排序指令,而不是通过展开循环的最终可能部分跳转的更复杂代码的成本。

所以建议是:

  1. 尽可能多地使用负载线程。
  2. 通过模拟循环迭代次数并为所有可能的循环次数(或最常见的循环次数,具有通用回退)或使用部分循环展开实例化来展开加载循环。
  3. 如果数据经过适当调整,请将其移至float2float4,以便使用相同数量的交易移动更多数据。