土法炼钢兴趣小组的算法知识备份

【编译器与 MLIR】Region 与 Block:IR 的控制流骨架

文章导航

分类入口
compilerarchitecture
标签入口
#mlir#llvm#compiler#region#block#ssa#control-flow#structured-control-flow

目录

Region 与 Block:IR 的控制流骨架

前面几章讲了 Operation 和 Value 的 C++ 表示——但单独一个 Op 做不了什么。真正让 IR 成为”程序”的是 Region 和 Block 提供的控制流骨架。

如果说 Operation 是 MLIR 的血肉,Region 就是骨骼。不理解 Region 能在多大程度上嵌套和隔离控制流,就无法理解 MLIR 为什么能表达从张量运算到硬件逻辑的完整降阶路径。

一、Region 不是 BasicBlock

先从 LLVM IR 的基本块模型开始。在 LLVM 中:

define i32 @max(i32 %a, i32 %b) {
entry:
  %cmp = icmp sgt i32 %a, %b
  br i1 %cmp, label %then, label %else
then:
  ret i32 %a
else:
  ret i32 %b
}

Function 包含 BasicBlock 的列表,BasicBlock 包含 Instruction 的列表。结构是扁平的——所有基本块都在同一个作用域中,通过 br 指令连接。

MLIR 中,控制流的嵌套在 Region 层:

func.func @max(%a: i32, %b: i32) -> i32 {
  %cmp = arith.cmpi sgt, %a, %b : i32
  %result = scf.if %cmp -> i32 {
    scf.yield %a : i32        // ← 这个 Block 在 scf.if 的 then-Region 内部
  } else {
    scf.yield %b : i32        // ← 这个 Block 在 scf.if 的 else-Region 内部
  }
  return %result : i32
}

关键区别:

flowchart LR
  subgraph llvm["LLVM IR(扁平)"]
    E1[entry] --> T1[then]
    E1 --> E2[else]
    T1 --> M[merge]
    E2 --> M
  end
  subgraph mlir["MLIR(嵌套 Region)"]
    IF["scf.if"] --> R1["then Region"]
    IF --> R2["else Region"]
  end
维度 LLVM IR MLIR
控制流表示 扁平 CFG(所有 BB 同级) 嵌套 Region(控制流结构在 IR 结构中可见)
值汇合 phi 指令(显式) Block 参数(隐式绑定)
循环 无特殊表示(br 回跳) scf.for / affine.for(结构化 Op + Region)
分支 br(无条件)/ br i1(条件) scf.if(结构化 Op + 两个 Region)

Region 的嵌套不是语法糖——它是语义上的边界。一个 Region 内的值不能逃逸到外层 Region,控制流也不能跳过 Region 边界(除非通过明确的 terminator)。

二、Block 参数:MLIR 对 phi 节点的答案

LLVM IR 用 phi 节点处理控制流汇合:

; LLVM: phi 在 BB 开头,显式列出每个前驱 BB 的值
bb.merge:
  %result = phi i32 [ %a, %then ], [ %b, %else ]

MLIR 用 Block 参数实现同样的语义:

// MLIR: 值通过 successor 绑定到目标 Block 的参数
^bb1(%arg0: i32):          // %arg0 是 Block 参数
  // 使用 %arg0 ...

跳转时绑定参数:

cf.br ^bb1(%a : i32)       // 从 then-路径跳转,绑定 %a
cf.br ^bb1(%b : i32)       // 从 else-路径跳转,绑定 %b

这两种表示在语义上是等价的,但 Block 参数有几个工程优势:

1. 不需要维护前驱列表:phi 指令中每个值都与一个前驱 BB 绑定——这意味着修改 CFG 必须同时更新 phi 节点。Block 参数不需要前驱信息——数据流和控制流的耦合更松。

2. 结构化控制流中的隐式绑定:在 scf.if 中,scf.yield 自动将值绑定到 if Op 的 result(即其 enclosing Region 的出口),不需要在 IR 中显式写绑定关系:

