首页 / PyTorch 入门教程 / Inductor 与图优化

PyTorch 入门教程

Inductor 与图优化

本教程共 60 篇 · 第 47 篇 · 更新于 2026-08-17 · 约 4 分钟阅读

PyTorchInductor算子融合max-autotuneGEMM性能优化

本节目标:认识 torch.compile 的默认后端 Inductor,理解算子融合与 GEMM 优化,学会用 max-autotune 和调试工具。

Inductor 是什么

上一章说 torch.compile 会把代码编译成高效内核。具体谁在干活?默认后端叫 Inductor(官方叫 TorchInductor)。

它的工作:把抓好的 FX 图翻译成真正的内核代码。GPU 上生成 Triton 内核,CPU 上生成 C++ 代码(配 OpenMP 多线程)。听起来复杂,但你基本不用直接碰它,通过 torch.compile 调用即可。

完整链路是这样的:

  1. TorchDynamo 抓取 Python 代码,得到 FX 图。
  2. AOTAutograd 把反向传播也纳入图中。
  3. Inductor 做图优化,生成内核代码。
  4. 编译成机器码,运行。

用 backend 参数定位问题

编译后结果不对,第一步要确定问题出在哪一环。torch.compilebackend 参数就是干这个的:

torch.compile(model, backend="eager")       # 只抓图,不优化
torch.compile(model, backend="aot_eager")   # 加上反向图
torch.compile(model, backend="inductor")    # 完整优化(默认)

eageraot_eager 都能跑,换成 inductor 出错,问题就在 Inductor。逐级替换,范围越缩越小。这是我调试编译问题的第一招。

算子融合:把多个算子拼成一个

Inductor 最核心的优化是算子融合(kernel fusion):把多个逐元素算子合并成一个内核,数据在内存里少倒腾几趟。

官方教程有个直观例子。一段 add → add → mul → add 的代码:

def func(a, b, c, d, e):
    t = a + b
    t = t + c
    t = t * d
    return t + e

eager 模式每个算子独立执行,中间结果写进内存再读出来。Inductor 把它融成一个内核,循环只跑一遍,4 个操作在一个循环体里完成。官方实测同一份数据:eager 5.78ms,编译后 0.96ms,快了约 6 倍。

为什么融合这么有效?很多算子属于内存受限(memory-bound):计算本身很快,时间全花在读写内存上。多个算子独立跑,同一份数据反复读好几遍;融成一个内核后,数据只读一次,自然快。逐元素操作(加减乘除、激活函数)全是内存受限,所以融合收益最大。反过来说,大矩阵乘法属于计算受限(compute-bound),融合帮不上什么忙,Inductor 对它做的是另一套优化:挑更快的 GEMM 实现。

类似地,Linear + 加偏置 + ReLU 这种经典组合,会被融成一个内核,名字形如 cpp_fused__mkl_linear_add_mul_relu_151

另一个优化是权重打包(weight packing)。全连接层本质是矩阵乘法(GEMM),Inductor CPU 后端会把权重按块内存格式重新排列,让 CPU 缓存命中率更高。打包后的权重布局对具体 CPU 型号有依赖,换机器会重新编译,属正常现象。官方测 MobileBert 问答模型:eager 802ms,Inductor 340ms,约 2.36 倍,其中 Linear 部分单算子就快了 1.63 倍。

想亲眼看看融合后的内核长什么样?设置环境变量 TORCH_LOGS="+output_code"torch.compile 会把生成的内核代码打印到日志里。你会看到 RECORD_FUNCTION 标记的核名、OpenMP 并行循环,甚至向量化指令。读生成的代码,是理解 Inductor 最好的方式。

max-autotune:编译时多花时间,运行时更快

默认模式下 Inductor 按经验选实现。max-autotune 模式相反:编译时把同一算子的多种实现挨个跑一遍,基准测试选最快的。用编译时间换运行速度:

import torch

model = MyModel()
compiled = torch.compile(model, mode="max-autotune")

日志会显示调优过程,比如:

