MLIR 全景图与设计哲学
上一篇讨论了”为什么需要 MLIR”——AI 编译器和领域专用架构正在撕裂传统单一 IR。这一篇回答”MLIR 是什么”:它的核心结构、设计原则,以及这些设计在真实项目中的样子。
读完这篇你应该对 MLIR 有一张清晰的心智地图。具体 API、Pass 写法、方言设计细节都留给后面的篇章。
一、MLIR 的四层表示结构
MLIR 的名字不是”Multi-Level IR”的缩写,但多层抽象确实是它最关键的特性。从结构上看,MLIR 由四层嵌套的容器组成:
Module
└── Operation (顶层 Op,如 func.func)
└── Region (区域,如函数体)
└── Block (基本块)
└── Operation (普通 Op,如 arith.addi)
└── Region (某些 Op 也有嵌套区域)
- Module:顶层容器,对等一个编译单元。一个
.mlir文件对应一个 Module。 - Operation(Op):MLIR 中一切皆
Op——函数定义(
func.func)、算术运算(arith.addi)、控制流(scf.for)、常量(arith.constant)。甚至 Module 本身也是一个 Op。 - Region:Op 可以包含一个有序的 Block 列表作为其 body。函数体的 Region 包含多个 Block,循环体也是 Region。
- Block:有序的 Op 序列,末尾可以有终结符(Terminator)将控制流转移到其他 Block。Block 可以有参数——这一点延续了 LLVM IR 的设计 [Lattner & Adve, 2004]。
这几层的设计意图是统一性:所有表示元素都是 Op,所有 Op 都可以有 Region。这意味着 IR 的嵌套和分拆能力不受限于特定节点类型——编译器可以在任何层级插入新的抽象或拆解现有的抽象。
一个具体的 .mlir 片段可以展示这种结构:
module {
func.func @matmul(%A: tensor<256x256xf32>, %B: tensor<256x256xf32>) -> tensor<256x256xf32> {
%c0 = arith.constant 0.0 : f32
%init = tensor.empty() : tensor<256x256xf32>
%filled = linalg.fill ins(%c0 : f32) outs(%init : tensor<256x256xf32>) -> tensor<256x256xf32>
%result = linalg.matmul ins(%A, %B: tensor<256x256xf32>, tensor<256x256xf32>)
outs(%filled : tensor<256x256xf32>) -> tensor<256x256xf32>
return %result : tensor<256x256xf32>
}
}
这个片段中有三个不同的方言(Dialect)共存:func(函数)、arith(算术)、linalg(线性代数)。每个方言定义了自己的
Op 集合和类型。它们可以在同一个 Module 中自由混合——这就是
MLIR 的”多方言混合”能力。
二、设计原则之一:渐进降阶(Progressive Lowering)
传统编译器的降阶(Lowering)是一次性的:前端 IR → 优化 → 后端 IR → 机器码。一旦降阶发生,高层语义就永久丢失了。
MLIR
的降阶是渐进的:源程序从最高抽象层的方言开始,逐步经过多个中间方言,最终到达硬件级的方言(如
llvm 或
spirv)。每一步降阶都是局部的、可验证的、可逆的分析性变换。
flowchart TD
T["tensor(不可变张量)"] --> L["linalg(结构化操作)"]
L --> A["affine(仿射循环)"]
A --> S["scf(结构化控制流)"]
S --> M["memref + arith(显式内存)"]
M --> G["llvm / spirv / nvgpu(目标方言)"]
每一步降阶都是一次”表示选择”:
- tensor → linalg:从不可变的数学张量降到可调度的结构化操作(matmul 的 tiling、fusion 在这一层做决策)。
- linalg → affine:将结构化操作展开为嵌套循环,但保留循环的仿射性质(即数组下标是循环变量的仿射函数),以便进行依赖分析和多面体变换。
- affine → scf:去除仿射约束,降为通用结构化控制流。此时不再能做多面体分析,但代码更逼近执行形式。
- scf → memref + arith:将控制流展开为显式的内存访问和标量算术。
- memref/arith → llvm/spirv:映射到目标 ISA 或虚拟机。
关键设计决策是:每一步降阶都保存了当前方言能表达的最精确信息。如果你在
affine 层做 tiling,你有完整的仿射依赖信息决定
tile 大小;如果你在 scf
层做循环展开,你依赖的是更局部的数据流分析。选择在哪个方言层做哪种优化,成了编译器设计者的核心自由。
三、设计原则之二:方言可组合性(Dialect Composability)
方言是 MLIR 最显眼的特性,但”可组合性”才是它的核心差异——光有多个方言不够,关键是方言之间怎么协作。
一个方言定义了三样东西:
- Operation 集合:该方言支持的 Op(如
linalg.matmul、scf.for、arith.addi)。 - Type 集合:该方言使用的类型(如
tensor<256x256xf32>、memref<?xi32>、!llvm.struct<...>)。 - 方言间的接口(Interface):不依赖具体方言的抽象契约——例如”这个
Op 支持内联”(
CallableOpInterface)、“这个 Region 是单入口单出口的”(RegionBranchOpInterface)、“这个循环可以获取其归纳变量”(LoopLikeOpInterface)。
三个特性让方言可以组合:
一是共存性:一个 Module 中可以同时存在
tensor、linalg、scf、arith、llvm
等方言的 Op,MLIR
的打印/解析/验证器不作区分。你可以写一个函数,其中一部分在
linalg 方言、另一部分已经降阶到
llvm 方言。
二是接口驱动:Pass
和变换不依赖具体方言名,而依赖接口。一个”内联 Pass”会检查 Op
是否满足 CallableOpInterface,而不关心它是
func.func
还是某个自定义方言的函数定义。这意味着一个方言只要实现了正确的接口,就能复用现有的
Pass。
三是渐进混合:降阶不需要一次性完成。一个 Module 可以处于部分降阶状态——有些 Op 仍然是高阶方言,有些已经是低阶方言。Pass 只处理它认识的方言的 Op,其他 Op 保持不动。
以 IREE 的编译流程为例 [IREE, GitHub]:
MHLO / Torch / StableHLO → linalg → (bufferization) → memref → llvm / spirv
↘
(tiling + vectorization 仍在 linalg 内部完成)
在
bufferization(内存化)之前,tiling、fusion、vectorization
都在 linalg 方言层完成——因为
tensor 是不可变的,没法写”这个缓冲应该 inline
分配”。Bufferization 是整条编译链的关键转换点:在此之前用
tensor 语义(纯函数式、易分析),在此之后用
memref
语义(显式内存、易生成代码)。两种语义在同一个 Module
的同一个函数中不能混用——这个边界由方言转换显式管理。
四、设计原则之三:基础设施复用(Infrastructure Reuse)
第一篇提到 MLIR 让方言作者可以复用大量通用编译器基础设施。下表列出这些可复用组件及其分工:
| 基础设施 | 做什么 | 方言作者需要做 |
|---|---|---|
| 打印/解析(Printer/Parser) | IR 的文本序列化和反序列化 | 定义 Op 的语法(assemblyFormat) |
| 验证器(Verifier) | 检查 Op 的输入输出类型、属性约束、Region 结构 | 写 verify() 方法声明约束逻辑 |
| Pass 管理器 | Pass 调度、并行执行、崩溃恢复 | 写 Pass 自身逻辑 |
| 模式重写引擎(Pattern Rewriter) | 驱动 IR 局部替换(matchAndRewrite) |
写匹配和替换规则 |
| 方言转换框架(Dialect Conversion) | 跟踪未转换的 Op,逐步转换为目标方言 | 声明转换目标、提供转换模式 |
| 折叠(Fold)接口 | 编译期常量计算 | 实现 fold() 方法 |
| 数据流分析框架 | 前向/后向数据流分析的通用骨架 | 定义 transfer function |
| TableGen 代码生成 | 从 .td 定义文件生成 C++ 样板代码 |
写 TableGen 定义 |
| 位置追踪(Location) | 每个 Op 携带其在源语言中的位置信息 | 无需额外工作 |
一个自定义方言的设计者需要实现的最小集合是:定义
Op 的 TableGen 描述(或 C++ 手动构建)、写
verify() 规则、定义 Op 的打印/解析格式、为关键
Op 实现 fold() 方法。其余一切——从 Pass
管理到模式重写到位置追踪——都从 MLIR 框架继承。
以 arith 方言为例,它是 MLIR
内建的最基础方言之一,提供整数和浮点数的算术/比较运算。arith.addi
的 TableGen 定义大致为:
// mlir/include/mlir/Dialect/Arith/IR/ArithOps.td (简化示意)
def Arith_AddIOp : Arith_Op<"addi", [Pure, SameOperandsAndResultType]> {
let summary = "integer addition operation";
let arguments = (ins SignlessIntegerLike:$lhs, SignlessIntegerLike:$rhs);
let results = (outs SignlessIntegerLike:$result);
let assemblyFormat = "$lhs `,` $rhs attr-dict `:` type($result)";
let hasFolder = 1; // 支持常量折叠
}
这一小段定义自动生成了:C++
构建器(build())、输入输出
accessor(getLhs()、getRhs()、getResult())、打印器、解析器、验证器——所有样板代码都消失,方言作者只需关注加法本身的行为定义。
五、实际案例:不同领域怎么用 MLIR
IREE:端侧 AI 编译器的全栈降阶
IREE(Intermediate Representation Execution Environment)是 Google 开发的端到端机器学习编译器,目标是将 PyTorch/TensorFlow/JAX 模型编译到移动端、边缘设备和嵌入式系统 [IREE, GitHub]。
IREE 的整个编译流程都在 MLIR 上:
PyTorch / TF / JAX
│ (Torch-MLIR / MHLO / StableHLO)
▼
MHLO / StableHLO 方言
│ (hlo → linalg)
▼
Linalg 方言 (tiling, fusion, vectorization)
│ (bufferization)
▼
MemRef 方言
│ (final lowering)
▼
LLVM / SPIR-V / VMVX 方言
IREE 选择 MLIR 的原因有三条:方言体系可以直接映射 AI 编译需要的每一层抽象(tensor → linalg → memref → 目标代码)、Pass 和模式重写框架不需要重新发明、当目标硬件改变时只需替换最后一级方言(LLVM、SPIR-V 或自定义),以上层方言的降阶逻辑可以复用。
CIRCT:硬件设计的方言生态
CIRCT(Circuit IR Compilers and Tools)将 MLIR
方言体系带到硬件设计领域 [CIRCT,
GitHub]。传统硬件设计流程——从 SystemVerilog/VHDL
到网表再到布局布线——依赖的黑箱工具链在可组合性和可定制性上存在不足。CIRCT
用 MLIR 方言建模从 RTL 级(hw
方言)、状态机(seq
方言)、组合逻辑(comb 方言)到 SystemVerilog
输出(sv 方言)的整条降阶路径。
FIRRTL / RTL
│
▼
hw + comb + seq 方言 (硬件语义)
│
▼
sv 方言 (SystemVerilog 输出)
ARM 和 Intel 等厂商在 CIRCT 生态中参与开发(见 CIRCT GitHub 贡献者列表)——不是因为 MLIR 本身适合硬件设计,而是因为 MLIR 的方言可组合特性让 EDA 工具链的每一层可以被独立替换和组合。
Torch-MLIR:PyTorch 的桥接可能
Torch-MLIR 将 PyTorch 的 torch.fx
计算图翻译为 MLIR 方言栈,利用与 IREE 共享的 Linalg
降阶管线生成代码 [Torch-MLIR, GitHub]。
PyTorch (torch.fx Graph)
│
▼
torch 方言 (PyTorch ops, ATen 语义)
│
▼
linalg-on-tensors (复用 IREE 管线)
│
▼
LLVM / SPIR-V
这个路径的价值在于:PyTorch
不需要自己维护从计算图到设备代码的整条编译链。一旦进入
linalg 方言层,就可以复用所有已有的 Linalg
Pass——tiling、fusion、vectorization——和已有的后端降阶路径。
六、心智模型:MLIR 不是什么
建立正确的预期能避免后面的学习走弯路。
MLIR 不是”高级 LLVM IR”:MLIR 和 LLVM IR
属于不同层次。LLVM IR 是一种中间表示,MLIR
是构建中间表示的框架。LLVM IR 在 MLIR
体系里只是一个方言(llvm
方言),和其他方言地位等同。
MLIR 不是编译器替代品:MLIR
没有自己的代码生成后端——它的 llvm 方言最终通过
LLVM 的代码生成器变成机器码。MLIR 的价值在 LLVM IR
之上:提供了在高抽象层做表示、分析和变换的能力,然后精细地控制何时、如何降到
LLVM IR。
MLIR 不是 Silver Bullet:设计一个方言没有免费午餐。TableGen 和共享基础设施省掉了样板代码,但方言的语义设计、Op 粒度的取舍、降阶路径的规划仍然是编译器工程师的工作。MLIR 的价值是让你专注于这些高价值决策,而不是每次重复实现一个 Pass 管理器。
七、本篇边界与后续路径
本篇给了全景图。后面的篇章按这个顺序展开:
- 第 3 章:动手搭建 MLIR 工程,用
mlir-opt运行第一个 Pass; - 第 4-7 章(第二部分):Operation、Type、Attribute、ODS/TableGen、Region/Block 的 C++ 工程细节;
- 第 8-10 章(第三部分):Pass 管理、模式重写、方言转换框架——如何写自己的 Pass;
- 第 11-14 章(第四部分):Tensor/Linalg、Affine/SCF、GPU 代码生成、AI 框架桥接;
- 第 15-18 章(第五部分):实战微型 DSL、调试工作流、IREE 集成、未来趋势。
参考资料
论文(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.
官方文档(A 级)
- MLIR Language Reference — https://mlir.llvm.org/docs/LangRef/
- MLIR Dialects Documentation — https://mlir.llvm.org/docs/Dialects/
- MLIR TableGen Documentation — https://mlir.llvm.org/docs/DefiningDialects/
社区项目(B 级)
- IREE — https://github.com/iree-org/iree
- CIRCT — https://github.com/llvm/circt
- Torch-MLIR — https://github.com/llvm/torch-mlir
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【编译器与 MLIR】从零构建一个微型 Tensor DSL
手把手构建微型 Tensor DSL:ODS 定义方言、写 tiny-to-linalg 降阶 Pass,经标准管线生成 LLVM IR,走完编译链闭环(参考 MLIR Toy 教程)。
【编译器与 MLIR】AI 时代的编译器基础设施
从三阶段编译器局限出发,系统讲解 MLIR 方言、渐进降阶与 Pass 基础设施,覆盖 Tensor/Linalg/Affine/GPU 到框架桥接的完整编译链。
【编译器与 MLIR】类型系统与属性
解析 MLIR 的类型体系:内建类型(Integer、Float、Tensor、MemRef)与自定义方言类型的注册机制;区分 Type 与 Attribute 的设计意图;通过 OpBuilder 理解类型和属性在 IR 构造中的实际角色。
【编译器与 MLIR】表驱动定义:ODS 与声明式编程
深入 MLIR 的 ODS 与 TableGen 工具链:从 .td 定义到自动生成的构建器、验证器、解析/打印器,理解声明式 IR 定义如何减少手写样板代码。