MLIR:面向领域专用计算的可扩展编译基础设施(动机与设计原则)
MLIR 是一个用于构建可复用、可扩展编译基础设施的项目;这篇论文是它的原始设计文献,解释了"为什么在 LLVM 之后还要再造一套编译器基础设施",并把答案落实为三条总体原则与一组可核查的设计原则。 论文发表于 CGO 2021,作者为 MLIR 核心团队(Lattner、Amini 等十人)[1]。端侧 NPU 的编译栈笔记同类概念(dialect、progressive lowering、region)均以本篇为源头,见文末与本站 kernel fusion 笔记的对照hexagon-mlir-note。
本文依据 IEEE CGO 2021 正式版 PDF(13 页),引文后的 [n](§…) 标注对应论文章节;引文中的 [3]、[17] 等编号一律是论文自带的参考文献编号,与本文末尾的参考文献无关。原始 PDF 已归档。
1. 动机:为什么要做 MLIR
1.1 摘要定位:解决碎片化与异构硬件编译
论文摘要第一句就是全文的承诺:
This work presents MLIR, a novel approach to building reusable and extensible compiler infrastructure. MLIR addresses software fragmentation, compilation for heterogeneous hardware, significantly reducing the cost of building domain specific compilers, and connecting existing compilers together.[1](摘要)
四个目标各自对应一章正文:软件碎片化(§I-B)、异构硬件编译(§IV)、降低领域编译器的构建成本(§II 的全部理由)、以及把现有编译器接起来(§IV 的导入用例)。它进一步说明 MLIR 的适用面:
MLIR facilitates the design and implementation of code generators, translators and optimizers at different levels of abstraction and across application domains, hardware targets and execution environments.[1](摘要)
注意这里的"不同抽象层级":MLIR 不是一个新 IR,而是一套可以承载任意多层 IR 的框架——这是它与 LLVM 最根本的区别,也是理解后文所有原则的前提。
1.2 通用 IR 的"一刀切"困境(§I)
论文先肯定 LLVM/JVM 这类成熟平台的价值,再指出它们的隐含假设:
A common characteristic of these popular systems is their "one size fits all" approach—a single abstraction level to interface with the system: the LLVM Intermediate Representation (IR) is roughly "C with vectors", and JVM provides an "object-oriented type system with a garbage collector" abstraction.[1](§I)
也就是说,LLVM IR 大约是"带向量的 C 语言",它只有一个抽象层级。对"恰好长成 C 的程序"这一层非常好用;但很多问题天生属于更高或更低的层级:
At the same time, many problems are better modeled at a higher- or lower-level abstraction, e.g. source-level analysis of C++ code is very difficult on LLVM IR.[1](§I)
降得太低,信息(类型结构、别名关系、循环语义)就没了。于是各领域开始自建 IR:
We observe that many languages (including e.g. Swift, Rust, Julia, Fortran) develop their own IR in order to solve domain-specific problems, like language/library-specific optimizations, flow-sensitive type checking (e.g. for linear types), and to improve the implementation of the lowering process.[1](§I)
1.3 自建 IR 的真正问题:重复且质量不齐
自建 IR 本身不是错,错在每次都要从零造一遍基础设施:
While the development of domain-specific IRs is a well studied art, their engineering and implementation cost remains high. The quality of the infrastructure is not always a first priority (or easy to justify) for implementers of these systems.[1](§I)
领域专家的精力在语义上,基础设施(解析、打印、验证、pass 管理)只是"为了能用"。后果直接暴露给用户:
Consequently, this can lead to lower quality compiler systems, including user-visible problems like slow compile times, buggy implementations, suboptimal diagnostic quality, poor debugging experience for optimized code, etc.[1](§I)
这就是 MLIR 的靶心:让"定义一个新的抽象层级"变得便宜,让公共基础设施"开箱即用":
The MLIR project aims to directly tackle these programming language design and implementation challenges—by making it cheap to define and introduce new abstraction levels, and provide "in the box" infrastructure to solve common compiler engineering problems.[1](§I)
具体手段有三(也是理解后续所有笔记的三个关键词):
MLIR does this by (1) standardizing the Static Single Assignment (SSA)-based IR data structures, (2) providing a declarative system for defining IR dialects, and (3) providing a wide range of common infrastructure including documentation, parsing and printing logic, location tracking, multithreaded compilation support, pass management, etc.[1](§I)
即:①统一的 SSA 数据结构;②dialect 的声明式定义系统——dialect 是 MLIR 组织"一套领域操作/类型/属性"的单位,类似"插件化的子 IR";③公共基础设施(解析打印、位置追踪、多线程编译、pass 管理等)。
1.4 起点:TensorFlow 的碎片化(§I-B)
MLIR 的直接导火索在 Google 内部:TensorFlow 没有统一编译栈。论文用 Figure 1 说明了这一状况:

图 1|TensorFlow 的执行散落在多个框架中(取自论文 Figure 1[1])。同一个 TensorFlow Graph lowering 到 XLA HLO、TensorRT、nGraph、Core ML、TensorFlow Lite 等多条互不相通的路径,终点分别是 LLVM IR、TPU IR、NNAPI 等各自的后端表示。
原文的描述:
Work on MLIR began with a realization that modern machine learning frameworks are composed of many different compilers, graph technologies, and runtime systems (see Figure 1)—which did not share a common infrastructure or design principles.[1](§I-B)
可见现象与建模本质是同一件事:
This manifested in multiple user-visible ways, including poor error messages, failures in edge cases, unpredictable performance, and difficulty generalizing the stack to support new hardware.[1](§I-B)
报错信息差、边界用例失败、性能不可预期、换新硬件难——这四条在本站 kernel fusion 笔记讨论的 NPU 编译场景里依然是最常见的痛点。
紧接着论文把镜头拉远:编译器行业整体有同构的问题。LLVM 在语言之间非常成功,但高层语言还是各自造高层 IR(参见论文 Figure 2):
We soon realized that the compiler industry as a whole has a similar problem: existing systems like LLVM are very successful at unifying and integrating work across a range of different languages, but high-level languages often end up building their own high-level IR and reinventing the same kind of technology for higher levels of abstraction (see Figure 2).[1](§I-B)

