归约是一种常见的数据并行原语,它将数组中的元素通过某种二元操作(如加法)合并成一个单一的值。通过逐步展示不同的CUDA实现版本,来演示重要的优化策略。
由于规约的算术操作很简单,对算力要求不高,因此我们逐步优化目标是尽可能达到最高的带宽利用率,基本想法是:
-
树状归约方法:在每个线程块内使用基于树的方法进行局部归约,然后需要处理如何跨线程块通信部分结果。
-
全局同步问题:CUDA没有全局同步机制,因为这样做在硬件上成本高昂,并且会限制程序运行的线程块数量,影响整体效率。
-
内核分解:通过分解计算为多个内核调用来避免全局同步,内核启动点作为全局同步点,具有较低的硬件和软件开销。
-
优化目标:对于归约操作,由于其算术强度很低(每个加载的元素仅有一次浮点操作),优化目标是达到峰值带宽。
基础实现
__global__ void reduceSum(int *g_idata, int* g_odata)
{
extern __shared__ int sdata[];
uint tid = threadIdx.x;
uint i = blockIdx.x*blockDim.x+threadIdx.x;
sdata[tid] = g_idata[i];
// printf("blockIdx=%d,sdata[%d]=%d ",blockIdx.x,tid,sdata[tid]);
__syncthreads();
for(uint s=1; s<blockDim.x; s*=2){
if (tid %(2*s) == 0){
sdata[tid] += sdata[tid+s];
}
__syncthreads();
}
if(tid ==0) {
g_odata[blockIdx.x] = sdata[0];
// atomicAdd(g_odata, sdata[0]);
}
}
Warp thread divergent
在 CUDA 编程中,高度发散的 warps 和使用 %(取模)运算符都会对性能产生负面影响。
高度发散的 Warps Warp 是 CUDA 中的一个基本执行单元。一个 Warp 包含 32 个线程,这些线程在同一个流多处理器(SM)中并行执行相同的指令。
如果一个 Warp 中的所有线程都执行相同的指令,则 Warp 是一致的,性能最好。
Warp 发散 发生在同一个 Warp 中的线程执行不同的指令路径时。通常是因为条件分支语句(如 if-else)导致不同线程走不同的代码路径。
- 当 Warp 发散时,CUDA 硬件必须序列化不同的执行路径。这意味着,虽然所有线程在逻辑上是并行的,但实际上它们不得不逐路径地执行不同的指令,这大大降低了并行效率。
- 举例来说,如果一个 Warp 中一半的线程执行一个路径,另一半执行另一个路径,那么两个路径将被顺序执行,每个路径只利用了一半的线程,效率降低。
% 运算符很慢
- % 运算符在很多硬件架构上实现起来比较复杂和耗时,因为它通常需要进行除法运算,而除法比加法、减法和乘法慢很多。
- 在 CUDA 编程中,特别是对于 GPU 的流多处理器(SM)来说,整数除法和取模操作更为耗时,因为这些操作需要更多的时钟周期来完成。
解决方案
- 减少 Warp 发散
- 最小化条件分支:尽量减少 if-else 语句的使用,特别是在 Warp 内部。
- 数据重构:尝试重构数据,使得同一个 Warp 中的线程能够执行相同的指令。
- 避免复杂的条件判断:如果条件判断无法避免,尝试使用其它算法或数据结构来最小化发散。
- 优化取模操作
- 使用位操作:如果取模的数是 2 的幂,可以使用位操作来代替 %。例如,x % 4 可以替换为 x & 3。
- 查找表:对于小范围的取模操作,可以使用查找表来替代计算。
- 简化算法:如果可能,重构算法以减少或避免取模操作。
__global__ void reduceSum1(int *g_idata, int* g_odata)
{
extern __shared__ int sdata[];
uint tid = threadIdx.x;
uint i = blockIdx.x*blockDim.x+threadIdx