GPU实现Prefix Sum Scan (cumulative sum )算法

背景

对给定NBATCH组数据

float Array[NBATCH][LENGTH];

对每组数据需要进行一次forward正向scan sum运算,然后进行backward反向一次scan sum。这种操作来源于地震道数据在偏移前的反假频滤波需要。其中NBATCH数量级可在10k左右,LENGTH数量级在10k左右。

现有Thrust或CUB库没有提供适合多个数组的批量操作,对正反向结合也需要较复杂的逻辑表达。此外对于LENGTH较大的数组,需要考虑单精度数累加过程中精度丢失问题(rounding error),在内部运算中采用两种方式:一种是用double作为临时累加求和计算;另一种用float2模拟double的求和运算,以期避免精度丢失问题。

技术路线
  • Thrust或CUB。针对本问题,构建有难度
  • 基于shared memory优化版: gpu_scansum_smem_v0。聚合访问global memory, 设置PAD避免bank conflict
  • 基于shared memory优化版: gpu_scansum_smem_v1。移除或减少分支,避免Warp内同步
  • 基于Warp Shuffles(__shfl_up_sync)原语: gpu_scansum_warp_v0。没有使用shared memory,直接在warp内线程的寄存器进行数据交换和计算
  • 针对float,double,float2三个累加求和类型,对kernel进行模板化,以便于性能测试。 float2模拟double计算 基于Lewis的书内源码,进行少量改装以适合C++模板!
  • 编译时不能打开浮点计算优化选项,保证计算满足IEEE754标准!
测试环境
环境#1
  • 系统: Ubuntu18.04
  • GCC: 7.5
  • CUDA Toolkit: 11.2
  • GPU: GeForce GTX 1070 Ti (计算能力6.1, Pascal架构)
  • 优化选项: -O3
环境#2
  • 系统: win10
  • vs2019
  • CUDA Toolkit: 11.1
  • GPU: GeForce GTX TITAN X(计算能力5.2,Maxwell架构)
