CUDA是一种通用的并行计算平台和编程模型,是在C语言基础上扩展的。
一、CUDA编程模型概述
1. CUDA编程结构
在一个异构环境中包含多个CPU和GPU,每个GPU和CPU的内存都由一条PCI-e总线分隔开,需要注意区分
(1)主机:CPU及其内存(主机内存)
(2)设备:GPU及其内存(设备内存)
从CUDA6.0开始,NVIDIA提出了名为“统一寻址”的编程模型的改进,连接了主机内存和设备内存空间,可使用单个指针访问CPU和GPU内存,无须彼此之间手动拷贝数据。重要的是如何为主机和设备分配内存空间以及如何在CPU和GPU之间拷贝共享数据。
内核(kernel)是CUDA编程模型的一个重要组成部分,其代码在GPU上运行。
多数情况下,主机可以独立地对设备进行操作。内核一旦被启动,管理权立刻返回给主机,释放CPU来执行由设备上运行的并行代码实现的额外的任务。
CUDA编程模型主要是异步的,因此在GPU上进行的运算可以与主机-设备通信重叠。一个典型的CUDA程序包括由并行代码互补的串行代码。串行代码在主机CPU上执行,而并行代码在GPU上执行。主机代码按照ANSI C标准进行编写,而设备代码使用CUDA C进行编程。可以将所有的代码统一放在一个源文件中,也可以使用多个源文件来构建应用程序和库。NVIDIA的C编译器(nvcc)为主机和设备生成可执行代码。
一个典型的CUDA程序实现流程遵循以下模式:
(1)把数据从CPU内存拷贝到GPU内存
(2)调用核函数对存储在GPU内存中的数据进行操作
(3)将数据从GPU内存传送回到CPU内存
2. 内存管理
CUDA编程模型假设系统是由一个主机和一个设备组成的,并且各自拥有独立的内存。核函数是在设备上运行的。为了拥有充分的控制权并使系统达到最佳性能,CUDA运行时负责分配与释放设备内存,并且在主机内存和设备内存之间传输数据。
cudaError_t cudaMalloc(void** devPtr, size_t size)
该函数负责向设备分配一定字节的线性内存,并以devPtr的形式返回指向所分配内存的指针。cudaMalloc与标准C语言中的malloc函数几乎一样,只是此函数在GPU的内存里分配内存。
cudaError_t cudaMemcpy(void* dst, const void* src, size_t count, cudaMemcpyKind kind)
此函数负责主机和设备之间的数据传输,从src指向的源存储区复制一定数量的字节到dst指向的目标存储区。复制方向由kind指定。
3. 线程管理
当核函数在主机端启动时,它的执行会移动到设备上,此时设备中会产生大量的线程并且每个线程都执行由核函数指定的语句。了解如何组织线程是CUDA编程的一个关键部分。
由一个内核启动所产生的所有线程统称为一个网格。同一个网格中的所有线程共享相同的全局内存空间。一个网格由多个线程块构成,一个线程块包含一组线程,同一线程块内的线程协作可以通过以下方式来实现:(1)同步;(2)共享内存。不同块内的线程不能协作。
线程依靠以下两个坐标变量来区分彼此:
blockIdx(线程块在线程格内的索引)
threadIdx(块内的线程索引)
以上变量是核函数中需要初始化的内置变量。当执行一个核函数时,CUDA运行时为每个线程分配坐标变量blockIdx和threadIdx。基于这些坐标,可以将部分数据分配给不同的线程。
CUDA可以组织三维的网格和块,网格和块的维度由下列两个内置变量指定:
(1)blockDim(线程块的维度,用每个线程块中的线程数来表示)
(2)gridDim(线程格的维度,用每个线程格中的线程数来表示)
它们是dim3类型的变量,是基于uint3定义的整数型向量,用来表示维度。
当定义一个dim3类型的变量时,所有未指定的元素都被初始化为1。
dim3类型变量中的每个组件可以通过它的x、y、z字段获得。
#include <stdio.h>
__global__ void checkIndex(void)
{
printf("threadIdx: (%d, %d, %d); "
"blockIdx: (%d, %d, %d); "
"blockDim: (%d, %d, %d); "
"gridDim: (%d, %d, %d)\n",
threadIdx.x, threadIdx.y, threadIdx.z,
blockIdx.x, blockIdx.y, blockIdx.z,
blockDim.x, blockDim.y, blockDim.z,
gridDim.x, gridDim.y, gridDim.z);
}
int main(void)
{
int nElem = 6;
dim3 block(3);
dim3 grid((nElem+block.x-1)/block.x);
printf("block: %d, %d, %d\n", block.x, block.y, block.z);
printf("grid: %d, %d, %d\n", grid.x, grid.y, grid.z);
checkIndex<<<grid, block>>>();
cudaDeviceReset();
return 0;
}
对于一个给定的数据大小,确定网格和块尺寸的一般步骤为:
(1)确定块的大小
(2)在已知数据大小和块大小的基础上计算网格维度
要确定块尺寸,通常需要考虑:(1)内核性能特性;(2)GPU资源的限制
4. CUDA内核函数
kernel_name <<<grid, block>>> (argument list);
核函数的调用与主机线程是异步的。核函数调用结束后,控制权立刻返回给主机端。可以调用以下函数强制主机端程序等待所有的核函数执行结束。
cudaError_t cudaDeviceSynchronize(void);
核函数的相关限制:
(1)只能访问设备内存
(2)必须具有void返回类型
(3)不支持可变数量的参数
(4)不支持静态变量
(5)显示异步行为