CUDA系统拆解-14-PTX、SASS 与编译链:CUDA代码如何落到机器指令

本文是「CUDA系统拆解」系列第 14 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-13-面试强化专题:PTX、Tensor Core、CUTLASS 与 Triton 怎么讲
下一篇:CUDA系统拆解-15-Tensor Core、WMMA 与 MMA:矩阵乘指令路径怎么打通

1. 这篇解决什么问题

这一篇要讲清 5 件事:

  • 为什么 CUDA 体系里需要 PTX 这一层,而不是直接把 .cu 编成某张卡的最终机器码。
  • PTXSASS 分别是什么,它们的本质区别在哪里。
  • 一个 CUDA 程序大致会经历怎样的编译和装载链路。
  • fatbin、JIT、前向兼容这些词到底在解决什么工程问题。
  • 在真实工程里,什么时候你需要关心这条链路,什么时候又不需要过度下钻。

如果这篇只记住一句话,那就是:

PTX 负责在高层 CUDA 代码和具体 GPU 架构之间提供一层可移植中间表示,SASS 才是最终真正落到某代芯片上的执行指令。

2. 先记住的核心结论

  • PTX 是 NVIDIA CUDA 路径中的虚拟 ISA / 中间表示,不等于最终机器码。
  • SASS 是面向具体 GPU 架构的真实机器指令,更接近芯片最终执行内容。
  • 之所以需要 PTX,核心是要在“高层编程抽象”和“具体硬件差异”之间加一层缓冲,兼顾编译灵活性和一定程度的前向兼容。
  • .cu 文件不会直接变成一份单一产物,中间可能涉及主机代码编译、设备代码编译、PTXcubinfatbin、运行时装载和可能的 JIT。
  • fatbin 的价值是把多种目标代码打包在一起,让同一个程序更容易覆盖多种 GPU。
  • JIT 的意义是:当没有完全匹配的预编译机器码时,可以基于 PTX 在运行时继续为目标设备生成更合适的代码。
  • 工程里通常只在性能定位、架构兼容、部署打包或指令路径分析时,需要认真关心这条链路。

3. 正文讲解

3.1 为什么需要 PTX

如果 CUDA 没有 PTX 这一层,最直接的方案就是:

  • 你写 .cu
  • 编译器直接为某个具体 GPU 架构吐出最终机器码

这当然能工作,但问题很多:

  • 不同 GPU 架构的指令细节并不完全一样
  • 同一份源码如果想兼容多代 GPU,打包和分发会更麻烦
  • 编译器优化和后续目标设备适配空间会变小

所以 PTX 的核心作用,就是在中间加一层“还没有完全绑死到某张卡”的表示。

你可以把它理解成:

  • 高层:程序员写的是 CUDA C++
  • 中层:编译器先把设备端逻辑降到 PTX
  • 底层:再根据具体目标架构生成真正执行的 SASS

这样设计的好处,是能把“程序语义”和“具体架构落地”部分解耦一些。

3.2 PTX 和 SASS 各是什么

最稳的理解方式是:

  • PTX:虚拟指令集 / 中间表示
  • SASS:具体 GPU 架构上的机器指令

两者的差别,不只是“一个更早、一个更晚”,而是抽象层级不同。

PTX 更像是在表达:

  • 这个 kernel 想做什么
  • 数据和指令的大致组织方式
  • 给后续编译 / JIT 留出进一步映射空间

SASS 更像是在表达:

  • 针对某代 SM、某套指令能力
  • 最终到底发出了哪些真实机器指令
  • 调度和执行最终更接近什么样子

所以不能把 PTX 当成最终执行内容,也不能把看过高层 CUDA 代码就等同于看过真实指令路径。

3.3 从 .cu 到最终执行,大致经历什么

最粗粒度的编译链路可以这样理解:

  1. 你写下 .cu
  2. 编译器把主机代码和设备代码拆开处理
  3. 设备代码先被降成 PTX,也可能进一步生成某些目标架构的 cubin
  4. 多种设备端产物可以一起打进 fatbin
  5. 程序运行时,CUDA runtime / driver 为当前设备选择可用代码
  6. 如果已经有匹配架构的机器码,就直接装载
  7. 如果只有 PTX 或没有完全匹配的机器码,就可能触发 JIT,再生成适合当前设备的最终代码