实现
  • cpu上
    template<class T>
    void  cpu_scansum_kernel(float *h_data, int  len, int nbatch)
    {
        for(int i=0; i<nbatch; i++)
        {
            T sum=0.0f;
            //forward
            for(int j=0; j<len; j++)
            {
                sum=sum+(h_data+i*len)[j];
                (h_data+i*len)[j]=(float)sum;
            }

            sum=0.0f;
            //backward
            for(int j=len-1;j>=0; j--)
            {
                sum=sum+(h_data+i*len)[j];
                (h_data+i*len)[j]=(float)sum;
            }
        }
    }
  • GPU: gpu_scansum_smem_v0。此实现一个warp处理32个数组,每次将每个数组的32个数加载到shared memory,然后在shared memory内每个thread进行累加求和。这里要考虑数组数目和长度不是32整数倍情况,所以内部会有边界判断。shared memory考虑到bank conflict情况,在第二维度上设为32+PAD, PAD=1时候可以避免bank conflict, PAD=0时候bank conflict最严重!

    template<typename T>
    __global__ void  gpu_cumsum_smem_v0(float *d_data, int len, int  nbatch)
    {
        //threads(32,1,1):
        //assert(blockDim.x==32&&blockDim.y==1 && blockDim.z==1)
	    
	    static const int PAD=1;
        __shared__ float my_floats[32][32+PAD];

        const int tid=threadIdx.x;
        const int bid=blockIdx.x;
        const int  blocks=gridDim.x;
        for(int i=32*bid; i<nbatch; i+=32*blocks)
        {   
            //forward scan sum (cumsum)
            T sum=0.0f;
            for(int j=0; j<len; j+=32)
            {
                //load from global mem to shared mem
                if(tid+j<len)
                {
                    for(int k=0; k<32; k++)
                    {
                        if(i+k<nbatch)
                            my_floats[k][tid]=(d_data+(i+k)*len)[j+tid];
                    }
                }
                //__syncthreads();
               for(int k=0; k<32; k++)
               {
                   sum=sum+my_floats[tid][k];
                   my_floats[tid][k]=(float)sum;
               }
                
                //__syncthreads();
                
                //store shared mem to global mem
                if(tid+j<len)
                {
                    //for(uint32_t k=0; k<32; k++)
                    for(int k=0; k<32; k++)
                    {
                        if(i+k<nbatch)
                            (d_data+(i+k)*len)[j+tid]=my_floats[k][tid];
                    }
                }
            }

            //backward scan sum (cumsum)
            sum=0.0f;
            for(int j=len-1; j>=0; j-=32)
            {
                //load from global mem to shared mem
                if(j>=tid)
                {
                    for(int k=0; k<32; k++)
                    {
                        if(i+k<nbatch)
                            my_floats[k][tid]=(d_data+(i+k)*len)[j-tid];
                    }
                }
                //__syncthreads();
                //scan sum op
                for(int k=0; k<32; k++)
                {
                    sum=sum+my_floats[tid][k];
                    my_floats[tid][k]=(float)sum;
                }

                //__syncthreads();
                
                //store shared mem to global mem
                if(j>=tid)
                {
                    for(int k=0; k<32;k++)
                    {
                        if(i+k<nbatch)
                            (d_data+(i+k)*len)[j-tid]=my_floats[k][tid];
                    }
                }
            }
        }
    }
  • GPU: gpu_scansum_smem_v1。相比较gpu_scansum_smem_v0版本,考虑到nbatch和len不是32整数倍情况,将循环内部边界判断if分支剥离出来,单独处理,以期望提高性能。实际测量对性能提高很小,可能和此问题是非计算密集型运算有关!对循环边界处理使代码明显变长
    template<typename T>
    __global__ void  gpu_cumsum_smem_v1(float *d_data, int len, int  nbatch)
    {
        __shared__ float my_floats[32][32+PAD];

        const int tid=threadIdx.x;
        const int bid=blockIdx.x;
        const int blocks=gridDim.x;

        int nbatch32=nbatch>>5;
        int nbatch_left=nbatch&31;
        int len32=len&~31;
        int len_left=len&31;
        int tid_lt_len_left=tid<len_left;
        int tid_lt_nbatch_left=tid<nbatch_left;

#define FORWARD(j,tid) (j+tid)
#define BACKWARD(j,tid) (j-tid)
       
        int i;
        for(i=bid; i<nbatch32; i+=blocks)
        {
            float *array=i*32*len+d_data;
            /********************************************
            *  forward scan sum
            ***********************************************/
            T sum=0.0f;
            int j;
            for(j=0; j<len32; j+=32)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    my_floats[k][tid]=(array+k*len)[FORWARD(j,tid)];
                }          
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    sum=sum+my_floats[tid][k];
                    my_floats[tid][k]=(float)sum;
                }
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    (array+k*len)[FORWARD(j,tid)]=my_floats[k][tid];
                }
            }
            
            if(len_left==0) goto STEP_BACKWARD;

            // handle len_left>0
            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    my_floats[k][tid]=(array+k*len)[FORWARD(j,tid)];
                }
            }

            for(int k=0; k<len_left; k++)
            {
                sum=sum+my_floats[tid][k];
                my_floats[tid][k]=(float)sum;
            }

            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                    (array+k*len)[FORWARD(j,tid)]=my_floats[k][tid];

            }

            STEP_BACKWARD:

            /***********************************************************
            *  backward scan sum
            ************************************************************/

            sum=0.0f;
            for(j=len-1; j>=len_left; j-=32)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    my_floats[k][tid]=(array+k*len)[BACKWARD(j,tid)];
                }
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    sum=sum+my_floats[tid][k];
                    my_floats[tid][k]=(float)sum;
                }
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    (array+k*len)[BACKWARD(j,tid)]=my_floats[k][tid];
                }
            }

            if(len_left==0) continue;

            // handle len_left>0
            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                {
                    my_floats[k][tid]=(array+k*len)[BACKWARD(j,tid)];
                }
            }

            for(int k=0; k<len_left; k++)
            {
                sum=sum+my_floats[tid][k];
                my_floats[tid][k]=(float)sum;
            }

            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                #pragma unroll 
                for(int k=0; k<32; k++)
                    (array+k*len)[BACKWARD(j,tid)]=my_floats[k][tid];
            }

        }

        //handle nbatch_left>0
        //if(bid==0 && nbatch_left)
        if(bid==blocks-1 && nbatch_left>0)
        {
            float *array=d_data+32*nbatch32*len;
            T sum=0.0f; 
            int j;
            for(j=0; j<len32; j+=32)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    my_floats[k][tid]=(array+k*len)[FORWARD(j,tid)];
                }

                //if(tid<nbatch_left)
                if(tid_lt_nbatch_left)
                {
                    #pragma unroll 
                    for(int k=0; k<32; k++)
                    {
                        sum=sum+my_floats[tid][k];
                        my_floats[tid][k]=(float)sum;
                    }
                }

                for(int k=0; k<nbatch_left; k++)
                {
                    (array+k*len)[FORWARD(j,tid)]=my_floats[k][tid];
                }
            }

            //handle len_left>0

            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    my_floats[k][tid]=(array+k*len)[FORWARD(j,tid)];
                }
            }

            //if(tid<nbatch_left)
            if(tid_lt_nbatch_left)
            {
                for(int k=0; k<len_left; k++)
                {
                    sum=sum+my_floats[tid][k];
                    my_floats[tid][k]=(float)sum;
                }
            }
            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    (array+k*len)[FORWARD(j,tid)]=my_floats[k][tid];
                }
            }

            //backward
            sum=0.0f;
            for(j=len-1; j>=len_left; j-=32)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    my_floats[k][tid]=(array+k*len)[BACKWARD(j,tid)];
                }

                //if(tid<nbatch_left)
                if(tid_lt_nbatch_left)
                {
                    #pragma unroll 
                    for(int k=0; k<32; k++)
                    {
                        sum=sum+my_floats[tid][k];
                        my_floats[tid][k]=(float)sum;
                    }
                }

                for(int k=0; k<nbatch_left; k++)
                {
                    (array+k*len)[BACKWARD(j,tid)]=my_floats[k][tid];
                }
            }

            //handle len_left>0
            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    my_floats[k][tid]=(array+k*len)[BACKWARD(j,tid)];
                }
            }

            //if(tid<nbatch_left)
            if(tid_lt_nbatch_left)
            {
                for(int k=0; k<len_left; k++)
                {
                    sum=sum+my_floats[tid][k];
                    my_floats[tid][k]=(float)sum;
                }
            }
            //if(tid<len_left)
            if(tid_lt_len_left)
            {
                for(int k=0; k<nbatch_left; k++)
                {
                    (array+k*len)[BACKWARD(j,tid)]=my_floats[k][tid];
                }
            }            
        }
