CUDA----规约

介绍

数组的一些如:求和、求最大值最小值、乘积等操作,在单核程序中我们往往是通过for循环遍历所有数组元素,并依次进行操作得到最终的结果。但是对于并行程序而言,这种串行思路需要结合原子操作来实现,性能比较差。利用归并的思想,线程之间两两结合,可以充分利用并行加速效果。本文我们以对数组所有元素求和为例:
s = ∑ i N A [ i ] where N ≥ 1 \begin{equation} s = \sum_i^N A[i] \quad\text{where}\quad N\ge 1 \end{equation} s=iNA[i]whereN1
本文所有的测试均在NVIDIA Tesla V100上进行,数组测试长度为 2 24 = 16777216 2^{24} = 16777216 224=16777216(64MB)。为了能够更好的统一测试环境,所有的kernel都被放在同一个源文件中。

代码实现

原子操作

    __global__ void kernel1(float* arr, int nElem, float* result)
    {
        int gid = blockIdx.x * blockDim.x + threadIdx.x;
        if(gid < nElem)
        {
            atomicAdd(result, arr[gid]);    //- 原子操作,累加
        }
    }

规约

规约——交叉配对

相邻配对规约

    __global__ void kernel2(float* arr, int nElem, float* result)
    {
        int gid = blockIdx.x * blockDim.x + threadIdx.x;
        __shared__ float tmp[BLOCK_SIZE];
        if(gid < nElem)
        {
            tmp[threadIdx.x] = arr[gid];
        }
        else
        {
            tmp[threadIdx.x] = 0.0f;
        }
        __syncthreads();
    
        for(int strip = 1; strip < blockDim.x; strip = strip*2)
        {
            if(threadIdx.x % (2*strip) == 0)
                tmp[threadIdx.x] += tmp[threadIdx.x + strip];
            __syncthreads();
        }
        if(threadIdx.x == 0)
            result[blockIdx.x] = tmp[0];
    }

规约——交错配对

交错配对规约

__global__ void kernel3(float* arr, int nElem, float* result)
{
    int gid = blockIdx.x * blockDim.x + threadIdx.x;
    __shared__ float tmp[BLOCK_SIZE];
    if(gid < nElem)
    {
        tmp[threadIdx.x] = arr[gid];
    }
    else
    {
        tmp[threadIdx.x] = 0.0f;
    }
    __syncthreads();

    for(int strip = blockDim.x / 2; strip > 0; strip = strip / 2)
    {
        if(threadIdx.x < strip)
            tmp[threadIdx.x] += tmp[threadIdx.x + strip];
        __syncthreads();
    }

    if(threadIdx.x == 0)
    {
        result[blockIdx.x] = tmp[0];
    }
}

规约——处理两个block数据

__global__ void kernel4(float* arr, int nElem, float* result)
{
    int gid = blockIdx.x * blockDim.x + threadIdx.x;
    int gid2 = gid + gridDim.x * blockDim.x;
    __shared__ float tmp[BLOCK_SIZE];
    tmp[threadIdx.x] = 0.0f;
    if(gid < nElem && gid2 < nElem)
    {
        tmp[threadIdx.x] = arr[gid] + arr[gid2];
    }
    if(gid < nElem && gid2 >= nElem)
    {
        tmp[threadIdx.x] = arr[gid];
    }
    __syncthreads();

    for(int strip = blockDim.x / 2; strip > 0; strip = strip / 2)
    {
        if(threadIdx.x < strip)
            tmp[threadIdx.x] += tmp[threadIdx.x + strip];
        __syncthreads();
    }

    if(threadIdx.x == 0)
    {
        result[blockIdx.x] = tmp[0];
    }
}

规约——循环展开

