学习目标:
了解CUDA C中的线程
了解不同线程之间的通信机制
了解并行线程执行线程的同步机制
问题:并行执行的Block是独立地计算结果还是可以相互协作解决问题?
在前面的学习中,各个处理器不需要了解其他处理器的执行状态而彼此独立地计算出结果,然后并行编程最重要的一个方面是,并行执行的各个部分如何通过相互协作解决问题,即代码的各个执行副本之间进行通信和协作。接下来我们介绍并行执行的CUDA C代码段之间实现这种通信。
问题:如何在GPU上启动并行Code?
实现方法:告诉CUDA运行时启动Kernel的多个并行副本,并且将这些并行副本成为Block。CUDA运行时把这些Block分解为多个Thread。通过调整<<<>>>参数来修改Block数量及每个Block中创建的thread数量。
问题:使用Block与使用thread相比存在哪些优势?
就目前而言没什么优势,但是Block的并行thread能够完成并行Block无法完成的工作。通过 对并行线程矢量求和程序的改进或许对你有新的启发。
1.kernel调用的修改
add<<<N,1>>>(dev_a,dev_b,dev_c);
//启动N个Block,每个Block对应一个thread
2.对数据进行索引的方法
int tid = blockIdx.x;
1.kernel调用的修改
add<<<1,N>>>(dev_a,dev_b,dev_c);
//启动1个Block,Block里面启动N个thread
2.对数据进行索引的方法
int tid = threadIdx.x;
问题:只使用并行thread的方法存在的局限性
硬件将Block的数量限制为不超过65535。同样,对于启动kernel时每个block的thread数量,硬件也进行了限制。通过笔记(二)中设备属性可以进行相关查询。具体来说,最大的thread数量不能超过Device属性中maxThreadPerBlock的值。即每个Block1024个thread。
CUDA Device Query (Runtime API) version (CUDART static linking)
Detected 1 CUDA Capable device(s)
Device 0: "GeForce GTX 950M"
Maximum number of threads per block: 1024
问题:如何通过并行thread对长度大于1024个矢量进行相加?
将block与thread结合起来才能实现这个计算。新的内置变量,blockDim(保存的是Block中每一维的thread数量),由于使用是一维的Block,因此只需要blockDim.x即可。回顾greidDim(Grid中每一维的Block的数量)
1.kernel调用的修改
任需要启动N个并行线程,因为我们通过多个Block进行启动,这样就不会超过1024个线程的最大数量限制,
故我们将Block中的大小设置为固定值。
例如每个Block中包含的thread设置为128,然后启动N/128个block
问题:N不是128的整数倍时,如何处理
N=127,此时N/128=0,即将启动0个线程,不会获得任何计算结果,更扩大范围来说,当N不是128的整数倍时,启动的线程数量将不少于预期数量,这种情况非常糟糕,如何解决?
解决:
通过计算大于或等于128的最小倍数,即当N不是128的整数倍时,将启动过多的线程。
add <<< (N+127)/N , 128 >>>(dev_a, dev_b, dev_c)
解决2:kernel已经解决启动过多线程的问题。
即在访问输入数组和输出数组之前,必须检查线程的偏移是否位于0到N之间
if(tid < N){
c[tid] = a[tid] + b[tid];
}
2.对数据索引的修改
需求:多个bolck,并且每个block包含多个thread。
对比:二维索引空间转换为线性空间的标准算法
int tid = threadIdx.x + blockIdx.x * blockDim.x
即:Block中的thread索引 + Block的索引 * Block的大小(thread的数量)
问题:如何通过并行thread对长度大于65535 * 128个矢量进行相加?
现在我们已经明确,Block的数量存在一个硬件限制(Block每一位的大小不超过65535)以及Block中thread的数量不能超过1024。矢量相加引出了新问题,如果启动N/128个Block,128thread/Block将矢量进行相加,那么当矢量长度超过65535 * 128时,(即超过Block的硬件限制),kernel会调用失败,我们如何来改变这种情况?
后面我们再讨论不同的值对性能产生的影响。
1.对数据索引的修改:
类比基于CPU实现。之前提到过,在多GPU或者多核版本中,每次递增的数量不是1,而是CPU的数量,现在我们在GPU中讲使用同样的方法
void add( int *a, int *b, int *c ) {
int tid = 0; //这是第0号CPU,因此索引从0开始
while (tid < N) {
c[tid] = a[tid] + b[tid];
tid += 1; //由于只有一个CPU,因此每次递增1
}
}
实现思想:
类比:将并行thread数量看成是CPU的数量,我们认为每个thread在逻辑上是并行执行的,并且硬件可 以调度这些线程以便实际执行。通过将并行化过程与硬件的实际执行过程decouple,正是CUDA C为软件开发人员减轻的负担之一。
具体实现:
每个thread在计算完当前索引上的任务后,就需要对索引进行递增,递增的步长为grid中正在运行的thread数量。这个数值 = 每个block中thread数量 * 每个greid中block的数量
__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;
}
}
2.kernel调用的修改
问题回顾:之所以采用上述数据索引的调用方式,是因为当(N+127)/128大于65535时,kernel调用add<<<(N+128)/128,128>>>( dev_a, dev_b, dev_c );会失败。为了确保不会启动过多的block,我们将block的数量固定为某个较小的值。