编译器的挑战与 IR 的裂变
这是「编译器与 MLIR」系列的第一篇。MLIR 是什么、怎么用,后面的篇章会用代码和实验讲清楚。这一篇只回答一个前置问题:为什么我们需要 MLIR 这个新东西?LLVM IR 不已经是”编译器基础设施的终结者”了吗?
要回答这个问题,需要先看过去二十年编译器工程发生了什么——尤其是 AI 编译器和领域专用架构(DSA)的崛起如何把传统中间表示(IR)推到了极限。
一、三阶段编译器的黄金年代
编译器架构的教科书模型——前端、中端(优化器)、后端——统治了半个世纪。这个模型的威力在于解耦:前端只管把源语言翻译成中间表示(IR),优化器在 IR 上做与语言和目标无关的变换,后端把 IR 翻译成目标机器的指令。
LLVM 是这个模型的巅峰。它的核心洞见写在 2004 年 CGO 论文里:
- IR 是编译器的心脏,所有语言前端和所有目标后端共享同一个 IR;
- IR 以 SSA 形式表达,分析精度和变换正确性都有坚实的形式化基础;
- Pass 管线是可组合的,优化策略通过串联独立的 Pass 来配置 [Lattner & Adve, 2004]。
这套设计让 LLVM 在通用编译器市场取得了压倒性成功。Clang(C/C++)、Rust、Swift、Julia、Zig 等语言的编译器都基于 LLVM IR,x86、ARM、RISC-V、WebAssembly 等目标都有成熟的后端。一次写成、到处运行。它很纯粹——一个 IR 承载了从高级语言到机器码的完整降阶路径。
但它有前提条件。
三阶段模型的隐含假设是:IR 的抽象层级是固定的。一条降阶路径走到底:前端把语言特性全部打散到 IR 的操作粒度,然后优化器在同一个抽象层级上做变换,最后后端把 IR 指令逐一映射到机器指令。
当编译目标是一颗通用 CPU(x86 或 ARM)时,这个假设基本成立。整数运算就是整数运算,跳转就是跳转,IR 的粒度刚好匹配 CPU 指令集的粒度。
但当编译目标是张量计算、图像处理流水线、硬件加速器指令集时,这个假设就塌了。
二、Halide 的第一次裂变:算法与调度不是一回事
2013 年,MIT CSAIL 的 Ragan-Kelley 等人在 PLDI 上发表了 Halide [Ragan-Kelley et al., 2013]。Halide 解决的问题很具体:怎么写图像处理代码既能让人看懂,又能跑得快?
在此之前,高性能图像处理的代码是两种东西的混合物:
// 伪代码——图像处理的典型写法
for (int y = 0; y < height; y++)
for (int x = 0; x < width; x++)
output[y][x] = blur_filter(input, y, x, kernel);算法逻辑(“对每个像素做模糊”)和调度策略(“先 y 后 x、分块、向量化”)搅在一起,改一下 tiling 策略就得重写整个循环。
Halide 的核心贡献是算法与调度的彻底分离:
- 算法:描述要算的东西——输入是什么、输出是什么、每个像素怎么算(纯函数式)。
- 调度(Schedule):描述怎么算——循环顺序、分块大小、向量化、并行化、计算存储位置。
一个极简的 Halide 程序长这样:
// Halide 语言
Func blur("blur");
Var x, y, c;
blur(x, y, c) = (input(x-1, y, c) + input(x, y, c) + input(x+1, y, c)) / 3;
// 算法定义结束
// 独立的调度策略
Var xi, yi;
blur.tile(x, y, xi, yi, 256, 32)
.vectorize(xi, 8)
.parallel(y);同一个算法可以绑定几十种不同的调度——只需修改 Schedule 部分,不用动算法。反过来,换一个算法就用同一套 Schedule 也无妨。
但从编译器的角度看,Halide 暴露了一个根本问题:传统 IR 的抽象层级是单层的,无法在”像素级纯函数”和”tiled-loops-with-vectors”之间切换。Halide 内部需要一套自己的 IR 来承载这个降阶过程——从函数式的算法定义,到循环体的中间表示,再到 LLVM IR。这套 IR 是专门为图像处理流水线设计的,跟通用 IR 完全不同。
Halide 的影响超出了图像处理。它证明了:当问题域足够特殊时,IR 需要专门设计。而调度分离的思想直接影响了后来的 TVM,也影响了 MLIR 的”渐进降阶”——让不同的抽象层各自有最合适的表示,而不是硬塞进同一个 IR。
三、AI 框架的 IR 爆炸:XLA、TVM 与计算图的困境
2015 年到 2020 年,深度学习框架经历了一场编译革命。核心驱动力是一样的:Python 代码太慢,需要编译到 GPU 或专用加速器上。每个框架都在做同一件事——设计一个 IR,把用户的计算图变成可优化的中间表示,然后生成设备代码。
XLA:Google 的加速线性代数
XLA(Accelerated Linear Algebra)的 HLO(High-Level
Optimizer)IR 工作在”操作”级别,而不是”标量指令”级别。一个
dot
操作包含整个矩阵乘法,不是逐个加法乘法的序列。HLO
的优化是代数级别的——算子融合(fusion)、布局选择、常数折叠
[Google, XLA documentation]。
// HLO IR 示意(dot 融合 relu)
%dot = f32[256,256] dot(%lhs, %rhs), lhs_contracting_dims={1}, rhs_contracting_dims={0}
%result = f32[256,256] maximum(%dot, %zero)
问题在于:HLO 之后的降阶路径仍然需要进入 LLVM IR 或 GPU
指令集。一旦掉到 LLVM IR 层级,dot
的语义就丢失了——编译器看到的是几百条标量
load/store/fma,而不是”这是矩阵乘法”。一些只有知道高级语义才能做的优化(比如根据输入形状选择不同的矩阵乘
kernel)就无法在这一层完成。
XLA 的做法是在 HLO 和 LLVM IR 之间加了一个”emitter”层——它在降阶之前根据高级语义做了所有关键决策(kernel 选择、tiling、内存分配),然后机械地生成 LLVM IR。这个 emitter 本质上是把编译知识写死在 C++ 代码里,每支持一种新硬件(TPU、GPU、CPU),就要写一套新的 emitter。
TVM:Relay + TensorIR 的双层抽象
TVM 在 2018 年 OSDI 论文中明确提出了一种双层 IR 架构 [Chen et al., 2018]:
- Relay IR:计算图层级——支持控制流、函数调用、Let 绑定、类型推导。Relay 是一个完整的高级函数式 IR,解决的是”用户写了什么样的计算图”。
- TensorIR / TE:张量计算层级——解决的是”这个计算操作怎么映射到硬件上的循环、缓存和向量指令”。
// Relay IR(计算图)
fn (%data: Tensor[(1, 3, 224, 224), float32]) {
%0 = nn.conv2d(%data, %weight, padding=[1, 1], kernel_size=[3, 3])
%1 = nn.batch_norm(%0, %gamma, %beta)
%2 = nn.relu(%1)
%3 = nn.max_pool2d(%2, pool_size=[2, 2])
}
// TensorIR(调度级)
@main = primfn(A: handle, B: handle, C: handle) {
for io, jo, ko, ii, ji, ki in grid(64, 64, 64, 8, 8, 8) {
// tiled, vectorized matrix multiply
}
}
TVM 的路径是:Python DSL → Relay IR → TE/TensorIR(调度搜索) → 代码生成 → LLVM IR / CUDA / OpenCL / 等。
这个架构有效,但它在工程上暴露了几个硬伤:
- IR 之间的桥接是手工维护的。Relay 到 TensorIR 的降阶需要大量 C++ 胶水代码,每增加一个新算子或新目标都要改桥接层。
- 自定义硬件支持困难。如果一家公司自研了一颗 AI 加速器,想在 TVM 里支持,需要理解并修改从 Relay 到代码生成的整条链路。TVM 的 IR 和 Pass 不是设计成可扩展的——它们是”够用就好”的内部抽象。
- 没有解决”通用编译知识”的复用问题。循环分块、依赖分析、寄存器分配、指令调度——这些是任何编译器都要做的事情,但 TVM 需要自己重新实现一遍,跟 Halide 的实现不共享,跟 LLVM 的实现也不共享。
四、分裂的代价:每个框架都在重新发明编译器
如果只看一个系统,IR 的裂变是合理的工程权衡。Halide 为图像流水线设计 IR,XLA 为 TPU 和 GPU 设计 IR,TVM 为自动调度设计 IR——每个选择都有充分理由。
但从整个工业界看,这是一个系统性浪费:
| 系统 | IR 设计 | 循环优化 | 内存管理 | 设备代码生成 |
|---|---|---|---|---|
| Halide | Halide IR | 内置调度原语 | 内置 | LLVM / CUDA |
| XLA | HLO | emitter 手写 | 手写缓冲区分配 | LLVM / CUDA / TPU |
| TVM | Relay + TensorIR | AutoTVM / AutoScheduler | 内置 | LLVM / CUDA / OpenCL / 等 |
| TensorFlow | GraphDef + (XLA) HLO | XLA 决定 | 手写 | LLVM / CUDA / TPU |
| PyTorch 2.0 | FX Graph + Aten IR | Inductor scheduler | Triton / 手写 | Triton / CUDA |
| Triton | Triton IR | 内置 tiling | 内置 | PTX |
每个系统都实现了循环分块、向量化、内存层次管理、依赖分析——但实现方式各不相同,不能互通。
如果硬件厂商想让自己的加速器被所有这些框架支持,它需要为每个框架写一套后端。框架开发者想让自己的编译器支持新硬件,也需要理解并修改每个框架内部的编译管线。这不是”百花齐放”——它是重复劳动。
MLIR 论文正是从这种局面出发的:不同抽象层级和领域各自在构建独立的编译基础设施,这些系统之间几乎没有代码复用,“软件碎片化”(software fragmentation)已经成为编译器工程的核心挑战 [Lattner et al., 2020]。
五、领域专用架构(DSA)推了一把
如果说 AI 框架的 IR 分裂是软件层的驱动力,那硬件的分化就是物理层面的推手。
通用 CPU 的性能提升在 2010 年代后明显放缓。与此同时,AI 训练和推理的算力需求每两年翻一倍。答案只能是专用硬件——GPU、TPU、NPU、FPGA、ASIC。这些硬件之间的差异不仅仅是”ARM 还是 x86”级别的 ISA 差异——它们连基本的数据移动模型都不一样。
- GPU 有层次化内存:global memory → shared memory → register file;
- TPU 有脉动阵列(systolic array),矩阵乘法在 VLIW 风格的 MXU 上执行;
- NPU 可能有更特殊的片上网络和精度模式(int4、int8、fp8、bf16);
- FPGA 没有”指令集”,计算和状态直接映射到硬件逻辑。
传统编译器后端的工作方式是指令选择 → 指令调度 → 寄存器分配。这条流水线假设目标是”某种有寄存器文件、能逐条执行指令的处理器”。当目标是脉动阵列或 FPGA 逻辑时,这个模型直接失效。编译器需要知道的是”你的计算单元怎么连接、内存带宽是多少、缓存线大小是多少”——这些信息无法在普通的指令级 IR 中表达。
硬件厂商的应对是各自提供自己的编译器工具链——NVIDIA 有 CUDA + nvcc, AMD 有 ROCm, Google 有 TPU compiler, Intel 有 oneAPI。每个都是完整、独立、封闭的编译器栈,跟 LLVM 的关系最多是”最后一步生成机器码时调一下 LLVM”。
结论很清楚:单一 IR 不可能同时高效表示 tensor 级语义、循环级优化、GPU 内存层次和脉动阵列数据流。需要的不是一个更强大的 IR,而是一个能够承载多重 IR 的框架。
六、MLIR 的答案:不说”一个 IR 统治一切”
MLIR 的论文标题很直白:MLIR: A Compiler Infrastructure for the End of Moore’s Law(摩尔定律终结后的编译器基础设施)。论文的核心主张是:
- 不追求单一全能 IR——允许每个抽象层级、每个领域有自己的”方言”(Dialect),方言之间可以共存于同一个 Module 中。
- 方言之间通过”降阶”(Lowering)衔接——编译过程从高层的领域方言逐步降到通用方言再到 LLVM IR,每一步都是一个正式定义的”方言转换”(Dialect Conversion)。
- 基础设施共享——所有方言共用同一套 Type 系统、Pass 管理、模式重写引擎、TableGen 工具链、打印/解析/验证器。Pass 管理、打印/解析、验证与模式重写等通用设施可直接复用,方言作者主要投入语义与降阶设计(参见 MLIR Toy Tutorial)。
用一句话说:MLIR 不是”另一种 IR”,它是”构建 IR 的框架”。
这样前面表格里的六套系统,如果能收敛到 MLIR 上(事实上 Triton(经桥接)、Torch-MLIR、IREE、CIRCT 已经在这么做):
- Halide 的调度分离思想 → 体现在 Linalg 方言的结构化操作 + 变换(Transform)方言的调度策略中;
- XLA 的算子融合 → 可以用 MLIR 的模式重写(Pattern Rewrite)框架表达,不再手写 C++ emitter;
- TVM 的自动调度 → MLIR 的 Transform 方言为调度搜索提供了比 TVM 内部 IR 更通用的表示;
- 多硬件后端 → 共享到 LLVM IR / SPIR-V / GPU 方言的降阶路径,不用每套系统自己写。
方言的可组合性是 MLIR 设计中最关键的一点。传统编译器的 Pass 管线是一条直线:源语言 → 中间表示 → 目标代码。MLIR 的管线可以分叉和收敛:
tensor@数学 → linalg@结构化 → affine@循环分析 → scf@控制流 → memref@内存化 → llvm
↗ ↘
torch/onnx/tf gpu@SPIR-V
↘ ↗
stablehlo@算子融合 → xla_runtime@设备选择
同一个 Module 的不同部分可以走不同的降阶路径。一个函数可以让 MLIR 自动 tile 和向量化,另一个函数可以保留手写优化不降阶。这在传统单一 IR 的编译器中是不可能的——要么全部降阶,要么全都不降。
七、不展开的话题与本篇边界
这篇是系列的第一篇,只讲了”为什么”。接下来的每一篇都会深入”是什么”和”怎么做”。有几件事本篇明确不展开:
- MLIR API 的细节——Operation、Op、Type、Attribute 的 C++ 类体系在第二部分的第 4-7 章中逐一展开。
- 具体方言的语义——tensor、linalg、affine、scf、gpu 等方言的设计细节是第四部分的主题。
- Pass 管理和模式重写的工程实践——在第三部分以可编译可运行的代码详细讨论。
- IREE、Torch-MLIR 等项目的实际架构——留到第十四章和第十七章。
如果读完这篇你只有一个印象,我希望是这个:编译器工程的下一代挑战不是写出更快更好的单一 IR——而是设计一个让多种 IR 能共存、能演进、能组合的框架。MLIR 是这个方向的第一个系统化尝试。
参考资料
论文(A 级)
- Lattner, C. et al. MLIR: A Compiler Infrastructure for the End of Moore’s Law. arXiv:2002.11054, 2020.
- Lattner, C. & Adve, V. LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation. CGO 2004.
- Ragan-Kelley, J. et al. Halide: A Language and Compiler for Optimizing Parallelism, Locality, and Recomputation in Image Processing Pipelines. PLDI 2013.
- Chen, T. et al. TVM: An Automated End-to-End Optimizing Compiler for Deep Learning. OSDI 2018.
官方文档(A 级)
- MLIR Toy Tutorial — https://mlir.llvm.org/docs/Tutorials/Toy/
- MLIR Language Reference — https://mlir.llvm.org/docs/LangRef/
- XLA Architecture — https://www.tensorflow.org/xla/architecture
- Halide Documentation — https://halide-lang.org/
社区项目(B 级)
- IREE — https://github.com/iree-org/iree
- Torch-MLIR — https://github.com/llvm/torch-mlir
- CIRCT — https://github.com/llvm/circt
- Triton — https://github.com/triton-lang/triton
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【编译器与 MLIR】操作、方言与 IR 的 C++ 表示
深入 Operation、Op、Value、Block、Region 的 C++ 内存布局与继承体系:CRTP 模板包装、SSA 值的两种来源、Use 链表的遍历方法。这是后续所有 Pass 写作的基础。
【编译器与 MLIR】AI 时代的编译器基础设施
从三阶段编译器局限出发,系统讲解 MLIR 方言、渐进降阶与 Pass 基础设施,覆盖 Tensor/Linalg/Affine/GPU 到框架桥接的完整编译链。
【编译器与 MLIR】MLIR 全景图与设计哲学
从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。
【编译器与 MLIR】环境搭建与第一个 MLIR 程序
从零构建 LLVM/MLIR 工程,用 mlir-opt 理解 .mlir 文本表示,运行规范化 Pass 并逐行解读转换结果,建立从命令行到 IR 变换的直觉。