What is CUDA

Inspired by my undergraduate project.

Graphical Processing Unit

大多数计院都不会在大学中系统地教授 GPU 这一至关重要的计算机部件。传统的教学内容都把重点放在计算机的五大部件、操作系统(包括网络协议栈)、编程语言等内容,而对 GPU 的关注出奇的少。诚然,学习完这些基本的知识已经足以应对大多数的应用场景,但 GPU 作为现代计算机高性能计算的核心,我们无法忽视这头横冲直撞的大象。

GPU 是图形处理器的英文字母缩写,顾名思义,就是一个特化的电子电路,专为辅助 CPU 进行电子图像处理而设计。进入千禧年后,Nvidia 推出了 GeForce 256,标志着 GPU 进入通用并行计算时代 (general-purpose parallel computing, 即 GPGPU)。这使得 GPU 或 GPGPU 不再局限于传统的图像处理,而是在科学计算和人工智能等领域发挥作用。

Embarrassingly Parallel Computing

有一个很确切的例子来描述 CPU 和 GPU 之间的关系:CPU 是一小撮数学领域的大佬(CPU 一般只有 24/16/8 个核心),能够解出各种复杂的问题,而且单个计算速度极快;而 GPU 更像是一个中学的所有学生(GPU 有上万个核心),虽然水平局限,但胜在人数多。

在解决一般性问题上,一个学校上千名学生解题的速度可比这一小撮大佬们要快多了。比方说,现在有 道加法题,一般情况下,我们会采取这种解决方法,时间复杂度为

// vec_c[n] = vec_a[n] + vec_b[n]
for(int i = 0; i < n; i++) {
	vec_c[i] = vec_a[i] + vec_b[i];
}

而把这些加法题分给全校的学生,那么问题就会这样被解决,时间复杂度可能为 ,其中 表示学生的数量,也就是 GPU 的核心数:

void vecAdd(int* vec_a, int* vec_b, int* vec_c) {
	int i = studentID;
	vec_c[studentID] = vec_a[studentID] + vec_b[studentID];
}

在 AI(如 CNN )等领域的应用上,GPU 提供的这种高并行计算完美的契合深度学习的计算需求。以卷积操作为例,每一次卷积都涉及到大量的矩阵运算,卷积核会不断地在输入数据上滑动(CPU)并进行点积计算,因为每次滑动的卷积计算都是独立的,所以天然的可以进行并行化处理。诸如此类几乎天然的就能拆分成独立任务的计算就被称为 embarrassingly parallel,也是通用并行计算的核心。

convolution_process.png

GPU Hardware

CPU 和 GPU 核心数的差异就使得操作系统和 CPU 的交互方式跟与 GPU 的交互方式大不同。由于较少的核心数量,操作系统对 CPU 核心的调度是精细化的,因此我们有各种调度算法来将线程放在 CPU 核心上运行,进而在系统程序员眼中,CPU 核心是可见的。CPU 计算中,一个任务通常会顺序地在一个核心上执行直到结束。

GPU 拥有成千上万个核心,操作系统没有办法逐一调度这么庞大数量的核心,因此会依赖 GPU 进行批量式的任务分配,通常会依赖于 OpenCL、CUDA 等这种 GPU 计算框架。对于操作系统而言,GPU 是不可见的(无从得知使用哪种指令集,有多少个核心等),系统通常只能够看到如 CUDA API 提供的高级抽象 API。

在物理结构上,GPU 通常会被分为多个图形处理簇 (Graphics Processing Cluster, GPC)。每个 GPC 又由流多处理器 (Streaming Multiprocessor, SM) 构成,SM 由进而由多个 Warps、一个光追核心 (Ray tracing core) 组成,每个 Warp 由有多个 CUDA 核心和 Tensor 核心构成。

早期,CUDA 核心叫做流处理器 (Streaming Processor, SP),用于描述 GPU 的内部计算单元。在 2006 年, Nvidia 推出了 CUDA 架构,引入了 CUDA 编程模型,因为使用 CUDA API 的每个线程由一个 SP 负责执行,所以 SP 被更名为 CUDA 核心。并行任务的执行调度由 Warp 调度器 (warp scheduler) 负责。
GPU_arch.png

通常来说,GPU 有自己的专用显存 (Video RAM),显存的吞吐量(TB/s)要远远大于内存的吞吐量(GB/s)。上面我们又提到,GPU 的优势在于并行运算,这就意味着在 embrrassingly parallel 的计算任务中,GPU 的数据处理能力要显著优于 CPU。

CUDA API

GPU 在系统中就像一个外设,因为系统并不能直接访问 GPU 的底层硬件架构。当我们需要使用 GPU 进行并行计算时,通常需要使用驱动程序(如 CUDA driver)与 GPU 进行通信。

GPU 有很多核心,运行着与 CPU 截然不同的指令集架构,所以用 CUDA driver 将指令发给 GPU 时,我们需要用特定的编译器(如 NVCC)把高级语言源程序编译为特定 GPU 架构下能够运行的机器指令。所以除了驱动层(CUDA driver),CUDA 还封装了 CUDA runtime,为程序员暴露更高级的接口。对于将 GPU 用于科学计算但不太懂编程的人,CUDA 还提供更高级封装好了的库(如cuDNN),使得通用并行计算更加高效。

所有的所有汇总在一起就是 Nvidia 提供 CUDA 计算模型(CUDA Libraries + CUDA Runtime + CUDA Driver),这是一个偏向软件层面的 architecture,屏蔽了 GPU 的底层 infrastructure。程序员可以通过 CUDA 提供的 API 提交并在 GPU 上执行并行计算任务。

CUDA.png

CUDA 计算模型会把用户的计算任务会被分成网格 (Grid)、线程块 (Block)、线程 (Thread)。每个网格包含多个块,每个块包含多个 CUDA 线程。在提交并行任务执行时,每个 CUDA 并行流(也就是 CUDA 线程)都会映射到一个 CUDA 核心上单独并行的执行计算任务,这些任务最终由 GPU 的 SM 进行调度,执行过程对操作系统屏蔽。

在 CUDA 编程模型中,一个并行线程由一个 CUDA 核心执行,多个线程(比如 32 个)组成一个 Warp,多个 Warp 组成一个 Block,多个 Block 组成一个 Grid 为 GPU 所调度。一个 Warp 中的所有的 CUDA 核心会执行相同的并行任务但可以处理不同的数据,而同一个 Block 中的不同 Warp 可以执行不同的任务。