Lesson 0003:Backend 族谱 + 训练反传如何被编译
前两课你把 torch.compile 的流水线搞清楚了。这一课深入一个前两课刻意略过的基础——
自动微分(autograd)到底怎么工作的,
然后再看 AOTAutograd 如何把它从"运行时查 tape"变成"编译时静态图"。
最后回答那个最关键的问题:TVM 到底能不能只复用 AOTAutograd 的联合图来做训练编译?
1. 先修:Autograd 基础 —— tape 是什么,梯度怎么算
PyTorch 的 autograd 是反向模式自动微分(reverse-mode automatic differentiation)的实现。
它是整个训练编译栈的基石——不理解它,就不理解为什么 AOTAutograd 是必要的。
1.1 从导数到梯度
我们先从最简单的概念开始,再逐步加入多变量和向量:
▸ 导数(derivative)—— 单变量标量函数
f(x) = x²
f'(x) = 2x # 一个数:函数在 x 处的变化率
▸ 偏导数(partial derivative)—— 多变量标量函数
f(x, w, b) = x·w + b
∂f/∂x = w # 固定其他变量,只看 f 随 x 的变化率
∂f/∂w = x
∂f/∂b = 1
▸ 梯度(gradient)—— 所有偏导数的向量
∇f = [∂f/∂x, ∂f/∂w, ∂f/∂b]
= [w, x, 1] # 一个向量:指向 f 增长最快的方向
关键区分:导数是一个标量(单变量函数的变化率),
梯度是一个向量(多变量函数每个参数的偏导数组成的向量)。
深度学习里说的"梯度"几乎总是指后者——loss 对每个参数的偏导数。
1.2 autograd 如何自动算梯度:计算图 + tape
PyTorch 的 autograd 分两步工作:
┌─────────────────────────────────────────────────┐
│ 前向传播(forward)—— 边算边记录 │
├─────────────────────────────────────────────────┤
│ │
│ x ──► [mul: y = x*w] ──► y ──► [add: z = y+b] ──► z
│ │ │ │
│ ▼ ▼ │
│ tape 记录: tape 记录: │
│ op = mul op = add │
│ inputs = (x, w) inputs = (y, b) │
│ saved = (x, w) saved = (y, b) │
│ backward_fn = ... backward_fn = ... │
│ │
│ 计算图(隐式构建在 tape 上): │
│ │
│ x ──┐ │
│ ├──► [mul] ──► y ──┐ │
│ w ──┘ ├──► [add] ──► z = loss │
│ b ──┘ │
│ │
│ (实际并不显式构建这个图——tape 是扁平的记录序列, │
│ 图结构在反向执行时通过输入/输出关系隐式还原) │
└─────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────┐
│ 反向传播(backward)—— 逆序遍历 tape │
├─────────────────────────────────────────────────┤
│ │
│ 起点:∂loss/∂loss = 1(标量 loss 对自己的梯度 = 1) │
│ 这个 1 也叫 "seed gradient" 或 "初始上游梯度" │
│ │
│ 逆序遍历 tape: │
│ │
│ 读到 add 的记录 ←── 当前上游梯度 = 1 │
│ ∂z/∂y = 1, ∂z/∂b = 1 → grad_y = 1·1 = 1, grad_b = 1 │
│ │ │
│ 读到 mul 的记录 ←── 当前上游梯度 = 1(grad_y)←───────────────┘
│ ∂y/∂x = w, ∂y/∂w = x → grad_x = 1·w, grad_w = 1·x │
│ │
│ 最终:grad_x = w, grad_w = x, grad_b = 1 │
│ 和前面手算的 ∇f = [w, x, 1] 一致 ✓ │
└─────────────────────────────────────────────────┘
Tape 是什么?
Tape(磁带)是一个运行时数据结构——PyTorch 在前向传播执行每个操作时,
把"这个操作是什么、输入是什么、反向传播时该用什么公式"记录在一条链上。
名字来源于磁带机的比喻:前向 = 录音(record),反向 = 倒带重放(rewind & replay)。
磁带只能单向读取,后记录的先读到——所以是后进先出,正好对应链式法则的从后往前。
关键点:tape 是运行时动态构建的。不是提前声明一个静态图——每执行一行 Python,
autograd 就往 tape 上追加一条记录。这是 PyTorch "define-by-run" 哲学的核心。
1.3 亲手看 tape:一个可跑的示例
import torch
x = torch.tensor([2.0], requires_grad=True)
w = torch.tensor([3.0], requires_grad=True)
b = torch.tensor([1.0], requires_grad=True)
# 前向 —— 边算边往 tape 上记录
y = x * w # tape 记录 mul
z = y + b # tape 记录 add
loss = z.sum() # tape 记录 sum
print(f"loss = {loss.item()}") # loss = 7.0 (x*w + b = 2*3 + 1)
# 反向 —— 逆序遍历 tape
loss.backward()
# 检查梯度
print(f"grad_x = {x.grad}") # ∂loss/∂x = w = 3.0
print(f"grad_w = {w.grad}") # ∂loss/∂w = x = 2.0
print(f"grad_b = {b.grad}") # ∂loss/∂b = 1.0
你可以用 torchviz 或 torch.fx 把这张图可视化出来:
# 查看 autograd 图(需要 pip install torchviz)
from torchviz import make_dot
make_dot(loss, params={"x": x, "w": w, "b": b}).render("autograd_graph", format="png")
重要限制:tape 是"一次性"的。
loss.backward() 执行完后,tape 被释放。
如果再次调用 backward(),需要 retain_graph=True。
这也是为什么编译时需要 AOTAutograd——它要在 tape 释放之前把图"冻结"成静态 FX Graph。
PyTorch autograd 官方文档 —
autograd 的完整机制,包括 tape、retain_graph、自定义 Function 等。
2. 编译栈的三层分工
在 autograd 的基础上,torch.compile 把编译栈分成三层。
不是每个 backend 都占三层——有些只占一层。
┌─────────────────────────────────────────────────┐
│ 第①层 Frontend(图捕获) │
│ Dynamo —— 劫持 Python 字节码 → FX Graph │
│ 这层总是 Dynamo,不换。 │
├─────────────────────────────────────────────────┤
│ 第②层 Autograd(训练反传) │
│ AOTAutograd —— 从 forward 图的 tape 推导 backward │
│ 图,并把 forward+backward 拼接为「联合图」 │
│ 推理时可以跳过这层。 │
├─────────────────────────────────────────────────┤
│ 第③层 Compiler Backend(图编译 → Kernel 生成) │
│ Inductor / TVM / XLA / TensorRT / eager … │
│ 接收联合图(或前向图),产出可执行代码 │
└─────────────────────────────────────────────────┘
关键认知:"后端"在 PyTorch 语境里通常指第③层——Dynamo 吐出图之后,谁来编译它。
但训练场景里,第②层 AOTAutograd 才是让反传可编译的关键。
没有它,Dynamo 只抓到 forward 图,backward 仍然走 eager tape 路径——编译器对 backward 无能为力。
3. AOTAutograd:从动态 tape 到静态联合图
§1 讲的原生 autograd 有一个根本问题:tape 是运行时动态的。
每一次 forward 执行,tape 的结构可能不同(取决于数据流过的分支)。
编译器需要一张静态图才能做优化——所以在 Dynamo 抓到 forward FX Graph 之后,
AOTAutograd 负责把 backward 也变成一张图。
3.1 AOTAutograd 的工作流
┌─────────────────────────────────────────────┐
│ AOTAutograd 的工作流 │
├─────────────────────────────────────────────┤
│ │
│ ① 拿到 forward FX Graph(Dynamo 产出) │
│ │ │
│ ▼ │
│ ② 用 eager mode 跑一遍 forward │
│ (这遍跑的目的是让 autograd tape 被记录) │
│ │ │
│ ▼ │
│ ③ 从 autograd tape 上读取每个 backward 函数 │
│ 并 trace 出 backward FX Graph │
│ │ │
│ ▼ │
│ ④ 将 forward 图 + backward 图 │
│ 拼接为一张「联合图」(joint graph) │
│ │ │
│ ▼ │
│ ⑤ 联合图发给 Compiler Backend(第③层) │
│ Inductor 一口气编译 forward+backward │
│ │
└─────────────────────────────────────────────┘
步骤②和③是 AOTAutograd 区别于 TVM 的核心能力:
在执行 forward 时,tape 被自动记录(这是 PyTorch C++ 核心做的事),然后 AOTAutograd
立刻读取 tape 上的信息,在 tape 被释放前把 backward 路径 trace 成 FX Graph。
3.2 联合图长什么样?一个可跑的具体例子
以下是一个最小但实际可跑的训练示例——注意 backward() 不在 torch.compile 包裹的函数里,
Dynamo 不会去 trace 它。
import torch
# ── 定义纯前向模型(torch.compile 只包前向)──
def model(x, w, b):
y = x @ w # matmul
y = y + b # add
y = y.relu() # relu
return y
# torch.compile 编译 model
compiled_model = torch.compile(model, fullgraph=True)
# ── 训练数据 ──
x = torch.randn(4, 4, device='cuda', requires_grad=True)
w = torch.randn(4, 4, device='cuda', requires_grad=True)
b = torch.randn(4, device='cuda', requires_grad=True)
target = torch.randn(4, 4, device='cuda')
# ── 训练步(backward 在 compile 之外)──
y = compiled_model(x, w, b) # ← Dynamo 抓前向图
loss = (y - target).pow(2).sum() # MSE loss(也在 compile 外)
print(f"loss: {loss.item():.4f}")
# backward 触发 AOTAutograd:
# Dynamo 已经把 model 的 forward 抓成 FX Graph
# → AOTAutograd 从 tape trace backward
# → 拼接联合图 → Inductor 编译 → 执行反传
loss.backward() # ← 不经过 Dynamo,直接走 autograd 引擎
print(f"grad_w[0,0]: {w.grad[0,0]:.6f}")
为什么 backward() 不能放在 torch.compile 函数里?
torch.compile 使用 Dynamo 劫持 Python 字节码来 trace 计算图。
但 Tensor.backward() 调用的是 C++ 的 autograd 引擎——它不是一个可以在 FX Graph
里表示的"算子",而是一个控制流指令("现在开始反向遍历 tape")。
Dynamo 看到 backward() 时会报:
torch._dynamo.exc.Unsupported: Unsupported Tensor.backward() call
正确做法:torch.compile 只包裹模型的前向函数。
当你在外面调用 loss.backward() 时,autograd 引擎检测到这个 loss 的图已经被 AOTAutograd 编译过,
于是直接执行编译好的 backward 图,而不是走 eager tape 路径。
这就是 AOTAutograd 的工作时机:不在编译时,而在第一次 backward 时。
PyTorch Dev Discuss: AOTAutograd and Training Support —
AOTAutograd 的设计动机和联合图机制。
4. Inductor 是谁开发的
Inductor 是 Meta(Facebook)PyTorch 团队开发的编译器后端,
作为 torch.compile 的默认 backend 随 PyTorch 2.0(2023 年 3 月)一起发布。
为什么叫 Inductor?
Inductor 这个名字来自电子工程中的电感器(inductor)——电路中储存磁场能量的元件。
命名有三层隐喻:
① "电感耦合"算子:电感通过磁场耦合不同的电路回路。
类似地,Inductor 把不同的 PyTorch 算子"耦合"在一起——融合多个算子成一个 kernel,
减少显存往返。
② "自感"优化:电感有自感效应——电流变化时感应出反向电压。
对应 Inductor 内部的 Loop IR 会根据算子的内存访问模式自动诱导出最优的 tiling 和 fusion 策略。
③ 与 Dynamo 配对:Dynamo(发电机)产生电流(捕获图),Inductor(电感器)储存和转换能量(编译优化)。
两者组成"发电机-电感器"电路,是 PyTorch 编译栈的命名主题。
Inductor 的核心设计思路:不直接生成 CUDA 代码,而是生成 Triton 代码(GPU 路径)
或 C++/OpenMP 代码(CPU 路径)。之所以选 Triton 而不是 CUDA 作为代码生成目标,
是因为 Triton 的 block-level 抽象大幅降低了代码生成器的复杂度——这点在 §6 详讲。
| 属性 | 详情 |
| 开发者 | Meta PyTorch 团队(核心贡献者:Jason Ansel 等) |
| 首次发布 | PyTorch 2.0(2023 年 3 月 15 日) |
| 代码位置 | torch/_inductor/(PyTorch 源码树内) |
| 输入 | FX Graph(Dynamo 产出,可经过 AOTAutograd 拼接) |
| 中间表示 | Loop IR(自研的循环级 IR) |
| GPU 代码生成 | Triton(Python DSL → PTX)、部分手写 CUDA template |
| CPU 代码生成 | C++ / OpenMP(通过 loop scheduling) |
| 关键优化 | 算子融合、tiling、内存规划、autotune |
PyTorch 2.0 技术白皮书(arXiv:2304.04487) —
Dynamo 和 Inductor 的设计动机与架构。
5. 所有 Backend 的一张表
| Backend | 第②层 训练反传 | 第③层 编译+生成 | 主战场 | 硬件覆盖 |
| eager |
PyTorch autograd(原生) |
不做编译——逐算子 kernel dispatch |
基线对比 |
所有 |
inductor (默认,Meta 开发) |
AOTAutograd ✓ |
FX Graph → Loop IR → Triton/C++/OpenMP |
训练 + 推理 |
NVIDIA GPU x86 CPU |
| aot_inductor |
AOTAutograd ✓ |
同 Inductor,但提前编译(AOT)产出 .so/.pt2 |
推理部署 (无 Python 依赖) |
NVIDIA GPU x86 CPU |
| tvm |
❌ 无 autograd 推导能力 |
Relay/Relax → TIR → 各硬件后端 |
推理(多硬件) |
GPU+CPU+Mobile+FPGA+… |
| tensorrt |
❌ 无 |
图优化 + cuDNN/cuBLAS 手写库 |
推理(极致) |
NVIDIA GPU only |
openxla (XLA/TPU) |
XLA 自己的 autograd |
HLO → LLO → 硬件 |
训练 + 推理 |
TPU + GPU + CPU |
| onnxrt |
❌ 无 |
ONNX Graph → 各硬件 EP(Execution Provider) |
跨框架推理 |
多 |
| openvino |
❌ 无 |
OpenVINO IR → Intel 硬件优化 |
Intel 推理 |
Intel CPU/GPU/VPU |
eager 不是编译后端——它是编译的对立面。
当你调用 torch.compile(backend="eager") 时(实际中很少这样做),
Dynamo 仍然抓图,但抓完图之后不做任何编译——直接用 eager dispatch 逐个执行算子。
它主要用于 debug:确认 Dynamo 抓的图本身没问题,排除 Inductor 的嫌疑。
6. TVM 能不能只复用 AOTAutograd 做步骤⑤联合图编译?
这是整个课程最核心的技术判断。从 §3 的流程图看,AOTAutograd 的步骤⑤产出的是
FX Graph 格式的联合图——它是一张由 PyTorch ATen 算子组成的静态计算图。
TVM 的 Relay/Relax 前端确实可以接收 FX Graph 并转换为 Relay IR。
所以在纯理论上:AOTAutograd 产出联合图 → 转 Relay → TVM 编译 → 训练可行。
但工程实践上,这条路有五个死结:
死结 1:算子覆盖不完整
FX Graph 里的算子(部分):
aten::linear, aten::matmul, aten::relu,
aten::batch_norm, aten::_scaled_dot_product_efficient_attention,
aten::native_group_norm_backward,
aten::_foreach_addcmul_, ... ← 几百个算子
TVM Relay 能映射的(推理场景):
nn.dense, nn.relu, nn.batch_norm, nn.softmax,
nn.conv2d, ... ← 覆盖推理常用算子 ~80%
TVM Relay 无法映射的:
• 带 _backward 后缀的训练专用算子(TVM 没有 autograd,自然不会实现)
• PyTorch 2.0+ 的新算子(scaled_dot_product_attention 系列)
• 复合算子(_foreach_addcmul_ 等 optimizer 用到的 in-place 操作)
• 任何 TVM 社区没手动添加映射的算子
死结 2:动态 shape
TVM 的 auto-scheduling(AutoTVM / AutoScheduler)假设静态 shape——
它针对固定的张量维度搜索最优 tiling 和 schedule 配置。但训练中 shape 经常是动态的:
训练中的动态 shape:
• 可变 batch size(最后一个 batch 可能不满)
• 可变序列长度(NLP 里 padding → 实际长度不同)
• 动态 control flow(if-else 条件取决于数据值)
• Gradient accumulation:batch 拆分多次 forward/backward,每次 shape 相同但行为不同
TVM 如果对每种 shape 都重新 auto-tune → 编译时间远超训练时间
TVM 如果只用一种 shape tuning → 其他 shape 性能大幅退化
死结 3:AOTAutograd 不是稳定公开 API
AOTAutograd 的定位:
• 它是 torch.compile 内部组件,不是 torch 的公开 API
• 它的输出格式(FX Graph 算子集合、metadata 结构)每个 PyTorch 版本都可能变
• PyTorch 团队不承诺向后兼容——他们是给 Inductor 用的,不是给 TVM 用的
这意味着:
• TVM 社区如果想依赖 AOTAutograd,每次 PyTorch 升级都要追着适配
• 这个维护负担没有任何一个 TVM 贡献者愿意承担
死结 4:In-place 操作和 optimizer 状态
训练不只是 forward + backward——还有 optimizer.step():
Adam/SGD 的 optimizer.step() 做了什么:
param.mul_(1 - lr * weight_decay) # ← in-place 修改 param
param.add_(grad, alpha=-lr) # ← in-place 修改 param
(Adam 还有动量 m 和 v 的 in-place 更新)
TVM 图执行器的问题:
• TVM 把图编译成纯函数——输入张量 → 输出张量,不修改输入
• In-place 操作破坏了纯函数假设
• Optimizer step 涉及大量参数的原地修改和优化器状态的持久化
• TVM 没有"变量"的概念——它的运行时设计来源于推理(只读 weights → 只读输出)
死结 5:无人维护这条集成路径
这是最现实的死结。PyTorch 团队不维护、TVM 社区不维护、没有公司资助。
历史上存在过 torch_tvm 之类的实验项目(把 FX Graph 转 Relay),
但它们都是推理 only,且大多已停止更新。
结论:TVM 不能做训练编译——但原因不是它"不会编译",而是它缺少围绕训练的一整套生态。
技术上可以拆成:"AOTAutograd 推导向后图 + TVM 编译融合 kernel"。
但这要求 TVM 支持完整的 PyTorch 训练算子集、动态 shape 高效编译、in-place 参数更新——
这三件事加起来的工作量,相当于在 TVM 里重新实现半个 PyTorch。
7. Triton 为什么能帮你写 kernel?
Inductor 选择 Triton 而非直接生成 CUDA 代码,是经过深思熟虑的架构决策。
要理解这个决策,先看两种编程模型的差异。
7.1 CUDA 的编程模型:thread-level
写 CUDA kernel 你需要管理三个层次:
Thread ──► Warp (32 threads) ──► Block ──► Grid
│ │ │
▼ ▼ ▼
每个线程 同一 warp 内 不同 block 之间
处理一个 shuffle 通信 只有 global memory
元素 (共享寄存器) (无直接通信)
你需要手动:
• 决定 grid/block 的维度
• 给每个线程分配它该处理的元素
• 管理 shared memory 的分配、填充、同步
• 确保全局内存访问是 coalesced(合并对齐)
• 手动 __syncthreads()
7.2 Triton 的编程模型:block-level
写 Triton kernel 你只需要管 block 层面:
每个 program(类似 grid)
└─ 每个 block 处理一个 tile(数据分块)
Triton 编译器自动处理:
• Thread 映射:每个 block 里多少个 thread、每个 thread 干什么——自动分配
• 内存合并:保证同一个 warp 的 thread 访问连续地址——自动优化
• Shared memory:block 内的共享内存分配和同步——自动管理
• Tile size:最优分块大小——运行时 autotune
你只写:
pid = tl.program_id(0) # 我是第几个 block
offsets = pid * BLOCK_SIZE + tl.arange(0, BLOCK_SIZE) # 我处理哪些元素
data = tl.load(x_ptr + offsets) # 加载数据
result = compute(data) # 计算
tl.store(y_ptr + offsets, result) # 写回
| CUDA | Triton |
| 抽象层次 | 每个 thread 独立编程 | 每个 block 一组 thread 统一编程 |
| 内存管理 | 手动 shared memory 分配 | 编译器自动分配 |
| 同步 | 手动 __syncthreads() | block 结束时隐式同步 |
| 合并访问 | 手动保证对齐 | 编译器自动优化 |
| 分块大小 | 手动选择 | autotune 自动搜索 |
| 代码量(matmul) | ~200 行 | ~30 行 |
| 性能(vs 手写 CUDA) | 100%(基线) | 75-95%(取决于算子复杂度) |
为什么 Inductor 选择 Triton 而不是直接生成 CUDA:
Inductor 是一个代码生成器,它需要把融合后的 Loop IR 翻译成 GPU 代码。
如果目标的抽象层次是 CUDA(thread-level),Inductor 需要做大量复杂决策(thread 映射、shared memory 布局、同步点插入)。
这些决策很容易做错,错了性能就崩。
Triton 把抽象层次抬到了 block 级别——Inductor 只需要决定「这个循环用什么 tile size」,
剩下的 thread 级细节 Triton 编译器自动处理。这大幅降低了 Inductor 代码生成的复杂度。
Triton 官方 Vector Add 教程 —
用 10 行代码感受 block-level 编程。
8. 反向传播是怎么做出来的?
从 torch.compile 的角度看,反向传播不是神秘的黑箱——它是一串确定的元素级操作,
可以被编译、可以被融合。
8.1 链式法则 = 本地乘 + 往前传
前向:
x ──► [op: y = f(x)] ──► y ──► [op: z = g(y)] ──► z ──► loss
反向(从 loss 往回走):
∂loss/∂z = 1 (标量 loss 对自己的梯度 = 1,起点的 seed)
∂loss/∂y = ∂loss/∂z * ∂z/∂y = 1 * g'(y)
∂loss/∂x = ∂loss/∂y * ∂y/∂x = g'(y) * f'(x)
每个反传步骤只需要:前向时保存的中间值 + 上游传来的梯度 + 本层操作的局部导数公式。
8.2 常见算子的 backward
| 前向操作 | 局部导数 (∂y/∂x) | Backward 需要什么 |
| y = x + c(add) | 1 | 上游梯度直接传递 |
| y = x * w(mul) | w | 上游梯度 * w → grad_x;上游梯度 * x → grad_w |
| y = relu(x) | (x>0) ? 1 : 0 | 前向时保存的 mask(x 的正负) |
| y = sum(x) | 全是 1 | 上游梯度广播到 x 的形状 |
| y = softmax(x) | y*(1-y) 形式 | 前向输出 y(不需要 x) |
| y = matmul(A, B) | A → B^T;B → A^T | 另一个输入矩阵 |
8.3 为什么编译能加速训练(不只是 forward)
Eager 模式:每个 backward 步骤独立 dispatch
grad ← backward_op1 → kernel launch → 显存写
grad ← backward_op2 → kernel launch → 显存写
grad ← backward_op3 → kernel launch → 显存写
# 3 个 kernel,2 次中间结果写回 HBM
编译模式:AOTAutograd 拼接 forward+backward → Inductor 融合
fwd_ops... → bwd_op1 → bwd_op2 → bwd_op3
└──────────────────┬──────────────────┘
1 个大 kernel(甚至 forward 的一部分也融进来)
# 减少 kernel launch + HBM 往返
同样的融合逻辑,forward 里有用,backward 里效果甚至更好——因为 backward
里充满了 element-wise 操作(梯度相乘、relu mask 应用、broadcast),
这些是最容易融合的算子类型。
9. 动手验证:一条命令看到 forward+backward 的联合编译
在你 GPU 环境里跑这段——它能正常运行,不会报 Unsupported Tensor.backward():
TORCH_LOGS="output_code" python -c "
import torch
# ═══ torch.compile 只包裹纯前向函数 ═══
def forward(x, w, b):
y = x @ w # matmul
y = y + b # add
y = y.relu() # relu
return y
fn = torch.compile(forward, fullgraph=True)
# ═══ 数据 ═══
x = torch.randn(4, 4, device='cuda', requires_grad=True)
w = torch.randn(4, 4, device='cuda', requires_grad=True)
b = torch.randn(4, device='cuda', requires_grad=True)
target = torch.randn(4, 4, device='cuda')
# ═══ 训练步 ═══
y = fn(x, w, b) # ← Dynamo 抓前向图
loss = (y - target).pow(2).sum() # ← MSE loss(不在 compile 里)
loss.backward() # ← AOTAutograd 首次 backward 时编译联合图
print(f'loss: {loss.item():.4f}, grad_w[0,0]: {w.grad[0,0]:.6f}')
" 2>&1 | grep -E "(triton_|def triton|backward|AOTAutograd)" | head -15
看输出时找:
• Triton kernel 名带 fused_ 前缀 → forward 多个算子被融合
• Triton kernel 名带 backward → AOTAutograd 推导的 backward 也被编译
• 如果看到 tl.where(z > 0, ..., ...) 前面连着 mul/add——relu 的 forward+backward 全在一个 kernel 里
10. 小结
- Autograd tape 是基础:前向记录操作序列,反向逆序回放。这是 PyTorch define-by-run 的核心——图不是在运行前声明的,而是在运行时动态构建的。
- AOTAutograd 把动态 tape 变成静态联合图:前向跑一遍 → 读 tape → trace backward 图 → 拼接 forward+backward 联合图 → 丢给 Inductor 编译。它在第一次 backward 时触发,不在编译时。
- Inductor 是 Meta 开发的默认 backend:随 PyTorch 2.0(2023.3)发布,
使用自研 Loop IR 做循环优化,生成 Triton(GPU)或 C++/OpenMP(CPU)代码。命名来自电感器(inductor)的工程隐喻。
- TVM 理论上可复用 AOTAutograd,实践上不行:五个死结——算子覆盖不全、动态 shape 支持差、AOTAutograd 不是稳定 API、in-place optimizer 操作、无人维护集成路径。工作量相当于在 TVM 里重写半个 PyTorch。
- Triton 抽象到 block 层:免去 thread 映射、shared memory 管理、同步这些 CUDA 的手工活——Inductor 不必操心这些就能生成高效 kernel。
- Backward 也是 matrix ops + element-wise:编译一样有效,甚至更有效(backward 中 element-wise 占比更高)。
- torch.compile 不包 backward():只包裹前向函数,backward 在外层调用——AOTAutograd 在首次 backward 时自动介入。
11. Quiz
检验一下
1. PyTorch 的 autograd tape 是什么?
一个提前声明好的静态计算图
前向执行时动态记录操作序列的运行时数据结构,反向时逆序回放
一个存储在硬盘上的日志文件
CUDA kernel 的调用栈记录
2. 导数(derivative)和梯度(gradient)的区别是什么?
没有区别,同一个概念的两种叫法
导数是标量函数的输出,梯度是向量函数的输出
导数是标量函数单变量的变化率,梯度是多变量函数所有偏导数的向量
导数用于前向传播,梯度用于反向传播
3. AOTAutograd 在什么时候触发联合图编译?
在 torch.compile() 被调用时
在 Python 脚本启动时
在首次 loss.backward() 被调用时
在每个 training step 的 forward 开始时
4. TVM 为什么不能简单地"复用 AOTAutograd 的联合图 + 只做步骤⑤编译"?
因为 TVM 不支持 GPU 硬件
因为联合图是加密的,TVM 无法读取
因为 TVM 没有代码生成能力
算子覆盖不全 + 动态 shape 支持差 + AOTAutograd 不是稳定 API + in-place optimizer 操作 + 无人维护集成
5. 以下哪个是 Inductor 的正确背景?
NVIDIA 开发的商业编译器,随 CUDA 12 发布
Meta PyTorch 团队开发,随 PyTorch 2.0(2023.3)发布,名字来自电感器的工程隐喻
Google 开发,原本给 TPU 用,后来移植到 GPU
开源社区独立项目,不属于任何公司
6. 以下代码有什么问题?(model 含可训练参数)
@torch.compile 不能用于训练,只能推理
backward() 不应该被调用——AOTAutograd 会自动处理
backward() 不能放在 torch.compile 函数内——它是 C++ autograd 引擎调用,Dynamo 无法 trace
代码没有问题,可以正常运行
@torch.compile
def train_step(model, x, target):
loss = loss_fn(model(x), target)
loss.backward() # ← 这行
return loss
💬 有疑问?
"tape 上记录的 backward 函数是怎么 trace 成 FX Graph 的?"
"如果 TVM 某天解决了这些死结,是否值得切换?"
"optimizer.step() 能不能也被编译?"——直接问我。
下一课预告:0004 自定义算子(上)——手写一个 forward + backward 的 torch.autograd.Function,注册进 torch.compile。