%result = scf.if %cond -> i32 {
  scf.yield %a : i32      // 隐式绑定到 %result
} else {
  scf.yield %b : i32      // 隐式绑定到 %result
}
// 这里 %result 可以直接使用——不需要显式 phi

3. 分析更简洁:确定”哪些值汇合到哪个 Block”只需要看 Block 的签名,不需要扫描 phi 节点。

Block 参数的设计源于 Swift Intermediate Language (SIL) [Groff & Lattner, 2014],在 MLIR 中得到推广。从 LLVM IR 转过来的工程师最初会觉得不习惯,但习惯后会发现在结构化控制流下 Block 参数比 phi 更好处理。

三、Region 的两种类型

MLIR 定义了两种 Region 类型 [MLIR Language Reference]:

3.1 SSACFG Region

SSACFG Region 允许多个 Block 和 CFG 边(cf.brcf.cond_br)。这是 MLIR 中表达任意控制流的底层机制:

func.func @loop(%n: index) -> i32 {
  %c0 = arith.constant 0 : i32
  %c0_index = arith.constant 0 : index
  %c1_index = arith.constant 1 : index
  cf.br ^loop_header(%c0, %n : i32, index)

^loop_header(%acc: i32, %i: index):
  %done = arith.cmpi eq, %i, %c0_index : index
  cf.cond_br %done, ^exit(%acc : i32), ^body(%acc, %i : i32, index)

^body(%acc2: i32, %i2: index):
  %new_acc = arith.addi %acc2, %acc2 : i32
  %next_i = arith.subi %i2, %c1_index : index
  cf.br ^loop_header(%new_acc, %next_i : i32, index)

^exit(%final_acc: i32):
  return %final_acc : i32
}

SSACFG Region 是最通用的表示,但它的自由度也意味着分析难度更高——你必须重建循环结构、支配树等。适合作为”所有结构都解构后的最低共同表示”。

3.2 Graph Region(单 Block Region)

Graph Region 只有一个 Block,不允许 CFG 边。这是结构化控制流 Op 中使用的 Region 类型:

scf.for %i = %lb to %ub step %step {
  // 这里只有一个 Block——这就是 Graph Region
  %val = arith.addi %i, %c1 : index
  scf.yield
}

Graph Region 保证了 Region 内部的执行是单入口、线性、单出口的——不存在内部跳转或分支。这让数据流分析变得 trivial:Region 内的值定义顺序就是执行顺序,不需要构建 CFG 或支配树。

3.3 为什么需要两种?

SSACFG Region 和 Graph Region 的分工对应了 MLIR 的”渐进降阶”哲学:

tensor (数学运算,无显式循环)        → Graph Region
  ↓
linalg (结构化操作,如 matmul)       → Graph Region
  ↓
affine (仿射循环,可分析)            → Graph Region
  ↓
scf (结构化控制流,for/if)          → Graph Region
  ↓
cf (通用 CFG,br/cond_br)           → SSACFG Region
  ↓
llvm (LLVM IR 方言)                 → SSACFG Region

四、Region 作为 Lambda 式抽象

Region 在 MLIR 中扮演的角色远不止 BasicBlock 的容器——它可以实现 Lambda-lifting 式的抽象边界。

考虑 scf.for 的定义:

scf.for %i = %lb to %ub step %step iter_args(%iter = %init) -> (i32) {
  %new_val = arith.addi %iter, %c1 : i32
  scf.yield %new_val : i32
}

scf.for 的 body Region 像一个闭包:它捕获外部值(%c1),接收参数(%i%iter),在每次迭代时产生一个新值(scf.yield 的参数)。Region 的隔离保证了 body 内的值不会泄露到循环外面——只有通过 scf.yield 显式传递的值才能逃逸。

Region 也用于建模高层抽象:

