CUDA编程入门系列(十)并行规约

一、什么是规约?

        规约,用白话来说,就是将多个值变成一个值的操作,如向量求和,向量内积操作。

        以向量求和为例,如果使用串行规约的话,那么就是依靠for循环来进行操作

                for(int i = 0; i < nums.size(); i++)   sum += nums[i];

        但串行规约的话,此时算法的复杂度为O(N).

        在并行计算中,其中有一种方法叫做二叉树算法,即先将元素两两求和,再进行计算。如下图所示。此时算法的复杂度为O(LogN).

         在GPU计算,存在多种策略进行计算。

        (1)使用单个block进行计算:不足以充分利用资源

        (2)使用多个block进行计算:每个块负责向量的部分计算,再把每个块的计算结果进行处理

          

        如果使用多个块进行并行规约,那么就可能存在同步问题,不同块的处理规约时间不同。所以,这里的关键就是需要一个全局的同步。

        然而,CUDA并不支持全局同步。第一,如果使用全局同步的话,就会使得一些已执行完程序的块处于空闲状态,这会导致资源的浪费。 第二,如果使用的块比较多的话,就有可能导致死锁。这里也比较好理解,即大部分的块都申请了SM资源,但不够,所以继续阻塞申请SM资源,也都不释放SM,这就会导致死锁。

        解决方法:分多个kernel进行处理。因为在不同的kenel之间存在隐式的同步,即第一个kernel执行完,才能执行第二个kernel。

二、并行规约算法 --- 二叉树算法

        使用并行规约就是为了最大化GPU的性能,指标包含:浮点运算数GFLOPS和有效带宽GB/s.

        二叉树算法,即每两个数相加得到部分和,依次进行该操作,直到最后一个数。步骤如下:

        ① 将数据从全局内存加载到共享内存当中

        ② 进行规约操作,每个线程负责规约两个数,每一次规约之后都有一半的线程处于空闲状态。不断进行上述操作,直到只剩下一个线程。

        ③ 将结果返回到全局内存当中

         该算法的伪代码如下图所示:

        其中 sdata[]代表所申请的共享内存,蓝色方框用以把数据从全局内存拷贝到共享内存。

        黄色方框负责进行规约操作,这里只有偶数号线程会进行操作。奇数好线程不操作。这里的__syncthreads()是为了在每一次规约后同步所有线程,直到所有的线程都执行完毕再进行下一次规约。

        橘色方框负责将规约结果从共享内存加载到全局内存当中。

          消耗时间及带宽如下图所示 

        因为一个warp里面的32线程执行相同的指令,如果指令不同(即指令分化)的话,可能会导致32个线程按照串行的方式进行执行,这与我们并行的思想是相悖的。所以,这里存在一个问题,因为此处的线程要进行if的分支结构判断,所以就有可能导致不同的线程有不同的指令,导致指令分化,影响执行速度。

        如下图所示,绿色方块代表线程处于活跃状态,灰白色方块代表线程处于空闲状态。在同一个warp之内,产生了指令分化。

三、并行规约算法改进 warp divergence

        由于上述算法存在指令分化问题,会影响执行的速度,所以需要进行改进。改进的思路就是要让一个warp里面的线程执行相同的指令。

        改进的方式如黄色框的代码所示,只需要修改三行代码,即可缓解指令分化问题。

        通过引入临时变量index,假如s = 1,blockdim=128,那么此时前64个线程都执行相同的指令,后64个线程也执行相同的指令,就避免了指令分化的问题。 但是随着计算的进行,s的增大,越来越少的线程处于活跃状态,当活跃的线程小于一个warp的数量的时候,还是会产生指令分化的问题。

        改进后的算法,其warp内线程的状态如下图所示:

        时间和带宽如下所示: 

        但这里又存在一个问题,会产生bank conflict。通过参考其他博客的内容,bank conflict的含义为:同一个warp中的不同线程,访问相同的bank,就会产生conflict。 在这个例子中,其实我没太理解是如何产生冲突的。有一种可能就是 比如说线程1 是将第0个数和第1个数进行相加,也就是说线程1要访问第0个数和第1个数,而线程2要访问第1个数和第2个数,这就导致了warp中的不同线程访问了相同的地址。不同的线程访问同一资源,要进行临界操作,由原来的并行操作变成串行操作,影响了执行速度。