__global__ void kernel5(float* arr, int nElem, float* result)
{
    int gid = blockIdx.x * blockDim.x + threadIdx.x;
    int gid2 = gid + gridDim.x * blockDim.x;
    __shared__ float tmp[BLOCK_SIZE];
    tmp[threadIdx.x] = 0.0f;
    if(gid < nElem && gid2 < nElem)
    {
        tmp[threadIdx.x] = arr[gid] + arr[gid2];
    }
    if(gid < nElem && gid2 >= nElem)
    {
        tmp[threadIdx.x] = arr[gid];
    }
    __syncthreads();

    if(blockDim.x == 1024)
    {
        if(threadIdx.x >= 512)
            return;
        tmp[threadIdx.x] += tmp[threadIdx.x + 512];
    }
    __syncthreads();

    if(blockDim.x >= 512)
    {
        if(threadIdx.x >= 256)
            return;
        tmp[threadIdx.x] += tmp[threadIdx.x + 256];
    }
    __syncthreads();

    if(blockDim.x >= 256)
    {
        if(threadIdx.x >= 128)
            return;
        tmp[threadIdx.x] += tmp[threadIdx.x + 128];
    }
    __syncthreads();

    if(blockDim.x >= 128)
    {
        if(threadIdx.x >= 64)
            return;
        tmp[threadIdx.x] += tmp[threadIdx.x + 64];
    }
    __syncthreads();

    if(blockDim.x >= 64)
    {
        if(threadIdx.x >= 32)
            return;
        tmp[threadIdx.x] += tmp[threadIdx.x + 32];
        __syncthreads();
        tmp[threadIdx.x] += tmp[threadIdx.x + 16];
        __syncthreads();
        tmp[threadIdx.x] += tmp[threadIdx.x + 8];
        __syncthreads();
        tmp[threadIdx.x] += tmp[threadIdx.x + 4];
        __syncthreads();
        tmp[threadIdx.x] += tmp[threadIdx.x + 2];
        __syncthreads();
        tmp[threadIdx.x] += tmp[threadIdx.x + 1];
    }

    if(threadIdx.x == 0)
    {
        result[blockIdx.x] = tmp[0];
    }
}

shuffle warp

__global__ void kernel6(float* arr, int nElem, float* result)
{
    __shared__ float tmp[32];
	int gid = blockIdx.x * blockDim.x + threadIdx.x;
    float data = 0.0f;
	if(gid < nElem)
	{
		data = arr[gid];
	}

    data += __shfl_down_sync(0xffffffff, data, 16);
    data += __shfl_down_sync(0xffffffff, data, 8);
    data += __shfl_down_sync(0xffffffff, data, 4);
    data += __shfl_down_sync(0xffffffff, data, 2);
    data += __shfl_down_sync(0xffffffff, data, 1);
	if(threadIdx.x % 32 == 0)
		tmp[threadIdx.x / 32] = data;
    __syncthreads();

    if(threadIdx.x >= 32)
        return;
    data = tmp[threadIdx.x];
    data += __shfl_down_sync(0xffffffff, data, 16);
    data += __shfl_down_sync(0xffffffff, data, 8);
    data += __shfl_down_sync(0xffffffff, data, 4);
    data += __shfl_down_sync(0xffffffff, data, 2);
    data += __shfl_down_sync(0xffffffff, data, 1);
	if(threadIdx.x == 0)
		result[blockIdx.x] = data;
}

性能分析

