设计院来了位实习生,老师傅带他认门:“咱们院的图纸分好几种画风——建筑师画概念,结构师出计算书,施工队要精确到毫米。它们都是图纸,但接收对象各不相同。”MLIR 的故事就从这“几种画风”开始。MLIR 是 LLVM 项目发起的一套多层级中间表示(IR)框架。这里的“方言”(Dialect),正如设计院里的“制图标准”:每种标准规定自己的图例符号(Operation)、材质标注(Type)和附加说明(Attribute)。一栋楼从概念图变成施工图,要经过概念设计(Linalg)→结构计算(Affine)→机电流程(SCF)→施工图(LLVM IR)→施工(机器码)的逐级深化,每一级由一位“校审员”(Pass)把关。这套“图纸流转体系”就是 MLIR。

岗位关联

在 LLM 时代,谁能把 AI 模型又快又稳地“翻译”到硬件上,谁就掌握推理性能的主动权。理解 MLIR 方言,就是理解 AI 编译器这张隐形的图纸流转网;这是 AI 系统工程师区分“调包侠”与“系统专家”的关键一环。

目录
  1. 可视化演示
  2. 认识方言体系
  3. 多层IR架构
  4. Linalg张量运算
  5. Affine循环系统
  6. SCF控制流
  7. 自定义Dialect
  8. Lowering管道
  9. LLVM生态整合
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

演示MLIR多级中间表示体系:分层降级展示高层Linalg经Affine/SCF到LLVM IR的渐进式Lowering;核心结构展示Operation/Region/Block三层数据;自定义方言展示ODS定义扩展领域专用操作。

本节配有交互动效(进入视口后自动加载,约 3KB)。

渐进式Lowering 高层→中层→底层 Linalg→LLVM 逐层降级到机器码

认识方言体系

设计院来了位实习生,老师傅带他认门:“咱们院的图纸分好几种画风——建筑师画概念,结构师出计算书,施工队要精确到毫米。它们都是图纸,但接收对象各不相同。”MLIR 的故事就从这“几种画风”开始。MLIR 是一套多层级中间表示(IR)框架,这里的“方言”(Dialect)正如设计院里的“制图标准”:每种标准规定自己有哪些图例符号(Operation)、哪些材质标注(Type)和哪些附加说明(Attribute)。

为什么编译器这么需要“方言”?因为一段 AI 代码,从 PyTorch 的矩阵乘法到 CPU 上的加法指令,跨度实在太大。传统 LLVM 只有一种 IR,就像施工队只看得懂施工图,而 AI 框架画出的“概念图”没人接得住。MLIR 让整个链条上每一层都有自己的 IR,每套 IR 就是一种方言;各层之间通过变换函数互相翻译。

一个典型的 MLIR 工作流里,编译器把高层方言的操作逐步翻译成低层方言:例如把 linalg.matmul 变成仿射循环。每次翻译称为一次 Lowering。只要翻译保持语义——也就是计算结果不变——就可以一层层往下走。

设计院里,方案图到施工图的每一轮深化,楼的形状没变,但标注越来越密。MLIR 的方言与 Lowering,正是把这套“逐级深化图纸”的规则固化成了编译器的基础设施。

IRk+1=Tk(IRk)IR_{k+1} = \mathcal{T}_k(IR_k)
每次变换函数 T 把第 k 级中间表示改写成第 k+1 级,像把一份建筑方案图改写成结构计算书。
  1. IR_k(第 k 级中间表示)输入的中间代码,如 Linalg 方言的一段矩阵乘法。
    IRkIR_k
  2. 变换函数 T_k一个 Lowering Pass,负责把一种方言翻译成另一种方言。
    Tk\mathcal{T}_k
  3. IR_{k+1}(输出结果)翻译后的新代码,例如 Affine 方言的三层循环。
    IRk+1IR_{k+1}
  4. 语义保持翻译前后计算结果不变,只是表达粒度变细了。
    sem(IRk+1)=sem(IRk)\text{sem}(IR_{k+1}) = \text{sem}(IR_k)