四、 并行规约算法改进 消除bank conflict

        在三中,通过不同的编码方式,消除了warp内指令分化的情况。但是也引进了bank conflict问题。bank conflict 会把访问相同bank的线程由并行执行 改为串行执行,严重的影响了执行速度。

        故而,我们对算法进行如下的改进,我们不再使用相邻数相加的方式,而是采用跳跃的方式进行相加,以”折半“的形式进行相加。每次规约都把后半部分的内容加到前半部分的中。

        其伪代码如下图所示:

        代码的执行速度和带宽如下图所示: 

五、并行规约算法改进 改进全局内存访问

        下图中,橘色方框所示代码为将数据从全局内存加载到共享内存中的方式,每一个线程只加载一个元素。而黄色方框所示代码进行了改进,每一个线程加载两个元素,并且执行了第一步的规约。这就提高了程序的执行速度。 但这里?黄色方框写的代码,不会导致g_idata访问越界吗? 这里没太懂啊。。。

        

速度和带宽:

 五、并行规约算法改进 warp内循环展开 

        四种的规约算法代码如下:在每一个步计算中都要使用同步语句来进行同步。

  但根据warp的特性,warp内中32个线程会执行相同的指令。那么当活跃的线程数小于32个时, 所有的活跃线程都在一个warp内执行相同的指令,因为执行相同的指令,所以这个时候就不需要__syncthreads() 同步点,因此减少开销。改进完的代码如下图所示:在黄色方框所示的代码当中,由于编译器会对代码进行优化,然而,共享内存中的数据是不断进行更新的。所以我们不希望编译器对代码进行优化,所以我们要对共享内存添加关键字volatile。

        速度和带宽: 

        

 

六、并行规约算法改进  完全循环展开

        在五中,我们对最后几个循环进行展开,减少了同步的次数,以此提高的执行效率。那么,能否将所有的循环全部展开?答案是可以的,但是存在以下前提: 每个块中最多只有512个线程,且必须为2的n次方

        

        速度和带宽: 

七、规约算法应用:向量内积 

             


/* dot product of two vectors: d = <x, y> */

#include "aux.h"
#include <assert.h>

typedef double FLOAT;

/* host, add */
FLOAT dot_host(FLOAT *x, FLOAT *y, int N)
{
    int i;
    FLOAT t = 0;

    assert(x != NULL);
    assert(y != NULL);

    for (i = 0; i < N; i++) t += x[i] * y[i];

    return t;
}

__device__ void warpReduce(volatile FLOAT *sdata, int tid)
{
    sdata[tid] += sdata[tid + 32];
    sdata[tid] += sdata[tid + 16];
    sdata[tid] += sdata[tid + 8];
    sdata[tid] += sdata[tid + 4];
    sdata[tid] += sdata[tid + 2];
    sdata[tid] += sdata[tid + 1];
}

/* partial dot product */
__global__ void dot_stg_1(const FLOAT *x, FLOAT *y, FLOAT *z, int N)
{
    __shared__ FLOAT sdata[256];
    int idx = get_tid();
    int tid = threadIdx.x;
    int bid = get_bid();

    /* load data to shared mem */
    if (idx < N) {
        sdata[tid] = x[idx] * y[idx];
    }
    else {
        sdata[tid] = 0;
    }

    __syncthreads();

    /* reduction using shared mem */
    if (tid < 128) sdata[tid] += sdata[tid + 128];
    __syncthreads();

    if (tid < 64) sdata[tid] += sdata[tid + 64];
    __syncthreads();

    if (tid < 32) warpReduce(sdata, tid);

    if (tid == 0) z[bid] = sdata[0];
}

/* sum all entries in x and asign to y
 * block dim must be 256 */
__global__ void dot_stg_2(const FLOAT *x, FLOAT *y, int N)
{
    __shared__ FLOAT sdata[256];
    int idx = get_tid();
    int tid = threadIdx.x;
    int bid = get_bid();

    /* load data to shared mem */
    if (idx < N) {
        sdata[tid] = x[idx];
    }
    else {
        sdata[tid] = 0;
    }

    __syncthreads();

    /* reduction using shared mem */
    if (tid < 128) sdata[tid] += sdata[tid + 128];
    __syncthreads();

    if (tid < 64) sdata[tid] += sdata[tid + 64];
    __syncthreads();

    if (tid < 32) warpReduce(sdata, tid);

    if (tid == 0) y[bid] = sdata[0];
}

