1 编译流程

随着 Nvidia 不断更新 GPU 硬件设计架构,每一代架构更迭都会引入新的功能,在同一代架构中不同的 GPU 型号在硬件配置上与性能上也会有一些细小的差异。

Nvidia 提供了一套代号来标识 GPU 的真实架构,它的格式是 sm_xyx 代表 GPU 架构世代的编号,y代表 GPU 在这个架构世代中的版本号。为了方便对比 GPU 能力,如果 x1y1 <= x2y2 那么sm_x1y1中所有非 ISA 相关的功能都包含在 sm_x2y2 中了。

然而这并不能保证新的 GPU 架构就能兼容老的 GPU 架构的二进制代码,比如在 Fermi 架构上编译的代码就无法在Kepler GPU 上运行,因为它们的指令集与指令编码都是不一样的。只用在相同的架构下才能保持二进制兼容。为了解决这个问题,CUDA 代码采用了两阶段编译过程。

第一个阶段

nvcc 编译器将 CUDA 代码编译成 PTX 代码,这是一种类似汇编的代码。它基于虚拟架构进行编译,虚拟架构是根据 GPU 能力、功能来定义的。

第二个阶段

nvcc 编译器是包含在 GPU 驱动中,编译器可以预先或者在运行的时候根据真实架构将 PTX 代码编译成实际的二进制代码。

第一个阶段的虚拟架构要兼容第二个阶段的真实架构虚拟架构的版本越低就越能兼容更多的真实架构,当然CUDA 代码支持的功能也就越少。

Two-Staged Compilation with Virtual and Real Architectures

对于在 CPU 上运行的 host code 与 GPU 上运行的 device code,编译器会分开进行编译。

  1. nvcc 会将 device code (kernel 函数) 编译成 PTX code 或者二进制格式 (cubin object) 。
  2. nvcc 会修改 host code 中调用 kernel 的部分,修改为 PTX code 或者 cubin object 中实际的代码。

架构代号列表

虚拟架构代号 真实架构代号 CUDA支持版本 支持架构 支持硬件
compute_50 sm_50 CUDA 6~11 Maxwell Tesla/Quadro M series
compute_52 sm_52 CUDA 6~11 Maxwell GTX-980, GTX Titan X
compute_53 sm_53 CUDA 6~11 Maxwell Tegra TX1, Jetson Nano
compute_60 sm_60 CUDA 8 Pascal Tesla P100
compute_61 sm_61 CUDA 8 Pascal GTX 1080, GTX1070
compute_62 sm_62 CUDA 8 Pascal Jetson TX2
compute_70 sm_70 CUDA 9 Volta Tesla V100
compute_72 sm_72 CUDA 9 Volta Jetson AGX Xavier
compute_75 sm_75 CUDA 10 Turing RTX 2080, RTX 2070 Tesla T4
compute_80 sm_80 CUDA 11.1 Ampere A100
compute_86 sm_86 CUDA 11.1 Ampere RTX 3090
compute_87 sm_87 CUDA 11.1 Ampere Jetson AGX Orin
compute_89 sm_89 CUDA 11.8 Lovelace RTX 4090
compute_90 sm_90 CUDA 12 Hopper H100 H200
compute_95 sm_95 CUDA 12 Blackwell B100

2 PTX

上一节提到的 PTX (Parallel Thread Execution) 是 Nvidia 为 CUDA 设计的一种低级虚拟机和指令架构,它是CUDA 代码的中间表示形式,有以下特征:

  1. 由虚拟指令集定义:用一套虚拟的并行指令集,用于表示 CUDA 设备功能。
  2. 目标独立:采用抽象的寄存器和线程模型,不依赖于具体的 GPU 架构,可以针对不同的 GPU 生成优化机器代码。
  3. 可移植: PTX 代码可以在不同的 CUDA 运行环境和 GPU设备上执行。
  4. JIT 编译: PTX 代码在执行前,有 GPU 驱动编译生成针对特定 GPU 的机器代码。
  5. 可读性好:PTX 代码结构类似汇编,以文本形式保存,便于阅读和调试。
  6. 虚拟寻址:提供了统一的虚拟寄存器和寻址空间

