Code
%%writefile /tmp/lesson01.mlir
module {
func.func @add(%a: i32, %b: i32) -> i32 {
%sum = ______ %a, %b : i32
func.return ______ : i32
}
}学完后你应能:读懂 operation/result/operand/region/block,并手写可被 mlir-opt 解析的函数。
通过必须同时满足:
在现代异构编译基础设施中,传统编译器的中间表示(如 LLVM IR)以一维指令流与基本块(Basic Block)CFG 为中心,虽然极其适合底层标量优化与机器码生成,但丢失了高层张量算子结构、循环多面体多维嵌套以及硬件特定语义。 为了统一从高层图计算(PyTorch / TensorFlow / JAX)到底层 GPU / NPU 汇编的全栈表示,MLIR(Multi-Level Intermediate Representation)提出了递归正交的层次化抽象: 整个编译世界由一个统一的核心原语构成——Operation(操作)。 顶层模块(builtin.module)、函数(func.func)、仿射循环(affine.for)、结构化条件控制(scf.if)以及基本算术计算(arith.addi),在 MLIR 统一对象模型中全部是 Operation。 本课解决的核心工程问题是:理解 Operation、Region、Block 与 SSA(静态单赋值)的递归嵌套树状结构,掌握基于 Block Argument 的现代无 节点控制流,以及手写符合 MLIR 验证器(Verifier)严密契约的语法树。
MLIR 递归嵌套层次与内存验证流程如图 1-1 与图 1-2 所示:
图 1-1:MLIR 嵌套包含层次 (Operation → Region → Block → Operation) 与 SSA 定值-引用链
图 1-2:MLIR Operation 解析验证与内存模型装载全流程
统一递归包含层次(Operation Tree):
func.func 的函数体是一个 Region,scf.if 的 then/else 分支各是一个 Region。^bb0)。Block 包含线性排列的 Operation 列表,通常以终止操作(Terminator Operation,如 func.return、scf.yield 或 cf.br)结尾;带 NoTerminator trait 的 op(如 builtin.module)的 region 不需要 terminator。消灭 节点的 Block Arguments(块参数控制流): 在 LLVM IR 中,基本块汇聚控制流依赖 节点(Phi Node), 必须位于块首,并且要和前驱块列表保持一一对应,变换 CFG 时需要同步维护。 MLIR 不使用 节点,而是采用类似函数调用的块参数(Block Arguments):每个 Block 可以声明参数列表(如 ^bb1(%x: i32):),上游块通过跳转指令 cf.br ^bb1(%val) 显式传参。
文本解析与语义验证(Verifier Pipeline): 如图 1-2 所示,文本源码经过 AsmParser 词法/语法解析并完成 SSA 名字解析后,生成内存中的 Operation 树。随后进入 verify() 阶段:验证所有操作数的类型、属性的合法性、支配关系(Dominance)、以及终止操作与外层 Region 的类型签名匹配。
SSA 值的严格支配约束(Dominance Invariant): 在同一个 Block 或存在嵌套关系的 Region 中,SSA 值必须先定值(Definition),后使用(Use)。这不仅是文本的书写顺序,更是支配树(Dominance Tree)的物理拓扑约束。未定义的 SSA 值无法通过符号绑定,会直接触发 use of undeclared SSA value 诊断错误。
孤儿块与终止指令缺失(Missing Terminator): 每个基本块(Block)的最后一条操作必须是具备 IsTerminator Trait 的终止操作。若遗漏 func.return 或 scf.yield,Verifier 会报错(如 block with no terminator 或 empty block: expect at least a terminator)。
Region 隔离与符号可见性(IsolatedFromAbove Trait): 拥有 IsolatedFromAbove 特征的操作(如 func.func、builtin.module)其内部 Region 是完全封闭的,绝对不能直接捕获外层作用域的 SSA 变量。任何外部数据必须显式通过参数传递进入。
结构化控制流(Structured Control Flow)的分析优势: MLIR 倡导在前端阶段保留结构化控制流(如 scf.if、scf.for、affine.for),其循环边界和条件分支作为 Operation 的属性或操作数显式存在。这使得循环展开、仿射多面体变换、并行化分析极为简明,避免了过早将循环打散成 CFG 跳转图后导致难以逆向分析控制流拓扑。
内存树开销 vs 表达力: 相比于扁平线性的传统三地址码,MLIR 在内存中维护的是一颗深度嵌套的 C++ 指针树(每个 Operation 包含 Operand 链表、Result 数组与 Region 向量)。这带来了一定的内存常数开销,但为多层级 Dialect 逐步降级(Lowering)提供了无可替代的灵活性。
以文本指令 %sum = arith.addi %a, %b : i32 为例:
arith.addi(隶属于算术方言 arith 的整数加法操作)。%a 与 %b,必须为合法的 SSA 值且类型均为 i32。%sum,类型为 i32。"arith.addi"(%a, %b) : (i32, i32) -> i32 通用格式暴露了 MLIR 一切皆 Operation 的统一内存模型本质。请在阅读后先合上这一节,用自己的语言复述“输入状态 → 中间状态 → 输出状态”,再做练习。
补齐整数加法 operation 和返回值。
规则:只能修改 TODO/______ 所在位置;不要删除断言或放宽误差。代码注释说明了每个边界条件。
有 MLIR 工具链时运行 mlir-opt /tmp/lesson01.mlir -verify-diagnostics;否则逐项检查 def-use 和类型。
提交时请给出:补齐后的代码、实际运行输出(环境不可用时注明“仅静态审查”)以及对失败用例的解释。
不要背定义:请从输入、状态变化和输出三个阶段解释“IR 层级、SSA 与 Region”的工作机制。
你的答案:
把 %sum 在定义之前使用,为什么不仅是文本顺序问题?
你的答案:
如何在 IR 中表达 if 的两个分支都产生一个值?
你的答案:
需要提示时按四级机制请求:概念区域 → 具体方向 → 关键局部 → 完整答案。
先完成题目再核对。即使代码一致,也要能解释关键步骤,并尝试更换一个输入规模。
从三个阶段拆解其运行机制:
输入阶段(操作数解析与局部作用域绑定):调用方将输入张量/标量绑定到函数或基本块参数(Block Arguments,如 %a: i32, %b: i32)。外层 Operation(如 func.func)开辟包含入口 Block ^bb0 的 Region 局部作用域,建立局部 SSA 符号表。
状态变化阶段(Operation 序列求值与 SSA 链条传递):顺序执行 Block 内的 Operation 序列。每个 Operation 消费上游的 SSA 操作数(Operands),根据自身语义规则(如 arith.addi 执行补码加法)计算并产生全新的强类型 SSA 结果变量(Results,如 %sum)。数据流严格沿着由 Def-Use 边构成的有向无环图流动,中间状态不可变。
输出阶段(终止操作截断与 Region 结果向上回传):Block 的最后一条指令必须是 Terminator 操作(如 func.return %sum : i32)。终止操作捕获当前的 SSA 状态,验证返回类型与函数外层签名的严格一致性,随后将控制流和计算结果移交给调用方,退出当前 Region。
在 SSA(静态单赋值)语义和编译器数据流分析中,在定值之前使用变量绝对不只是简单的代码文本行数先后问题,而是触碰了编译器的核心代数不变式:
支配性(Dominance)与因果链崩溃:SSA 的核心形式化基础是支配性(Dominance)——定义点必须在控制流图(CFG)中严格支配(Dominate)所有的使用点。如果在使用点之前该变量尚未被定义,意味着存在一条控制流路径使得该变量未经初始化就被读取,在物理机器上对应读取未初始化的悬空寄存器或脏内存。
数据流依赖(Def-Use Chain)的不可构造性:MLIR 在构建内存 IR 树时,每个 SSA Value 维护着一条引用它的 User 双向链表。若在使用时尚无定义实体,编译器无法绑定操作数的来源指针,无法进行常量折叠、活性分析(Liveness Analysis)以及寄存器生命周期分配,直接破坏编译器的数据结构完整性。
在 MLIR 中,表达 if-else 双分支均产生返回值有两种标准范式,其中现代高层 MLIR 最推崇结构化控制流(SCF: Structured Control Flow):
结构化控制流范式(scf.if): scf.if 操作自身直接产生 SSA 返回值,then 和 else 分支 Region 各自以 scf.yield 将分支内计算的值传出:
%res = scf.if %cond -> (i32) {
%val1 = arith.constant 1 : i32
scf.yield %val1 : i32
} else {
%val2 = arith.constant 2 : i32
scf.yield %val2 : i32
}
此种结构下完全不需要 节点,数据流与控制流高度结构化闭环。
底层 CFG 块参数范式(cf.cond_br + Block Arguments): 在降级到底层控制流方言时,通过条件跳转进入两个分支 Block,两个分支在汇合块 ^merge(%phi_val: i32) 处通过显式 Block Argument 传参: cf.br ^merge(%val1 : i32) 与 cf.br ^merge(%val2 : i32)。
资料用于建立事实基线;面试回答仍需用自己的语言组织。
---
title: "第 1 课:IR 层级、SSA 与 Region"
jupyter: python3
---
## 本课目标与完成标准
学完后你应能:读懂 operation/result/operand/region/block,并手写可被 `mlir-opt` 解析的函数。
通过必须同时满足:
- 独立补齐本课唯一的代码填空题,并通过给定检查;
- 三个问答题均说明因果链,而不是只报术语;
- 能指出至少一个正确性边界和一个性能取舍;
- 总分不低于 8/10,且没有一票否决级概念错误。
## 前置关系
- 课程前置:编译原理基础、C++ 阅读能力
- 本课在路线中的作用:MLIR 的统一节点是 Operation;Module、函数、循环都通过 operation 嵌套 region/block 表达,多结果 SSA 连接数据流。
## 核心心智模型
### 1. 它是什么,解决什么问题
在现代异构编译基础设施中,传统编译器的中间表示(如 LLVM IR)以一维指令流与基本块(Basic Block)CFG 为中心,虽然极其适合底层标量优化与机器码生成,但丢失了高层张量算子结构、循环多面体多维嵌套以及硬件特定语义。
为了统一从高层图计算(PyTorch / TensorFlow / JAX)到底层 GPU / NPU 汇编的全栈表示,**MLIR(Multi-Level Intermediate Representation)**提出了递归正交的层次化抽象:
整个编译世界由一个统一的核心原语构成——**Operation(操作)**。
顶层模块(`builtin.module`)、函数(`func.func`)、仿射循环(`affine.for`)、结构化条件控制(`scf.if`)以及基本算术计算(`arith.addi`),在 MLIR 统一对象模型中全部是 Operation。
本课解决的核心工程问题是:**理解 Operation、Region、Block 与 SSA(静态单赋值)的递归嵌套树状结构,掌握基于 Block Argument 的现代无 $\phi$ 节点控制流,以及手写符合 MLIR 验证器(Verifier)严密契约的语法树**。
### 2. 它如何工作
MLIR 递归嵌套层次与内存验证流程如图 1-1 与图 1-2 所示:
```{=html}
<div class="fd fd-flow" role="img"><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-sg" data-nid="Module" data-depth="0"><div class="fd-sg-title">ModuleOp (顶层容器)</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-sg" data-nid="Func" data-depth="1"><div class="fd-sg-title">func.func @kernel(%arg0: f32) -> f32</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-sg" data-nid="Region" data-depth="2"><div class="fd-sg-title">Region 0</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-sg" data-nid="Block0" data-depth="3"><div class="fd-sg-title">Block ^bb0 (入口块)</div><div class="fd-stack"><div class="fd-comp fd-td"><div class="fd-rank"><div class="fd-node fd-box" data-nid="Op1">%0 = arith.addf %arg0, %arg0 : f32</div></div><div class="fd-gap" style="--gl:114px;--gt:53px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Op2">%1 = arith.mulf %0, %cst : f32</div></div><div class="fd-gap" style="--gl:114px;--gt:53px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Term">func.return %1 : f32</div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div><i class="fd-e" data-s="Op1" data-t="Op2" data-k="solid" data-h="1" data-b="0">SSA定值传值</i><i class="fd-e" data-s="Op2" data-t="Term" data-k="solid" data-h="1" data-b="0">SSA定值传值</i></div>
```
<p class="caption" align="center"><em>图 1-1:MLIR 嵌套包含层次 (Operation → Region → Block → Operation) 与 SSA 定值-引用链</em></p>
```{=html}
<div class="fd fd-flow" role="img"><div class="fd-stack"><div class="fd-comp fd-lr" data-minw="1390"><div class="fd-rank"><div class="fd-node fd-box" data-nid="Source">MLIR 文本源码 (.mlir)</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Lexer">词法/语法解析 (AsmParser)</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="OpBuild">Operation 实例构建 & 操作数类型推导</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="Verify">SSA 名字解析与 Block 前向引用绑定</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="SSA">Dialect & Trait 合法性校验 (verify())</div></div><div class="fd-gap" style="--gl:44px;--gt:38px"></div><div class="fd-rank"><div class="fd-node fd-box" data-nid="InMem">内存 IR 树 (OwningOpRef<ModuleOp>,类型/属性由 MLIRContext 唯一化)</div></div></div></div><i class="fd-e" data-s="Source" data-t="Lexer" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="Lexer" data-t="OpBuild" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="OpBuild" data-t="Verify" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="Verify" data-t="SSA" data-k="solid" data-h="1" data-b="0"></i><i class="fd-e" data-s="SSA" data-t="InMem" data-k="solid" data-h="1" data-b="0"></i></div>
```
<p class="caption" align="center"><em>图 1-2:MLIR Operation 解析验证与内存模型装载全流程</em></p>
1. **统一递归包含层次(Operation Tree)**:
$$ \text{Operation} \longrightarrow \text{Region} \longrightarrow \text{Block} \longrightarrow \text{Operation} $$
- **Operation**:语义执行单元。它可以拥有 0 或多个操作数(Operands)、产生 0 或多个静态单赋值返回值(SSA Results)、绑定一组编译期属性(Attributes)、拥有特定类型(Types),并嵌套 0 或多个 Region。
- **Region**:封装一个局部的控制流与局部作用域容器。例如 `func.func` 的函数体是一个 Region,`scf.if` 的 then/else 分支各是一个 Region。
- **Block**:Region 内部的基本块(如 `^bb0`)。Block 包含线性排列的 Operation 列表,通常以终止操作(Terminator Operation,如 `func.return`、`scf.yield` 或 `cf.br`)结尾;带 `NoTerminator` trait 的 op(如 `builtin.module`)的 region 不需要 terminator。
2. **消灭 $\phi$ 节点的 Block Arguments(块参数控制流)**:
在 LLVM IR 中,基本块汇聚控制流依赖 $\phi$ 节点(Phi Node),$\phi$ 必须位于块首,并且要和前驱块列表保持一一对应,变换 CFG 时需要同步维护。
MLIR 不使用 $\phi$ 节点,而是采用类似函数调用的**块参数(Block Arguments)**:每个 Block 可以声明参数列表(如 `^bb1(%x: i32):`),上游块通过跳转指令 `cf.br ^bb1(%val)` 显式传参。
3. **文本解析与语义验证(Verifier Pipeline)**:
如图 1-2 所示,文本源码经过 `AsmParser` 词法/语法解析并完成 SSA 名字解析后,生成内存中的 Operation 树。随后进入 `verify()` 阶段:验证所有操作数的类型、属性的合法性、支配关系(Dominance)、以及终止操作与外层 Region 的类型签名匹配。
### 3. 正确性条件与常见误区
- **SSA 值的严格支配约束(Dominance Invariant)**:
在同一个 Block 或存在嵌套关系的 Region 中,SSA 值必须**先定值(Definition),后使用(Use)**。这不仅是文本的书写顺序,更是支配树(Dominance Tree)的物理拓扑约束。未定义的 SSA 值无法通过符号绑定,会直接触发 `use of undeclared SSA value` 诊断错误。
- **孤儿块与终止指令缺失(Missing Terminator)**:
每个基本块(Block)的最后一条操作必须是具备 `IsTerminator` Trait 的终止操作。若遗漏 `func.return` 或 `scf.yield`,Verifier 会报错(如 `block with no terminator` 或 `empty block: expect at least a terminator`)。
- **Region 隔离与符号可见性(IsolatedFromAbove Trait)**:
拥有 `IsolatedFromAbove` 特征的操作(如 `func.func`、`builtin.module`)其内部 Region 是完全封闭的,**绝对不能直接捕获外层作用域的 SSA 变量**。任何外部数据必须显式通过参数传递进入。
### 4. 性能与工程取舍
- **结构化控制流(Structured Control Flow)的分析优势**:
MLIR 倡导在前端阶段保留结构化控制流(如 `scf.if`、`scf.for`、`affine.for`),其循环边界和条件分支作为 Operation 的属性或操作数显式存在。这使得循环展开、仿射多面体变换、并行化分析极为简明,避免了过早将循环打散成 CFG 跳转图后导致难以逆向分析控制流拓扑。
- **内存树开销 vs 表达力**:
相比于扁平线性的传统三地址码,MLIR 在内存中维护的是一颗深度嵌套的 C++ 指针树(每个 Operation 包含 Operand 链表、Result 数组与 Region 向量)。这带来了一定的内存常数开销,但为多层级 Dialect 逐步降级(Lowering)提供了无可替代的灵活性。
## 具体演示
以文本指令 `%sum = arith.addi %a, %b : i32` 为例:
- **Operation 标识**:`arith.addi`(隶属于算术方言 `arith` 的整数加法操作)。
- **Operands**:`%a` 与 `%b`,必须为合法的 SSA 值且类型均为 `i32`。
- **Result**:产生唯一的 SSA 变量 `%sum`,类型为 `i32`。
- **Generic 展开形式**:
该语句的语法糖底层完全等价于通用规范形式:
`"arith.addi"(%a, %b) : (i32, i32) -> i32`
通用格式暴露了 MLIR 一切皆 Operation 的统一内存模型本质。
请在阅读后先合上这一节,用自己的语言复述“输入状态 → 中间状态 → 输出状态”,再做练习。
## 实践任务:唯一代码填空题
补齐整数加法 operation 和返回值。
规则:只能修改 `TODO`/`______` 所在位置;不要删除断言或放宽误差。代码注释说明了每个边界条件。
```{python}
#| course_role: exercise
%%writefile /tmp/lesson01.mlir
module {
func.func @add(%a: i32, %b: i32) -> i32 {
%sum = ______ %a, %b : i32
func.return ______ : i32
}
}
```
### 检查方法
有 MLIR 工具链时运行 `mlir-opt /tmp/lesson01.mlir -verify-diagnostics`;否则逐项检查 def-use 和类型。
提交时请给出:补齐后的代码、实际运行输出(环境不可用时注明“仅静态审查”)以及对失败用例的解释。
### Q1
不要背定义:请从输入、状态变化和输出三个阶段解释“IR 层级、SSA 与 Region”的工作机制。
**你的答案:**
### Q2
把 `%sum` 在定义之前使用,为什么不仅是文本顺序问题?
**你的答案:**
### Q3
如何在 IR 中表达 if 的两个分支都产生一个值?
**你的答案:**
## 评分与通过规则
- 代码 4 分:正常输入 2 分,边界输入 1 分,解释实现 1 分;
- Q1~Q3 各 2 分;
- 一票否决:结果碰巧正确但核心因果链错误、删除边界检查、把未运行结果说成实测。
需要提示时按四级机制请求:概念区域 → 具体方向 → 关键局部 → 完整答案。
::: {.callout-tip collapse="true" title="💡 点击展开参考答案与详细代码实现" icon="false"}
## 参考答案(仅 answer 分支)
先完成题目再核对。即使代码一致,也要能解释关键步骤,并尝试更换一个输入规模。
```{python}
#| course_role: solution
%%writefile /tmp/lesson01.mlir
module {
func.func @add(%a: i32, %b: i32) -> i32 {
%sum = arith.addi %a, %b : i32
func.return %sum : i32
}
}
```
### Q1 参考答案
从三个阶段拆解其运行机制:
1. **输入阶段(操作数解析与局部作用域绑定)**:调用方将输入张量/标量绑定到函数或基本块参数(Block Arguments,如 `%a: i32, %b: i32`)。外层 Operation(如 `func.func`)开辟包含入口 Block `^bb0` 的 Region 局部作用域,建立局部 SSA 符号表。
2. **状态变化阶段(Operation 序列求值与 SSA 链条传递)**:顺序执行 Block 内的 Operation 序列。每个 Operation 消费上游的 SSA 操作数(Operands),根据自身语义规则(如 `arith.addi` 执行补码加法)计算并产生全新的强类型 SSA 结果变量(Results,如 `%sum`)。数据流严格沿着由 Def-Use 边构成的有向无环图流动,中间状态不可变。
3. **输出阶段(终止操作截断与 Region 结果向上回传)**:Block 的最后一条指令必须是 Terminator 操作(如 `func.return %sum : i32`)。终止操作捕获当前的 SSA 状态,验证返回类型与函数外层签名的严格一致性,随后将控制流和计算结果移交给调用方,退出当前 Region。
### Q2 参考答案
在 SSA(静态单赋值)语义和编译器数据流分析中,在定值之前使用变量绝对不只是简单的代码文本行数先后问题,而是触碰了编译器的核心代数不变式:
1. **支配性(Dominance)与因果链崩溃**:SSA 的核心形式化基础是**支配性(Dominance)**——定义点必须在控制流图(CFG)中严格支配(Dominate)所有的使用点。如果在使用点之前该变量尚未被定义,意味着存在一条控制流路径使得该变量未经初始化就被读取,在物理机器上对应读取未初始化的悬空寄存器或脏内存。
2. **数据流依赖(Def-Use Chain)的不可构造性**:MLIR 在构建内存 IR 树时,每个 SSA Value 维护着一条引用它的 User 双向链表。若在使用时尚无定义实体,编译器无法绑定操作数的来源指针,无法进行常量折叠、活性分析(Liveness Analysis)以及寄存器生命周期分配,直接破坏编译器的数据结构完整性。
### Q3 参考答案
在 MLIR 中,表达 `if-else` 双分支均产生返回值有两种标准范式,其中现代高层 MLIR 最推崇**结构化控制流(SCF: Structured Control Flow)**:
1. **结构化控制流范式(`scf.if`)**:
`scf.if` 操作自身直接产生 SSA 返回值,then 和 else 分支 Region 各自以 `scf.yield` 将分支内计算的值传出:
```mlir
%res = scf.if %cond -> (i32) {
%val1 = arith.constant 1 : i32
scf.yield %val1 : i32
} else {
%val2 = arith.constant 2 : i32
scf.yield %val2 : i32
}
```
此种结构下完全不需要 $\phi$ 节点,数据流与控制流高度结构化闭环。
2. **底层 CFG 块参数范式(`cf.cond_br` + Block Arguments)**:
在降级到底层控制流方言时,通过条件跳转进入两个分支 Block,两个分支在汇合块 `^merge(%phi_val: i32)` 处通过显式 Block Argument 传参:
`cf.br ^merge(%val1 : i32)` 与 `cf.br ^merge(%val2 : i32)`。
## 参考资料
- [MLIR Language Reference](https://mlir.llvm.org/docs/LangRef/)
- [MLIR Toy Tutorial](https://mlir.llvm.org/docs/Tutorials/Toy/)
资料用于建立事实基线;面试回答仍需用自己的语言组织。
:::
::: {.course-pagination}
[**← 上一课**<br>第 8 课:Kernel 组合、Dispat...](../cutlass/lesson08_kernel_selection.qmd){.pagination-link .pagination-prev}
[**下一课 →**<br>第 2 课:Dialect、Type、Att...](lesson02_dialects_types.qmd){.pagination-link .pagination-next}
:::