216 lines
9.5 KiB
Markdown
216 lines
9.5 KiB
Markdown
# 阶段 1:MLIR 速查(对应规划阶段 1)
|
||
|
||
> 2026-08-14。面向「已有 TVM 经验、看过 IREE pipeline」的 MLIR 速查。
|
||
> 重点是 dialect 概念 + 各 dialect 职责,配合阶段 2 的 pipeline dump 一起看。
|
||
|
||
## 1. MLIR 的核心思想
|
||
|
||
MLIR 不是一个固定 IR,而是一个 **IR 框架**:它定义了一套通用的 IR 结构(Operation/Block/Region/Type),
|
||
然后让你**自己定义 dialect**。每个 dialect 定义自己的 op、类型、属性。
|
||
|
||
- **builtin**:最核心的部分(module、基础类型)
|
||
- **dialect**:一组相关的 op/type/attr 的集合(如 `arith`、`linalg`、`flow`)
|
||
- **op**:具体操作(如 `arith.addf`、`linalg.matmul`)
|
||
|
||
IREE 的每一层都是一个 dialect:Flow、Stream、HAL、VM 都是自定义的 MLIR dialect。
|
||
|
||
## 2. IR 结构核心概念
|
||
|
||
| 概念 | 是什么 | 类比 TVM |
|
||
|---|---|---|
|
||
| **Operation (op)** | 基本单位:名字 + operands + results + attributes + regions | IR 节点 / expr |
|
||
| **Value** | SSA 值,只能赋值一次;是 op 的 result 或 block 的 argument | 变量/临时值 |
|
||
| **Block** | 顺序执行的一串 op,可带 block argument,以 terminator 结尾 | 基本块 |
|
||
| **Region** | 一个或多个 block 的列表,支持嵌套(实现控制流) | 子图/作用域 |
|
||
| **Type** | 值的类型:`i32` `f32` `tensor<2x4xf32>` `memref<...>` `vector<...>` | dtype |
|
||
| **Attribute** | 编译期常量/元数据:`arith.constant 5 : i32` 里的 5,或 op 的 `{...}` | 常量/annotations |
|
||
| **Symbol** | 命名实体(函数名、全局名),通过符号引用 | 函数名 |
|
||
|
||
### Op 的通用语法(重点,读 IR 必会)
|
||
|
||
```mlir
|
||
%result = linalg.matmul ins(%lhs, %rhs : tensor<2x4xf32>, tensor<4x2xf32>)
|
||
outs(%init : tensor<2x2xf32>) -> tensor<2x2xf32>
|
||
```
|
||
|
||
- `%result` = 结果 SSA 值;`-> tensor<2x2xf32>` = 结果类型
|
||
- `ins` / `outs` = 输入 / 输出操作数(linalg 特有的命名,其他 op 直接用位置参数)
|
||
- `(%lhs, %rhs : type1, type2)` = 操作数及其类型
|
||
|
||
## 3. 什么是 Dialect?
|
||
|
||
**Dialect = 一组相关的 op、类型、属性的集合**,定义在同一个命名空间下。例如:
|
||
- `arith.addf` 是 `arith` dialect 里的 op,名字用 `arith.` 前缀
|
||
- `linalg.matmul` 是 `linalg` dialect 里的 op
|
||
- `flow.dispatch.region` 是 IREE 的 `flow` dialect 里的 op
|
||
|
||
为什么要分 dialect?因为**不同抽象层次需要不同的"语言"**:
|
||
- 高层用 `linalg`(表达"这是一个矩阵乘"的语义)
|
||
- 低层用 `llvm`(表达"这是 fmuladd + 循环 + 指针")
|
||
- 图级用 `flow`、异步用 `stream`、设备用 `hal`
|
||
|
||
编译过程就是**从高层 dialect 逐层 lowering 到低层 dialect**(这正是阶段 2 看到的 7 个阶段)。
|
||
|
||
## 4. 基础 dialect(IR 的积木)
|
||
|
||
### `func` —— 函数
|
||
```mlir
|
||
func.func @matmul(%arg0: tensor<2x4xf32>) -> tensor<2x2xf32> {
|
||
%0 = func.call @helper(%arg0) : (tensor<2x4xf32>) -> tensor<2x2xf32>
|
||
func.return %0 : tensor<2x2xf32>
|
||
}
|
||
```
|
||
- 定义函数 `func.func`、调用 `func.call`、返回 `func.return`
|
||
|
||
### `arith` —— 算术/比较/转换
|
||
```mlir
|
||
%0 = arith.constant 5 : i32
|
||
%1 = arith.addf %a, %b : f32 // 浮点加
|
||
%2 = arith.addi %x, %y : i32 // 整数加
|
||
%3 = arith.cmpf ogt, %a, %b : f32 // 浮点比较
|
||
%4 = arith.mulf %a, %b : f32 // 浮点乘
|
||
```
|
||
- 标量算术的"标准库",几乎所有计算最终都落到这里
|
||
|
||
### `scf` —— 结构化控制流(for / if / while)
|
||
```mlir
|
||
%0 = scf.for %i = %lb to %ub step %step iter_args(%acc = %init) -> (tensor<...>) {
|
||
%1 = arith.addi %i, %acc : i32
|
||
scf.yield %1 : i32
|
||
}
|
||
%2 = scf.if %cond -> (f32) {
|
||
scf.yield %a : f32
|
||
} else {
|
||
scf.yield %b : f32
|
||
}
|
||
```
|
||
- "结构化"= 带明确的循环语义,便于分析和变换(IREE tiling 产生的就是 `scf.for`)
|
||
|
||
### `cf` —— 非结构化控制流(底层分支)
|
||
```mlir
|
||
cf.br ^bb1(%0 : i32) // 无条件跳转
|
||
cf.cond_br %cond, ^bb1(%0 : i32), ^bb2 // 条件跳转
|
||
```
|
||
- 最低层的控制流,`scf.for` 最终会 lower 成 `cf.br` 循环
|
||
|
||
### `affine` —— 仿射循环与访存
|
||
```mlir
|
||
%0 = affine.for %i = 0 to 10 {
|
||
%1 = affine.load %memref[%i, %i + 1] : memref<10x10xf32>
|
||
}
|
||
```
|
||
- 循环边界和下标必须是仿射表达式(`i`、`i+1`、`2*i+3`),可做 polyhedral 分析
|
||
- IREE 历史上有用,现在更多用 scf + linalg
|
||
|
||
### `index` 类型
|
||
- `index` 是"可移植的整数",循环变量、下标、shape 都用它
|
||
- 编译到具体目标时再决定是 i32 还是 i64
|
||
|
||
## 5. 数据/计算 dialect(IREE codegen 的核心)
|
||
|
||
### `tensor` —— 不可变张量(值语义)
|
||
```mlir
|
||
%0 = tensor.empty() : tensor<2x2xf32> // 建空张量
|
||
%1 = tensor.extract_slice %t[0, 0][2, 2][1, 1] : tensor<4x4xf32> to tensor<2x2xf32>
|
||
%2 = tensor.insert_slice %s into %t[0, 0][2, 2][1, 1] : tensor<2x2xf32> into tensor<4x4xf32>
|
||
%3 = tensor.extract %t[0, 0] : tensor<2x2xf32> // 取标量
|
||
```
|
||
- **值语义**:不可变,像纯函数式。编译期的高层抽象
|
||
|
||
### `memref` —— 内存缓冲区(可变的 buffer)
|
||
```mlir
|
||
%0 = memref.alloc() : memref<2x2xf32>
|
||
%1 = memref.load %0[%i, %j] : memref<2x2xf32> // 读
|
||
memref.store %v, %0[%i, %j] : memref<2x2xf32> // 写
|
||
```
|
||
- 可变,带 layout/stride,对应"真实内存"。**bufferize 后 tensor 变 memref**
|
||
|
||
### `vector` —— 固定大小 SIMD 向量
|
||
```mlir
|
||
%0 = vector.load %memref[%i, %j] : memref<2x2xf32>, vector<4xf32>
|
||
%1 = vector.transfer_read %t[0, 0], %pad : tensor<4x4xf32>, vector<4xf32>
|
||
```
|
||
- 固定大小的向量,用于表达 SIMD 指令(IREE 向量化后产生 `vector` op)
|
||
|
||
### `linalg` —— 结构化计算(**IREE 的心跳**)
|
||
```mlir
|
||
// 命名 op:语义明确的高层计算
|
||
%0 = linalg.matmul ins(%lhs, %rhs : tensor<2x4xf32>, tensor<4x2xf32>)
|
||
outs(%init : tensor<2x2xf32>) -> tensor<2x2xf32>
|
||
%1 = linalg.fill ins(%cst : f32) outs(%empty : tensor<2x2xf32>) -> tensor<2x2xf32>
|
||
|
||
// generic op:自定义的"结构化计算",用 region 定义元素计算 + 迭代空间
|
||
%2 = linalg.generic {
|
||
indexing_maps = [...],
|
||
iterator_types = ["parallel", "parallel", "reduction"]
|
||
} ins(%a : tensor<...>) outs(%b : tensor<...>) {
|
||
^bb0(%in: f32, %out: f32):
|
||
%sum = arith.addf %in, %out : f32
|
||
linalg.yield %sum : f32
|
||
}
|
||
```
|
||
- **关键思想**:linalg 把"计算"和"数据如何存/遍历"分离。
|
||
它只声明"这是矩阵乘"或"元素级计算 + 迭代空间",具体 tiling/向量化由后续 pass 做。
|
||
这就是 IREE codegen 的起点(对应 TVM 的 TE 层,但声明式更强)。
|
||
|
||
## 6. 易混淆概念对照(重点)
|
||
|
||
| 对比 | 区别 |
|
||
|---|---|
|
||
| `tensor` vs `memref` | 值语义(不可变)vs 内存(可变)。编译期用 tensor,bufferize 后变 memref |
|
||
| `scf.for` vs `affine.for` | 结构化(可标并行/归约)vs 仿射(可 polyhedral 分析) |
|
||
| `arith` vs `math` | 基本算术(add/mul/cmp)vs 数学函数(`math.sqrt`、`math.exp`、`math.absf`) |
|
||
| `tensor.empty` vs `memref.alloc` | 抽象空张量(值)vs 真实内存分配 |
|
||
|
||
## 7. Pass 基础设施(看懂 pipeline 的关键)
|
||
|
||
**Pass** = 遍历 IR 做变换的基本单位。IREE 那 731 次 pass 就是一个个变换。
|
||
|
||
两种主要机制(深入各层源码必懂):
|
||
|
||
### (1) Pattern Rewriting(局部重写)
|
||
声明式:写"匹配 pattern → 替换成新 op"的规则,引擎(如 `applyPatternsAndFoldGreedily`)自动遍历应用。
|
||
适合局部的、等价的化简。
|
||
|
||
### (2) Dialect Conversion(跨 dialect 转换)
|
||
三件套:`ConversionTarget`(哪些 op 合法/非法)+ `TypeConverter`(类型怎么换)+
|
||
patterns(非法 op 怎么重写成合法 op)。
|
||
适合把整个 dialect 换成另一个(如 Flow→Stream、tensor→memref)。
|
||
|
||
> 你在 pipeline 里看到的 `ConvertToStreamPass`、`ConvertToLLVMPass`、`IREEComprehensiveBufferizePass`
|
||
> 都是 Dialect Conversion;`CanonicalizerPass` 是 Pattern Rewriting。
|
||
|
||
### 常用观察工具
|
||
|
||
```bash
|
||
iree-compile --mlir-print-ir-after-all in.mlir -o out.vmfb 2> dump.txt # 每个 pass 后 dump
|
||
iree-compile --mlir-print-ir-after=ConvertToStreamPass --mlir-print-ir-module-scope in.mlir -o out.vmfb 2>&1 # 只看某个 pass
|
||
```
|
||
|
||
## 8. 与 TVM 0.6 对照
|
||
|
||
| MLIR | TVM 0.6 |
|
||
|---|---|
|
||
| Operation | IR node / expr |
|
||
| Pass | pass / IR visitor |
|
||
| Dialect Conversion | 逐层 lowering(Relay→TE→TIR) |
|
||
| Pattern Rewriting | pass 里的 rewrite 规则 |
|
||
| linalg.matmul/generic | TE 的 compute + 描述 |
|
||
| scf.for + vector | TIR 里的 loop + vectorize |
|
||
| memref | 缓冲区/物理内存 |
|
||
|
||
## 9. IREE 自己的 dialect(概览,详细见各层笔记)
|
||
|
||
| dialect | 职责 | 关键 op(在 dump 里见过) |
|
||
|---|---|---|
|
||
| `util` | 通用工具(函数/全局包装) | `util.func`、`util.global`、`util.call`、`util.return` |
|
||
| `flow` | 图级优化 + dispatch 划分 | `flow.dispatch.region`、`flow.dispatch`、`flow.executable` |
|
||
| `stream` | 异步执行模型 | `stream.executable`、`stream.binding`、`stream.tensor.import`、`stream.async.execute`、`stream.timepoint.await` |
|
||
| `hal` | 设备抽象 | `hal.executable`、`hal.buffer_view`、`hal.command_buffer`、`hal.tensor.import/export` |
|
||
| `vm` | VM 字节码 | `vm.module`、`vm.func`、`vm.call` |
|
||
| `iree_tensor_ext` | dispatch 张量 + 屏障 | `iree_tensor_ext.dispatch.tensor.load/store`、`iree_tensor_ext.compute_barrier.start/end` |
|
||
| `iree_linalg_ext` | 扩展 linalg op(softmax/attention 等) | 复杂算子的结构化 op |
|
||
|
||
> 这些 dialect 的源码在 `src/iree/compiler/src/iree/compiler/Dialect/<X>/`,
|
||
> 每个 dialect 目录下有 `*.td`(op 定义,TableGen 格式)和 `*.cpp`(实现)。
|