[mmhe@k057 20221012]$ nvprof ./test 
==15379== NVPROF is profiling process 15379, command: ./test
nElem = 16777216: sum = -8.364376e+06 theory = -8.388608e+06
nElem = 16777216: sum = -8.388608e+06 theory = -8.388608e+06
nElem = 16777216: sum = -8.388608e+06 theory = -8.388608e+06
nElem = 16777216: sum = -8.388608e+06 theory = -8.388608e+06
nElem = 16777216: sum = -8.388608e+06 theory = -8.388608e+06
nElem = 16777216: sum = -8.388608e+06 theory = -8.388608e+06
==15379== Profiling application: ./test
==15379== Profiling result:
            Type  Time(%)      Time     Calls       Avg       Min       Max  Name
 GPU activities:   73.42%  43.314ms         1  43.314ms  43.314ms  43.314ms  kernel1(float*, int, float*)
                   21.22%  12.517ms         1  12.517ms  12.517ms  12.517ms  [CUDA memcpy HtoD]
                    2.65%  1.5608ms         1  1.5608ms  1.5608ms  1.5608ms  kernel2(float*, int, float*)
                    1.06%  623.65us         1  623.65us  623.65us  623.65us  kernel3(float*, int, float*)
                    0.62%  364.23us         1  364.23us  364.23us  364.23us  kernel6(float*, int, float*)
                    0.60%  354.69us         1  354.69us  354.69us  354.69us  kernel4(float*, int, float*)
                    0.39%  228.77us         1  228.77us  228.77us  228.77us  kernel5(float*, int, float*)
                    0.06%  34.689us         6  5.7810us  1.6640us  7.2960us  [CUDA memcpy DtoH]
      API calls:   88.80%  594.95ms         3  198.32ms  386.71us  594.00ms  cudaMalloc
                    8.85%  59.294ms         7  8.4706ms  261.03us  43.330ms  cudaMemcpy
                    1.32%  8.8224ms       576  15.316us     310ns  610.01us  cuDeviceGetAttribute
                    0.89%  5.9854ms         6  997.57us  988.40us  1.0013ms  cuDeviceTotalMem
                    0.11%  756.00us         6  126.00us  120.92us  134.45us  cuDeviceGetName
                    0.03%  172.00us         6  28.666us  15.323us  79.021us  cudaLaunchKernel
                    0.00%  20.879us         6  3.4790us  2.7580us  5.0700us  cuDeviceGetPCIBusId
                    0.00%  8.4910us        12     707ns     390ns  1.6300us  cuDeviceGet
                    0.00%  3.1300us         3  1.0430us     333ns  1.7900us  cuDeviceGetCount
                    0.00%  2.8750us         6     479ns     427ns     585ns  cuDeviceGetUuid

单纯从运行时间来看,kernel1 >> kernel2 > kernel3 > kernel6 > kernel4 > kernel5.下面我们逐个分析性能提升的原因。

规约

kernel2 和 kernel1

相比较于原子操作,规约性能提升非常明显,最朴素的实现也有接近30倍的提升。不过这里需要注意的是,规约只在block中进行,因此block局部和的结果需要传到Host上进行最终的求和,因此实际运算量要小于理论结果,但是考虑到减少量仅为 1 1024 \frac{1}{1024} 10241,因此可以忽略不计。

kernel1和kernel2的整体性能比较
从这个结果我们可以清楚的看到,原子操作的计算和访存利用率都非常低,这是由于在每个block中的thread需要串行的去执行原子加操作,从而大量的时间花在了等待上面,并且可想而知,warp的效率也是非常低的。

warps per schedulerwarp states
可以看到,kernel1 的warp 调度器每次几乎没有就绪的warp可供发射,至于具体的原因,则是由于存在大量的Stall LG ThrottleStall Drain和明显的Stall Long Scoreboard

根据官方文档描述:

  • Stall LG Throttle高的原因是全局内存的高频访问,这里可能锁定区检测的原因,具体原因暂时不清楚。
  • Stall Drain高的原因是对内存写入的串行性比较高。
  • Stall Long Scoreboard高的原因是指令需要等待如全局内存数据的加载完成之后才能执行下一条指令,一般来说,通过足够多的warp可以掩盖这个延迟,但是我们可以看到,该kernel的就绪warp很少,因此这个问题也就非常突出。

kernel3 base kernel2

kernel2 的相邻配对会导致warp内的活动thread数逐渐变少,出现严重的warp发散,进而导致共享内存访问效率低下。

