解释GPU NVIDIA执行时间
Explain GPU NVIDIA execution time
我编写了简单的代码来使用 GPU 对数组求和。我使用一维网格和块。每个块计算数组的“2 倍块大小”元素并将这些元素减少为 1 个值。然后主机代码将对这些块的减少值求和。这是代码:
__global__ void reduceBlksKernel1(int * in, int n, int * out) {
int i = blockIdx.x * blockDim.x * 2 + threadIdx.x * 2;
for (int stride = 1; stride < 2 * blockDim.x; stride *= 2) {
if (threadIdx.x % stride == 0) {
if (i + stride < n) {
in[i] += in[i + stride];
}
}
__syncthreads();
}
// use 1 thread to write the reduced value to out array
if (threadIdx.x == 0) {
out[blockIdx.x] = in[blockIdx.x * blockDim.x * 2];
}
}
我使用 Google colab,这里是 GPU 的信息:
我已经 运行 一些不同块大小的测试:
有人可以帮我解释为什么尽管所有情况下的占用率都是 100%,但执行时间会随着每个 SM 块的增加而减少吗?谢谢!
让我们考虑一下内核的这一部分,其中“大部分”工作正在完成:
for (int stride = 1; stride < 2 * blockDim.x; stride *= 2) {
if (threadIdx.x % stride == 0) {
if (i + stride < n) {
in[i] += in[i + stride];
}
}
__syncthreads();
}
每次通过for-loop,让我们先问一个问题,有多少个“活动”线程?这是第一个if-test给出的,也就是threadIdx.x%stride==0
。 stride
一开始是 1,然后是 2,然后是 4,依此类推。所以第一遍有完整的线程,第二遍选择每第二个线程,第三遍选择每第四个线程,依此类推。
现在让我们问一个问题,对于给定的线程块大小,在所有迭代中我们有多少平均 个活动线程for-loop。
最后,让我们针对上述问题构建一个 table,显示每个线程块配置的平均活动线程数,同时考虑驻留多个线程块的乘法因子。 table 中的第一个平均值将像这样计算:
1024+512+256+128+64+32+16+8+4+2+1 = 2047/11 = ~186 active threads (average)
对于这种情况,只能驻留 1 个线程块,因为 Turing 每个 SM 最多可补充 1024 个线程。 table 在其他情况下会是什么样子?
threads/block average per block number of blocks resident average per SM
1024 186 1 186
512 102 2 204
256 56.8 4 227
128 31.9 8 255
因此您可以看到,使用较小的线程块,但使用更多的线程块(至少对于这个范围和这部分分析)会导致每个 SM 的平均活动线程数更多。实际上,SM 平均消耗更高的内存流量,因此可以更快地完成此 memory-bound 工作负载。
那我们该怎么办呢?
我的建议是在缩减操作之前使用 grid-stride 循环。您可以通过查看缩减 #7 中的 canonical parallel reduction material. In that material, note the introduction of the grid-stride loop 了解这一点。这个想法是选择线程块的数量来填充您的 SM,并使用网格步幅循环来有效地加载“所有”数据。这将数据加载和部分缩减移动到非常有效的代码配置,同时将 less-efficient 扫描样式缩减保留到整个工作的希望较小的部分。这将最大限度地减少在扫描阶段花费的内核时间,从而最大限度地减少线程块大小对性能可变性的影响。
当然,即使有上述建议,如果您想要最后一点性能,您可能希望在各种线程块配置下评估您的代码以获得最佳性能。这种“ninja-tuning”的建议几乎适用于所有 GPU 代码。
顺便说一句,这里只使用全局内存的缩减方法不是我推荐的方法(请参阅链接的规范 material 以获得更好的方法),但这似乎不是你的问题的主旨。
我编写了简单的代码来使用 GPU 对数组求和。我使用一维网格和块。每个块计算数组的“2 倍块大小”元素并将这些元素减少为 1 个值。然后主机代码将对这些块的减少值求和。这是代码:
__global__ void reduceBlksKernel1(int * in, int n, int * out) {
int i = blockIdx.x * blockDim.x * 2 + threadIdx.x * 2;
for (int stride = 1; stride < 2 * blockDim.x; stride *= 2) {
if (threadIdx.x % stride == 0) {
if (i + stride < n) {
in[i] += in[i + stride];
}
}
__syncthreads();
}
// use 1 thread to write the reduced value to out array
if (threadIdx.x == 0) {
out[blockIdx.x] = in[blockIdx.x * blockDim.x * 2];
}
}
我使用 Google colab,这里是 GPU 的信息:
我已经 运行 一些不同块大小的测试:
有人可以帮我解释为什么尽管所有情况下的占用率都是 100%,但执行时间会随着每个 SM 块的增加而减少吗?谢谢!
让我们考虑一下内核的这一部分,其中“大部分”工作正在完成:
for (int stride = 1; stride < 2 * blockDim.x; stride *= 2) {
if (threadIdx.x % stride == 0) {
if (i + stride < n) {
in[i] += in[i + stride];
}
}
__syncthreads();
}
每次通过for-loop,让我们先问一个问题,有多少个“活动”线程?这是第一个if-test给出的,也就是threadIdx.x%stride==0
。 stride
一开始是 1,然后是 2,然后是 4,依此类推。所以第一遍有完整的线程,第二遍选择每第二个线程,第三遍选择每第四个线程,依此类推。
现在让我们问一个问题,对于给定的线程块大小,在所有迭代中我们有多少平均 个活动线程for-loop。
最后,让我们针对上述问题构建一个 table,显示每个线程块配置的平均活动线程数,同时考虑驻留多个线程块的乘法因子。 table 中的第一个平均值将像这样计算:
1024+512+256+128+64+32+16+8+4+2+1 = 2047/11 = ~186 active threads (average)
对于这种情况,只能驻留 1 个线程块,因为 Turing 每个 SM 最多可补充 1024 个线程。 table 在其他情况下会是什么样子?
threads/block average per block number of blocks resident average per SM
1024 186 1 186
512 102 2 204
256 56.8 4 227
128 31.9 8 255
因此您可以看到,使用较小的线程块,但使用更多的线程块(至少对于这个范围和这部分分析)会导致每个 SM 的平均活动线程数更多。实际上,SM 平均消耗更高的内存流量,因此可以更快地完成此 memory-bound 工作负载。
那我们该怎么办呢?
我的建议是在缩减操作之前使用 grid-stride 循环。您可以通过查看缩减 #7 中的 canonical parallel reduction material. In that material, note the introduction of the grid-stride loop 了解这一点。这个想法是选择线程块的数量来填充您的 SM,并使用网格步幅循环来有效地加载“所有”数据。这将数据加载和部分缩减移动到非常有效的代码配置,同时将 less-efficient 扫描样式缩减保留到整个工作的希望较小的部分。这将最大限度地减少在扫描阶段花费的内核时间,从而最大限度地减少线程块大小对性能可变性的影响。
当然,即使有上述建议,如果您想要最后一点性能,您可能希望在各种线程块配置下评估您的代码以获得最佳性能。这种“ninja-tuning”的建议几乎适用于所有 GPU 代码。
顺便说一句,这里只使用全局内存的缩减方法不是我推荐的方法(请参阅链接的规范 material 以获得更好的方法),但这似乎不是你的问题的主旨。