图 2|各高层语言都在 LLVM IR 之前自建中间层(取自论文 Figure 2[1])。C/C++ 系经 Clang AST 直达 LLVM IR;Swift 有 SIL IR、Rust 有 MIR IR、Julia 有 Julia IR——同一类"中层"技术被反复发明。
对照图 2,各语言前端的第一站都是各自的 AST,差别在下一站:C/C++ 系从 Clang AST 直接降到 LLVM IR,中间没有 SIL/MIR 那样的缓冲层,类型结构、作用域等源码级信息在进入 LLVM IR 时就已丢失,§1.2 所引"source-level analysis of C++ code is very difficult on LLVM IR"的原因正在于此。论文还把 ASTs 与 ML 计算图、多面体、CFG、LLVM IR 并列为 MLIR 内核应当能表达、但不硬编码的抽象之一[1](§II)(见 §3.1);§3.3 的 Clang 五级固定 lowering 同样以 AST 为第一级。
AST 是编译器前端对源码做词法与语法分析后得到的树形表示:节点对应语法构造(表达式、语句、声明、函数定义等),子节点是它的组成部分;括号、分号这类只为解析服务的符号不进树,嵌套关系即语法结构。它贴近源码,保留类型、作用域等高层信息,适合做类型检查、语义分析、报错定位与 IDE 重构;但它不是 SSA 形式——没有 use-def 链,也没有基本块与显式控制流,数据流相关的优化通常要等它 lowering 成 IR 之后才进行。
而 LLVM 社区自己也有解决不了的问题——并行构造的表示,以及跨语言共享前端 lowering 基础设施:
At the same time, the LLVM community struggled with the representation of parallel constructs, and how to share front-end lowering infrastructure (e.g. for C calling conventions, or cross-language features like OpenMP), with no satisfactory solutions.[1](§I-B)
于是 Google 的选择不是"再造 N 个编译器",而是投资一套能让多个领域共同受益的基础设施,渐进式升级现有系统:
Faced with this challenge, given we could not afford to implement N improved compiler instances, we decided to go for a more general solution: investing in a high quality infrastructure which would benefit multiple domains, progressively upgrading existing systems, making it easier to tackle pressing problems like heterogeneous compilation for specialized accelerators, and provide new research opportunities.[1](§I-B)
论文对自身贡献的概括与此一致:
The contributions of this paper are: (1) positioning the problem of building scalable and modular compiler systems in terms of proven design and engineering principles; (2) a description of a novel compiler infrastructure that follows these principles, with important industrial and research applications; (3) exploration of selected applications to diverse domains, illustrating the generality of the approach and sharing experience developing systems that build on the MLIR infrastructure.[1](§I-A)
2. 总体原则(overarching principles)
论文在引言末尾给出三条总体原则(italic 原文均为引言原文,编号是论文自查注,非本文参考文献):
Parsimony: Apply Occam's razor to builtin semantics, concepts, and programming interface. Harness both intrinsic and incidental complexity by abstracting properties of operations and types. Specify invariants once, but verify correctness throughout. Query properties in the context of a given compilation pass. With very little builtin, this opens the door to extensibility and customization.[1](§I)
Traceability: Retain rather than recover information. Declare rules and properties to enable transformation, rather than step wise imperative specification. Extensibility comes with generic means to trace information, enforced by extensive verification. Composable abstractions stem from "glassboxing" their properties and separating their roles—type, control, data flow, etc.[1](§I)
Progressivity: Premature lowering is the root of all evil. Beyond representation layers, allow multiple transformation paths that lower individual regions on demand. Together with abstraction-independent principles and interfaces, this enables reuse across multiple domains.[1](§I)
中文概括如下表。
表 1|MLIR 三条总体原则
| 原则 | 一句话含义 | 反面 |
|---|---|---|
| Parsimony(简约性) | 核心内置的东西越少越好,对内置语义/概念/接口用奥卡姆剃刀;少内置才可扩展 | LLVM 把指令集、类型、调用约定全部硬编码进内核 |
| Traceability(可追溯性) | 信息保留而不是丢失后再费力恢复;声明式规则而非命令式逐步指定 | 传统编译器优化后无法从结果追溯到来源(WYSINWYX 问题,见 §3.5) |
| Progressivity(渐进性) | 过早 lowering 是"万恶之源";允许 IR 各部分按需、小步 lowering 到不同深度 | 固定层级的编译管线(如 Clang 的五级固定 lowering),lowering 完即丢结构 |
论文紧接着承认这些原则互相之间有张力:
While these principles are well established, one of them is often implemented at the expense of another; e.g., layering in network and operating system stacks aligns with the progressivity principle but breaks parsimony. This has also been the case in compilers with multiple layers of IR. Also, following these principles may hurt expressiveness and effectiveness; e.g., traceability in safety-critical and secure systems involves limiting optimizations and their aggressivity.[1](§I)
分层设计符合 Progressivity 但破坏 Parsimony;为了完全可追溯甚至要限制优化的激进程度。MLIR 的立场是在一个"窄中间带"里求生存:
In a nutshell, we identify design and engineering principles for compiler construction to thrive in a narrow middle that support an open semantics ecosystem. We discovered complexity can be tamed without restricting expressivity, allowing for fast IR design exploration and consolidation across domains, both of which are severely lacking in production systems.[1](§I)
3. 设计原则逐条(§II Design Principles)
§II 把三条总体原则落成五个可检验的设计点,每点标题的方括号标注它服务哪条(或哪几条)总体原则。支撑关系与各设计点的规则要点如下:
简约性"] P2["Traceability
可追溯性"] P3["Progressivity
渐进性"] end subgraph DP["五条设计原则(§II)"] direction LR D1["3.1 Little Builtin
内置仅 Type / Op / Attribute"] D2["3.2 SSA and Regions
嵌套 region 替代扁平 CFG"] D3["3.3 保高层语义
按需渐进 lowering"] D4["3.4 Declaration and Validation
声明式规则 + 验证"] D5["3.5 Source Location Tracking
Location 保留出身"] end P1 --> D1 P1 --> D2 P1 --> D4 P2 --> D4 P2 --> D5 P3 --> D3
图 3|总体原则如何落成设计原则(自绘示意)。论文的方括号标注即图中的箭头:3.1、3.2 源自 Parsimony,3.3 源自 Progressivity,3.4 兼取 Parsimony 与 Traceability,3.5 源自 Traceability。
下面把五条设计点压成凝练介绍;每条标题下的悬停标签「论文原文(§II)」收纳了该设计点的全部原文引文,鼠标移上去即可展开对照阅读。
3.1 Little Builtin, Everything Customizable [Parsimony]:内核只内置三个抽象——Type(类型)、Operation(操作,缩写 op)、Attribute(属性),连"函数"也是一个 op,其余概念全部由它们组合表达;成功标准是表达的多样性,同一内核能不加硬编码地装下 ML 计算图、AST、多面体、CFG 与 LLVM IR。互不兼容的抽象有"内部分裂"风险,论文选择以鼓励可复用的抽象设计来对冲。论文原文(§II)The system is based on a minimal number of fundamental concepts, leaving most of the intermediate representation fully customizable. A handful of abstractions—types, operations and attributes, which are the most common in IRs—should be used to express everything else, allowing fewer and more consistent abstractions that are easy to comprehend, extend and adopt.[1](§II)A success criterion for customization is the possibility to express a diverse set of abstractions including machine learning graphs, ASTs, mathematical abstractions such as polyhedral, Control Flow Graphs (CFGs) and instruction-level IRs such as LLVM IR, all without hard-coding concepts from these abstractions into the system.[1](§II)Certainly, customizability creates a risk of internal fragmentation due to poorly compatible abstractions. While there is unlikely a purely technical solution, the system should encourage one to design reusable abstractions and assume they will be used outside of their initial scope.[1](§II)
3.2 SSA and Regions [Parsimony]:保留 SSA 这一现代 IR 的通用地基(每值只赋值一次,数据流显式化为 use-def 链),但放弃 LLVM 式"扁平基本块 + 跳转"的线性 CFG,用可嵌套的 region(op 内部可再套 block 与 op)原样承载循环、并发构造、闭包等高层结构。真正的技术挑战是让基于扁平 CFG 的分析与变换组合地适用于嵌套结构;为此 MLIR 有意放弃 LLVM 的规范化,把形态选择交给 pass——同一循环嵌套,想利用循环结构的 pass 看嵌套形态,需要线性 CFG 的 pass 再把它线性化,两种形态可并存、可转换。论文原文(§II)The Static Single Assignment (SSA) form
[3]is a widely used representation in compiler IRs. It provides numerous advantages including making dataflow analysis simple and sparse, is widely understood by the compiler community for its relation with continuation-passing style, and is established in major frameworks.[1](§II)As a result, the IR enforces the value-based semantics of SSA, its referential transparency and algorithmic efficiency, all considered essential to a modern compiler infrastructure.[1](§II)However, while many existing IRs use a flat, linearized CFG, representing higher level abstractions push introducing nested regions as a first-class concept in the IR.[1](§II)To support heterogeneous compilation, the system has to support the expression of structured control flow, concurrency constructs, closures in source languages, and many other purposes.[1](§II)One specific challenge is to make CFG-based analyses and transformations compose over nested regions.[1](§II)In doing so, we agree to sacrifice the normalization, and sometimes the canonicalization properties of LLVM.[1](§II)We aim at offering users a choice: depending on the compilation algorithm of interest, of the pass in the compilation flow, nested loops may be captured as nested regions, or as linearized control flow. By offering such a choice, we depart from the normalization-only orientation of LLVM while retaining the ability to deal with higher level abstractions when it matters.[1](§II)3.3 Maintain Higher-Level Semantics [Progressivity]:从降级后的低层 IR"恢复"高层语义是脆弱的,把结构信息伪装成调试信息塞进低层更是伤筋动骨——应当保持计算结构并小步 progressive lowering,"丢结构"必须是有意识的动作。判据非常具体:循环一旦 lowering 成 CFG,就等于宣告不会再有利用循环结构的优化。两个推论是这一原则的日常工作形态:同一 IR 内混搭多个抽象层级(一部分保留高层结构、另一部分已 lowering 为指令);沿多个抽象做小步 progressive lowering,而不是一次 lowering 到底。Open64 WHIRL 与 Clang 的五级是"别人规定好的台阶",MLIR 用 dialect 让层级数量不封顶。论文原文(§II)The system needs to retain the information and structure that are required for analysis or optimizing performance. Attempts to recover abstract semantics once lowered are fragile and shoehorning this information at low-level often invasive (e.g., all passes need to be revisited in the case of using debug information to record structure).[1](§II)Instead, the system should maintain the structure of computations and progressively lower to the hardware abstraction. The loss of structure is then conscious and happens only where the structure is no longer needed to match the underlying execution model.[1](§II)For example, the system should preserve the structured control flow such as loop structure throughout the relevant transformations; removing this structure, i.e. lowering to a CFG essentially means no further transformations will be performed that exploits the structure.[1](§II)As a corollary, mixing different levels of abstraction and different concepts in the same IR is a key to allowing a part of the representation to remain in higher-level abstraction while another part is lowered.[1](§II)Another corollary is that the system should support progressive lowering, from the higher-level representation down to the lowest-level, performed in small steps along multiple abstractions.[1](§II)Previous compilers have been introducing multiple fixed levels of abstraction in their pipeline—e.g. the Open64 WHIRL representation
[9]has five levels, as does the Clang compiler which lowers from ASTs to LLVM IR, to SelectionDAG, to MachineInstr, and to MCInst. More flexible designs are required to support extensibility.[1](§II)3.4 Declaration and Validation [Parsimony and Traceability]:定义 representation 修饰物应当像引入新抽象一样简单;常见变换(重写规则)应写成机器可分析的声明式格式,而非命令式 C++ pass——这样重写的复杂度、终止性才可被工具推理,变换也才能跨越多层抽象复用。开放生态同时要求配套的验证机制:以"单一事实来源"的方式尽量声明式地定义不变量,并全程检验。论文原文(§II)Defining representation modifiers should be as simple as introducing new abstractions; a compiler infrastructure is only as good as the transformations it supports. Common transformations should be implementable as rewrite rules expressed declaratively, in a machine-analyzable format to reason about properties of the rewrites such as complexity and completion.[1](§II)The openness of the ecosystem also calls for an extensive validation mechanism. While verification and testing are useful to detect compiler bugs, and to capture IR invariants, the need for robust validation methodologies and tools is amplified in an extensible system.[1](§II)The mechanism should aim to make this easy to define and as declarative as practical, providing a single source of truth.[1](§II)
3.5 Source Location Tracking [Traceability]:op 的"出身"(原始位置与施加过的变换)应在系统内全程可追溯,MLIR 把它落实为每个 op 自带的 Location,从高层一路降到机器码都保留。对安全关键与安全编译场景,这不是调试便利,而是认证成立的前提:优化可能改掉甚至完全抵消内嵌的安全防御,即 WYSINWYX(What You See Is Not What You eXecute);准确地把高层信息传播到低层,正是这项工作的间接目标之一。论文原文(§II)The provenance of an operation—including its original location and applied transformations—should be easily traceable within the system. This intends to address the lack-of-transparency problem, common to complex compilation systems, where it is virtually impossible to understand how the final representation was constructed from the original one.[1](§II)Optimizations may alter or completely invalidate such protections
[17]; this lack of transparency is known as WYSINWYX[18]in secure compilation.[1](§II)One indirect goal of accurately propagating high-level information to the lower levels is to help support secure and traceable compilation.[1](§II)
五条设计点合起来读,正好把三条总体原则收拢成一条主线:内置要少(Parsimony)、信息要留(Traceability)、lowering 节奏要可控(Progressivity)。每条的取舍最终都落在同一处——要不要把控制结构 lowering 成最朴素的"指令 + 跳转"形态;这里反复出现的 CFG 概念值得一并交代:
CFG 是把程序执行流程画成有向图的标准表示:节点是基本块(一段没有内部分支、只能从块首进入、块尾离开的线性指令序列),有向边是可能发生的控制转移(分支、跳转、调用与返回)。执行顺序完全显式化在图的边上,数据流分析、死代码消除、寄存器分配等优化因此都能归结为在这张图上做遍历与不动点求解——这是 CFG 成为编译领域通用底座的原因。
本文反复出现 CFG,对比的是两种控制流表达:LLVM IR 把源码里的 if、while 等结构全部降成"基本块 + 跳转",顺序显式、对优化友好,但层级信息(哪个 while 包着哪个 if)从此丢失;MLIR 的 region 则在 op 内部原样保留结构化控制流,想走 CFG 路线的 pass 再显式地把它线性化(见 3.2)。所以"降成 CFG"在本文里是"丢结构"的代名词——3.3 的判据(循环一旦 lowering 成 CFG,就宣告不再有利用循环结构的优化)说的正是这件事。
i = 0"] COND{"loop_cond
i < N ?"} BODY["loop_body
读入 A[i] 与 B[i]"] ADD["then 块
C[i] += A[i]*B[i]"] SUB2["else 块
D[i] -= A[i]"] JOIN["join 汇合块"] EXIT["exit"] ENTRY --> COND COND -- "谓词真:循环继续" --> BODY BODY --> ADD BODY --> SUB2 ADD --> JOIN SUB2 --> JOIN JOIN -- "i = i + 1(回边)" --> COND COND -- "谓词假:循环结束" --> EXIT
图 4|嵌套控制结构摊平成 CFG 的形状(自绘示意)。源片段 for i in 0..N { if …then C[i] += A[i]*B[i], else D[i] -= A[i] } 被析成基本块(entry、loop_cond、loop_body、then、else、join、exit),谓词块决定转移边;循环迭代只剩一条从 join 回到 loop_cond 的回边——"这是个循环"这一结构信息,在线性形态下正是摊平后被压进这一条边里的,要与 3.2 的嵌套形态(scf.for 的 region)对照读。
CFG 的两种表达方式引出了下一组问题:MLIR 为什么坚持嵌套、为什么放弃规范形——这是 3.2 那句"形态选择"的来龙去脉:
这个设计回答的,正是第 1 章的三类现实问题:①多面体、融合、tiling 这类循环级优化必须"看得见迭代空间",扁平 CFG 把循环摊成回边后无从下手,事后恢复又脆弱(见 3.3);②OpenMP、GPU 并行这类"进出配对、树状嵌套"的执行域,在 LLVM 社区多年没有令人满意的表示(§I-B),天然适配 region 的配对作用域;③TensorFlow 式碎片化证明多个 domain 需要在同一条管线里并存多个 abstraction level——单一规范形的扁平 IR 生来只有一个层。SSA 保底不变、region 承结构、线性化按需,是对这三题改动最小的答案。
"嵌套不限层"也不是刻意设计,而是内核不写死层数的自然结果——对照 3.3,Open64 WHIRL 与 Clang 的五级台阶是设计者预先规定的,新增一层抽象得改编译器本体;MLIR 用一条递归规则("op 可开 region、region 装 block")换来层数由 dialect 与用户定义,加层只需定义新 dialect。双形态并存则是把 lowering 从隐含约定变成显式抉择:LLVM 式"全局唯一规范形"逼所有 pass 过早 lowering 到同一层,MLIR 让线性化成为显式 pass——丢结构的时刻由 pass 自主决定;合法性不再靠规范形保证,而靠 3.4 的声明式定义加验证器逐 op 检查。
于是 3.2 的分工就清楚了:SSA 部分两边都保留——每个值只赋值一次,任何使用点都能顺着 use-def 链一步找到唯一定义,数据分析不用在图上反复迭代;变的只是控制流的装法。一个 for 循环,嵌套形态下是"scf.for 盒子"(见下),线性形态下是基本块加回边,两个形态对应:基于扁平 CFG 的分析与变换要"组合地"推广到嵌套盒子上——这正是该设计点承认的技术挑战,MLIR 为此连 LLVM 那种"同形唯一"的规范化都放弃了。
把"线性形态 vs 嵌套形态"放到同一个循环上看(for i in 0..N { C[i] += A[i]*B[i] },伪 IR 示意):嵌套形态下,循环本身就是 scf.for 这个 op,循环体整体装在它的 region 里,没有显式跳转;归纳变量 %i 是入口 block 参数,由 op 语义按 0 to N 迭代赋值。fusion、tiling、向量化这些"需要循环在场"的优化直接读这个盒。
scf.for %i = 0 to N {
%a = load %A[%i] // 循环体整体位于该 op 的 region 内
%p = mulf %a, %B[%i]
store %p, %C[%i]
} // 无 br/cond_br,迭代语义即结构本身
线性形态则把同一个盒摊平为基本块加边:入口进条件块,条件块按谓词决定进循环体或退出,循环体执行完回跳条件块(回边);归纳变量在块间显式传递(LLVM IR 里用 φ 节点,MLIR 里统一为 block 参数)。控制流完全显式,指令调度、寄存器分配、代码生成类 pass 在这个形态上工作。
^entry : br ^cond
^cond : %done = cmp %i, N
cond_br %done, ^exit, ^body // 谓词决定继续循环或退出
^body : %a = load %A[%i] ...
br ^cond // 回边
^exit :
两者由跨 dialect 的改写显式切换,不是非此即彼:region 内 block 多于一个时,terminator 的后继图恰好构成一张 CFG——线性形态一直就在嵌套结构的内层(见第 4 节第 3 条的论文原文)。
最后是贯穿管线的组织概念 pass——它既是 §1.3"开箱即用"公共设施的组成,也回答"形态切换由谁执行":
一个贯穿全文的高频词 pass:pass 是"对 IR 完成一遍处理"的模块,分变换(改写 IR,如常量折叠、死代码消除)与分析(只读 IR,如找出全部循环)两类。编译器把若干 pass 排成一条管线顺序套在 IR 上,直到 lowering 到目标层;pass 之间共享同一份 IR 但互相独立,各自可测、可插拔、可单独验证。
按"输入 → 输出"把 pass 细分:①变换 pass(IR 进 IR 出)——同层重写(结构变了、抽象层没变:canonicalize;tiling 后仍是 Linalg;memref 复写消除)、跨层 lowering(输出更低层 IR:scf→cf 把结构化循环摊成 CFG、linalg→vector/LLVM dialect、TOSA→linalg)、跨层 lifting(输出更高层 IR,少见:lifting 恢复高层结构;注意前端导入——如把 TensorFlow 图导入为 MLIR——是"进入管线",不是 pass);②分析 pass:输入 IR、输出不是 IR,而是供别的 pass 查询的分析结果(循环列表、别名关系、支配树);③管线出口构件:把 IR 翻译成非 IR 产物(LLVM bitcode、C 源码、PTX 汇编)或仅打印文本;验证器不产生 IR,只判合法与否。
顺带把 3.2 那句"把形态选择交给 pass"钉死:"线性形态 ↔ 嵌套形态"的相互切换,本身就是转换 pass 的职责——线性化把结构化 dialect 摊成 cf(线性 CFG),结构化(如前端导入的 CFG 需要重建结构时)反向把 CFG 重建成 scf;tiling、fusion 这类结构变换则在嵌套形态内部同层进行。论文引文说"形态取决于 pass 在管线中的位置",说的正是这条分工;pass 管理器负责调度与失效校验,属于 §1.3 引文③"开箱即用"的公共设施,§III 里 Op 自带的 verifiers 也参与整条管线的 IR 校验。
4. 论文 §III:IR 具体设计
§III 按上一节的原则给出 IR 的可检验机制:一切语义单元都是 Op、Op 声明式定义、属性与位置随行、region 嵌套组织控制流、dialect 分区管理扩展。论文同时给出 MLIR 的 generic textual representation(Fig 3:与内存表示一一对应的通用文本形式,服务追溯与手工校验;Fig 4 示意 op → region → block → op 的递归结构,Fig 5 是 ODS 声明示例)。下面逐条压缩介绍,原文引文收进每条标题下的悬停框(用法同 §3);贯穿全节的高频词 pass 与 SSA 的解释,见 §3 末尾的 CFG、形态与 pass 三则备注。
Op:唯一的语义单元:从"指令"到"函数"再到"模块"都是 Op(§1.3 的断言在此落地);内核不预置固定操作集,按"一切可定制"鼓励用户扩展;Op 经 TableGen 的 ODS 声明式定义(Fig 5)——正是 §3.4"声明式"原则的实例。结构上,Op 除 opcode(带 dialect 前缀)与 SSA 操作数/结果之外,还可能携带 Attributes、Regions、Successor Blocks 与 Location。论文原文(§III)The unit of semantics in MLIR is an "operation", referred to as Op. Everything from "instruction" to "function" to "module" are modeled as Ops in this system. MLIR does not have a fixed set of Ops, but allows (and encourages) user-defined extensions, according to the parsimony and "everything customizable" principles. The infrastructure provides a declarative syntax for defining Ops based on TableGen
[19], as illustrated in Figure 5.[1](§III)Ops (see Figure 4) have a unique opcode, a string identifying the operation and its dialect. Ops take and produce zero or more values, called operands and results respectively, and these are maintained in SSA form. Values represent data at runtime, and have a Type that encodes the compile-time knowledge about the data. In addition to an opcode, operands and results, Ops may also have Attributes, Regions, Successor Blocks, and Location Information.[1](§III)Attribute 与 Location:属性是编译期已知的静态信息,类型化、开放集合(每个 Op 一个 string→value 字典),没有全局固定集;Location 落实 §3.5 的可追溯性——鼓励全程处理与传播,并标准化编译器的诊断输出。论文原文(§III)MLIR attributes contain compile-time information about operations, other than the opcode. Attributes are typed (e.g., integer, string), and each Op instance has an open key-value dictionary from string names to attribute values. In the generic syntax, attributes are found in a brace-enclosed comma-separated list of pairs. Figure 3 uses attributes to define bounds of a loop that are known to be constant affine forms.[1](§III)MLIR provides a compact representation for location information, and encourages the processing and propagation of this information throughout the system, following the traceability principle. It can be used to keep the source program stack trace that produced an Op, to generate debug information. It standardizes the way to emit diagnostics from the compiler, and is used by a wide range of testing tools.[1](§III)
Region 与 Block:嵌套"盒"与线性 CFG 同居一个 IR:region 是 block 列表,block 是 op 序列并以 terminator 收尾——terminator 声明控制流可能转移到哪些后继 block;region 内有多于一个 block 时,后继图恰好构成一张 CFG,即线性形态从不缺席,只是被收容在 region 的内层。φ 节点由"函数式 SSA"取代:terminator 把值作为参数传给后继 block(block arguments 是普通 SSA 值),入口 block 的参数由 enclosing Op 的语义给出——affine.for 的 %arg4 即循环归纳变量。§3.2 说的"两种形态可并存、可转换",对应的就是这套机制:单 block 的 region 是纯结构化的"盒"(嵌套形态),多 block 的 region 自长成 CFG(线性收容于内),跨 dialect 的改写负责在两类之间显式切换。论文原文(§III)A region provides the nesting mechanism in MLIR: it contains a list of blocks, each of which contains a list of operations (that may contain further regions). As with attributes, the semantics of a region are defined by the operation they are attached to, however the blocks inside the region (if more than one) form a Control Flow Graph (CFG). For example, the affine.for operation in Figure 3 is a loop with the single-block body attached as a region. The Op specifies the flow of control across regions.[1](§III)The body of each region is a list of blocks, and each block ends with a terminator operation, that may have successor blocks to which the control flow may be transferred. Each terminator (e.g. "switch", "conditional branch" or "unwind") defines its own semantics. It may choose to transfer the control flow to another block in the same region, or return it to the Op enclosing the region.[1](§III)Instead of using φ nodes, MLIR uses a functional form of SSA
[20]where terminators pass values into block arguments defined by the successor block. Each block has a (potentially empty) list of typed block arguments, which are regular values and obey SSA. The semantics of terminator Ops defines what values the arguments of the block will take after the control is transferred. For the first (entry) block of the region, the values are defined by the semantics of the enclosing Op. For example, affine.for uses the entry block argument %arg4 as loop induction variable. Finally, this explicit graph design and the extensibility of Ops is reminiscent of the sea-of-nodes representation[21]; this connection is intentional and has been a major influence for the selection of MLIR's flavor of SSA.[1](§III)作用域:支配关系、嵌套可见性与 isolated-from-above:值的使用受 SSA 支配关系约束;region 层面则按简单嵌套——外层定义的值可供内层使用(affine.for 的循环体可以直接用外面的值),方向不可逆;Op 还可声明为 isolated from above(如 std.func 内不得引用函数外定义的值),它既是语义检查,也保证 use-def 链不跨越屏障,从而使包含此类 Op 的 module 可被编译器并行处理(多核编译的前提)。论文原文(§III)Ops can only use values that are in scope, i.e. visible according to SSA dominance, nesting, and semantic restrictions imposed by enclosing operations. Values are visible within a CFG if they obey standard SSA dominance relationships, where control is guaranteed to pass through a definition before reaching a use. Region-based visibility is defined based on simple nesting of regions: if the operand to an Op is outside the current region, then it must be defined lexically above and outside the region of the use. This is what allows Ops within an affine.for operation to use values defined in outer scopes.[1](§III)MLIR also allows operations to be defined as isolated from above, indicating that the operation is a scope barrier—e.g. the "std.func" Op defines a function, and it is not valid for operations within the function to refer to values defined outside the function. In addition to providing useful semantic checking, a module containing isolated-from-above Ops may be processed in parallel by an MLIR compiler since no use-def chains may cross the isolation barriers.[1](§III)
Symbol 与 Symbol Table:用字符串名字(全局变量、函数、命名模块)标注 IR 对象,同表不可重复定义但可在定义前使用——为递归函数这类"不服从 SSA 的命名实体"兜底,用途不规定、留给 Op 定义。论文原文(§III)Ops can have a symbol table attached. This table is a standardized way of associating names, represented as strings, to IR objects, called symbols. The IR does not prescribe what symbols are used for, leaving it up to the Op definition. Symbols are most useful for named entities that need not obey SSA: they cannot be redefined within the same table, but they can be used prior to their definition.[1](§III)
Dialect:ecosystem 的 namespace 分区(为什么要设多个):①语义分区——相关概念归拢进同一 dialect:每个 Op、Type、Attribute 恰属一个 dialect,opcode 以点前缀命名标明归属(
affine.load= dialectaffine内的load);dialect 级行为声明一次即对其下全部 op 生效——典型如 constant folding(dialect 级 fold 逻辑写一次,本 dialect 内全部 op 可用),论文原文即 "(e.g., constant folding behavior for all ops in the dialect)";整体相当于一组 modular libraries 的打包方式;②内核 Parsimony——接入一个新 domain(新的 frontend 语言、framework 的计算图、accelerator 的指令档)只须定义新 dialect,内核零改动;③多 abstraction level 的载体——dialect 边界大致就是 abstraction-level 边界,IR 显式支持多 dialect 混排(Fig 3 同屏 std 与 affine),lowering 因此可按 IR 局部粒度逐层推进,每层 pass 以本层 vocabulary 表达;④常见误解的纠正——dialect 不是"每个 backend 一套算子 IR":绝大多数 dialect 是各 backend 共享的通用层(tensor、linalg、scf、gpu 等),CUDA、ROCm 各自独有的只有最底层的 vendor instruction dialect(nvvm 对应 NVIDIA 的 PTX/NVVM IR,rocdl 对应 AMD 的 GCN 指令);op-level IR(Linalg/TOSA)与 pipeline 通用,backend = 共享 pipeline + vendor instruction dialect + runtime。同时注意 dialect(namespace 单位)与 abstraction level(lowering 阶梯上的海拔)不是一一对应:同一 level 可并列多个 dialect(tensor 与 memref 是同一计算的 value 语义 / buffer 视图;scf 与 cf 是同一控制流层的两种形态),一个 dialect 也可跨 level 使用(arith 标量运算在高层 Linalg 循环体内大量出现)。反向论证见论文:全部 Ops、types、attributes 塞进单一 dialect 会迅速 unmanageable(概念堆积与命名冲突);单一 namespace 既无法界定语义边界,也无法表达"lowering 至哪一层"。Type System:每个 value 都有类型:类型由产生该值的 Op 或定义该值的 block 给出;系统用户可扩展、可复用外部类型系统;严格类型相等检查、不内置转换规则(需要转换时显式建模为 Op)——与 §3.1"少内置"对读。论文原文(§III)Every value in MLIR has a type, which is specified in the Op that produces the value or in the block that defines the value as an argument. Types encode compile-time information about a value. The type system in MLIR is user-extensible, and may, for example, refer to existing foreign type systems. MLIR enforces strict type equality checking and does not provide type conversion rules. Ops list their inputs and result types using trailing function-like syntax.[1](§III)
Function 与 Module:也只是 Op:module = 单 region、单 block 的 Op(体里装函数、全局变量等顶层构造);function = 单 region、可含 0(纯声明)或多个 block 的 Op。二者都以 Op 实现,是对 §3.1"连函数也是 op"的最后印证。论文原文(§III)Similarly to conventional IRs, MLIR is usually structured into functions and modules. However, these are not separate concepts in MLIR: they are implemented as Ops in the builtin dialect, again an illustration of parsimony in the design. A module is an Op with a single region containing a single block, and terminated by a dummy Op that does not transfer the control flow. Like any block, its body contains a list of Ops, which may be functions, global variables, compiler metadata, or other top-level constructs. Modules may define a symbol in order to be referenced. Similarly, a function is an Op with a single region that may contain zero (in case of declaration) or more blocks.[1](§III)
把本节的 dialect、pass、转换与 lowering 串成一个全景(自绘示意):
TensorFlow / PyTorch / onnx"] TOSA["tosa:跨厂商中立算子层"] LIN["linalg:结构化张量代数
(matmul / fill / generic)"] TEN["tensor ↔ memref
值视图 / 地址视图(bufferization 切换)"] AFF["affine:多面体形态(可选路)"] SCF["scf:结构化控制流(for / if / while)"] CF["cf:线性控制流(块 + terminator)"] VEC["vector:机器向量(SIMD)"] ARITH["arith / math:标量运算"] LVM["llvm dialect:LLVM 桥"] NV["nvvm:NVIDIA 指令层(PTX/NVVM IR)"] RD["rocdl:AMD 指令层(AMDGCN)"] EX["GPU 可执行档(Cubin/HSACO)
+ 运行时 kernel launch"] FE -->|"导入 pass"| TOSA TOSA -->|"tosa→linalg 转换"| LIN LIN -->|"bufferization"| TEN LIN -.->|"可选:多面体路线"| AFF LIN -->|"向量重写"| VEC LIN -->|"lowering 到结构化循环"| SCF AFF -->|"lowering 为 scf"| SCF VEC -->|"lane-wise 改写"| ARITH SCF -->|"线性化"| CF CF -->|"汇入"| LVM ARITH -->|"汇入"| LVM TEN -->|"汇入"| LVM LVM -->|"NVIDIA 分岔"| NV LVM -->|"AMD 分岔"| RD NV -->|"nvptx / ptxas"| EX RD -->|"roc 工具链"| EX
图 5|从计算图到 GPU 汇编:dialect 阶梯与转换 pass(自绘示意)。每个框是一个 dialect(前端算子 → 结构代数 → 控制/标量 → vendor instruction 层),每条箭头是一类转换 pass;实线主干为各后端共享,affine/vector 两条旁路汇入 llvm dialect 后按 NVIDIA/AMD 分岔,终出汇编与二进制档;主机端 kernel launch 走 gpu dialect 与 LLVM 目标码,本图略。
对照图 5,把三个常被问起的问题钉在一张便签里——dialect 是否分层、它与 IR 层级的关系、跨 dialect 的转换由谁完成:
上述机制里的两个骨架概念——"三元素分工"与 op / region / block 层次——合起来值得一张速查:
3.1"内核只内置三抽象"的读法:op 内部的 block、region 与连接 op 的 value 也是内核固有的结构,"少内置"指的是不再向内核硬编码函数、循环这类语义抽象。三者的分工是——op 是骨架节点,type 标注它产生与消费的值,attribute 携带 op 自身的编译期静态信息;所以"MLIR 一切皆 op"的直觉基本正确,只是 type 与 attribute 不是和 op 并列的"零件",而是挂在 op(及其值)上的标注。
再细分三层结构:op 是骨架节点;region 不是 op,而是 op 内部装 block 的"子空间"(类比嵌套子图,作用域随 region 边界隔离——region 里定义的值默认不能被外面使用,须经出口值或 block 参数显式穿出);block 也不是 op,而是 region 中的线性 op 序列,自带入口参数——外部值以 block 参数显式传入,块内值仍按 SSA 组织,不依赖扁平 CFG 的 phi 节点。结构链条是 op → region → block →(还可再开 region 的)op,树状嵌套、深度不限。region 的执行语义由定义该 op 的 dialect 决定:scf.for 的 region 是循环体,scf.if 的是分支体,并行 op 的可以是并发单元——这正是"少内置、一切可定制"在结构层的落点。
结构看清之后,论文余下章节的导览如下表——§IV 的各个应用案例,即是对本节机制的领域实测:
表 2|论文其余章节导览(§IV 起;§III 已展开为上文)
| 章节 | 内容 | 与本篇的关系 |
|---|---|---|
| §IV Evaluation: Applications of MLIR | 多领域用例:TensorFlow 图、polyhedral 代码生成、Fortran IR(FIR)等 | 证明 §4 的机制能承载 §2"一个内核服务多个领域" |
| §V Consequences of the MLIR Design | 设计选择的后果与经验教训,含"Unopinionated Design Provides New Challenges"、Looking Forward | 对复读第二遍最有价值 |
| §VI Related Work / §VII Conclusion | 相关工作与结论、API 稳定化等后续路线 | — |
| 附录 A/B | Artifact 摘要与 artifact check-list | 论文参加 artifact 评审的配套说明 |
TODO 按 §IV–§VII 逐章阅读后补充:评估案例、设计后果与经验教训;本文当前承诺动机、总体原则、设计原则与 §III IR 设计部分。
5. 与本站 kernel fusion 笔记的关系
kernel fusion 子分类讨论的 NPU 编译栈几乎全部构建在本论文的原则之上:Linalg dialect 是"多面体 + 结构化张量"抽象按 §2 Parsimony 方式的用户自定义 dialect;Hexagon-MLIR 的"tile → HVX 向量化 → LLVM IR"链路是 §3.3 progressive lowering 的实例;这些笔记里的 region-not-CFG 表达(结构化循环不降为跳转)对应 §3.2 的 SSA and Regionshexagon-mlir-note。读这三条原则,再回头看那些编译流程图,可以看到同一条设计主线。
hexagon-mlir-note. Hexagon-MLIR:编译栈、tile 流水与融合边界。 ↩
参考文献
[1] LATTNER C, AMINI M, BONDHUGULA U, et al. MLIR: scaling compiler infrastructure for domain specific computation[C/OL]//2021 IEEE/ACM International Symposium on Code Generation and Optimization (CGO). IEEE, 2021: 2-14. https://ieeexplore.ieee.org/document/9370308. DOI:10.1109/CGO51591.2021.9370308.