3 JIT 编译

PTX 代码在目标机器上执行前,会由 GPU 驱动将 PTX 代码优化、编译为机器代码。PTX 代码的编译结果会被缓存下来,以免重复编译。但是如果更新了 GPU 驱动,那么 PTX 代码会重新编译。

在不同操作系统下 JIT 缓存目录

操作系统 缓存目录
Linux ~/.nv/ComputeCache
Windows %APPDATA\%NVIDIA\ComputeCache
MacOS $HOME/Library/Application Support/NVIDIA/ComputeCache

与 JIT 相关的环境变量配置

环境变量 默认值 说明
CUDA_CACHE_DISABLE 0 是否禁用 JIT cache,1 代表禁用 0 代表不禁用
CUDA_CACHE_MAXSIZE 256MB cache 的尺寸,最大值 4GB, 在 334 版本驱动前,默认值是 32MB
CUDA_CACHE_PATH 设置 cache 目录
CUDA_FORECE_PTX_JIT 0 设置为 1 时,强制忽略预编译好的机器代码,使用 JIT 从 PTX 代码中编译出机器代码

4 编译参数 arch & code

nvcc 编译器在编译 CDUA 代码的时候提供了 -gencode 参数,这个参数接收 arch=compute_xxcode=sm_xx 作为参数。也可以单独使用 -arch-code 来指定。

nvcc x.cu -gencode arch=compute_50,code=sm_50

–gpu-architecture(-arch)

这个参数接受虚拟架构作为参数,通常来说这个参数与最终编译出来的 PTX 代码无关,它只是作为编译 CUDA 代码时候的预处理参数。

在 CUDA 编程中提供了宏 __CUDA_ARCH__ 可以通过这个宏来控制编译的内容,当编译参数为 -arch=compute_35 时,__CUDA__ARCH__ 的值就是 350。

–gpu-code(-code)

这个参数指定了 CUDA 代码实际要编译、优化的 PTX 代码对应的 Nvidia GPU。 其中虚拟架构代码 compute_xx 用来指定 PTX 代码版本,真实架构代码 sm_xx 用来指定机器代码。 如果程序在执行前无法找到设备 GPU 对应的二进制代码,那么就是使用 JIT 从 PTX 代码中编译出对应的机器代码。

同时使用 -arch-code 参数时候,要确保使用的虚拟架构与真实架构兼容的。

当编译时只提供了 -arch 参数,这个时候其实是 -arch-code 的简写,-code 的值默认等于前者。

# 提供虚拟架构代码
nvcc -arch=compute_61 
# 等价于
nvcc -arch=compute_61 -code=compute_61

# 提供真实架构代码
nvcc -arch=sm_61 
# 等价于
nvcc -arch=compute_61 -code=sm_61,compute_61

胖二进制(Fat Binaries)

nvcc x.cu 
	-gencode arch=compute_50,code=sm_50
	-gencode arch=compute_60,code=sm_60
	-gencode arch=compute_79,code=\'compute_70,sm_70\'
			

在编译的时候可以同时制定多套参数,编译出来的代码就可以支持多种不同架构的 GPU 设备,在具体执行的时候,驱动会选择当前真实架构对应的机器代码,如果找不到,那么就使用兼容的 PTX 代码 JIT 编译出对应的机器代码,这会导致在第一次执行时候速度会比较慢。

这种将同一份代码编译出多个不同架构代码的方式就叫”fat binaries”, 它带来的好处是让代码一次编译就能支持在多种设备上运行;它的缺点是,随着指定的架构版本越来越多,编译速度也越来越慢,编译出来的代码体积也越大。