多层IR架构

实习生翻档案馆,发现同一栋楼有“方案图、初设图、施工图、竣工图”四套。老师傅说:“咱们院用四级图纸体系:概念图表达创意,结构图定位梁柱,机电图组织流程,施工图给工人照着做。”MLIR 也内置了类似的四套制图标准:Linalg、Affine、SCF 和 LLVM IR。

这四层并非随意排列,而是“上下游”分明。Linalg 面向张量整体运算,便于做算子融合和向量化;Affine 把循环写成仿射形式,便于做循环变换和依赖分析;SCF 提供结构化控制流,便于并行化与分布;LLVM IR 则直接对接成熟的 LLVM 后端。高层 IR 关心“优化空间大不大”,低层 IR 关心“机器能不能直接执行”。

多层 IR 的核心价值在于“渐进式 Lowering”:把一个巨大的、从张量到寄存器的问题,拆成一串小步骤。每步只拆一点,每层都保留自己那层特有的信息。好比设计院不会让一个人同时把建筑、结构、机电全画完,而是一层层深化、一层层会签。

就像四张图纸都描述同一栋楼,只是粒度不同;MLIR 的四层 IR 也描述同一个计算,只是越来越接近机器。最终施工队拿到的不是“曲面网格”,而是每根构件的精确坐标。

P=L3∘L2∘L1\mathcal{P} = \mathcal{L}_3 \circ \mathcal{L}_2 \circ \mathcal{L}_1
这是一个 Lowering 管道:先把 Linalg 变 Affine,再把 Affine 变 SCF,最后把 SCF 变 LLVM IR;复合函数表示三步串联执行。
  1. L1Linalg 到 Affine:把张量的整体运算展开成循环加访存。
    L1:Linalg→Affine\mathcal{L}_1: \text{Linalg} \to \text{Affine}
  2. L2Affine 到 SCF:把仿射循环转为通用结构化控制流。
    L2:Affine→SCF\mathcal{L}_2: \text{Affine} \to \text{SCF}
  3. L3SCF 到 LLVM IR:把结构化控制流转为基本块与跳转。
    L3:SCF→LLVM IR\mathcal{L}_3: \text{SCF} \to \text{LLVM IR}
  4. 复合管道L1 先执行,再 L2,最后 L3。
    P=L3(L2(L1(IR)))\mathcal{P} = \mathcal{L}_3(\mathcal{L}_2(\mathcal{L}_1(IR)))

Linalg张量运算

建筑师在概念图上写:“玻璃幕墙的双曲网格由 M×N 个单元组成,每个单元通过插值生成。”他不写“先把第 1 根钢条焊上,再焊第 2 根”。这正是 Linalg 方言的思维方式:描述“算什么”,而不是“怎么算”。

Linalg 的名字来自 Linear Algebra(线性代数),是 MLIR 里的高层张量运算方言。一个 linalg.matmul 代表整个矩阵乘法,操作对象是 tensor<4x4xf32> 这样的张量类型。linalg.generic 是更通用的形式,用 indexing_maps 说明每个输入维度如何映射到输出维度,相当于概念图上标注了“这块曲面由哪些参数生成”。

这种“整体视角”带来三个好处。其一,融合:A×B+C 可以看作一次整体操作,方便复用中间结果而不必写回内存。其二,并行:看到 matmul,就知道输出的每个元素彼此独立,天然可以并行计算。其三,匹配硬件:识别出是矩阵乘法后,编译器可以针对 GPU 的 Tensor Core 或 TPU 的脉动阵列生成专门代码。一旦拆成逐条标量指令,这些信息就全丢了。

概念图的好处是一眼看懂意图,代价是工人没法直接干活。Linalg 也一样:它让优化器一眼看到“这是矩阵乘”,但 CPU 还不知道每一步做什么——所以必须交给下一层。

