CUDA并行所需要的基本任务包含以下几点:
使用特定的网格维度加载核函数(线程块和线程的数目)。
明确哪些函数编译后运行在设备(GPU)上、主机(CPU)上,或者两者之上。
访问和运用线程块和线程的计算索引值。
分配内存和传输数据。
让我们从介绍核函数的加载开始。正如上面讨论的,核函数是一种特殊的函数,加载核函数与常规的函数调用看起来很像,详细来说,加载核函数从一个函数名开始,比如aKernel,然后以一个包含了以逗号分开的参数列表的括号结尾。现在我们来看一下编程语言拓展:为了自然地表达并行并且声明计算网格,在函数名和含有参数的括号中间加入了网格的维度和线程块的维度(被放在三个尖括号中):
需要注意的是,Dg、网格中的线程块数和Db、线程块中的线程数目,一起组成了加载核函数中的执行配置和维度的声明。
这构成了加载一个和函数的语法,但是如何声明一个从主机端调用但是在设备端执行的函数仍有一些疑问。CUDA使用在函数前面添加下面的一个函数标识符来作为区分:
__global__
是标志着和函数的标识符(可以在主机端调用并在设备端执行)。
__host__
函数从主机端调用在主机端执行。(这是一个默认的限定符,通常被省略。)
__device__
函数从设备端调用并在设备端执行。(从核函数中调用的函数需要有__device__
限定符。)
在函数头添加__host____device__
函数会让系统分别编译这个函数的主机版本和设备版本。
核函数有几个值得注意的权限和限制:
核函数不能带有返回值,因此返回类型通常为void。并且核函数需如下声明:
核函数提供了对于每一个线程块和线程的维度数和索引变量。
维度数目变量:gridDim
声明了网格中的线程块数目。
blockDim
声明了每个线程块中的线程数目。
索引变量:blockIdx
给出了这个线程块在网格中的索引。
threadIdx
给出了这个线程在线程块中的索引。
在GPU上执行的核函数通常不能访问主机端CPU可以访问的内存中的数据。
CUDA运行时API提供了一些可以将输入数据传输到设备端和将结果传回到主机端的函数,如下所示:
cudaMalloc()
函数可以分配设备端内存。
cudaMemcpy()
将数据传入或传出设备。
cudaFree()
释放掉设备中不再使用的内存。
核函数并行进行多次运算,但是它们也放弃了对执行顺序的控制。CUDA为需要同步和并发执行时提供了相应的函数:
__syncThreads()
可以在一个线程块中进行线程同步。
cudaDeviceSynchronize()
函数可以有效地同步一个网格中的所有线程。
原子操作,例如atomicAdd()
,可以防止多线程并发访问一个变量时造成的冲突。
除了上文介绍的函数和标识符以外,CUDA同时提供了一些额外的有用的数据类型:
size_t
:代表内存大小的专用变量类型。
cudaError_t
:错误处理的专用变量。
向量类型:CUDA将标准C的向量数据类型拓展到了4个。独立的组件通过后缀.x、.y、.z和.w进行访问。