### CUDA 并行规约 (Reduce) 的基本概念 CUDA 中的并行规约是一种常见的操作模式,用于将大量数据简化为单一的结果。这种技术广泛应用于求和、最大值/最小值查找以及逻辑运算等领域。通过 GPU 上的大规模并行处理能力,可以显著提高效率。 #### 主要实现过程 在 CUDA 编程中,并行规约可以通过多个阶段完成。以下是其核心组成部分: 1. **初始化数据** 数据被分配到主机内存中,并随后传输至设备内存。这一过程中需要考虑数据大小及其存储方式[^2]。 2. **主机串行规约** 为了验证结果,在 CPU 上执行一次串行版本的规约算法作为对比基准。这一步骤有助于确认 GPU 计算的准确性。 3. **GPU 并行规约** 利用 CUDA kernel 启动大规模线程来执行并行化操作。具体来说,每个线程负责一部分输入数组的操作,最终逐步减少至单个结果。此步骤涉及以下优化技巧: - **相邻配对实现**: 将每两个相邻元素相加,从而逐渐缩小问题规模。 - **消除线程束分化**: 避免不同分支路径导致的部分线程闲置情况,提升硬件利用率。 - **消除 Bank Conflict**: 调整共享内存访问模式以防止冲突发生,进而改善带宽使用率。 - **加载时计算**: 减少全局内存读取次数,尽可能多地依赖寄存器或缓存机制。 - **循环展开**: 增大每次迭代的工作量,降低控制开销比例。 - **循环完全展开**: 对于小型固定长度的任务,可手动移除所有跳转指令以进一步加速程序运行速度。 - **Shuffle 指令优化**: 替代传统共享内存通信手段,直接交换同一 warp 内部的数据项。 4. **输出结果** 完成上述各步之后,从设备端检索得到最终答案并与原始序列总和比较校验正确性。 #### 示例代码展示 下面给出一段简单的 CUDA Reduce 实现示例: ```cpp #include <cuda_runtime.h> #include <iostream> __global__ void reduceKernel(int *input, int *output, size_t N){ extern __shared__ int sdata[]; unsigned int tid = threadIdx.x; unsigned int i = blockIdx.x*(blockDim.x*2)+tid; if(i<N && i+blockDim.x<N){ input[i]+=input[i+blockDim.x]; } __syncthreads(); sdata[tid]=0; if(i<N){sdata[tid]=input[i];} __syncthreads(); for(unsigned int s=blockDim.x /2;s>0;s>>=1){ if(tid<s){ sdata[tid] += sdata[tid+s]; } __syncthreads(); } if(tid==0){ atomicAdd(output,sdata[0]); } } void runReduceOnDevice(const int *h_input,int &result,size_t N){ const size_t numThreadsPerBlock=512; const size_t numBlocks=(N+(numThreadsPerBlock*2-1))/(numThreadsPerBlock*2); int *d_input,*d_output; cudaMalloc(&d_input,N*sizeof(int)); cudaMemcpy(d_input,h_input,N*sizeof(int),cudaMemcpyHostToDevice); int h_result=0; int *d_result; cudaMalloc(&d_result,sizeof(int)); cudaMemcpy(d_result,&h_result,cudaMemcpyHostToDevice); reduceKernel<<<numBlocks,numThreadsPerBlock>>>(d_input,d_result,N); cudaMemcpy(&result,d_result,sizeof(int),cudaMemcpyDeviceToHost); cudaFree(d_input);cudaFree(d_result); } int main(){ const size_t N=1<<20;// One million elements. int *h_input=new int[N]; srand(0); for(size_t i=0;i<N;++i){ h_input[i]=rand()%100; } int result=0; runReduceOnDevice(h_input,result,N); std::cout<<"Result:"<<result<<"\n"; delete[] h_input; return 0; } ``` 该例子展示了如何构建一个高效的并行规约流程,其中包含了必要的同步点与共享资源管理策略。 ---
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值