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

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

文章导航

分类入口
compilerarchitecture
标签入口
#mlir#llvm#compiler#dialect#lowering#iree#circt#torch-mlir#design-philosophy

目录

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 也有嵌套区域)

这几层的设计意图是统一性:所有表示元素都是 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 的降阶是渐进的:源程序从最高抽象层的方言开始,逐步经过多个中间方言,最终到达硬件级的方言(如 llvmspirv)。每一步降阶都是局部的、可验证的、可逆的分析性变换。

flowchart TD
  T["tensor(不可变张量)"] --> L["linalg(结构化操作)"]
  L --> A["affine(仿射循环)"]
  A --> S["scf(结构化控制流)"]
  S --> M["memref + arith(显式内存)"]
  M --> G["llvm / spirv / nvgpu(目标方言)"]

每一步降阶都是一次”表示选择”:

关键设计决策是:每一步降阶都保存了当前方言能表达的最精确信息。如果你在 affine 层做 tiling,你有完整的仿射依赖信息决定 tile 大小;如果你在 scf 层做循环展开,你依赖的是更局部的数据流分析。选择在哪个方言层做哪种优化,成了编译器设计者的核心自由。

三、设计原则之二:方言可组合性(Dialect Composability)

方言是 MLIR 最显眼的特性,但”可组合性”才是它的核心差异——光有多个方言不够,关键是方言之间怎么协作。

一个方言定义了三样东西:

  1. Operation 集合:该方言支持的 Op(如 linalg.matmulscf.forarith.addi)。
  2. Type 集合:该方言使用的类型(如 tensor<256x256xf32>memref<?xi32>!llvm.struct<...>)。
  3. 方言间的接口(Interface):不依赖具体方言的抽象契约——例如”这个 Op 支持内联”(CallableOpInterface)、“这个 Region 是单入口单出口的”(RegionBranchOpInterface)、“这个循环可以获取其归纳变量”(LoopLikeOpInterface)。

三个特性让方言可以组合:

一是共存性:一个 Module 中可以同时存在 tensorlinalgscfarithllvm 等方言的 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 管理器。

七、本篇边界与后续路径

本篇给了全景图。后面的篇章按这个顺序展开:

参考资料

论文(A 级)

官方文档(A 级)

社区项目(B 级)

同主题继续阅读

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

2026-06-09 · compiler / architecture

【编译器与 MLIR】类型系统与属性

解析 MLIR 的类型体系:内建类型(Integer、Float、Tensor、MemRef)与自定义方言类型的注册机制;区分 Type 与 Attribute 的设计意图;通过 OpBuilder 理解类型和属性在 IR 构造中的实际角色。


By .