C[i,j]=∑k=0K−1A[i,k]×B[k,j]C[i, j] = \sum_{k=0}^{K-1} A[i, k] \times B[k, j]
这是 linalg.matmul(A, B, C) 所隐含的数学语义:结果矩阵第 i 行第 j 列元素,等于 A 的第 i 行与 B 的第 j 列做点积。
  1. 输出位置结果矩阵的第 i 行第 j 列。
    C[i,j]C[i, j]
  2. 归约求和对 k 从 0 到 K-1 遍历累加。
    ∑k=0K−1\sum_{k=0}^{K-1}
  3. 对应乘积A 与 B 在 k 维度上的配对相乘。
    A[i,k]×B[k,j]A[i, k] \times B[k, j]
  4. 整体封装一条 Linalg 操作等价于上述求和式。
    linalg.matmul(A,B)→C\texttt{linalg.matmul}(A, B) \to C

Affine循环系统

结构工程师拿到概念图后,把曲面网格拆成“第 i 行第 j 根钢柱的位置可以用数学公式算出来”。他画的是精确定位图,上面全是循环和索引。这就是 Affine 方言:它提供 affine.for(带仿射边界的循环)、affine.load 和 affine.store(带仿射索引的访存)。

所谓“仿射”,指循环的边界和下标必须是关于循环变量的线性组合加常数,形如 a*i + b*j + c。这个限制看似死板,实则是“黄金枷锁”。因为编译器可以用整数线性代数工具做三件事:依赖分析——判断两次访存会不会碰到同一位置;循环变换——交换、分块、倾斜循环并保证语义不变;并行提取——发现无依赖的循环直接并行化。如果换成任意下标表达式(比如 A[x[i]]),所有这些数学工具就都失效了。

Affine 是 MLIR 中“高层可优化”与“低层可执行”之间的桥梁。它比 Linalg 更细:把一个大矩阵乘法拆成了三层循环;又比 SCF 更严格:循环边界必须是仿射表达式。正是这份严格,让编译器敢在它身上做各种数学上可证明的变换。

结构图上的每个数字都能用公式推出来、都可以验证。这正是 Affine 被编译器信赖的原因——数学上站得住,机器也容易推理。

ϕ(i)=M⋅i+b\phi(\mathbf{i}) = \mathbf{M} \cdot \mathbf{i} + \mathbf{b}
仿射映射:循环变量向量 i 经过整数矩阵 M 的线性变换再加偏置 b,得到新的下标。
  1. 循环变量向量例如二维循环中的 (i, j)。
    i=(ij)\mathbf{i} = \begin{pmatrix} i \\ j \end{pmatrix}
  2. 整数矩阵规定各循环变量之间的线性组合权重。
    M∈Zm×n\mathbf{M} \in \mathbb{Z}^{m \times n}
  3. 整数偏置平移常数,比如 i+2 里的 2。
    b∈Zm\mathbf{b} \in \mathbb{Z}^m
  4. 具体示例下标变换 (i, j) 变为 (i, i+j+1)。
    ϕ(i,j)=(i,  i+j+1)\phi(i, j) = (i, \; i + j + 1)

SCF控制流

机电工程师关心的不是哪根柱子,而是“电怎么从变压器流到机房、遇到控制器要分个支”。他会画流程图:每一层敷设桥架;如果这层是设备层,就加大线径。这正是 SCF 方言的活。

SCF 全称 Structured Control Flow(结构化控制流),提供 scf.for(通用循环)、scf.if(条件分支)和 scf.while(条件循环)。它比 Affine 更宽容:循环边界不需要仿射,可以是任意变量;条件分支也是结构化的,没有 goto。MLIR 里还有一个 scf.parallel,为 GPU Kernel 的并行划分提供结构基础。

为什么需要这一层“宽容”?两个原因。第一,AI 程序里大量循环无法用仿射刻画,比如 while error > eps 这种根据运行结果决定是否继续的循环。第二,结构化控制流让编译器能轻松画出控制流图(CFG)、做 SSA 分析、做并行化;如果直接坠入 llvm.br 这种裸跳转,分析难度会陡增。SCF 是 Affine 与真正的底层之间最舒适的“过渡层”。