Op Region 的语义
func.func 函数体 Region——捕获全局符号,定义局部 SSA 值
scf.if 两个 Region(then/else)——值通过 scf.yield 汇合
scf.for 循环体 Region——值通过 scf.yield 跨迭代传递
scf.while 条件 Region + 循环体 Region——前置条件+迭代
gpu.launch 并行计算 Region——捕获共享内存和线程索引
linalg.generic 结构化计算 Region——捕获输入/输出 tensor,定义逐元素计算

每种情况下,Region 的作用是:限制值的可见性范围、定义计算的语义边界、提供可替换的抽象单元

五、Region 隔离与 SSA 值的可见性

SSA 值的可见性遵循嵌套作用域规则:

外层 Region/Block
  ├── 定义了 %a, %b → 在所有内层可见
  │
  ├── 内层 Region 1
  │     ├── 可以使用 %a, %b
  │     └── 定义了 %x → 仅在 Region 1 内可见
  │
  └── 内层 Region 2
        ├── 可以使用 %a, %b
        ├── 不能使用 %x(不在作用域内)

这个规则的意义:一个 Region 内部的值无法逃逸——除非通过 terminator(如 scf.yieldfunc.return)显式传递。对于编译器优化,这意味着:

六、遍历 Region 的 C++ 模式

回到 C++。遍历嵌套的 Region 结构使用 Walk 模式:

// 访问 Module 中的所有 Operation(深度优先)
moduleOp.walk([](Operation *op) {
  // 对每个 Op 做操作
});

// 访问特定类型的 Op
moduleOp.walk([](scf::ForOp forOp) {
  // 只处理 scf.for
  Region &body = forOp.getRegion();
  Block &entryBlock = body.front();
  // ...
});

// 访问 Region 中的所有 Block
region.walk([](Block *block) {
  for (BlockArgument arg : block->getArguments()) {
    // 处理 Block 参数
  }
});

// 在特定 Region 中遍历
Operation *parentOp = region.getParentOp();
if (auto funcOp = dyn_cast<func::FuncOp>(parentOp)) {
  // 这是一个函数体 Region
}

七、与经典 SSA 和 CPS 的比较

MLIR 的 Region + Block 参数设计同时受到了两种传统的启发:

经典 SSA(Cytron et al., 1991):所有的值都是静态单赋值的,通过 phi 节点汇合控制流。MLIR 保留了 SSA 的核心不变性(每个值在程序文本中只定义一次),但用 Block 参数替代了显式的 phi 指令。

Continuation-Passing Style (CPS):将控制流”接下来执行什么”表示为 cont 参数。Region 可以被视为一种轻量级的 CPS——一个 terminator 描述了”接下来跳到哪个 Block,带什么参数”。但 MLIR 没有像真正的 CPS 那样将所有控制流都转为函数调用——它保留了基本块和控制流图的表示,只是加了结构化包装。

Lambda 演算与闭包转换:Region 捕获外部值的能力类似闭包。方言转换中的一个常见操作是”Region outline”——将一个 Region 提取为独立的函数。反之,“inlining”——将函数调用展开,函数体 Region 嵌入调用点。这两项操作在 MLIR 中有框架级支持。

八、本篇后续

至此,MLIR 核心概念(Operation、Value、Type、Attribute、Region、Block、ODS)全部覆盖。下一篇进入 Part 3——Pass 管理与分析:如何用这些数据结构构建编译流水线。

参考资料

官方文档(A 级)

论文(A 级)

源码(A 级)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-09 · compiler / architecture

【编译器与 MLIR】操作、方言与 IR 的 C++ 表示

深入 Operation、Op、Value、Block、Region 的 C++ 内存布局与继承体系:CRTP 模板包装、SSA 值的两种来源、Use 链表的遍历方法。这是后续所有 Pass 写作的基础。

2026-06-09 · compiler / architecture

【编译器与 MLIR】MLIR 全景图与设计哲学

从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。


By .