待解决的问题:

第三章 CUDA执行模型

3.5 循环展开

3.5.2 展开 线程的归约(最后一个warp内32个线程展开)

__syncthreads()是用于块内同步的,在归约核函数中,用来保证线程进入下一轮之前,每一轮的所有线程都把局部结果写入全局内存。

只剩下一个线程束时(线程<=32),因为线程束执行是SIMT单指令多线程,每条指令之后有隐式的warp内同步过程。归约循环的最后6个迭代(最后一个warp内)可以用语句展开。

if(tid < 32){
	volatile int *vmem = idata;
	vmem[tid] += vmem[tid + 32];
	vmem[tid] += vmem[tid + 16];
	vmem[tid] += vmem[tid + 8];
	vmem[tid] += vmem[tid + 4];
	vmem[tid] += vmem[tid + 2];
	vmem[tid] += vmem[tid + 1];
}

这个线程束的展开避免了执行循环控制和线程同步逻辑

volatile关键字:表明编译器对访问该变量的代码就不再进行优化,告诉编译器每次赋值的时候把vmem[tid]的值存回全局内存中。如果省略volatile关键字,就不能正常工作,因为编译器或缓存会对全局/共享内存优化读写
如果全局/共享内存中的变量有volatile修饰,编译器假定它的值可以被其它线程任何时间修改使用,所以volatile修饰的变量强制读/写内存,而不是读写缓存或寄存器

代码与reduceUnrolling8相比的区别:在写回最后的结果之前多了上面展开的代码,对最后一个warp块内进行手动线程规约,对应上面的就地归约中,限制条件也需要改成stride>32

最后一个warp手动规约+一个线程块手动处理8个数据块
//展开线程规约4:手动展开八个块block的处理+最后一个块内手动规约
__global__ void reduceUnrollWarps8(int *g_idata, int *g_odata, unsigned int n){
    //设置线程id
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    //全局数据指针转换为当前块内数据指针
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    //展开八个数据块,八个数据块加到第一个位置
    if(idx + 7 * blockDim.x < n){
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + blockDim.x * 2];
        int a4 = g_idata[idx + blockDim.x * 3];
        int a5 = g_idata[idx + blockDim.x * 4];
        int a6 = g_idata[idx + blockDim.x * 5];
        int a7 = g_idata[idx + blockDim.x * 6];
        int a8 = g_idata[idx + blockDim.x * 7];
        g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;
    }
    __syncthreads();

    //就地归约,一个数据块内
    for(int stride = blockDim.x/2; stride > 32; stride >>= 1){
        if(tid < stride){
            idata[tid] += idata[tid + stride];
        }
        __syncthreads();
    }

    //【新增!!!】展开warp,当归约到最后一个warp时,手动展开
    if(tid < 32){
        volatile int *vmem = idata;//volatile关键字说明编译器不对该变量优化
        vmem[tid] += vmem[tid + 32];
        vmem[tid] += vmem[tid + 16];
        vmem[tid] += vmem[tid + 8];
        vmem[tid] += vmem[tid + 4];
        vmem[tid] += vmem[tid + 2];
        vmem[tid] += vmem[tid + 1];
    }

    //把这个块的结果写回全局内容
    if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

对应的main函数代码也需要修改:

//kernel7:reduceUnrollWarps8 展开规约5:一个线程块处理八个数据块
    cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice);
    cudaDeviceSynchronize();
    iStart = seconds();
    //因为现在每个线程块处理两个数据块,需要调整内核的执行配置,网格大小减为原来的一半
    reduceUnrollWarps8<<<grid.x/8, block>>>(d_idata, d_odata, size);//这里写错成了bytes
    cudaDeviceSynchronize();
    iElaps = seconds() - iStart;
    cudaMemcpy(h_odata, d_odata, grid.x/8*sizeof(int), cudaMemcpyDeviceToHost);//需要除2
    gpu_sum = 0;
    for(int i = 0; i<grid.x/8; i++){//需要除
        gpu_sum += h_odata[i];
    }
    printf("gpu reduceUnrollWarps8  elapsed %f ms gpu_sum: %d <<<grid %d block %d>>>\n", iElaps, gpu_sum, grid.x/8, block.x);//需要除2

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 
   with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.041231 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.031330 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.000483 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.000276 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.000239 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000156 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000125 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000118 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

存在的问题:书上说比reduceUnrolling8快,但是实际上并没有,和书上结论不一致

reduceUnrollWarps8更快的原因是由于__syncthreads()的同步,更少线程束阻塞。
说明:__syncthreads()能减少核函数中的阻塞。

3.5.3 (交错归约)完全展开 的归约

如果编译时已知一个循环中的迭代次数,可以把循环完全展开。
因为Fermi或Kep1er架构中,每个块的最大线程数是1024,并且归约核函数中循环迭代次数是基于一个块block的维度。

