确定CUDA中#pragma unroll N的最佳值

3

我了解 #pragma unroll 的作用,但如果我有以下示例:

__global__ void
test_kernel( const float* B, const float* C, float* A_out)
{
  int j = threadIdx.x + blockIdx.x * blockDim.x;
  if (j < array_size) {
     #pragma unroll
     for (int i = 0; i < LIMIT; i++) {
       A_out[i] = B[i] + C[i];
     }
  }
}

我希望确定内核中的LIMIT的最佳值,该内核将使用x个线程和y个块启动。 LIMIT可以在21<<20之间任何位置。1百万似乎对于变量来说是非常大的数字(1百万次循环展开会引起寄存器压力,而我不确定编译器是否会展开它),那么有没有“合理”的数字?如何确定这个限制?

ABC是什么,它们存储在哪里?为什么循环完全串行?您希望从展开完全串行的循环(看起来像线程本地变量)中获得什么优势? - talonmies
A、B、C是全局变量,不是内核本地的。这可能是一个不好的例子,但我只是想弄清楚我能展开多少。 - Blizzard
要么这是一个非常糟糕的例子,要么你对CUDA的工作原理有根本性的误解。你能否将其扩展为一个真正可编译的内核呢? - talonmies
我已经使用一个内核进行了编辑,可以通过类似于test_kernel<<<1, 1>>>(d_idata_B, d_idataC, d_odataA);的方式进行调用。 - Blizzard
2个回答

4

您的示例内核完全串行化,无法在实际应用中使用循环展开,但让我们局限于编译器将执行多少循环展开的问题。

以下是添加了一些模板修饰符的可编译版本的内核:

template<int LIMIT>
__global__ void
test_kernel( const float* B, const float* C, float* A_out, int array_size)
{
  int j = threadIdx.x + blockIdx.x * blockDim.x;
  if (j < array_size) {
     #pragma unroll
     for (int i = 0; i < LIMIT; i++) {
       A_out[i] = B[i] + C[i];
     }
  }
}

template __global__ void test_kernel<4>(const float*, const float*, float*, int);
template __global__ void test_kernel<64>(const float*, const float*, float*, int);
template __global__ void test_kernel<256>(const float*, const float*, float*, int);
template __global__ void test_kernel<1024>(const float*, const float*, float*, int);
template __global__ void test_kernel<4096>(const float*, const float*, float*, int);
template __global__ void test_kernel<8192>(const float*, const float*, float*, int);

你可以将此编译为PTX,亲自验证(至少在CUDA 7版本编译器和默认的2.0计算能力目标架构下),带有LIMIT = 4096的内核完全展开。 LIMIT = 8192 情况未展开。如果你比我更有耐心,可以尝试使用模板来查找此代码的确切编译器限制,但我认为了解这一点并不特别有教育意义。
通过编译器,你还可以自己看到所有重复展开版本使用相同数量的寄存器(因为内核的基本性质)。

循环实际上完全串行吗?如果CUDA在这里忽略可能的指针别名,它可以将每个操作视为独立的。然后展开可以提供更多的指令级并行性。 - Roger Dahl
谢谢你的回答!Roger提出了另一个问题,我也很好奇。如果我启动的线程数超过<1,1>,那么它就不是串行的了,对吗?你能详细解释一下并行性吗?我对CUDA还很陌生,所以这肯定会是有帮助的信息! - Blizzard

1
CUDA利用线程级并行性,通过将工作分成多个线程来实现,并利用指令级并行性,在编译代码时查找独立指令。
@talonmies的结果表明,您的循环可能在4096到8192次迭代之间展开,这让我感到惊讶,因为在现代CPU上,大多数迭代开销已经通过诸如分支预测和推测执行等技术进行了优化,因此循环展开的收益急剧减少。
在CPU上,我怀疑展开超过10-20次迭代不会有太多收益,并且展开的循环会占用更多的指令缓存空间,因此展开也有成本。 CUDA编译器将在确定要展开多少次循环时考虑成本/效益权衡。 因此,问题是,展开4096+次迭代的好处是什么? 我认为这是因为它为GPU提供了更多的代码,可以在其中搜索独立指令,然后使用指令级并行性并发运行。
你的循环体是A_out[i] = B[i] + C[i];。由于循环中的逻辑没有访问外部变量,也没有访问之前迭代的结果,因此每次迭代都是独立的。所以i不必按顺序递增。即使循环以完全随机的顺序迭代i的每个值,最终结果也是相同的。这种特性使得循环成为并行优化的好选择。
但是有一个问题,就是我在评论中提到的。只有当A缓冲区与B和/或C缓冲区分开存储时,循环的迭代才是独立的。如果A缓冲区部分或全部重叠B和/或C缓冲区,则会创建不同迭代之间的连接。一次迭代可能通过写入A来更改另一次迭代的BC输入值。因此,取决于哪个迭代先运行,您将获得不同的结果。
指向内存中相同位置的多个指针称为指针别名。因此,通常情况下,指针别名可能会导致代码部分之间出现“隐藏”的连接,这些代码部分看起来是分开的,因为通过一个指针执行的写入可能会改变另一个指针读取的值。默认情况下,CPU编译器会考虑到可能的指针别名,生成产生正确结果的代码。问题是CUDA会做什么,因为回到talonmies的测试结果,我唯一能想到如此大量展开的原因是它打开了指令级并行性的代码。但这意味着CUDA在这种特定情况下不考虑指针别名。
关于您关于运行多个线程的问题,当您增加线程数量时,常规串行程序并不会自动变成并行程序。您需要确定可以并行运行的工作部分,并在CUDA核心中表达出来。这就是所谓的线程级并行性,也是代码性能提高的主要来源。此外,CUDA将在每个内核中搜索独立的指令,并可能同时运行这些指令,这就是指令级并行性。高级CUDA程序员可能会考虑指令级并行性,并编写有助于实现该目标的代码,但我们普通人应该专注于线程级并行性。这意味着您应该重新审视您的代码,并考虑哪些部分可能可以并行运行。由于我们已经得出结论,循环体是一个很好的并行化候选者,因此您的任务是重写内核中的串行循环,以向CUDA表达如何并行运行单独的迭代。

非常感谢您出色的解释!真的很有帮助,我越来越了解CUDA,现在开始有点明白了。 - Blizzard
在我看来,迭代次数更多地展开并不应该让人感到惊讶,原因有两个:(1)展开循环可能会为临时数据分配寄存器(当操作在展开循环内重新排序时);GPU 比 CPU 有更多的通用寄存器。(2)GPU 更喜欢预测而不是分支预测。尽管如此,4096 次迭代仍然令人惊讶... - einpoklum

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接