AUTOTUNE linear_unary(64x16, 32x16, 32)
cpp_packed_gemm_0 0.2142 ms 100.0%
_linear_pointwise 0.2441 ms 87.7%

选中的是 cpp_packed_gemm_0。CPU 上这套基于 C++ 模板的 GEMM,还支持尾声融合(epilogue fusion):bias 和 ReLU 直接融进矩阵乘法的内核里,结果都不用落内存。调优结果也会被缓存,同一个算子第二次编译,不会重新跑基准测试。

GPU 上 max-autotune 会在 ATen、Triton、CUTLASS 等实现之间选择,思路一样。

不想让它挨个试,也可以跳过调优直接指定后端:export TORCHINDUCTOR_MAX_AUTOTUNE_GEMM_BACKENDS=CPP,强制用 C++ 模板实现。想随时看到调优过程的日志,编译前打开 config.trace.log_autotuning_results = True

Warning

CPU 上的 max-autotune 目前要求模型冻结:设置环境变量 TORCHINDUCTOR_FREEZING=1,且编译和推理都要放在 torch.no_grad() 里。训练场景别开。

调试三板斧

编译出错或者精度不对,按这个顺序来。

第一步,开调试模式。 设置环境变量 TORCH_COMPILE_DEBUG=1 再跑,会转储一整套中间文件:fx_graph_runnable.py(可执行的 FX 图)、ir_pre_fusion.txt / ir_post_fusion.txt(融合前后的 IR)、output_code.py(生成的内核代码)。对照着看,问题通常一目了然。output_code.py 可以直接改、直接跑。

第二步,换 backend 缩小范围。 前面说过了,eager / aot_eager / inductor 逐级替换。

第三步,精度问题用 Minifier。 它自动把图里的节点一个个删掉,直到剩下能复现问题的最小图:

TORCHDYNAMO_REPRO_AFTER="aot" TORCHDYNAMO_REPRO_LEVEL=4 python xx.py

日志会显示精简过程,最后给你一个几行代码的复现片段。

性能分析也有配套姿势:先设置 config.cpp.enable_kernel_profile = True,再配合第 32 章的 Profiler 跑一遍。Profiler 表格里会多出一排形如 graph_0_cpp_fused_xxx 的融合内核条目,它们的耗时就是优化后的真实成本。对比 eager 表格里一堆零散的 aten::addaten::mul,一眼就能看出融合省了多少。

Tip

想对比编译前后结果是否一致,用 torch._dynamo.utils.same(a, b),它内部做了形状和数值容差检查。

Windows 上的准备

Inductor CPU 后端要生成并编译 C++ 代码,Windows 上需要 MSVC 编译器。装 Visual Studio 时勾选「使用 C++ 的桌面开发」,然后在命令行执行:

"C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Auxiliary/Build/vcvars64.bat"

注意这条命令只对当前命令行窗口生效,重开终端要再执行一次。嫌麻烦可以把它写进项目的构建脚本里。

想换编译器也行:set CXX=clang-cl 用 LLVM,set CXX=icx-cl 用 Intel 编译器,性能可能更好。Windows 上没装编译器,最常见的报错是找不到 cl.exe,就是这一步没做。

装好之后怎么确认环境没问题?跑一段 torch.compile 的小函数,能正常出结果就说明编译器链路通了。第一次编译要等一会儿,属正常。

还有个进阶选项:config.cpp_wrapper = True 让 Inductor 生成 C++ 包装器,替代 Python 包装器,进一步减少调用开销。追求极致性能时再考虑。

小结

Inductor 是 torch.compile 的发动机:算子融合减少内存搬运,GEMM 打包提高缓存命中,max-autotune 自动选最优实现。遇到问题用 backend 参数定位、用调试文件深挖、用 Minifier 精简。编译器相关的坑,八成都能在这三板斧里找到答案。下一章看看 FX 图本身,那是整个编译体系的入口。到时候你会发现,Inductor 拿到的图,正是 FX 的产物。