Overview
CUDA(Compute Unified Device Architecture,统一计算设备架构)是NVIDIA(英伟达)提出的并行计算架构,结合了CPU和GPU的优点,主要用来处理密集型及并行计算。CPU和GPU是两个独立的处理器,通过单个计算节点中的PCI-Express总线相连,GPU用来提高计算密集型应用程序中并行程序段的执行速度,CPU则负责管理设备端的资源。CUDA编程的独特优势在于开放的架构特性可以使程序员在功能强大的硬件平台上充分挖掘其并行性,既满足了计算密集型程序的需要,又实现了程序的易读性及便捷性。
术语
- 主机(host):CPU及其内存(主机内存)
- 设备(device):GPU及其内存(设备内存)
- 内核(kernel):是CUDA编程模型的一个重要组成部分,其代码在GPU上运行。
多数情况下,主机可以独立地对设备进行操作。内核一旦被启动,管理权立刻返回给主机,释放CPU来执行由设备上运行的并行代码实现的额外的任务。CUDA编程模型主要是异步的,因此在GPU上进行的运算可以与主机-设备通信重叠。
一个典型的CUDA程序实现流程遵循以下模式。
1.把数据从CPU内存拷贝到GPU内存。
2.调用核函数对存储在GPU内存中的数据进行操作。
3.将数据从GPU内存传送回到CPU内存。
内存管理
CUDA编程模型假设系统是由一个主机和一个设备组成的,而且各自拥有独立的内存。核函数是在设备上运行的。为使用户拥有充分的控制权并使系统达到最佳性能,CUDA运行时负责分配与释放设备内存,并且在主机内存和设备内存之间传输数据。
用于执行GPU内存分配的是cudaMalloc函数,其函数原型为:
该函数负责向设备分配一定字节的线性内存,并以devPtr的形式返回指向所分配内存的指针。cudaMalloc与标准C语言中的malloc函数几乎一样,只是此函数在GPU的内存里分配内存。
cudaMemcpy函数负责主机和设备之间的数据传输,其函数原型为:
此函数从src指向的源存储区复制一定数量的字节到dst指向的目标存储区。复制方向由kind指定,其中的kind有以下几种。
这个函数以同步方式执行,因为在cudaMemcpy函数返回以及传输操作完成之前主机应用程序是阻塞的。
在GPU内存层次结构中,最主要的两种内存是全局内存和共享内存。全局类似于CPU的系统内存,而共享内存类似于CPU的缓存。然而GPU的共享内存可以由CUDA C的内核直接控制。
注意,CUDA 6.0提出了统一寻址,可以使用一个指针来访问CPU和GPU的内存。
线程管理
当核函数在主机端启动时,它的执行会移动到设备上,此时设备中会产生大量的线程并且每个线程都执行由核函数指定的语句。
如上图所示,由一个内核启动所产生的所有线程统称为一个网格。同一网格中的所有线程共享相同的全局内存空间。一个网格由多个线程块构成,一个线程块包含一组线程,同一线程块内的线程协作可以通过以下方式来实现。
- 同步
- 共享内存
不同块内的线程不能协作。
线程依靠以下两个坐标变量来区分彼此。
- blockIdx(线程块在线程格内的索引)
- threadIdx(块内的线程索引)
这些变量是核函数中需要预初始化的内置变量。当执行一个核函数时,CUDA运行时为每个线程分配坐标变量blockIdx和threadIdx。基于这些坐标,可以将数据分配给不同的线程。
该坐标变量是基于uint3定义的CUDA内置的向量类型,是一个包含3个无符号整数的结构,可以通过x、y、z三个字段来指定:
CUDA可以组织三维的网格和块。上图展示了一个线程层次结构的示例,其结构是一个包含二维块的二维网格。网格和块的维度由下列两个内置变量指定。
- blockDim(线程块的维度,用每个线程块中的线程数来表示)
- gridDim(线程格的维度,用每个线程格中的线程数来表示)
它们是dim3类型的变量,是基于uint3定义的整数型向量,用来表示维度。当定义一个dim3类型的变量时,所有未指定的元素都被初始化为1。dim3类型变量中的每个组件可以通过它的x、y、z字段获得。
线程层次结构
CUDA的特点之一就是通过编程模型揭示了一个两层的线程层次结构。由于一个内核启动的网格和块的维数会影响性能,这一结构为程序员优化程序提供了一个额外的途径。
网格和块的维度存在几个限制因素,对于块大小的一个主要限制因素就是可利用的计算资源,如寄存器,共享内存等。某些限制可以通过查询GPU设备撤回。网格和块从逻辑上代表了一个核函数的线程层次结构。
CUDA内核调用是对C语言函数调用语句的延伸,<<<>>>运算符内是核函数的执行配置。
利用执行配置可以指定线程在GPU上调度运行的方式。执行配置的第一个值是网格(grid)维度,也就是启动块的数目。第二个值是块(block)维度,也就是每个块中线程的数目。通过指定网格和块的维度,你可以进行以下配置:
- 内核中线程的数目
- 内核中使用的线程布局
同一个块中的线程之间可以相互协作,不同块内的线程不能协作。
核函数编写
核函数是在设备端执行的代码。在核函数中,需要为一个线程规定要进行的计算以及要进行的数据访问。当核函数被调用时,许多不同的CUDA线程并行执行同一个计算任务。以下是用__global__声明定义核函数:
注意,核函数必须有一个void返回类型。