把原来交错归约的步长自动迭代for循环具体手写。

    //【新增!!!】把就地归约步长迭代,完全展开
    if(blockDim.x >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素
    __syncthreads();

    if(blockDim.x >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素
    __syncthreads();

    if(blockDim.x >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素
    __syncthreads();

    if(blockDim.x >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素
    __syncthreads();
    //tid此时已经<64了,<32的情况下面处理
手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块
//完全展开的规约5:手动展开八个块block的处理+最后一个块内手动规约
__global__ void reduceCompleteUnrollWrap8(int *g_idata, int *g_odata, unsigned int n){
    //设置线程id
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    //全局数据指针转换为当前块内数据指针
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    //展开八个数据块,八个数据块加到第一个位置
    if(idx + blockDim.x * 7 < n){
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + blockDim.x * 2];
        int a4 = g_idata[idx + blockDim.x * 3];
        int a5 = g_idata[idx + blockDim.x * 4];
        int a6 = g_idata[idx + blockDim.x * 5];
        int a7 = g_idata[idx + blockDim.x * 6];
        int a8 = g_idata[idx + blockDim.x * 7];
        g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;
    }
    __syncthreads();

    //【新增!!!】把就地归约步长迭代,完全展开
    if(blockDim.x >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素
    __syncthreads();

    if(blockDim.x >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素
    __syncthreads();

    if(blockDim.x >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素
    __syncthreads();

    if(blockDim.x >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素
    __syncthreads();
    //tid此时已经<64了,<32的情况下面处理

    //展开warp,当归约到最后一个warp时,手动展开
    if(tid < 32){
        volatile int *vmem = idata;
        vmem[tid] += vmem[tid + 32];
        vmem[tid] += vmem[tid + 16];
        vmem[tid] += vmem[tid + 8];
        vmem[tid] += vmem[tid + 4];
        vmem[tid] += vmem[tid + 2];
        vmem[tid] += vmem[tid + 1];
    }

    //把这个块的结果写回全局内容
    if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 
   with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.039004 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.038595 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.001485 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.001385 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.001355 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000496 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000456 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000908 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000190 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnrollWrap8  elapsed 0.000469 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

书上说比reduceUnrollWarps8又有了小加速,实际运行并没有

3.5.4 模板函数 的归约

在手动展开交错归约的基础上,用模板函数进一步减少分支消耗,CUDA支持模板参数,指定块大小作为模板函数的参数。

与上一个代码的区别
参数模板iBlockSize代替原来的块大小blockDim.x。检查块大小的if语句在编译的时候会被评估,如果该条件为false,编译时就删除,循环更有效率。
例如:块大小永远是256,下面语句永远是false,直接不进入该归约步骤,编译器自动从执行内核中移除它。
②调用的时候用switch-case语句,编译器可以为不同大小的代码块自动优化代码。

iBlockSize >= 1024 && tid < 512

关键代码修改:

    //【新增改变!!!】把就地归约步长迭代,完全展开+iBlockSize代替原来
    if(iBlockSize >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素
    __syncthreads();

    if(iBlockSize >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素
    __syncthreads();

    if(iBlockSize >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素
    __syncthreads();

    if(iBlockSize >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素
    __syncthreads();
    //tid此时已经<64了,<32的情况下面处理
模板函数+手动展开交错归约+最后一个warp手动规约+一个线程块手动处理8个数据块
//完全展开的规约6:模板参数替换块大小+手动展开八个块block的处理+最后一个块内手动规约
template <unsigned int iBlockSize>
__global__ void reduceCompleteUnroll(int *g_idata, int *g_odata, unsigned int n){
    //设置线程id
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x * 8 + threadIdx.x;

    //全局数据指针转换为当前块内数据指针
    int *idata = g_idata + blockIdx.x * blockDim.x * 8;

    //展开八个数据块,八个数据块加到第一个位置
    if(idx + blockDim.x * 7 < n){
        int a1 = g_idata[idx];
        int a2 = g_idata[idx + blockDim.x];
        int a3 = g_idata[idx + blockDim.x * 2];
        int a4 = g_idata[idx + blockDim.x * 3];
        int a5 = g_idata[idx + blockDim.x * 4];
        int a6 = g_idata[idx + blockDim.x * 5];
        int a7 = g_idata[idx + blockDim.x * 6];
        int a8 = g_idata[idx + blockDim.x * 7];
        g_idata[idx] = a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8;
    }
    __syncthreads();

    //【新增改变!!!】把就地归约步长迭代,完全展开+iBlockSize代替原来
    if(iBlockSize >= 1024 && tid < 512) idata[tid] += idata[tid + 512];//线程块大小>1024时,前512个线程把1024个元素归约为512个元素
    __syncthreads();

    if(iBlockSize >= 512 && tid < 256) idata[tid] += idata[tid + 256];//线程块大小>512时,前256个线程把512个元素归约为256个元素
    __syncthreads();

    if(iBlockSize >= 256 && tid < 128) idata[tid] += idata[tid + 128];//线程块大小>256时,前128个线程把256个元素归约为128个元素
    __syncthreads();

    if(iBlockSize >= 128 && tid < 64) idata[tid] += idata[tid + 64];//线程块大小>128时,前64个线程把128个元素归约为64个元素
    __syncthreads();
    //tid此时已经<64了,<32的情况下面处理

    //展开warp,当归约到最后一个warp时,手动展开
    if(tid < 32){
        volatile int *vmem = idata;
        vmem[tid] += vmem[tid + 32];
        vmem[tid] += vmem[tid + 16];
        vmem[tid] += vmem[tid + 8];
        vmem[tid] += vmem[tid + 4];
        vmem[tid] += vmem[tid + 2];
        vmem[tid] += vmem[tid + 1];
    }

    //把这个块的结果写回全局内容
    if(tid == 0) g_odata[blockIdx.x] = idata[0];
}

对应main函数调用也修改:

    //kernel9:reduceCompleteUnroll 展开规约5:一个线程块处理八个数据块
    //调用方式修改为在switch case结构
    cudaMemcpy(d_idata, h_idata, bytes, cudaMemcpyHostToDevice);
    cudaDeviceSynchronize();
    iStart = seconds();
    //改变【新增!!!】允许编译器为特定线程块大小自动优化代码
    switch(blocksize){
        case 1024:
            reduceCompleteUnroll<1024><<<grid.x/8, block>>>(d_idata, d_odata, size);
            break;
        case 512:
            reduceCompleteUnroll<512><<<grid.x/8, block>>>(d_idata, d_odata, size);
            break;
        case 256:
            reduceCompleteUnroll<256><<<grid.x/8, block>>>(d_idata, d_odata, size);
            break;
        case 128:
            reduceCompleteUnroll<128><<<grid.x/8, block>>>(d_idata, d_odata, size);
            break;
        case 64:
            reduceCompleteUnroll<64><<<grid.x/8, block>>>(d_idata, d_odata, size);
            break;
    }
    //因为现在每个线程块处理两个数据块,需要调整内核的执行配置,网格大小减为原来的一半
    //reduceCompleteUnroll<<<grid.x/8, block>>>(d_idata, d_odata, size);//这里写错成了bytes
    cudaDeviceSynchronize();
    iElaps = seconds() - iStart;
    cudaMemcpy(h_odata, d_odata, grid.x/8*sizeof(int), cudaMemcpyDeviceToHost);//需要除2
    gpu_sum = 0;
    for(int i = 0; i<grid.x/8; i++){//需要除
        gpu_sum += h_odata[i];
    }
    printf("gpu reduceCompleteUnroll  elapsed %f ms gpu_sum: %d <<<grid %d block %d>>>\n", iElaps, gpu_sum, grid.x/8, block.x);//需要除2

执行结果:

./3-3reduceInteger2 starting reduction atdevice 0: NVIDIA GeForce RTX 3090 
   with array size 16777216 grid 32768 block 512
cpu reduce         elapsed 0.040380 ms cpu_sum: 2139353471
gpu Warmup          elapsed 0.053873 ms gpu_sum: 0 <<<grid 32768 block 512>>>
gpu Neighbored          elapsed 0.000478 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu NeighboredLess      elapsed 0.000273 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceInterleaved   elapsed 0.000235 ms gpu_sum: 2139353471 <<<grid 32768 block 512>>>
gpu reduceUnrolling2    elapsed 0.000157 ms gpu_sum: 2139353471 <<<grid 16384 block 512>>>
gpu reduceUnrolling4    elapsed 0.000124 ms gpu_sum: 2139353471 <<<grid 8192 block 512>>>
gpu reduceUnrolling8    elapsed 0.000117 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceUnrollWarps8  elapsed 0.000120 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnrollWrap8  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>
gpu reduceCompleteUnroll  elapsed 0.000119 ms gpu_sum: 2139353471 <<<grid 4096 block 512>>>

在这里插入图片描述
最大的性能增益是reduceUnrolling8核函数获得的,每个线程规约前处理8个数据块,有8个独立的内存访问,更好让内存带宽饱和,并且还能隐藏加载/存储延迟。

Logo

欢迎来到由智源人工智能研究院发起的Triton中文社区,这里是一个汇聚了AI开发者、数据科学家、机器学习爱好者以及业界专家的活力平台。我们致力于成为业内领先的Triton技术交流与应用分享的殿堂,为推动人工智能技术的普及与深化应用贡献力量。

更多推荐