这条链路的重点不是背每个工具细节,而是理解:

  • 主机和设备代码不是一锅编完
  • 设备代码可能同时以多种形式存在
  • 最终执行内容和部署时打包内容不一定完全同一份

3.4 cubin、fatbin、PTX、SASS 的关系

这几个词经常一起出现,容易混。

可以这样记:

  • PTX:中间表示
  • SASS:最终机器指令
  • cubin:包含某个目标架构机器码的二进制产物
  • fatbin:把多个设备端产物打包到一起的容器

从工程视角看,fatbin 最实用的意义是:

  • 你可以同时放入多个架构的预编译代码
  • 也可以保留 PTX 作为兜底路径
  • 程序部署后,不必只服务单一型号 GPU

所以 fatbin 解决的是“分发和兼容”的问题,PTX 解决的是“中间表示和后续映射”的问题,SASS 解决的是“具体设备最终执行”的问题。

3.5 JIT 和前向兼容的工程意义

JIT 常常被说得很抽象,其实直觉并不复杂。

假设你发布程序时,没有为用户当前这张 GPU 预编译完全匹配的机器码,但你保留了 PTX
那么运行时系统仍然有机会:

  • 读取 PTX
  • 针对当前设备继续编译
  • 生成可执行的目标代码

这就是 JIT 的意义。

它为什么重要?

  • 提高部署覆盖面
  • 减少“每个设备都必须完全重新打包”的压力
  • 给未来硬件留出一定前向兼容空间

但也要看到代价:

  • 首次运行可能增加编译开销
  • 最终生成结果受驱动和环境影响
  • 如果你特别关心启动延迟或完全可控的部署表现,往往更希望提前准备好更匹配的目标代码

所以工程上常见做法不是“只靠 JIT”,而是:

  • 对主流目标架构准备预编译代码
  • 同时保留 PTX 作为兼容兜底

3.6 什么时候需要关心这条链路

不是每个 CUDA 开发者都需要天天盯着 PTXSASS
更实际的问题是:什么时候这条链路会变成重要信息?

典型场景有:

  • 你在做跨架构部署,想知道包该怎么打
  • 某个 kernel 在新旧 GPU 上表现差异明显
  • 你怀疑高层代码没有走到预期指令路径,例如没走 Tensor Core
  • 你在分析启动延迟,怀疑有 JIT 成本
  • 你在看更底层的性能问题,想确认编译器最终生成了什么

反过来说,如果你只是正常调用成熟库,且没有性能或兼容性异常,那么没必要把所有精力都花在反汇编级细节上。

4. 和 AI 推理的关系

这一篇对推理工程的价值,主要在 3 个地方:

  • 你更容易理解为什么同一套推理程序在不同 GPU 上表现会不同
  • 你更容易判断某个 kernel 到底有没有落到预期的 Tensor Core / 低精度路径
  • 你更容易看懂部署、打包、运行时兼容和启动开销之间的取舍

尤其在 AI infra 场景里,常见问题都会碰到这条链路:

  • 为什么某台机器第一次加载 engine 或 kernel 比较慢
  • 为什么同样模型换一代 GPU 吞吐差很多
  • 为什么某些构建产物要同时带多种架构目标
  • 为什么性能定位时有时需要继续看 PTXSASS

所以这不是“编译器内部知识点”,而是会直接影响推理部署和性能判断的一层背景。

5. 常见误区

  • PTX 不是最终机器码,它更像中间表示。
  • SASS 才更接近真实执行指令,所以两者不能混为一谈。
  • PTX 不代表一定没有额外成本;运行时 JIT 可能带来首次启动开销。
  • fatbin 不是一种指令,而是打包多种设备端产物的容器思路。
  • 不是所有场景都要下钻到 PTX / SASS,但一旦涉及性能异常、兼容性或部署问题,这条链路就很关键。

6. 复习自测

  • 为什么 CUDA 体系里需要 PTX 这一层?
  • PTXSASS 的本质区别是什么?
  • .cu 到最终执行,通常会经历哪些关键阶段?
  • cubinfatbin 分别在解决什么问题?
  • JIT 为什么能带来前向兼容能力?它的代价又是什么?
  • 在 AI 推理工程里,哪些问题会让你必须开始关心 PTX / SASS / fatbin / JIT 这条链路?

系列导航