#undef FORWARD
#undef BACKWARD        
    }
  • GPU: gpu_scansum_warp_v0。用__shfl_up_sync() 指令原语进行warp内进程寄存器数据交换,实现scan sum计算! __shfl_up_sync对4Byte数据性能可以,对8Byte宽类型延迟明显增加。
    template<typename T>
    __device__  void warpscan_forward(int tid, float *d_data, int n)
    {
        static_assert(sizeof(T)==4 || sizeof(T)==8);
        static_assert(std::is_same<T,float>::value || std::is_same<T,double>::value || std::is_same<T,FLOAT2>::value);

        T pre_sum=0.0f;
        T sum;
        int j=0; 
        for(j=0; j<n-31; j+=32)
        {
            sum=d_data[j+tid];
            for(int d=1; d<32; d*=2)
            {   
                T temp;
   
                if(std::is_same<T,FLOAT2>::value)
                    *(uint64_t*)&temp=__shfl_up_sync(-1,*(uint64_t*)&sum,d);
                else 
                    temp=__shfl_up_sync(-1,sum,d);
   
                if(tid>=d)
                    sum=sum+temp;
            }
            d_data[j+tid]=(float)(sum+pre_sum);

            if(std::is_same<T,FLOAT2>::value)
                *(uint64_t*)&sum=__shfl_sync(-1,*(uint64_t*)&sum,31);
            else 
                sum=__shfl_sync(-1,sum,31);
   
            pre_sum=pre_sum+sum;
        }

        if(j+tid<n)
            sum=d_data[j+tid];

        for(int d=1; d<n-j; d*=2)
        {   
            T temp;

            if(std::is_same<T,FLOAT2>::value)
                *(uint64_t*)&temp=__shfl_up_sync(-1,*(uint64_t*)&sum,d);
            else 
                temp=__shfl_up_sync(-1,sum,d);

            if(tid>=d)
                sum=sum+temp;
        }
        if(j+tid<n)
            d_data[j+tid]=(float)(sum+pre_sum);
   }//warpscan_forward

    template<typename T>
    __device__  void warpscan_backward(int tid, float *d_data, int n)
    {

        static_assert(sizeof(T)==4 || sizeof(T)==8);
        static_assert(std::is_same<T,float>::value || std::is_same<T,double>::value || std::is_same<T,FLOAT2>::value);

        T pre_sum=0.0f;
        T sum;
        int j; 
        for(j=n-1; j>=31; j-=32)
        {
            sum=d_data[j-tid];

            for(int d=1; d<32; d*=2)
            {   
                T temp;
    
                if(std::is_same<T,FLOAT2>::value)
                    *(uint64_t*)&temp=__shfl_up_sync(-1,*(uint64_t*)&sum,d);
                else 
                    temp=__shfl_up_sync(-1,sum,d);
    
                if(tid>=d)
                    sum=sum+temp;
            }

            d_data[j-tid]=(float)(sum+pre_sum);

            if(std::is_same<T,FLOAT2>::value)
                *(uint64_t*)&sum=__shfl_sync(-1,*(uint64_t*)&sum,31);
            else 
                sum=__shfl_sync(-1,sum,31);
            pre_sum=pre_sum+sum;
        }

        if(j-tid>=0)
            sum=d_data[j-tid];

        for(int d=1; d<j; d*=2)
        {   
            T temp;

            if(std::is_same<T,FLOAT2>::value)
                *(uint64_t*)&temp=__shfl_up_sync(-1,*(uint64_t*)&sum,d);
            else 
                temp=__shfl_up_sync(-1,sum,d);

            if(tid>=d)
                sum=sum+temp;
      }

        if(j-tid>=0)
            d_data[j-tid]=(float)(sum+pre_sum);

    }//warpscan_backward
    
   template<typename T>
    __global__ void gpu_scansum_warp_v0(float *d_data, int length, int nbatch)
    {
        int tidx=threadIdx.x;
        int tidy=threadIdx.y;

        int n_per=blockDim.y*gridDim.x;

        for(int i=tidy+blockIdx.x*blockDim.y; i<nbatch; i+=n_per)
        {
            float *array=d_data+i*length;
            warpscan_forward<T>(tidx, array, length);
            warpscan_backward<T>(tidx,array, length);
        }
    }

