当运行 CUDA 核心时会发生什么:从源码到 SASS
当运行 CUDA 核心时会发生什么:从源码到 SASS
启动 CUDA 核心涉及主机 CPU、NVIDIA 用户态驱动、内核态驱动以及 GPU 硬件之间的复杂协同。该过程将高级 C++ 代码转换为硬件特定的指令流和 SASS 指令,利用专门的内存映射门铃系统在成千上万的并行线程上触发执行。
编译流水线:PTX 与 SASS
执行 CUDA 程序需要多个编译阶段,以弥合设备无关代码与硬件特定机器指令之间的差距。
nvcc 充当驱动程序,协调多个编译器。主机代码交给标准主机编译器,而设备代码走特定路径:cicc(基于 LLVM 的编译器)生成 PTX(Parallel Thread Execution),随后 ptxas 将其转换为 SASS(Streaming Assembler)。
PTX 与 SASS
- PTX 是一种虚拟指令集架构(ISA)。它与设备无关,使用无限数量的类型化寄存器。它充当前向兼容的后备方案;如果二进制在预编译 SASS 未覆盖的 GPU 架构上运行,驱动程序可以在加载时 JIT 编译 PTX 为全新的 SASS。
- SASS 是实际在 GPU 上运行的机器码。它将虚拟寄存器映射到有限的物理寄存器,并将复杂的 PTX 操作融合为单条硬件指令(例如
IMAD.WIDE)。
最终的二进制是一个 fatbin,其中同时包含 SASS(以 ELF 容器形式)和压缩的 PTX。该 fatbin 被嵌入主机可执行文件的 .nv_fatbin 段中。
从主机触发 GPU
由于 GPU 位于 PCIe 总线另一侧,CPU 不能直接 “调用” GPU 函数。它必须通过驱动管理的工作队列进行通信。
主机启动桩
当 nvcc 编译一次核函数调用(例如 vadd<<<4096, 256>>>)时,它会用生成的主机启动桩替换该表达式。该桩将核函数参数按特定字节偏移打包到主机内存的缓冲区中,然后调用 __cudaLaunch,该函数使用主机侧的函数指针作为查找键,以在 fatbin 中找到对应的设备侧符号。
驱动桥接
CUDA 运行时与闭源用户态驱动(libcuda.so)交互,后者再通过对 /dev/nvidiactl 等设备文件的 ioctl 调用与内核态驱动(nvidia.ko)通信。
自 CUDA 12.2 起,模块加载默认 惰性。驱动程序会推迟将 SASS cubin 上传到 GPU 内存,直到首次真正启动该核函数时才进行。
硬件启动机制
GPU 执行由一系列从主机内存读取的命令驱动,这些命令通过一个由三大结构组成的 “通道” 协调:
- Pushbuffer:主机内存中的一块区域,驱动在此写入 “methods”(寄存器地址和值),定义 GPU 的动作。
- GPFIFO:指针环形缓冲区,告诉 GPU 从 pushbuffer 的哪些区段读取。
- USERD:设备内存中的小结构,包含光标(
GP_GET与GP_PUT),用于跟踪工作消耗情况。
门铃
现代 GPU(Turing 及以后)不再监视 GP_PUT 光标。相反,驱动通过写入工作提交令牌到 门铃(一个内存映射寄存器)来触发。此操作会通知 GPU 的主机引擎去获取更新后的 GP_PUT 并通过 DMA 从 pushbuffer 拉取方法。
队列元数据(QMD)
最关键的方法之一是 Queue Meta Data (QMD) 的流式传输。QMD 是启动描述符,告诉 GPU:
- 网格和块的维度(例如 4096 个块,每块 256 线程)。
- 每线程所需的寄存器数和共享内存大小。
- SASS 代码的内存地址。
- 保存核函数参数的常量区的地址。
在流式多处理器(SM)上的执行
一旦 QMD 交给计算工作分配器(GigaThread Engine),GPU 将线性 SASS 指令映射到可用的流式多处理器(SM)上。
资源约束与占用率
在 RTX 4090(AD102)上,SM 受线程容量和寄存器文件大小限制。对于每线程使用 16 个寄存器、每块 256 线程的核函数:
- 寄存器容量:$65,536 / (256 \times 16) = 16$ 块。
- 线程容量:$1,536 / 256 = 6$ 块。
线程容量是更紧的瓶颈,这意味着每个 SM 最多容纳 6 个驻留块(48 个 warp)。
Warp 合格性与延迟隐藏
与 CPU 不同,GPU 不使用复杂的乱序执行逻辑,而是通过在众多驻留 warp 之间切换来隐藏延迟。warp 是否 合格 运行取决于 ptxas 在 SASS 指令中打包的控制码负载:
- 静态停顿计数:对于固定延迟操作,编译器精确编码 warp 必须停留的周期数。
- Yield Hint:一个提示位,建议调度器优先调度其他 warp。
- 依赖屏障索引:对于可变延迟操作(如全局内存加载
LDG),硬件使用六个物理 scoreboard 屏障。warp 在其等待的特定屏障被清除之前不可调度。
内存层次结构与数据传输
当 warp 发出加载指令(LDG.E)时,SM 的加载/存储单元执行 请求合并。如果 warp 中的 32 条线程访问连续的 4 字节浮点数,硬件会将这些请求合并为四个 32 字节的扇区请求,以最小化总线流量。
数据流经 L1 数据缓存 → L2 缓存 → 内存控制器 → GDDR6X 显存。在算力强度低的核函数(如向量加法)中,性能通常受 DRAM 总线带宽限制,而非计算能力。
将结果返回给 CPU
当最后一个块完成后,GPU 会在 QMD 中定义的完成信号量上打标记。随后 GPU 的拷贝引擎会将结果从 L2 缓存(或显存)通过 DMA 传回主机内存。拷贝完成后,它会再打一个信号量,允许主机的 cudaMemcpy 调用返回,CPU 继续执行(例如调用 printf)。
摘要
深入探讨 CUDA 核心的生命周期,追踪其从 nvcc 编译、PTX/SASS 生成到在 RTX 4090 上硬件层面的执行全过程。