__global__ void dot_stg_3(FLOAT *x, int N)
{
    __shared__ FLOAT sdata[128];
    int tid = threadIdx.x;
    int i;

    sdata[tid] = 0;

    /* load data to shared mem */
    for (i = 0; i < N; i += 128) {
        if (tid + i < N) sdata[tid] += x[i + tid];
    }

    __syncthreads();

    /* reduction using shared mem */
    if (tid < 64) sdata[tid] = sdata[tid] + sdata[tid + 64];
    __syncthreads();

    if (tid < 32) warpReduce(sdata, tid);

    if (tid == 0) x[0] = sdata[0];
}

/* dz and d serve as cache: result stores in d[0] */
void dot_device(FLOAT *dx, FLOAT *dy, FLOAT *dz, FLOAT *d, int N)
{
    /* 1D block */
    int bs = 256;

    /* 2D grid */
    int s = ceil(sqrt((N + bs - 1.) / bs));
    dim3 grid = dim3(s, s);
    int gs = 0;

    /* stage 1 */
    dot_stg_1<<<grid, bs>>>(dx, dy, dz, N);

    /* stage 2 */
    {
        /* 1D grid */
        int N2 = (N + bs - 1) / bs;

        int s2 = ceil(sqrt((N2 + bs - 1.) / bs));
        dim3 grid2 = dim3(s2, s2);

        dot_stg_2<<<grid2, bs>>>(dz, d, N2);

        /* record gs */
        gs = (N2 + bs - 1.) / bs;
    }

    /* stage 3 */
    dot_stg_3<<<1, 128>>>(d, gs);
}

int main(int argc, char **argv)
{
    int N = 10000070;
    int nbytes = N * sizeof(FLOAT);

    FLOAT *hx = NULL, *hy = NULL;
    FLOAT *dx = NULL, *dy = NULL, *dz = NULL, *d = NULL;
    int i, itr = 20;
    FLOAT asd = 0, ash;
    double td, th;

    if (argc == 2) {
        int an;

        an = atoi(argv[1]);
        if (an > 0) N = an;
    }

    /* allocate GPU mem */
    cudaMalloc((void **)&dx, nbytes);
    cudaMalloc((void **)&dy, nbytes);

    cudaMalloc((void **)&dz, sizeof(FLOAT) * ((N + 255) / 256));
    cudaMalloc((void **)&d, sizeof(FLOAT) * ((N + 255) / 256));

    if (dx == NULL || dy == NULL || dz == NULL || d == NULL) {
        printf("couldn't allocate GPU memory\n");
        return -1;
    }

    printf("allocated %e MB on GPU\n", nbytes / (1024.f * 1024.f));

    /* alllocate CPU mem */
    hx = (FLOAT *) malloc(nbytes);
    hy = (FLOAT *) malloc(nbytes);

    if (hx == NULL || hy == NULL) {
        printf("couldn't allocate CPU memory\n");
        return -2;
    }
    printf("allocated %e MB on CPU\n", nbytes / (1024.f * 1024.f));

    /* init */
    for (i = 0; i < N; i++) {
        hx[i] = 1;
        hy[i] = 2;
    }

    /* copy data to GPU */
    cudaMemcpy(dx, hx, nbytes, cudaMemcpyHostToDevice);
    cudaMemcpy(dy, hy, nbytes, cudaMemcpyHostToDevice);

    /* let dust fall */
    cudaDeviceSynchronize();
    td = get_time();

    /* call GPU */
    for (i = 0; i < itr; i++) dot_device(dx, dy, dz, d, N);

    /* let GPU finish */
    cudaDeviceSynchronize();
    td = get_time() - td;

    th = get_time();
    for (i = 0; i < itr; i++) ash = dot_host(hx, hy, N);
    th = get_time() - th;

    /* copy data from GPU */
    cudaMemcpy(&asd, d, sizeof(FLOAT), cudaMemcpyDeviceToHost);

    printf("dot, answer: %d, calculated by GPU:%f, calculated by CPU:%f\n", 2 * N, asd, ash);
    printf("GPU time: %e, CPU time: %e, speedup: %g\n", td, th, th / td);

    cudaFree(dx);
    cudaFree(dy);
    cudaFree(dz);
    cudaFree(d);

    free(hx);
    free(hy);

    return 0;
}

  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值