性能优化
  • Grid和Threads运行配置对性能影响
  • nvprof 性能数据收集分析
  • Nsight Compute 性能数据收集分析
  • cudaEvent进行内核运行计时
环境#1运行时间结果

表中数字是每个kernel在GPU上的运行时间,单位毫秒
在这里插入图片描述

结论
  • warp版本整体效果最差,尤其是累加计算采用double和float2时候。一个原因是__shfl_up_sync调用时warp内有一定数量的线程等待不干活。warp版本对内存对齐不敏感,所有操作是在寄存器内进行,L2+L1 cache会一定程度上掩盖内存对齐问题!
  • gpu_smem_v1整体表现最好,但对数据内存对齐比较敏感,移除分支对性能提高有帮助
  • 合适的grid和threads参数应该保证每个SM有一定数量的线程块运行,才能保证负载均衡,但也不要太大,会增加GPU调度时间
  • 对shared memory的访问应该采用类似PADDING方式,尽可能避免bank conflict!
  • float2的版本明显比double版本性能高。GPU天生对单精度浮点支持最好
总结思考
  • 使用shared memory,避免bank conflict对性能有较大影响,尤其对内存访问密集型操作
  • kernel运行布局配置(Grid & Block )对性能有影响,要保证调度给每个SM分配到足够多的线程块,有一定负载!
  • warp shuffles调用多次或是进行8Byte宽类型的移动操作,对性能有明显影响(可能不同体系结构结论会有区别)
  • 对内存访问密集运算,输入数据在global memory的布局,会产生明显影响,应尽可能保证一定的对齐,对齐的内存访问会有明显性能提升
  • 对7.0(Volta)以上计算能力GPU, warp同步不是显式的,需要调用__syncwarp
  • 减少内层if语句
  • 循环变量用int,避免用uint, 避免溢出问题!
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值