# code

从零学习MLIR(1)

📅 2026-10-14 ✍️ Layla

从一个demo开始

怎么编译看这里:https://mlir.llvm.org/getting_started/

准备工作

先从一个.mlir文件开始:/tmp/a.mlir

func.func @add(%a: i32, %b: i32) -> i32 {   # 一个函数,属于 func 方言
  %r = arith.addi %a, %b : i32              # 一次加法,属于 arith 方言
  return %r : i32                            # func.return
}
  • 它不是机器码,也不是 C/LLVM IR,而是 MLIR IR 的文本表述——相当于”MLIR 世界里的汇编源文件”。

  • @add 是符号名;%a/%b/%r 是 SSA 值(%a、%b 是函数参数,%r 是一次计算的结果)。

现在,在仓库根目录下执行:../build/bin/mlir-opt /tmp/a.mlir,它做的事是一条固定流水线:

读文本 → 解析(parse)成内存 IR → 校验(verify) → 跑 pass(可选) → 打印(print)成文本
  • 它是解析器 + 校验器 + pass 执行器 + 打印器四合一。
  • 不加任何 pass 时,它只是”读进来再写出去”,这叫 round-trip,用来验证 parser/printer 是否正确。
  • 加 --canonicalize、--pass-pipeline='...' 等才会真正变换 IR。
  • 它只认识”已注册的方言”——这正好是后面你自己造 dialect 要挂进去的地方

打印结果:

# === 不加 pass(round-trip)===
module {
  func.func @add(%arg0: i32, %arg1: i32) -> i32 {
    %0 = arith.addi %arg0, %arg1 : i32
    return %0 : i32
  }
}

看到这里,肯定有点懵。SSA是啥?pass又是啥?方言又是啥?执行命令之后好像没有很大的变化?不急,我们一个个来看。

概念解释

SSA

SSA(Static Single Assignment,静态单赋值):每个值只能被”赋值”一次,之后不再变;需要新结果就生成一个新值,而不是覆盖旧的。

对比一下普通命令式代码:

int s = 0;            // 同一个变量 s
s = s + 1;            // 被反复覆盖 → 不是 SSA
s = s + 2;

同样的逻辑在 MLIR(SSA 形式)里是这样,每次加法都产生一个全新的值:

%0 = arith.addi %s, %c1    // %0 是新值
%1 = arith.addi %0, %c2    // %1 是新值;%0 从没被改过
  • 每个 %名字 有且只有一个定义点(def),可以有多个使用点(use)。%0 在哪定义、被谁用,是显式可查的。

  • 值有作用域:一个 SSA 值只在其所在的 region 内可见(MLIR 里 region 是 Op 内部的代码块容器)。

  • 名字毫无语义。你写 %a、%r,解析后变成长整型 ID,打印时按顺序重新编号成 %arg0、%0、%1——这就是为什么刚才 %a 变成了 %arg0。

  • 为什么全世界编译器都要 SSA?因为它让优化变得简单可靠:常量折叠、死代码消除、公共子表达式消除,都变成”看 def-use 图”的机械操作。

pass

Pass(遍 / 处理步骤):Pass 是”读一段 IR、做一件事、可能改写它”的一个独立步骤。类似编译器流水线里的一环,或 Unix 管道里的一条命令。

那么,可不可以这样理解:“MLIR是多级IR,然后每次pass就相当于把IR降了一级”?

不完全对。

我们继续用实例来讲解:

  1. 原文件:/tmp/ssa.mlir
func.func @sum_to(%n: index) -> index {
  %c0 = arith.constant 0 : index
  %c1 = arith.constant 1 : index
  %r = scf.for %i = %c0 to %n step %c1 iter_args(%acc = %c0) -> (index) {
    %s = arith.addi %acc, %i : index
    scf.yield %s : index
  }
  return %r : index
}

一个 scf.for 求和循环。三个方言:func(函数)、arith(常量/加法)、scf(结构化循环)。SSA 值:%n(参数)、%c0/%c1(常量)、%r(结果)、%i(归纳变量)、%acc(累加器)、%s(单轮和)。

  1. round-trip(不加pass)

../build/bin/mlir-opt /tmp/a.mlir

module {
  func.func @sum_to(%arg0: index) -> index {
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    %0 = scf.for %arg1 = %c0 to %arg0 step %c1 iter_args(%arg2 = %c0) -> (index) {
      %1 = arith.addi %arg2, %arg1 : index
      scf.yield %1 : index
    }
    return %0 : index
  }
}

变化:

%n   → %arg0
%i   → %arg1     %acc → %arg2
%r   → %0        %s   → %1
外部多包了一层 module { ... }

结构零变化,只有命名被规范化 → 证明 SSA 名字纯粹是给人看的。

  1. --canonicalize(同层优化)

输出和 1(Round-trip) 一模一样。这本身是个重要观察:canonicalize 只在有规则命中时才改 IR,这里没有可化简的东西,所以它是 no-op。这就说明了,“跑 pass 不一定变样,更不一定降级”。

  1. --convert-scf-to-cf(降一层:结构化循环 → 基本块 + 跳转)
module {
  func.func @sum_to(%arg0: index) -> index {
    %c0 = arith.constant 0 : index
    %c1 = arith.constant 1 : index
    cf.br ^bb1(%c0, %c0 : index, index)
  ^bb1(%0: index, %1: index):  // 2 preds: ^bb0, ^bb2
    %2 = arith.cmpi slt, %0, %arg0 : index
    cf.cond_br %2, ^bb2, ^bb3
  ^bb2:  // pred: ^bb1
    %3 = arith.addi %1, %0 : index
    %4 = arith.addi %0, %c1 overflow<nsw> : index
    cf.br ^bb1(%4, %3 : index, index)
  ^bb3:  // pred: ^bb1
    return %1 : index
  }
}

循环解体:

scf.for 的 iter_args(%arg2 = %c0)  →  ^bb1(%0: index, %1: index) 的块参数
%i(归纳变量)                      →  %0,初始由 cf.br ^bb1(%c0, %c0) 传入
%acc(累加器)                      →  %1,同理
循环条件                            →  arith.cmpi slt, %0, %arg0   +   cf.cond_br
scf.yield %s : index                →  cf.br ^bb1(%4, %3)(把新值回灌给块参数)

后面就不详细展开讲了,大概能理解pass的意思就行。

方言(dialect)

Dialect 是一组相关 Op/Type/Attribute/规则 的命名空间,是 MLIR 的”扩展单元”。MLIR 本身不是一种固定指令集的 IR。它只是一个”IR 框架”,提供对象模型和基础设施;所有具体指令都来自某个方言。

需要注意的是,并不是“一层代表一个方言”,方言是”词汇表”,层级是”抽象高度”,两者是正交的两个维度。 一次 lowering 改的是”抽象高度”(靠 ConversionTarget 判定),而”换了哪个方言”只是随之发生的结果之一,不是定义。

常见的方言:

方言 主要用途
arith 加法、乘法等算术运算
tensor 表示张量值
linalg 矩阵乘法、张量计算等线性代数操作
memref 表示带有内存布局信息的内存引用
scf 表示循环、条件分支等控制流
vector 表示向量化计算
gpu 表示 GPU 线程层级和 GPU Kernel
llvm 表示接近 LLVM IR 的底层操作

# 研究讨论 / Discussions