CUDA是Nvidia提出的一套并行计算的平台和编程模型,可以用来充分利用Nvidia自家的GPU做通用的并行计算。GPU作为图像处理单元,由于图像渲染的特性,使得GPU由成百上千一系列小的,简单的的核心组成可以同时处理很多任务。
1. Nvidia GPU结构
一个典型的Nvidia GPU由一系列流处理器组成 (Streaming Multiprocessor), 每一个流处理器中又由一系列CUDA core, Ray Tracing cores, Tensor cores以及一些辅助单元组成。这是一个典型的安培架构的流处理器的结构图,Nvidia RTX 3090有82个这样的流处理器组成,每个流处理器中有128个CUDA核心,总共10496个CUDA核心。流处理器的个数,每个流处理器中CUDA核心的个数,每流处理器中寄存器的大小和共享内存的大小,是我们想要写一个高性能的CUDA应用程序时需要重点考虑的东西。因为这跟CUDA应用程序的结构有关。
2. CUDA应用程序的结构
一个CUDA应用程序由两部分组成,一部分运行在CPU上,一部分运行在GPU上。需要运行在GPU上的代码称之为Kernel。一个CUDA应用程序中可以包含多个kernel用来实现不同的功能。当一个kernel被运行时,根据你的设置不同,可能有成百上千个线程会被创建去共同完成这个kernel想要完成的任务。
2.1 那如何有效的组织和协调这些线程呢?
CUDA将这些线程组织成4个不同的层级。
- threads grid
- thread block
- warp
- thread
一个kernel可以包含一个或多个grid,每一个grid又根据设置的不同包含一个或多个thread block,而每个thread block又由warps组成,每个warp必须由32个threads组成,并且这32个threads会同时运行,因为warp里的线程总是同时运行,所以我们从软件角度来看,也可以将一个warp看成一个最小的执行单元。这32个线程会按相同的步骤执行相同的代码,唯一的不同的是每一个线程可能处理的数据不同。如果一个kernel创建的总的线程数不是32的整数倍,意味着有些warp凑不够32的线程,而这些warp还是会创建32个线程,只是有些线程并没有做实际的任务,由于一个线程将会对应一个CUDA核心,所以这些做无用功的线程将会占用CUDA核心浪费资源。
2.2 如何将这些线程分配给CUDA核心去执行呢?
首先我们需要将这些线程分配到不同的流处理器上,CUDA是以thread block为最小单位分配到不同的流处理器上的。一个流处理器可以包含多个不同的thread block,但一个thread block不能分配到多个不同的流处理器上,原因是每个thread block里面的的线程可以共享内存,如果跨流处理器的话,会大大降低共享内存的效率。每个流处理器能容纳的thread block的个数取决的这个流处理器的共享内存的容量和寄存器的个数以及有多少个threads在这个thread block里和每个线程的对共享资源的消耗量, 而跟这个流处理器中的CUDA核心数量无关。
当所有的流处理器都没有足够的资源, thread block无法被分配到流处理器上时,会被放在一个等待队列中等待有足够的资源去分配。当一个thread block被分配到一个流处理器上时,只有当其中所有的threads完成其工作,这个thread block才会被释放,其所占用的资源才会被流处理器回收以便分配给在等待队列里的thread block。当一个thread block被分配到一个流处理器之后,其所包含的线程开始被组织成warp被CUDA核心执行。一个warp由32个线程组成,当 其开始运行时,会占用32个CUDA核心,由于内存读取的指令比算术运算的指令运行的要慢,所以当warp中的线程在等待数据时,CUDA核心会处于闲置的状态,这是对资源的一种浪费,所以每一个流处理器上都会配备warp调度器用来将等待的warp调出CUDA核心将准备好数据可以直接运行的warp调入CUDA核心来充分利用CUDA核心的计算资源。
2.3 一个例子
未完待续。。。