CUDA编程模型将CPU作为主机(Host),GPU作为协处理器或者设备(Device)。在一个系统中可以存在一个主机和若干设备。CPU负责进行逻辑性强的事务处理和串行计算,GPU则专注于执行高度线程化的并行处理任务。CPU和GPU拥有各自独立的存储地址空间:主机端的内存和设备端的显存。
CUDA对内存的操作同一般的C程序相同,而对显存则需要调用CUDA API中的存储管理函数。
一旦确定了程序中的并行部分,就可以考虑把这部分计算工作交给GPU。运行在GPU上的CUDA并行计算函数成为kernel(内核函数)。
一个完整的CUDA程序是由一系列的设备端kernel函数并行步骤和主机端的串行处理步骤共同组成。这些处理步骤会按照程序中相应语句的顺序依次执行,满足顺序一致性。
运行在GPU上的内核函数必须通过__global__函数类型限定,并且只能在主机端代码调用。在调用时,必须声明内核函数的执行参数。
__global__ void VecAdd(float* A,float* B,float* C)
{
}
int main()
{//
VecAdd<<<1,N>>>(A,B,C);//1代表block数,N代表每个block中的thread数
}
在设备端运行的kernel函数时并行执行的,每个线程均执行kernel函数的指令,每一个线程有自己的block ID和thread ID,block ID和thred ID只能在kernel中通过内建变量访问。
需要注意的时内建变量不需要自己定义,由设备中的专用寄存器提供。因此,内建变量时只读的,并且只能在GPU端的kernel函数中使用
kernel以线程网格(Grid)的形式组织,每个线程网格由若干个线程块(block)组成,而每个线程块又由若干个线程(thread)组成,但是block之间不能相互通信,也没有执行顺序,但是同一个block里面的线程可以相互通信,在一个block里面,有shared memory,并通过栅栏同步保证线程间能够正确地共享数据。具体来说,可以在kernel函数中通过设置_syncthreads()函数实现。
内建变量使用了dim3类型,所谓dim3是基于uint3定义的矢量类型
对于一维的block,线程的threadID就是threadIdx.x
对于二维的block(Dx,Dy),线程的threadID就是threadIdx.x+threadIdx.y*Dx
对于三维的block(Dx,Dy,Dz)的三维block,线程的threadID是threadIdx.x+threadIdx.y*Dx+threadIdx.z*Dx*Dy
前面讲的线程结构,编程模型,都只是逻辑模型,这种逻辑模型还需要映射到硬件上。
kernel实际上是在block上运行的,映射到硬件上就是在SM(流多处理器)中执行,而block中的每一个thread则放到SP上执行,但是一个SM上面可以同时有多个block。
在实际运行中,block会被分割为更小的线程束(warp)。线程束的大小由硬件的计算能力决定,在telsa架构的CPU中,一个线程束是由连续的32个线程组成。例如每个block中,ID为0-31的线程为一束,ID为32-63的为第二束。
为什么是32,这是因为每发射一个warp指令,SM中的8个SP会将这个指令执行4遍。值得注意的是,warp是一个由硬件决定的概念,在抽象的CUDA模型中见不到,但是仍有相当的影响
CUDA采用了SIMT(Single Instruction,Multiple Thread)执行模型,即单指令多线程模型。需要注意的是,如果在SIMT中要控制单个线程的某个行为,即需要用到分支,这会降低效率,因为一个warp指令,在个线程中是相同的,执行时间就是单个线程的执行时间,但是如果线程执行有差异,进入SP需要决定某些指令需不需要执行,执行时间就变成了各个执行时间之和,开发CUDA程序时,应尽量避免分支。