CUDA
CUDA是英伟达推出的GPU架构平台,通过GPU强大的并行执行效率,为计算密集型应用加速,CUDA文件以.cu结尾,支持C++语言编写,在使用CUDA前需要下载 CUDA Toolkit
内存与显存
CPU可以访问内存,GPU可以访问显存,如果需要使用GPU进行计算,必须把数据从内存复制到显存
指向显存的指针
创建一个指向显存的指针,下面的代码可以告诉你为什么要使用 (void**)类型
1 | int* p; // 这是一个指向int变量的内存指针 |
在GPU中申请显存,并获得指向显存的指针
1 | int length = size * sizeof(int); |
此时的dev_a, dev_b, dev_c已经指向显存地址,空间大小为 length
内存与显存的数据交换
在使用GPU计算前,需要把数据复制到显存
1 | cudaMemcpy(dev_a, a, length, cudaMemcpyHostToDevice); |
dev_a是显存指针,a是内存指针cudaMemcpyHostToDevice表示把长度为length的内存数据复制到显存里
计算完成后,需要把数据从显存复制到内存以供CPU计算
1 | cudaMemcpy(c, dev_c, length, cudaMemcpyDeviceToHost); |
这段代码的含义是把dev_c指向的显存地址的数据复制到c指向的内存地址
在计算结束后,应该释放显存空间
1 | cudaFree(dev_a); |
栅格结构
GPU的结构包含栅格(grid),块(block),线程(thread),许多线程组成一个“块”,许多个“块”组成一个“栅格”,其中grid和block都可以用三维向量表示,假设一个block有1024个线程,如果创建4个block,则总共有4096个线程同时运行
下面的代码展示了如何获取block和thread的编号
1 | int i = threadIdx.x; |
合理使用这些编号,可以帮助你定位变量位置
1 | __global__ void DoInKernel(int* a, int* b, int* c) { |
函数限定词
核函数
核函数使用 __global__ 修饰,它在CPU上调用,在GPU上执行
1 | __global__ void DoInKernel(int* a, int* b, int* c) { |
其中 <<< >>>运算符决定了执行核函数的方式,第一个参数是block的数量,即一个grid里有几个block,它实际上是一个dim3类型的变量,在处理多维数组时它可以让你的代码编写更加方便,但是这里不做演示
1 | dim3 dg(10, 10, 10); |
第二个参数是thread的数量,即一个block里有几个线程,它同样是dim3类型的变量,如果输入的是int,则默认y和z都是1
后面还有两个可选参数,分别用来表示共享内存大小和流,共享内存大小限制了可以动态分配的共享内存的最大值,流指定使用哪个IO通道在内存和显存之间复制数据,使用不同的流可以防止阻塞
内联函数
内联函数使用 __device__ 修饰,它必须在GPU上调用,只能在GPU上执行
1 | __device__ int add(int a, int b) { |
主机函数
所有不加修饰的函数都是主机函数,它也可以使用 __host__ 修饰,主机函数只能在CPU上调用和执行,例如 main 就是一个主机函数
1 | __host__ int main(){ |
异常处理
CUDA代码极难调试,因此最好在每一步都检查一次错误,一旦发生错误,立即转到错误处理
1 | int main() |
主机同步
由于GPU的代码是异步执行的,如果两个核函数被写在一起,那么它们很可能会被同时执行,使用cudaDeviceSynchronize()阻塞主机线程,可以确保所有的核函数或者IO流都已经执行完毕,才会继续执行下面的代码
1 | DoInKernel_1 << <1, 10 >> > (); |
其中cudaMemcpy()函数必须在所有GPU任务完成后才能执行,所以它已经自带主机同步,不再需要手动阻塞