C++学习|CUDA编程入门

CUDA编程入门

  • GPU介绍
  • CUDA 介绍
  • NVCC——CUDA编译器
  • cu/cuh文件——CUDA文件
  • 核函数——操作GPU
  • 线程模型——CUDA逻辑结构
  • CUDA程序运行流程
  • CUDA内存管理

GPU介绍

GPU(Graphics Processing Unit,图形处理器,显卡):最早主要是进行图形处理,现在已经被广泛应用于深度学习,最大的优势就是高效的并行计算能力。

GPU和CPU的区别:

  • 结构上来说,虽然CPU和GPU都有控制单元、运算单元和缓存,但是CPU需要大量空间存放控制单元和缓存,而GPU中运算单元占了大部分。
  • 功能上来说,由于结构上占比,所以CPU在大规模并行计算能力上极受限制,而更擅长逻辑控制。而GPU有数量众多的计算单元和超长的流水线,非常适合计算海量同类型处理的数据。

为什么要用GPU:GPU比CPU的优势在于能提供高性能的并行运算,对于大规模同类型数据处理,可以通过算法并行优化来提高运行效率。

CUDA 介绍

CUDA(Compute Unified Device Architecture):2006年,NIVIDIA公司发布了CUDA。CUDA是操作GPU计算的硬件和软件的架构,提供了GPU编程的简易接口。

CUDA支持多语言:C/C++、Python、Fortran等。

CUDA提供两层API接口:CUDA驱动(Driver)和CUDA运行时(Runtime)API。两者调用性能几乎差异不大,但Runtime API使用更加友好。因为Driver API 可以通过直接操作硬件执行一些复杂的功能,Runtime API 对 Driver API 进行了一定的封装,隐藏了部分实现细节。
C++学习|CUDA编程入门_第1张图片

NVCC——CUDA编译器

NVCC:NVCC是CUDA的编译器,位于bin/目录中。

工作流程:CUDA程序是包含主机(Host)代码和设备(Device)代码的统一源代码,Host代码在CPU运行,Device代码在GPU运行。NVCC编译器在编译过程中将两者区分开来,Host代码是用C语言编译的,那么就会交给C编译器进一步编译,以一个CPU进程的方式运行;Device代码用C扩展语言编写,GPU运行的核函数(后面有介绍核函数)代码会由NVCC进一步编译并在GPU上执行。

cu/cuh文件——CUDA文件

cu/cuh文件:cu和cuh都是CUDA的后缀格式。cu文件只是含有CUDA代码的cpp文件,cuh相当于CUDA的头文件后缀名。

CUDA文件里面可以包含Host代码和Device代码。

核函数——操作GPU

核函数(Kernel function):核函数在GPU上进行并行执行。

函数编写形式:

  • 限定词:“__global__”修饰。
  • 返回值:返回值必须是void。
  • 文件:在.cu文件中编写。
__global__ void func(argument arg)
{
	printf("GPU: Hellow World\n");
}

编写注意事项:

  • 核函数只能访问GPU内存。GPU和CPU之间内存是要通过特定API才能进行交互的。
  • 核函数不能使用变长参数、静态变量和函数指针。
  • 核函数具有异步性。CPU不会等GPU核函数运行完了才继续。

核函数的调用:使用“<<>>”来调用线程执行核函数,grid、block和线程的概念会在线程模型里面详细解释。

int main(void){
	func<<<1, 1>>>();
	return 0;
}

线程模型——CUDA逻辑结构

CUDA 的并行计算:通过成千上万个线程(Thread)的并行执行来实现。

线程模型Thread(线程)、Block(线程块)、Grid(网格)三者关系:如下图所示,Thread组成Block,Block组成Grid,Kernel执行的时候启动的是一个Grid。这些线程块的划分都是逻辑上,并不是物理上,划分是为了方便编写代码。
C++学习|CUDA编程入门_第2张图片
理解核函数调用:这时候其实就能理解前面核函数的“<<>>”调用,grid_size参数指的是一个Grid里面有多少个Block,block_size参数指的是一个Block里面有多少个线程。参数grid_size×block_size就是线程数量,可以理解为有多少个一模一样的函数线程在同时跑。

func<<<1, 1>>>();//输出一次:GPU: Hellow World,因为有1个线程。
func<<<2, 4>>>();//输出八次:GPU: Hellow World,因为有8个线程。

多维线程:前面代码部分提到的都是一维的Grid和Block,但Grid和Block可以不止一维甚至最多能到三维。例如在上面线程模型图中,Grid和Block里面的结构都是二维的,从索引的坐标可以看得出来。在执行核函数前,用dim3变量规定一下grid_size和block_size参数的维度就行了。

dim3 grid_size(2,2);//二维
dim3 block_size(16, 16, 2);//三维
func<<<grids, blocks>>>();

线程索引:每个线程都会一个唯一编号标识。下面示范一个二维Grid和二维Block的线程索引计算,可以配合上面的线程模型图理解。

// blockIdx、threadIdx都是unit3类型,表示它们的坐标。
// gridDim、blockDime都是dim3类型,表示网格的大小。
int blockId=blockIdx.x+blockId.y*gridDim.x;// block在grid的ID
int threadId=threadIdx.y*blockDim.x+threadIdx.x;// thread在block的ID
int id=blockId*(blockDim.x*blockDim.y)+threadId;// thread在grid的ID

为什么要有多维线程:主要是为了简化一些三维建模以及二维图像处理的问题。例如二维图片做图像处理,每个核函数要对图片像素进行操作,但是传入的是图片的矩阵,那么核函数还需要计算出自己处理的是那个像素。这个时候因为block结构本身就是二维,可以直接用thread本身的x和y来索引。如果block结构是一维的,还得花很多心思去调整偏移量和步幅来找到要处理的像素。

// 多维线程实现矩阵加法
__global__ void MatAdd(int A[N][N], int B[N][N], int C[N][N]){
    int x = threadIdx.x;// 在block中线程的x坐标
    int y = threadIdx.y;// 在block中线程的y坐标
    C[x][y] = A[x][y] + B[x][y];
}
 
int main(){
	...
    int grid_size = 1;
    dim3 block_size(N,N);
    MatAdd<<<grid_size,block_size>>>(A,B,C);
    ...
}

CUDA程序运行流程

  1. CPU初始化:分配 Host 内存,并进行数据初始化。
  2. GPU初始化:分配 Device 内存,并从 Host 将数据拷贝到 Device 上。
  3. GPU并行运算:调用 CUDA 的核函数在 Device 上完成指定的运算。
  4. 结果从GPU传回CPU:将 Device上的运算结果拷贝到 Host 上。
  5. 初始化清空:释放 Device 和 Host 上分配的内存。

CUDA内存管理

了解了CUDA程序运行流程,复杂的CUDA程序会涉及到CPU和GPU之间的数据交互。CPU和GPU之间的数据是不共享的,所以需要分配、拷贝、传递以及释放。

功能 函数
内存分配 cudaMalloc
数据传递 cudaMemcpy
内存初始化 cudaMemset
内存释放 cudaFree

你可能感兴趣的:(C++学习,c++,开发语言,CUDA,GPU)