Cuda 为';运行不同的内核变体是否是一种良好的做法;完整';和';部分尾部';阻碍?
通常,当您编写GPU内核时,关于具有完整块/扭曲的假设[1]非常有用:您可以在扭曲中的所有线程之间移动;不同的warp可以假定部分必要的工作将由其他warp等完成。当您有大量的输入要处理时(许多块线程),这将转化为有意义的性能增益 当然,如果您做出这样的假设,那么对于输入数据的“余数”,或者更确切地说是网格中未满的单个“余数”块,您需要使用特殊情况。充其量,在某个地方有一个额外的Cuda 为';运行不同的内核变体是否是一种良好的做法;完整';和';部分尾部';阻碍?,cuda,gpgpu,idioms,Cuda,Gpgpu,Idioms,通常,当您编写GPU内核时,关于具有完整块/扭曲的假设[1]非常有用:您可以在扭曲中的所有线程之间移动;不同的warp可以假定部分必要的工作将由其他warp等完成。当您有大量的输入要处理时(许多块线程),这将转化为有意义的性能增益 当然,如果您做出这样的假设,那么对于输入数据的“余数”,或者更确切地说是网格中未满的单个“余数”块,您需要使用特殊情况。充其量,在某个地方有一个额外的if(blockIdx.x我同意Jez的观点,即内核启动开销可能是一个严重的约束。GaTech的一篇研究论文对此进行了
if(blockIdx.x
,最坏的情况是,它是一个完全不同的计算方法,不管你想要什么
有人想知道,本质上有两个独立的内核是否是一个好主意,一个用于剩余块,另一个用于大容量数据,这样大容量内核就不需要浪费时间来确保它在完整块上工作了
当然,在实践中,这可能只是一个静态检查,即
template <bool IsRemainder>
__global__ void fooKernel(int* a, int* b)
{
if (IsRemainder) {
/* special-casing */
}
else {
/* regular case, full block. */
}
}
模板
__全局无效内核(int*a,int*b)
{
如果(IsRemainder){
/*特殊套管*/
}
否则{
/*正常情况下,完全阻塞*/
}
}
注:
- 假设所有数据都在GPU上,并且不会很快返回CPU
- 假设网格是一维的
- 这允许常规案例代码根本不检查块是否已满-在运行时检查那里的条件 [1] (用CUDA的说法;用OpenCL:工作组,嗯,波阵面,对吧?)
其他需要考虑的因素包括编译时间和可执行的大小,如果代码中的每一个都使用这种模式,这两个因素都会显著地提高。
< P>我同意Jez的观点,即内核启动开销可能是一个严重的约束。GaTech的一篇研究论文对此进行了评估: 这里的作者更关心GPU代码中存在的动态并行机会,并使用NVIDIA的机制在运行时启动新内核 本研究的后续工作位于(如果有兴趣):对于这么广泛的问题,恐怕有很多可能的排列方式,没有一般的答案(例如,通常在主机上与GPU并行进行剩余计算是一种可行且性能良好的选择)@Talonmes:试图限制我问题的范围。如果1的答案为真,也就是说,您有足够的数据,可以从运行大部分完整块版本中获得不可忽略的好处-那么,对2的回答可能是“不是问题”。您可能会遇到这样一种情况,即检查成本很高,但一个块会发生什么问题?假设我有足够的块来填充GPU 100次。我仍然只有两次发射。或者我遗漏了什么?如果你有足够的内存来填充GPU 100次,那么这可能不会是一个问题。虽然这不是你最初的假设之一……好吧,我确实说过“大部分数据”是完整块的情况,但我将重新措辞以希望澄清。嗯,那篇文章讨论了一个动态并行场景,其中发生了大量内核“启动”,而这不是这个问题的内容。或者我误解你了吗?虽然这篇论文是关于动态并行的,但它揭示了现代GPU的一个重要方面,即对可运行的并发内核数量的限制(以及启动新内核的开销)。GPU上可能有许多图形内核(例如AMD异步着色器),您的计算内核必须与之竞争资源。在这种情况下,您需要警惕创建新内核的影响(这与您提出的问题有点关联)。