一、什么是规约?
规约,用白话来说,就是将多个值变成一个值的操作,如向量求和,向量内积操作。
以向量求和为例,如果使用串行规约的话,那么就是依靠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 =
*/ -
- #include "aux.h"
- #include
-
- 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<<
>>(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<<
>>(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;
- }