图编译在 vLLM 中的应用:从 torch.compile 到 Kunlun Compiler Backend¶
约 3119 个字 138 行代码 6 张图片 预计阅读时间 17 分钟
本文的主线只有一个:vLLM 中的图编译不是简单打开 torch.compile,而是把 PyTorch 编译栈改造成适合 LLM Serving 的图捕获、图切分、图优化、编译缓存和运行时重放体系。
这条主线可以按“总-分”理解:
- 总:服务化编译管线。vLLM 让
torch.compile负责捕获模型前向图,但由VllmBackend决定哪些图段能编译、哪些图段要留给 attention/KV cache/runtime 处理、哪些 shape 需要专门化。 - 分:三层职责。Dynamo 负责动态图到 FX Graph;vLLM 负责推理语义相关的 graph policy;底层 compiler backend 负责把 piecewise graph 编成当前硬件可执行的 callable。
- 落地:不同硬件不同后端。CUDA 主线走 Inductor/Triton;Ascend 通过
CompilerInterface接入AscendCompiler;当前 vLLM-Kunlun 主要保留 CustomOp/eager 边界,后续可以参照 Ascend 建设自己的 compiler backend。
0. 术语表¶
| 术语 | 在本文中的含义 |
|---|---|
torch.compile | PyTorch 2.x 的编译入口。它返回一个可编译包装对象,第一次执行时通常触发 Dynamo 捕获和后端编译。 |
| TorchDynamo | torch.compile 的前端捕获组件,从 Python 动态执行中捕获可编译张量计算,生成 FX Graph 和 guards。 |
| FX Graph | PyTorch FX 图表示,vLLM 会在其中保留普通 ATen op、vLLM CustomOp 和 torch.ops.vllm_ir.*。 |
| Guards | Dynamo 捕获图时记录的复用条件,如 shape、dtype、device、对象属性和部分分支条件。 |
| Graph Break | Dynamo 无法继续捕获 Python 执行路径时产生的断点。vLLM 希望在可控边界内自己切图,而不是让 Python break 随机决定优化边界。 |
| Inductor | PyTorch 默认编译后端。CUDA 上常见结果是生成 Triton/C++ kernel 或调用 ATen/cuBLAS/CUTLASS 等实现。 |
CompilationConfig | vLLM 编译控制面,包含 mode、backend、splitting_ops、compile_sizes、cudagraph_mode、pass_config 等。 |
VllmBackend | vLLM 传给 torch.compile 的自定义 backend,接收 Dynamo 捕获的 FX Graph,配置 pass,切分 piecewise graph,并调度 compiler backend。 |
PiecewiseBackend | vLLM 管理每个子图编译和运行时 shape dispatch 的组件,会按 compile_ranges 和 compile_sizes 生成多个 callable。 |
CompilerInterface | vLLM 对内部 compiler backend 的抽象。Inductor、Eager、Ascend 这类 backend 都可以实现 compile/load/hash/cache 这一组接口。 |
| vLLM IR | vLLM 在 FX Graph 中引入的 functional IR dialect,例如 torch.ops.vllm_ir.rms_norm,用于把算子语义和具体 kernel 实现解耦。 |
| CustomOp | vLLM 旧有自定义 op 机制,常用于 attention、RMSNorm、quant、MoE 等。它能屏蔽复杂实现,但也可能让 compiler pass 看不见高层语义。 |
splitting_ops | vLLM 指定的图切分点。attention、KV cache update 等常作为 split op,以保持运行时灵活性。 |
| CUDA Graph / ACLGraph / XPU Graph | 运行时 capture/replay 机制,不等同于 Inductor 编译。主要收益是减少重复 launch 和 Python 调度。 |
1. 为什么 vLLM 不能只用普通 torch.compile¶
普通 PyTorch 场景中,torch.compile(model) 的核心问题是:forward(x) 能否被 Dynamo 捕获,并交给 Inductor 生成更快的执行函数。
LLM Serving 的问题更复杂:
- 请求动态到达,batch size、num tokens、sequence length 每步都可能变化。
- KV Cache 是长期存在并持续更新的 runtime state。
- Attention backend 强依赖硬件、KV layout、paged metadata、prefix cache、speculative decoding 和 graph replay 支持程度。
- Decode 阶段单步 token 数少但执行频率高,Python 调度和 kernel launch 开销会被放大。
- 在线服务不能因为 shape 变化频繁 re-trace/re-compile,否则会引入冷启动和延迟抖动。
因此 vLLM 的编译目标不是“尽可能把整模型交给 Inductor”,而是:
- 把稳定的 tensor 计算段编译掉,减少 Python/framework 调度开销。
- 在图层做推理语义优化,如 norm+quant、activation+quant、attention+quant、allreduce+rmsnorm、qk_norm+rope 等融合。
- 把动态 shape 收敛为可管理的编译版本,通过 compile range、compile size、bucket 和 runtime replay 提高缓存命中。
- 把 attention/KV cache 这类强 runtime 逻辑留在明确边界上,避免图编译吞掉服务系统真正需要动态控制的部分。
2. vLLM 编译的控制面:CompilationConfig¶
vLLM 编译行为主要由 CompilationConfig 控制。它不是单个开关,而是把编译、切图、缓存和运行时图重放分成几组策略。
2.1 顶层编译模式¶
| 配置 | 作用 | 关键理解 |
|---|---|---|
mode | 控制是否编译以及使用哪种编译模式 | V1 默认会选择 VLLM_COMPILE,即 vLLM 自定义编译路径。 |
backend | 指定内部 compiler backend | 在 VLLM_COMPILE 下,它不是 VllmBackend 本身,而是 VllmBackend 内部调用的 backend,如 inductor、eager 或自定义 backend。 |
custom_ops | 控制哪些 CustomOp 启用/禁用 | 默认逻辑会根据是否使用 Inductor 决定 CustomOp 是否展开,以便给 Inductor 更多融合空间。 |
ir_enable_torch_wrap | 是否启用 vLLM IR torch custom op 包装 | 使用 Inductor + vLLM compile 时默认倾向开启,使 torch.ops.vllm_ir.* 能留在 FX Graph 里。 |
mode 可以单独看:
mode | 含义 | 执行路径 |
|---|---|---|
0 / NONE | 不编译 | eager 执行 |
1 / STOCK_TORCH_COMPILE | 标准 PyTorch 编译 | Dynamo 直接把图交给指定 PyTorch backend |
2 / DYNAMO_TRACE_ONCE | 只捕获一次 | 适合不依赖动态 shape 控制流的场景 |
3 / VLLM_COMPILE | vLLM 自定义编译 | Dynamo -> VllmBackend -> piecewise backend -> compiler backend |
2.2 图切分和 shape 策略¶
| 配置 | 作用 |
|---|---|
splitting_ops | 指定 FX-level 或 Inductor partition 的切分点。默认会围绕 attention op 建立 piecewise graph。 |
compile_sizes | 给高频 size 生成静态专门化版本,如 decode 常见 batch/token size。 |
compile_ranges_endpoints | 生成一组 shape range,让一个 callable 覆盖一个 token/batch 范围。 |
use_inductor_graph_partition | 关闭时 vLLM 先在 FX 层切图;开启时让 Inductor 在更后的位置 partition。 |
pass_config | 控制 vLLM 自定义 pass,如 norm/quant、attention/quant、sequence parallel、GEMM+通信融合等。 |
PiecewiseBackend 的职责可以概括成一句话:一张 piece graph 不是只编一个版本,而是按 range 和 size 编译多个可复用 callable,运行时按真实 token/batch size dispatch。
2.3 CUDA Graph 与编译不是同一层¶
cudagraph_mode、cudagraph_capture_sizes、max_cudagraph_capture_size 控制的是运行时 capture/replay。它和 Inductor compile 有联系,但不是同一个东西。
- Inductor 编译:把 FX Graph 编成 callable,可能生成 Triton/C++/CUDA 代码。
- CUDA Graph:把已经稳定执行的一段 runtime 操作 capture 下来,后续 replay,减少 CPU launch 和调度开销。
- Piecewise CUDA Graph:vLLM V1 常把 attention 留在图外,只 capture attention 之间的 token-wise 计算段。
- Full CUDA Graph:如果 attention backend 也兼容 capture,decode 或小模型场景可尝试把 attention 包进去。
3. vLLM 编译主流程¶
官方 torch.compile integration 文档把流程拆成 Python code compilation、computation graph processing、computation graph compilation 和 cudagraph capture。结合源码,可以按下面这条线读:
model.forward
-> torch.compile(..., fullgraph=True, dynamic=False, backend=VllmBackend)
-> TorchDynamo captures FX Graph
-> VllmBackend configures pre/post grad passes
-> split graph by splitting_ops
-> PiecewiseBackend compiles ranges and sizes
-> compiler backend returns callable
-> optional graph capture/replay wraps runtime execution
3.1 torch.compile 入口¶
vLLM 的 wrapper 会调用:
self._compiled_callable = torch.compile(
compiled_ptr,
fullgraph=True,
dynamic=False,
backend=backend,
options=options if options else None,
)
这里有三个点容易误解:
fullgraph=True要求 Dynamo 在当前 callable 内捕获完整图,不代表 shape 一定完全静态。dynamic=False表示 vLLM 不希望 Dynamo 自由推广动态 shape,而是由 vLLM 自己用 compile range/size 管理服务化 shape。backend在VLLM_COMPILE下是VllmBackend对象;VllmBackend内部再根据compilation_config.backend选择 Inductor、Eager 或平台自定义 compiler。
3.2 VllmBackend 做什么¶
VllmBackend 是 vLLM 编译体系的中枢。它主要做四件事:
- 创建当前平台的 pass manager。默认平台通常用 vLLM 的 post-grad pass;Ascend/Kunlun 也可以覆盖
get_pass_manager_cls()。 - 把 vLLM IR 的 pre-grad pass 和 post-grad pass 注入到 compiler config 中。
- 按
splitting_ops切分 FX Graph,生成外层 stitching graph 和多个 piecewise subgraph。 - 通过
CompilerManager和PiecewiseBackend调用真实 compiler backend,并管理 cache/load。
官方 API 文档中 VllmBackend 的描述也很直接:它是 VLLM_COMPILE 模式下的 torch.compile backend,主要工作是把图切成 piecewise graphs,再交给 piecewise backend。
3.3 为什么 attention 常是切分边界¶
Attention 不只是一个普通 tensor op。它依赖:
- block table、slot mapping、seq_lens、query_start_loc 等 serving metadata;
- KV Cache 的页式布局和更新逻辑;
- prefix cache、spec decode、MLA/sparse MLA 等模型特性;
- 后端 kernel 是否支持 graph capture;
- runtime 地址、workspace、stream 和通信约束。
所以 vLLM 默认让 attention 成为 splitting_ops 的核心候选。切图后,attention 可以 eager 或由平台自定义 op 执行,attention 前后的稳定计算段则进入 compiler backend 和 graph replay。
4. vLLM IR:让 pass 看见“语义”而不是只看 kernel¶
旧的 CustomOp 路线把很多高性能实现包成 opaque op,好处是集成简单,坏处是 compiler pass 不容易识别高层语义。比如 RMSNorm、量化、fused add norm 可能表现为不同 custom kernel、不同 ATen 展开,pass 要分别适配。
vLLM IR 的目标是把算子语义和具体 kernel 实现分开:
- 模型 forward 调用
ir.ops.rms_norm(...)、ir.ops.fused_add_rms_norm(...)。 - Dynamo 捕获后,FX Graph 中出现
torch.ops.vllm_ir.rms_norm.default这样的高层 op。 - vLLM 先在高层 IR 上做 fusion/transformation。
- 到 lowering 阶段再按 provider priority 选择
native、vllm_c、aiter、triton_*或外部平台实现。
4.1 每个 piece 内部的 IR 流程¶
- Dynamo tracing:
vllm_ir.*作为 opaque torch op 出现在 FX Graph 中,不提前展开。 - Pre-grad inplace functionalization:把
maybe_inplace改成 functionaldefaultoverload,同时记录 donated inputs。 - AOTAutograd functionalization:继续规范 mutation,让后续 pass 面对语义稳定的 functional graph。
- IR fusion/transformation:在高层 IR 上做 norm+quant、attention+quant、sequence parallel、collective fusion 等。
- IR lowering:
VllmIRLoweringPass根据 priority list 和supports_args选择具体实现,并把 IR op 替换为实现图。 - Clone cleanup:如果 lowering 为保护 functional 语义插入了
clone,UnsafeCloneEliminationPass会利用 donated input 信息删除安全的多余 clone。 - Backend compile:最后才进入 Inductor 或平台自定义 compiler 的后端优化、代码生成或 runtime graph 编译。
4.2 对 out-of-tree 平台的意义¶
vLLM IR 对 Kunlun 这类外部平台特别关键。外部平台可以不改 vLLM 主仓,直接注册自己的实现:
from vllm import ir
@ir.ops.rms_norm.register_impl("kunlun", supported=True)
def rms_norm_kunlun(x, weight, epsilon, variance_size=None):
return torch.ops._kunlun.rms_norm(x, weight, epsilon)
然后通过优先级配置让 lowering 优先选 Kunlun 实现。这样 compiler pass 面对的仍是统一的 rms_norm 语义,而不是每个平台各写一套 pattern。
5. 不同硬件后端的路径差异¶
上半段是共享的:torch.compile、Dynamo、FX Graph、VllmBackend、piecewise graph。真正差异发生在 compilation_config.backend 选择 compiler 之后。
5.1 CUDA:Inductor 主线¶
CUDA 主线通常是:
VllmBackend
-> PiecewiseBackend
-> InductorAdaptor
-> torch._inductor.compile_fx.compile_fx
-> Triton/C++/CUDA/ATen/cuBLAS/CUTLASS
-> optional CUDA Graph capture/replay
vLLM 负责图切分、pass 注入、shape specialization、compile cache 和 runtime replay;Inductor 负责每个 piece 的后端编译。官方文档也强调,piecewise cudagraph 和 piecewise compilation 对齐:attention 之间的计算通常 token-wise 且更适合 capture,而 attention 本身兼容性更复杂。
5.2 Ascend:通过 CompilerInterface 接入自定义 backend¶
Ascend 的思路不是让 Inductor 生成 CUDA/Triton,而是实现自己的 compiler backend:
NPUPlatform.get_compile_backend()
-> "vllm_ascend.compilation.compiler_interface.AscendCompiler"
AscendCompiler(CompilerInterface)
-> compute_hash()
-> initialize_cache()
-> compile()
-> load()
平台层还会提供:
pass_key:Ascend 自己的 pass manager 注入 key。get_pass_manager_cls():返回GraphFusionPassManager。get_compile_backend():返回AscendCompiler的 qualified name。
AscendCompiler.compile() 内部大致有两条路径:
enable_npugraph_ex=True:优先使用npugraph_ex.get_npu_backend()创建 NPU FX compiler;如果npugraph_ex不可用,则回退到torchair.get_npu_backend()。这条路径把 FX Graph 交给 Ascend NPU 图编译后端,后续配合 ACLGraph/static kernel 做运行时复用。enable_npugraph_ex=False:不进入 NPU 图编译增强路径,只运行 Ascend 插件侧 FX fusion pass,再用 AOTAutograd 包装成 callable。这是更保守的 fusion-only fallback。
这个设计给 Kunlun 的启发是:新硬件不一定要接 Inductor;只要实现 CompilerInterface,vLLM 的 VllmBackend/PiecewiseBackend/compile cache 仍然可以复用。
5.3 当前 vLLM-Kunlun:保留 CustomOp/eager 边界¶
当前 vLLM-Kunlun 的路径更像“平台插件 + import hook + CustomOp 保留”。在启用 graph/cudagraph 的主路径上,KunlunPlatform.check_and_update_config() 会把编译策略收敛到 eager/custom op 边界:
vllm.platform_plugins
-> vllm_kunlun:register
-> install import hook
-> vllm.compilation.wrapper => vllm_kunlun.compilation.wrapper
-> KunlunPlatform.check_and_update_config
-> backend = "eager"
-> custom_ops = ["all"]
-> pass_config.enable_fusion = False
几个关键点:
KunlunPlatform._enum = PlatformEnum.OOT,但device_type和device_name仍返回"cuda",这是为了复用上游 GPU Worker 和部分 CUDA 命名接口。get_piecewise_backend_cls()返回CUDAPiecewiseBackend,get_static_graph_wrapper_cls()返回CUDAGraphWrapper,这里的 “CUDA” 更像复用 vLLM 内部接口名称,不代表底层一定是 NVIDIA CUDA。opaque_attention_op()返回True,并通过vllm::unified_attention_with_output_kunlun作为 split op,让 Kunlun attention 保留为图边界。- attention、RMSNorm、MoE、采样等关键执行由
vllm_kunlun自定义 op 和 XPU runtime 承担。
这条路径工程上稳健,但也意味着:在这类 graph 模式下,Inductor/Triton 路线被显式绕开,vLLM IR pass 和平台 compiler backend 的优化空间还没有充分打开。
6. 如何为 vLLM-Kunlun 建设 compiler backend¶
目标不是把 Kunlun 直接塞进 Inductor,而是参考 Ascend:在 vLLM 的 CompilerInterface 下实现 Kunlun 自己的 FX compiler backend,并保留 eager/custom op fallback。
7. 小结¶
vLLM 图编译的本质是一个服务化编译系统:
Dynamo 捕获模型 forward
-> vLLM 用 VllmBackend 接管 FX Graph
-> splitting_ops 把 attention/KV runtime 边界切出来
-> PiecewiseBackend 按 range/size 管理多个 compiled callable
-> vLLM IR 让 pass 先处理高层推理语义
-> compiler backend 把 piece 编成当前硬件可执行形式
-> runtime graph replay 进一步降低 decode/prefill 调度成本
CUDA 的答案是 Inductor + Triton + CUDA Graph;Ascend 的答案是 AscendCompiler + NPU graph backend + ACLGraph;Kunlun 当前答案是 CustomOp/eager 保留。下一步更合理的路线,是给 vLLM-Kunlun 建立 KunlunCompiler,先做 fusion-only 和 IR provider,再逐步接入 XPU graph compile 与 KunlunGraphWrapper。
参考资料¶
- vLLM 官方文档:torch.compile integration
- vLLM 官方文档:How to debug the vLLM-torch.compile integration
- vLLM 官方文档:vLLM IR: Functional Intermediate Representation
- vLLM 官方文档:CustomOp
- vLLM 官方文档:CUDA Graphs
- vLLM API:CompilerInterface
- vLLM API:PiecewiseBackend
- vLLM API:CompilationConfig
- vLLM-Ascend:compiler_interface.py
- vLLM-Ascend:platform.py
- PyTorch 官方文档:torch.compile