机电流程图比结构图灵活得多,但仍然有序、可审查。SCF 也是如此:它把“走哪条路、循环几遍”说得明明白白,又不至于沦落到跳转满天飞的境地。

[for  i=l … u  {S}]≡S(l); S(l+1); …; S(u−1)\left[ \texttt{for} \; i = l \, \ldots \, u \; \{ S \} \right] \equiv S(l); \, S(l+1); \, \ldots; \, S(u-1)
这条公式说明:一个 for 循环在语义上等价于把循环体 S 按顺序展开执行多次。
  1. 循环边界i 从 l 到 u 但不包含 u。
    l≤i<ul \le i < u
  2. 循环体 S每次迭代执行的一组操作。
    S(i)S(i)
  3. 等价展开循环等于依次执行 S(l) 到 S(u-1)。
    S(l);S(l+1);… ;S(u−1)S(l); S(l+1); \dots; S(u-1)

自定义Dialect

设计院接了一个“张拉膜体育馆”项目,标准图例画不出膜面张力节点。于是院领导说:“给这个项目定制一套张拉膜图例,并在设计软件里注册好。”MLIR 的机制与此如出一辙:自定义 Dialect。

MLIR 允许用户定义自己的方言:一套专属操作(Operation)、类型(Type)与属性(Attribute)。常用方法是 ODS(Operation Definition Specification),用 TableGen 语言声明式描述,再由工具自动生成 C++ 主体代码。一个自定义操作的声明包括:操作名(如 tpu.mma)、操作数、结果、属性,以及验证器。

声明完成后,MLIR 会自动为这个操作生成打印、解析、验证、文档等功能。你只需要实现 Pass,把这些自定义操作逐步 Lowering 到其他方言。芯片厂商常定义 rocm.*、tpu.* 这类方言来表达自家指令;AI 框架也可以定义自己的方言来承载框架级语义。这就是 MLIR 被称为“编译器的编译器”的原因。

定制图例必须被全院登记认可,才能与其他专业的图纸混用。自定义方言也必须注册进 MLIRContext,才能参与整个编译流程的流转。

Op:(Operands)×(Attributes)→Results,Verifier(Op)↦{0,1}\text{Op}: (\text{Operands}) \times (\text{Attributes}) \to \text{Results}, \qquad\text{Verifier}(\text{Op}) \mapsto \{0, 1\}
操作签名:一个操作由操作数和属性共同决定输出结果;验证器检查这个操作是否合法,合法返回 1,非法返回 0。
  1. 操作数参与运算的输入,例如矩阵 A 和 B。
    Operands={A,B}\text{Operands} = \{A, B\}
  2. 属性编译期已知的常量参数,如转置标记。
    Attributes={transpose}\text{Attributes} = \{\text{transpose}\}
  3. 结果操作产生的输出。
    Results={C}\text{Results} = \{C\}
  4. 验证器判定操作是否合法,非法则拒绝进入 IR。
    Verifier(Op)=1  ⟺  合法\text{Verifier}(\text{Op}) = 1 \iff \text{合法}

Lowering管道

设计院有一套“校对→审核→审定”的出图流程。每位校审员只查一个专业,但按固定顺序过一遍,最终图纸质量就有保证。MLIR 的 Lowering Pass 管道,就是这套校审流程。

Pass 是 MLIR 中“一次 IR 变换或优化”的独立单元。比如“仿射展开 Pass”把 affine.for 变成 scf.for,“死代码消除 Pass”删掉没有用的代码。Pass 按顺序执行就形成管道(Pipeline)。用 mlir-opt 工具时,命令行写什么标志,就是选什么管道:

Pass 之间并非完全孤立:MLIR 有 AnalysisManager,上游 Pass 的分析结果可以传给下游复用。比如依赖分析的结果,可以直接喂给循环分块 Pass,避免重复计算。管道的设计是 MLIR 工程的核心艺术:Pass 太碎片化则开销大,太粗犷则难以组合复用。

校审员各有分工,但串联成流程后,整条链路就可靠了。MLIR 不要求一个 Pass 干完所有事,而是用小而专的 Pass 组合出强大管道——这正是渐进式 Lowering 的工程化体现。

