我写了一个看起来像这样的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__
上的其他限定符(buff
,globalMem
等)是否有助于确保编译器执行我想要的操作?
我怀疑问题与编译时buffSz
未知的事实有关(实际数据是2-D,适当的缓冲区大小取决于矩阵维度)。为了做我想做的事,编译器需要为飞行中的每个LD操作分配一个单独的寄存器,对吧?如果我手动展开循环,编译器会重新排序指令,以便在相应的ST需要访问该寄存器之前有一些LD在飞行中。我尝试了#pragma unroll
,但编译器只展开循环而不重新排序指令,所以这没有帮助。我还能做什么?
答案 0 :(得分:3)
编译器没有机会将存储重新排序到共享内存,而不是来自全局内存的负载,因为__syncthreads()
屏障紧随其后。
由于所有线程都必须在屏障处等待,因此使用更多线程进行加载会更快。这意味着更多的全局内存事务可以在任何时候进行,并且每个加载线程都必须减少全局内存延迟。
到目前为止,所有CUDA设备都不支持无序执行,因此加载循环将在每次循环迭代中产生一个全局内存延迟,除非编译器可以在存储之前将其展开并重新排序。
要允许完全展开,需要在编译时知道循环迭代次数。你可以使用talonmies&#39;建议模仿循环行程来实现这一目标。
您也可以使用部分展开。使用#pragma unroll 2
注释加载循环将允许编译器发出两个加载,然后每两个循环迭代发出两个存储,从而实现与加倍nLoadThreads
类似的效果。可以用更高的数字替换2
,但是您将在某个时刻达到飞行中的最大事务数量(使用float2或float4移动以传输具有相同数量事务的更多数据)。此外,很难预测编译器是否更愿意重新排序指令,而不是通过展开循环的最终可能部分跳转的更复杂代码的成本。
所以建议是:
float2
或float4
,以便使用相同数量的交易移动更多数据。