我理解#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
可以是2
到1<<20
之间的任何位置。由于100万似乎是变量的一个非常大的数字(展开的100万个循环将导致寄存器压力,我不确定编译器是否会这样做展开),什么是“公平”数字,如果有的话?我如何确定该限制?
答案 0 :(得分:3)
你的示例内核是完全串行的,并且在任何情况下都不是用于循环展开的有用的现实用例,但是让我们自己解决了展开编译器将执行多少循环的问题。
这是一个可编译的内核版本,带有一些模板修饰:
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
案例未展开。如果你有更多的耐心,你可以使用模板来找到这段代码的确切编译器限制,尽管我怀疑这一点特别有用。
您还可以通过编译器自行查看所有严重展开的版本使用相同数量的寄存器(因为您的内核具有琐碎的性质)。
答案 1 :(得分:1)
CUDA利用线程级并行性,通过将工作分成多个线程来公开,以及CUDA通过在编译代码中搜索独立指令而找到的指令级并行性。
@talonmies的结果,显示你的循环可能在4096和8192次迭代之间展开,这对我来说是令人惊讶的,因为循环展开在现代CPU上的回报率急剧下降,其中大多数迭代开销已经通过诸如分支之类的技术进行了优化预测和推测执行。
在CPU上,我怀疑从展开10-20次迭代以及展开的循环在指令缓存中占用更多空间会有很多好处,因此也需要展开成本。在确定要展开多少时,CUDA编译器将考虑成本/收益权衡。所以问题是,展开4096次迭代可能带来哪些好处?我想这可能是因为它为GPU提供了更多的代码,在这些代码中它可以搜索独立的指令,然后可以使用指令级并行来同时运行。
你的循环体是A_out[i] = B[i] + C[i];
。由于循环中的逻辑不访问外部变量,并且不访问循环的早期迭代的结果,因此每次迭代都独立于所有其他迭代。所以i
不必按顺序增加。即使循环以完全随机的顺序迭代i
和0
之间的LIMIT - 1
的每个值,最终结果也是相同的。该属性使循环成为并行优化的良好候选者。
但是有一个问题,这就是我在评论中提到的。如果A
缓冲区与B
和C
缓冲区分开存储,则循环的迭代仅是独立的。如果A
缓冲区与内存中的B
和/或C
缓冲区部分或完全重叠,则会创建不同迭代之间的连接。现在,一次迭代可以通过写入B
来更改另一次迭代的C
和A
输入值。因此,根据首先运行的两个迭代中的哪一个,您会得到不同的结果。
指向内存中相同位置的多个指针称为指针别名。因此,通常,指针别名可能导致代码段之间的“隐藏”连接看起来是分开的,因为一段代码通过一个指针完成的写入可以改变从另一个指针读取的另一段代码读取的值。默认情况下,CPU编译器会生成将可能的指针别名考虑在内的代码,生成无论如何都能产生正确结果的代码。问题是CUDA做了什么,因为,回到talonmies的测试结果,我能看到如此大量展开的唯一原因是它打开了指令级并行的代码。但那意味着CUDA在这种特殊情况下不会考虑指针别名。
重新。关于运行多个线程的问题,当增加线程数时,常规串行程序不会自动成为并行程序。您必须确定可以并行运行的工作部分,然后在CUDA内核中表达。这就是所谓的线程级并行性,它是代码性能提升的主要来源。此外,CUDA将在每个内核中搜索独立指令,并且可以同时运行这些指令,这是指令级并行。高级CUDA程序员可能会记住指令级并行性并编写有助于实现这一点的代码,但我们凡人应该专注于线程级并行。这意味着您应该再次查看代码并考虑可以并行运行。由于我们已经得出结论,循环体是并行化的良好候选者,因此您的工作将重写内核中的串行循环,以向CUDA表达如何并行运行单独的迭代。