P=Pn∘Pn−1∘⋯∘P1\mathcal{P} = P_n \circ P_{n-1} \circ \cdots \circ P_1
整条管道等价于先执行 P1,再执行 P2,依次类推;每一步只做一件小事。
  1. 单个 Pass一次原子的 IR 变换或优化。
    PiP_i
  2. 复合管道多个 Pass 串行执行。
    P=P3(P2(P1(IR)))\mathcal{P} = P_3(P_2(P_1(IR)))
  3. 顺序敏感性改变 Pass 顺序可能带来不同结果。
    P1≠P2⇒结果可能不同\mathcal{P}_1 \neq \mathcal{P}_2 \Rightarrow \text{结果可能不同}
text1 行
--linalg-to-affine --affine-to-scf --convert-scf-to-cf --convert-cf-to-llvm

LLVM生态整合

图纸最后流转到施工队项目经理手里,他看一眼就喊:“按图施工,标准工艺,没问题。”施工队有成熟的模板、机具和验收标准——这就是 LLVM。

MLIR 的 llvm 方言与 LLVM IR 几乎一一对应:llvm.load、llvm.add、llvm.br 等。把 SCF、CF 等方言转换到 llvm 方言后,交给 LLVM 就完成了从中间表示到目标机器码的最后一步。这一步是真正的“施工”:指令选择、寄存器分配、指令调度、机器码生成,全部由 LLVM 后端包办。

LLVM 给 MLIR 送来两样大礼。其一是成熟的优化器:指令合并、循环展开、向量化,这些经过十多年打磨的优化直接可用。其二是遍布 x86、ARM、RISC-V、NVIDIA 等平台的后端代码生成器。MLIR 因此不必重建编译器最苦最累的机械工作,而是专注于“高层语义的表达与转换”。这就是为什么一个 PyTorch 模型能从 torch.matmul 开始,经由 MLIR 缓缓下沉,最终变成 GPU 上的一条条指令。

施工队把标准施工工艺都包了,设计院只需要把图画到底。MLIR 专注“高层的图怎么画”,LLVM 负责“最底层的活怎么干”——这对黄金搭档撑起了现代 AI 编译器的半壁江山。

MC=Gtarget(LLVM IR)\text{MC} = \mathcal{G}_{\text{target}}(\text{LLVM IR})
机器码 = 目标架构的代码生成函数作用于 LLVM IR。生成函数负责把低层 IR 翻译成具体芯片的二进制指令。
  1. 输入 LLVM IRMLIR 逐层下沉后产出的低级中间表示。
    LLVM IR\text{LLVM IR}
  2. 目标架构例如 x86、AArch64、NVIDIA。
    target∈{x86,AArch64,…}\text{target} \in \{\text{x86}, \text{AArch64}, \ldots\}
  3. 代码生成函数包含指令选择、寄存器分配、指令调度。
    Gtarget\mathcal{G}_{\text{target}}
  4. 输出机器码芯片真正执行的可执行指令。
    MC\text{MC}

AI 应用场景

AI芯片编译器的方言桥

阶段:模型编译与推理优化

解决的问题:NVIDIA、AMD、寒武纪等芯片要用自家指令计算矩阵乘加,PyTorch 代码不能直接跑在自研芯片上。传统做法是给每个框架各写一套后端,工作量大且难维护。MLIR 提供了一条“方言桥”:芯片厂商自定义 tpu.* 方言表达硬件算子,再写 Lowering Pass 将 Linalg 等高层方言逐步下沉到自研方言,最后交 LLVM 后端。就像设计院为特殊材料定制图例,再纳入统一图纸流转。

具体案例:某 AI 芯片公司新增了一个“块状稀疏矩阵乘”硬件单元。编译器团队用 ODS 定义一个 sparse.mma 方言操作,写一个 Pass 把 Linalg 的 linalg.matmul 识别为稀疏模式并替换,再经过常规 Lowering 管道落到 LLVM 方言,最终生成芯片指令。

