torch.compile 做了什么?
和 eager mode 有什么区别?
先弄清楚:每个概念是什么
- Eager mode
- PyTorch 默认的跑法。Python 执行到一个 op 就 launch 一个 kernel,结果写回 HBM。y = relu(x * w + b) 是 3 个 op,也就是 3 个 kernel。
- TorchDynamo
- 在 Python 执行函数之前把它拦下来,把里面的 tensor op 记成一张 FX graph。
- AOTAutograd
- 提前把 backward 的 graph 也 trace 出来,forward 和 backward 都能交给 compiler 优化。
- TorchInductor
- 默认的 backend compiler,对 graph 里的 op 做 fusion,在 GPU 上生成 Triton kernel(见 Triton 和 CUDA)。
- Guard
- 编译结果能复用的前提条件,每次调用先查。tensor 的 guard 会查 dtype、device 和 shape,shape 变了就失败。
看图:编译的三步,和之后每次调用
30 秒回答
In eager mode, Python runs one op at a time: each op launches its own kernel and writes its result back to GPU memory. torch.compile does three things. TorchDynamo captures the Python function into a graph. AOTAutograd traces the backward pass ahead of time, so both directions can be optimized. TorchInductor fuses ops and generates Triton kernels on GPU, so there are fewer launches and less memory traffic. Compiled code is protected by guards, for example on input shapes; when a guard fails, it recompiles. The compiler can only fuse ops it can see into.
三步:捕获 graph,拿到 backward,生成 kernel
| 步骤 | 组件 | 要记住的一点 |
|---|---|---|
| ① | TorchDynamo | 捕获 graph 时同时生成 guard。遇到不支持的代码会 graph break,已捕获的部分先编译,这一段回 Python 跑,后面另起一张 graph |
| ② | AOTAutograd | forward 和 backward 都能被加速,所以训练也能用 |
| ③ | TorchInductor | fusion 的收益见 kernel fusion |
和 eager 的区别,以及 recompile
| Eager | torch.compile | |
|---|---|---|
| 执行单位 | 一个 op 一个 kernel,Python 逐个调度 | 一张 graph 编译一次,fusion 后 kernel 更少 |
| 第一次调用 | 直接跑 | 要先编译,前几次调用更慢 |
| 输入的 shape 变了 | 没有影响 | guard 失败,recompile |
已有的编译结果 guard 全都不通过时才 recompile。参数 dynamic 默认是 None,发现 shape 变过,recompile 时会生成一个更 dynamic 的 kernel。次数到上限(默认 8)后这个函数回到 eager,shape 一直在变的输入最后可能还是跑 eager。
Compiler 的边界:FlashVector 的 embedding bag
FlashVector Case 2 的 baseline 用 AOTInductor 编译。torch.compile 是 JIT,AOTInductor 是 TorchInductor 的提前编译版本,把 torch.export 导出的模型编译成 shared library,部署时不需要 Python。
编译过之后 embedding bag 仍然占 28.1% 的 GPU time,因为 PyTorch 把它映射成一个 "opaque ATen dispatcher kernel",compiler 看不到里面。agent 把 masked-mean pooling 拆成基本的 PyTorch op,compiler 才把它们融成一个 Triton kernel。这是 agent 能补上的地方。
必答点
- eager:一个 op 一个 kernel,Python 逐个调度,中间结果写回 HBM
- 三步:TorchDynamo 捕获 graph,AOTAutograd 拿到 backward,TorchInductor 做 fusion 并生成 Triton kernel
- 收益来自 launch 更少、访存更少,计算量不变
- guard 失败就 recompile;到上限后回到 eager
- 不支持的 Python 代码会 graph break;看不进去的 op 融不了
- AOTInductor 提前编译,部署不需要 Python,FlashVector 的 baseline 就是它
追问
CUDA Graph 和 torch.compile 是什么关系?(滴滴二面问过)
省的东西不同。torch.compile 靠 fusion 减少 kernel 的数量和访存,CUDA Graph 减少每次 launch 的 overhead。reduce-overhead 模式把两者叠在一起用,文档说它适合小 batch。
什么时候 torch.compile 反而更慢?
前几次调用要付编译时间,shape 一直变会反复 recompile,graph break 会把 graph 切碎。
benchmark 时为什么要和 torch.compile 比?
eager 太弱,快过它说明不了什么,所以 CUDA Agent 的 reward 把快过 torch.compile 单列一档(见 kernel RL 的 reward)。
来源PyTorch 文档:torch.compiler(TorchDynamo、AOT Autograd、TorchInductor 的定义)· torch.compile API(mode、dynamic)· torch.compile troubleshooting(graph break、guard、recompile 上限)· AOTInductor · FlashVector §5.2 · ml-八股 08 第 12 题 · 面经出处见 面试题与 JD 1.4、3.4 节和 NVIDIA 题库笔记 1.9 节