CUDA基础的基础教程二、线程协作

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/lingzidong/article/details/82595563

1.回顾:

上一篇博客我们介绍了CUDA调用核函数的形式 <<<N,M>>>>
N 代表线程块的数量,M 代表线程块中有多少个并行的线程,那么就是创建了N×M个并行线程,当然 M=1 时就是 N 个,我们利用 CUDA 这个性质,重新回顾下上一篇说到的矢量求和,我们之前用的是线程块,我们用线程来写的话,只要变成<<<1,N>>> 就行了,但是区别在哪里呢?
由于每个 GPU 能支持的最大进程块的数量不能超过 65535,每个块内的线程数不能超过 512,注意每个硬件的规格可能是不一样的。于是我们只能将这两个东西结合一下了。其他的都不变,只需要改变一下定位的方式就可以了

2.任意长度的矢量求和:

首先我们要对线程块再度包装,介绍一下线程格。线程格相当于是一个二维的平面,每个平面上的格子都是一个线程块,这样就方便找到了线程块。

线程格需要预先定义 dim3 gird(DIM,DIM) , dim3 代表一个三维数组(封装的)但是一般来说不声明第3维的话就是1,这个语句声明了一个 DIM×DIM 大小的线程格,调用的时候,要用dim3这个变量代替第一个参数。

我们想要知道每个线程块的位置,就可以使用预先定义的 gridIdx.x 代表这个格子的“x坐标”也就是第一维的位置。进程格的大小就是 gridDim.x,gridDim.y

同理,我们想知道某个线程的位置,就需要用到 threadIdx.x 这个变量,我们来列个表格

位置参数 线程格 线程块 线程
大小 gridDim blockDim
位置 blockIdx threadIdx

我们知道了这些预先定义的变量,就知道怎么找到正确的位置了

3:改装函数

我们先上改装过的函数:

__global__ void add(int *a,int *b,int *c)
{
    int tid = threadIdx.x + blockIdx.x * blockDim.x;
    while(tid < N) 
    {
        c[tid] = a[tid] + b[tid];
        tid += blockDim.x * gridDim.x;
    }
}

首先我们这里进行了重新的取值,新的 tid 就是这个线程在相关的线程块的位置,然后就是确定增量,我们给每个进程多分配一点任务,每次给tid一个增量,这个增量是我们现在正在并行的线程数,这个地方不太好理解,画画图就ok了.
其他和上一个例子一样就ok,但是调用的时候,我们要使用<<<128,128>>>,或者根据具体的大小进行设置

4. 一个新的例子:点积运算

两个向量的点积是:
( x 1 , x 2 , x 3 , x 4 ) ( y 1 , y 2 , y 3 , y 4 ) = x 1 y 1 + x 2 y 2 + x 3 y 3 + x 4 y 4 (x1,x2,x3,x4)*(y1,y2,y3,y4) = x1y1 + x2y2 + x3y3 + x4y4
我们使用CUDA进行优化,首先还是写个核函数,将问题分解成对应位置的相乘,然后加在一起。我们首先完成核函数的划分:
我们首先需要每个进程计算两个对应位置的乘积,然后顺次移动到下一个位置,移动的增量是线程的数量

	__shared__ float cache[threadsPerBlock];
     int tid = threadIdx.x + blockIdx.x * blockDim.x;
     int cacheIndex = threadIdx.x;
 
     float   temp = 0;
     while (tid < N) {
         temp += a[tid] * b[tid];
         tid += blockDim.x * gridDim.x;
     }
 	cache[cacheIndex] = temp;

这里先解释一下 __shared__ 的作用,这里这个修饰符修饰了一个数组,代表了正式个共享的内存空间,相当于在这个数组是一个静态的变量(CPU下)。他的大小是每个线程快的线程数,也就是我们给一个线程块内的线程分配了一个全局的变量,每个线程将自己的答案存到正确的位置上,然后我们就要将这个线程块的答案累加到一起。进行这一步之前,我们要确保每个线程已经结束了第一步的乘法运算,因为我们要对共享内存进行读取,如果没有计算完成,就会出现问题。

我们调用 __syncthreads() 运行之前的代码之后在这里等待其他的线程也运行到此。
我们现在需要对共享数组进行一个规约 (Reduction) 操作,也就是求和的fancy说法,我们分析一下,会发现这个操作也是可以并行的,那我们就利用已经有的线程继续计算:

扫描二维码关注公众号,回复: 3357962 查看本文章
     int i = blockDim.x/2;
     while (i != 0) {
         if (cacheIndex < i)
             cache[cacheIndex] += cache[cacheIndex + i];
         __syncthreads();
         i /= 2;
     }
 
     if (cacheIndex == 0)
         c[blockIdx.x] = cache[0];
 

这里每个线程每次只选择两个数操作,然后我们等待同步,这时只有前一半线程参与了工作,并且重写了前一半的内存,然后我们参与线程的数字减半,然后只对刚才一半被重写的内存接着进行重写……依次类推,最终答案会被保存在 cache[0] 中。
这里还有一个优化的地方,我们只让第0个线程进行了写入操作,实际上,我们每个线程都可以进行写入操作,但是我们何必产生那么多信号量呢?

下面是核函数完整的代码,之后在CPU上进行一步求和就行了,因为每个线程块分配到的任务是不一样的,但是最麻烦的操作已经完成,剩下的放到CPU上就可以了。

__global__ void dot( float *a, float *b, float *c ) {
     __shared__ float cache[threadsPerBlock];
     int tid = threadIdx.x + blockIdx.x * blockDim.x;
     int cacheIndex = threadIdx.x;

     float   temp = 0;
     while (tid < N) {
         temp += a[tid] * b[tid];
         tid += blockDim.x * gridDim.x;
     }
     
     // set the cache values
     cache[cacheIndex] = temp;
     
     // synchronize threads in this block
     __syncthreads();
 
     // for reductions, threadsPerBlock must be a power of 2
     // because of the following code
     int i = blockDim.x/2;
     while (i != 0) {
         if (cacheIndex < i)
             cache[cacheIndex] += cache[cacheIndex + i];
         __syncthreads();
         i /= 2;
     }
 
     if (cacheIndex == 0)
         c[blockIdx.x] = cache[0];
 }

猜你喜欢

转载自blog.csdn.net/lingzidong/article/details/82595563