大模型推理引擎的热点优化

阶段:推理部署与性能调优

解决的问题:vLLM、TensorRT-LLM 等推理引擎要对注意力机制做算子融合、对 KV Cache 做内存优化。手动写 CUDA Kernel 改一处就要重新编译,迭代极慢。工程师用 MLIR 在 Linalg 层把注意力里的多个算子融合成一个大 Kernel,再逐层 Lowering 到 CUDA 代码。融合是“把多张图纸合成一张施工图”,减少中间搬运。

具体案例:某推理框架团队发现在 Linalg 层可以将 QK^T、Softmax、PV 三段融合为一个算子。他们在 MLIR 中写了一个融合 Pass,把三个 linalg.generic 合并为一个,内存读写减少 40%,端到端推理吞吐提升 18%。

新硬件架构的快速适配

阶段:异构计算平台开发

解决的问题:AI 芯片新增了专用激活函数单元,标准 LLVM 完全不认识。没有 MLIR 时,要改 LLVM 后端、加内建函数、改框架代码,至少数月。有了 MLIR,开发者只需定义一个新 Dialect 描述该指令的语义,写一个 Pass 将其 Lowering 到 LLVM 方言,再借助内联汇编或 LLVM 内建机制接住。设计院的“新图例”几天就能上线。

具体案例:某自动驾驶芯片团队增加了一个“行归一化加速器”。他们用 ODS 定义了 ada.norm 方言操作,用 2000 行 TableGen 与 C++ 完成定义与 Lowering,两周内让全栈 AI 框架跑通该加速器。

Python 演示

python66 行
# mini-mlir.py# 演示 MLIR 渐进式 Lowering: 向量加法 a = b + c 在四层方言中的形态变化 class MiniIR:    LEVELS = ['linalg', 'affine', 'scf', 'llvm']     def __init__(self, text, level):        self.text = text        self.level = level        self.templates = [            # level 0 -> 1: linalg.generic 展开为仿射循环            "affine.for %i = 0 to 4 {\n"            "  %vb = affine.load %b[%i] : memref<4xf32>\n"            "  %vc = affine.load %c[%i] : memref<4xf32>\n"            "  %v = addf %vb, %vc\n"            "  affine.store %v, %a[%i] : memref<4xf32>\n"            "}",            # level 1 -> 2: 仿射循环转为通用 scf 循环            "scf.for %i = 0 to 4 step 1 {\n"            "  %vb = scf.load %b[%i] : memref<4xf32>\n"            "  %vc = scf.load %c[%i] : memref<4xf32>\n"            "  %v = addf %vb, %vc\n"            "  scf.store %v, %a[%i] : memref<4xf32>\n"            "}",            # level 2 -> 3: scf 转为 llvm 基本块与跳转            "llvm.br ^bb1\n"            "^bb1:\n"            "  %i = llvm.phi ...\n"            "  %vb = llvm.load %b[%i]\n"            "  %vc = llvm.load %c[%i]\n"            "  %v = llvm.add %vb, %vc\n"            "  llvm.store %v, %a[%i]\n"            "  llvm.cond_br %cond, ^bb1, ^bb2"        ]     @property    def level_name(self):        return MiniIR.LEVELS[self.level]     def lower(self):        # 下降一级, 返回新的 MiniIR 实例        if self.level >= len(MiniIR.LEVELS) - 1:            return self        return MiniIR(self.templates[self.level], self.level + 1)  def main():    print("=== MLIR 渐进式 Lowering 演示: a = b + c (向量加法) ===")    ir = MiniIR(        "linalg.generic {\n"        "  indexing_maps = [affine_map<(i)->(i)>,\n"        "                   affine_map<(i)->(i)>,\n"        "                   affine_map<(i)->(i)>]\n"        "} ins(%b, %c: tensor<4xf32>) outs(%a: tensor<4xf32>)",        0    )    while True:        print("\n----- " + ir.level_name.upper() + " -----")        print(ir.text)        if ir.level == len(MiniIR.LEVELS) - 1:            break        ir = ir.lower()  if __name__ == "__main__":    main()
目的:用一个迷你版“MLIR 模拟器”展示向量加法内核从 Linalg 高层方言逐级下沉到 LLVM 方言的过程。
  1. class MiniIR:定义一个迷你 IR 对象,level 表示当前方言层级。
  2. self.templates预置三次降级的文本模板,对应 Linalg→Affine→SCF→LLVM 的三跳。
  3. def lower(self):下降一级并返回新实例,模拟一个 Lowering Pass 的产物。
  4. while True:循环打印每一层形态,直到抵达最低层 LLVM。
  5. if __name__ == "__main__":提供可执行入口,让脚本可以直接运行。

