TVM 与 TileLang:深度学习编译器
我之前容易把 TVM 当成”另一个 ONNX”,或者把 TileLang 当成”Python 写 CUDA 的语法糖”。聊完之后才发现,编译器这条线和推理引擎(vLLM、TensorRT-LLM)解决的问题不一样,而且 TileLang 的低延迟也不是靠”语法糖”实现的。
这篇把编译器和推理引擎的分工理清楚,再讲 TileLang 到底在做什么。
编译器 vs 推理引擎
| TVM / TileLang | vLLM / TensorRT-LLM | |
|---|---|---|
| 输入 | 计算图(Relay/Relax/TIR) | 模型权重 + 配置 |
| 输出 | 硬件指令(CUDA/Metal/OpenCL/LLVM) | 推理服务(请求调度 + kernel 执行) |
| 优化层级 | 图级 + 算子级 + 自动调优 | 手工优化 kernel + 系统调度 |
| 硬件覆盖 | CPU/GPU/FPGA/自定义加速器 | 主要是 NVIDIA GPU |
推理引擎管的是”怎么把请求排好队、喂给 GPU、把结果送回去”。编译器管的是”这个算子在这个硬件上怎么跑最快”。
两者可以组合:MLC-LLM 就是基于 TVM Unity(Relax),把 Llama、Mistral 编译到手机、浏览器 WebGPU、苹果 Metal 上运行。编译器生成 kernel,推理引擎做调度。
TVM 的工作流程
TVM 的核心是分层 lowering:
PyTorch/TensorFlow/ONNX 模型
│
▼
Relay / Relax(高层计算图表示)
│── 算子融合、常量折叠、死代码消除
▼
TensorIR / TIR(底层张量运算表示)
│── loop split、reorder、vectorize、bind to thread
▼
AutoTVM / MetaSchedule(自动搜索最优调度)
│── 搜索 tile size、loop order、unroll factor
▼
Codegen(生成目标硬件代码)
│── CUDA、OpenCL、Vulkan、LLVM
▼
Runtime(在目标硬件上加载执行)
Relay / Relax 是图级别的 IR,类似 ONNX,但可编程、可优化。可以做算子融合、布局转换这些图级优化。
TensorIR / TIR 是循环级别的 IR,描述具体的 for-loop 嵌套。这里的关键抽象是 Block(带边界谓词的计算单元)和 Schedule(一系列变换原语)。优化和正确性分离,调度变换不会改变计算结果。
AutoTVM / MetaSchedule 是自动调优。以矩阵乘法为例,TVM 会搜索:
- tile size(分块大小)
- loop order(循环顺序)
- vectorize width(向量化宽度)
- thread binding(绑到 GPU 几维 block/thread)
AutoTVM 需要手写搜索模板,Ansor / MetaSchedule 是无模板搜索,用进化算法 + 成本模型探索调度空间。
TileLang 的定位
TileLang 是 MLC 团队(TVM 生态)推出的 kernel DSL,目标是:用接近 Python 的语法,写出媲美手写 CUDA(cuBLAS / Cutlass)性能的算子,同时保持低发射延迟。
低延迟从哪来
TileLang 的 Python 代码只是前端 DSL,实际运行时:
- 编译时全特化:shape、dtype、tile size 这些在编译时确定,运行时直接调用,没有动态 dispatch
- 零开销抽象:
tl.copy、tl.load这些不是函数调用,是编译期内联原语,没有临时对象分配 - 显式内存层级控制:强制程序员管理 Global Memory → Shared Memory → Register 的数据流动,减少冗余搬运
- TVM 后端:lower 到 TIR 后,继承所有循环优化和代码生成能力
# TileLang 风格(示意)
@tl.jit
def matmul(A: tl.Buffer, B: tl.Buffer, C: tl.Buffer):
with tl.Tile([128, 128]):
shared_A = tl.copy(A) # Global → Shared
shared_B = tl.copy(B)
acc = tl.zeros([128, 128])
for k in tl.range(0, K, 32):
local_A = tl.load(shared_A) # Shared → Register
local_B = tl.load(shared_B)
acc += local_A * local_B
tl.store(C, acc)
编译后,tl.copy 变成直接的 ld.global → st.shared PTX,没有 Python runtime 介入。
TileLang vs Triton vs Cutlass
| 维度 | TileLang | Triton | Cutlass |
|---|---|---|---|
| 抽象层级 | 显式 tile + 内存层级 | 隐式 tile(block 级) | C++ template,最底层 |
| 编译方式 | TVM 编译栈 | MLIR/Triton IR → LLVM | C++ 编译器 |
| 延迟优化 | 编译时全特化 + 零开销抽象 | 编译优化好,少量 runtime dispatch | 极致,但开发成本高 |
| 易用性 | Python DSL,中等 | Python DSL,较易 | C++,难 |
| 硬件覆盖 | TVM 支持的后端 | 主要是 NVIDIA GPU | NVIDIA only |
Triton 的 tile 管理是半自动的,编译器帮你决定一些细节。TileLang 是显式的,控制精度更高,代价是程序员需要理解内存层级。
我之前搞混的地方
“编译优化”和”推理框架优化”的区别。编译器优化的是单次 kernel 的执行效率,推理框架优化的是多个请求在 GPU 上的排布效率。两者正交:
- 编译器把 MLP 融合成一个 kernel,减少 HBM 访问
- 推理框架把多个请求的 MLP 打包成一个 batch,提高 GPU 利用率
AutoTVM 的搜索成本。我原来以为自动调优是”免费”的,但其实:
- 搜索一个算子的最优调度可能需要几百次 trial
- 每次 trial 要在目标硬件上实际运行测量
- 搜索时间可能几十分钟到几小时
所以 AutoTVM 的结果通常要缓存(tuning log),不是每次编译都重新搜。
实际应用场景
- TVM:适合需要跨平台部署的场景(手机、边缘设备、国产芯片),或者需要自定义算子和硬件后端
- TileLang:适合写高性能 GPU kernel,特别是融合算子、MoE、量化 kernel 这些推理框架里没有现成实现的
- MLC-LLM:基于 TVM Unity,把大模型编译到手机、浏览器,做端侧推理
写到这才把”编译器管什么、推理引擎管什么”分清楚。之前看 MLC-LLM 的代码,总觉得和 vLLM 重叠,现在明白一个是生成 kernel 的,一个是调度请求的,分工很清晰。