常见误区

误区MLIR 是像 PyTorch 一样的深度学习框架,可以直接加载模型、做训练和推理。

正解MLIR 不训练、不推理,它是一套“编译器积木”,帮助把各种框架的代码最终翻译成芯片指令。比喻:设计院本身不盖楼,它管理图纸的流转与深化。PyTorch 是建筑师,MLIR 是图纸流转系统,LLVM 是施工队。

为何常见:MLIR 常出现在 AI 芯片与框架优化论文中,名字又带"ML",容易被误认为是又一个端到端的 AI 框架。

误区既然 MLIR 被称为“下一代编译器基础设施”,它迟早完全取代 LLVM。

正解MLIR 的下沉链路末端就是 LLVM IR,MLIR 负责高层表达与优化,LLVM 负责底层后端代码生成,两者是上下游互补关系,不是替代关系。比喻:设计院把图画到施工图,但真正施工还得靠经验丰富的施工队。

为何常见:MLIR 与 LLVM 同出 LLVM 项目,名称相似,初学者容易把它们放在同一个生态位里比较。

误区代码经过越多层变换就越低效,应该一步到位从高层 IR 直接生成机器码。

正解多层 IR 不是多余的中间环节,而是每一层都保留了不同粒度的优化机会,让每个 Pass 只做一件简单的事,总成本反而更低。比喻:写文章一稿到位容易逻辑混乱,逐章修改反而更稳妥。

为何常见:直觉上“中间步骤少=开销少”,忽略了编译器分析复杂度的指数级增长;一次性完成全部 Lowering 的 Pass 几乎无法编写和验证。

误区用 MLIR 自定义一个方言至少要手写几千行 C++ 基础设施,普通人根本碰不了。

正解用 ODS/TableGen 声明式描述后,生成器会自动产出打印、解析、验证等大量样板代码,这是 MLIR 的核心卖点之一。比喻:设计院定了新图例后,制图软件会自动生成符号库和校验规则,不用手工刻印章。

为何常见:MLIR 的 C++ 源码庞大,初学者点开源码看到海量模板代码就望而生畏,忽略了框架本身已替你完成了大部分机械工作。

面试题

Q1. MLIR 中用“方言(Dialect)”指代什么?

单选题 · easy

  • A.一套图纸校审流程
  • B.一套制图标准体系← 正确答案
  • C.一个模型训练框架
  • D.一台代码优化机器

答案:B。方言是 MLIR 中一组操作、类型与属性的集合,类似设计院中“各专业的制图标准”。选项 A 比喻的是 Pass,C 是 PyTorch,D 是 LLVM 优化器,都不符合“方言”的定义。

Q2. 渐进式 Lowering 的核心含义是?

单选题 · easy

  • A.高层IR逐步拆成低层IR← 正确答案
  • B.一次性翻译成机器码
  • C.反向提升IR抽象级别
  • D.只检查语法不改变IR

答案:A。Lowering 是一层层把高层中间表示拆解成低层中间表示,像设计院把概念图逐步深化为施工图。B 是“一步到位”式做法,C 方向反了,D 是校验而非转换。

Q3. 以下四种方言按抽象层级从高到低排列,正确的是?

单选题 · mid

  • A.LLVM IR, SCF, Affine, Linalg
  • B.Linalg, Affine, SCF, LLVM IR← 正确答案
  • C.SCF, LLVM IR, Linalg, Affine
  • D.Affine, Linalg, SCF, LLVM IR

答案:B。Linalg 描述张量整体运算最抽象,Affine 暴露循环与索引,SCF 提供通用控制流,LLVM IR 最贴近机器码,对应方案图→结构图→机电图→施工图的顺序。

Q4. Linalg 方言的一大特色是?

单选题 · mid

  • A.把张量运算当作整体优化← 正确答案
  • B.循环边界必须为常数
  • C.直接生成机器码输出
  • D.只处理标量 load 指令

答案:A。Linalg 把 matmul 这类张量运算看作整体,便于算子融合与并行识别,如同建筑师把曲面网格看作整体。B 描述的是更底层的限制,C 是 LLVM 后端的工作,D 与 Linalg 的 tensor 语义相反。

Q5. Affine 循环索引的优势在于?

单选题 · mid

  • A.索引满足仿射关系便于分析← 正确答案
  • B.索引可以是任意哈希函数
  • C.循环体内允许任意 goto
  • D.不再需要对循环做分析

答案:A。仿射索引让循环的下标依赖能用整数线性代数分析,从而做依赖检测与循环变换,好比结构图的每个数字都能用公式验证。B 会破坏可分析性,C 是非结构化跳转,D 与事实相反。

Q6. 下列属于 SCF 结构化控制流的包括?

多选题 · mid

  • A.scf.for 循环结构← 正确答案
  • B.scf.if 条件分支结构← 正确答案
  • C.llvm.br 无条件跳转
  • D.scf.while 循环结构← 正确答案

答案:ABD。SCF 方言提供 for、if、while 等结构化控制流。llvm.br 属于 LLVM 方言的非结构化跳转,是 SCF 被 Lowering 之后才会出现的东西,好比施工图里的定位线已经不含设计意图。

Q7. 自定义 MLIR Dialect 通常使用?

单选题 · mid

  • A.ODS 与 TableGen 声明定义← 正确答案
  • B.手工编写全部解析代码
  • C.修改 LLVM 后端源码
  • D.使用 JSON 描述所有 Pass

答案:A。MLIR 官方推荐用 ODS(Operation Definition Specification)配合 TableGen 声明操作,自动生成大量 C++ 代码。手写全部解析与验证代码违背 MLIR 的设计初衷,JSON 也不是标准定义方式。

Q8. 关于 Pass 与 Pass 管道,正确的有?

多选题 · hard

  • A.每个 Pass 只做一种变换← 正确答案
  • B.管道中 Pass 顺序毫无影响
  • C.管道渐进下沉到 LLVM IR← 正确答案
  • D.Pass 必须一口气完成降级

答案:AC。Pass 的设计哲学是“小而专”,每个只做一种变换或优化;多个 Pass 串成管道后实现渐进式 Lowering,最终到达 LLVM IR。Pass 顺序对结果有影响,也无需一口气完成全部降级。

Q9. MLIR 下沉到 LLVM IR 之后,主要依靠?

单选题 · hard

  • A.LLVM 后端的指令选择与分配← 正确答案
  • B.再次手工写一遍 TOSA 方言
  • C.用 Python 逐条解释执行
  • D.把 IR 重新提交给前端模型

答案:A。LLVM IR 之后的工作——指令选择、寄存器分配、指令调度——全部由 LLVM 后端完成。MLIR 不必重建这些机械工作,就像设计院把施工交给成熟施工队。TOSA 是更早的方言,Python 执行与流程无关。

Q10. 为什么需要多层 IR 而非一层?

多选题 · hard

  • A.高层 IR 便于整体融合优化← 正确答案
  • B.低层 IR 贴近硬件指令语义← 正确答案
  • C.各层可以复用 LLVM 后端← 正确答案
  • D.多层 IR 只是为了展示美观

答案:ABC。高层 IR 保留张量结构便于融合与并行,低层 IR 贴近硬件便于代码生成,LLVM 后端作为最终出口被多层管道复用。D 是干扰项,多层 IR 是为了工程可维护性与优化空间,而不是为了好看。