想象你是大型快递转运中心的调度员。清晨,顺丰、京东、邮政的货车同时停靠收货口,每家面单格式完全不同:喷码、电子标签、五联单,乱七八糟。但你没慌,因为这座中心的核心能力不是认识所有面单,而是把任何面单的包裹都送上同一条传送带——先统一成一式三联的标准单,再逐级细化成街道、楼栋、门牌,最后交到只认门牌的快递员手里。AI 编译世界的 MLIR(多级中间表示)工具链,就是这样的转运中心。PyTorch、TensorFlow、ONNX 是各家快递,计算图是面单,各部门的标签体系叫方言,一道道分拣工序叫编译优化,末端快递员叫 LLVM。这篇文章,我们沿着这条传送带走一遍,看一个模型如何从框架手里,被工具链一步步“翻译”成能在芯片上奔跑的机器码。
学会 MLIR 工具链,你就掌握了 AI 编译器的“总装车间”:无论模型来自哪个框架、要跑到哪块芯片,你都能读懂中转流程、参与优化、完成落地。
可视化演示
展示MLIR工具链核心机制:多级中间表示逐层降级、优化/转换/折叠等工具链流水线、MLIR多级IR带来的性能提升,通过交互理解MLIR工具链原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
多级IR:逐层降级到后端 高层IR逐步降级到低层IR 每级表示保留合适抽象层次 L0 → L1 → L2 → L3 多级IR兼顾优化与代码生成
工具链全景
傍晚六点,转运中心的传送带嗡嗡转动。收货口刚卸下的包裹贴着五花八门的面单,可几分钟后,它们都变成了同一套条形码,在传送带上匀速前进。你问调度员秘诀,他指了指头顶的大屏:“我们不是认识每一张面单,而是把'任何格式'先翻译成'中心格式',再逐级翻译成'派送格式'。”MLIR 工具链,就是 AI 编译器世界里的这套传送系统。
“中心格式”就是 MLIR 的多级中间表示。之所以叫“多级”,是因为同一份程序可以同时以不同抽象等级存在:高层还能看出“这是一个卷积”,低层已经变成“读取这几块内存、做乘加、写回”。每一级被封装成一种 dialect(方言)。框架集成要做的第一件事,就是把 PyTorch、TensorFlow、ONNX 手里的计算图接进来,送上这条传送带。
整条工具链由三个环节组成:前端导入(frontend)把框架图转换为高层方言;中端优化由 mlir-opt 驱动一系列 pass,把高层方言逐级下降(lowering)到更低层;后端导出由 mlir-translate 对接 LLVM 等后端,最终生成机器码。这套设计理念可以概括为两点:可扩展——新硬件只需新增方言与转换规则;渐进式下降——每层只做自己擅长的事。这条“逐级下降”的链条可以用一句公式概括:
1. graph:框架侧的高层计算图,例如 PyTorch 导出的 torch 算子序列,此时还带着 Python 层语义。
2. tosa:统一面单层,把不同框架的算子标准化成可移植的张量算子集合。
3. linalg:结构化线性代数层,算子变成带显式循环结构的张量运算,暴露出可优化的形状信息。
4. affine:循环结构显式化,借助仿射映射精确分析数组访问与依赖关系。
5. llvm:下降到 LLVM 方言,操作与 LLVM IR 指令一一对应,是交给后端前的最后一站。
6. asm:最终产物——目标机器的汇编/机器码,由 LLVM 后端生成。
从收货口到装车口,包裹每过一个环节就换一张更精确的标签。框架集成不是把某个框架“宠上天”,而是给所有框架一条同等的传送带——这正是 MLIR 工具链最迷人的地方。
Dialect方言体系
转运中心里并不只有一种标签。收货部用“大区编号”,仓储部用“库位层架”,派送部用“街道门牌”。同一个包裹,在不同部门眼里有不同的“方言”,但谁都不会搞混,因为每套标签都贴在同一个包裹上。
MLIR 的 dialect(方言)正是这样的标签体系。一个方言定义了一组合法的 Operation(操作)、Type(类型)和 Attribute(属性)。比如 func 方言描述函数骨架,arith 方言描述加减乘除,scf 方言描述结构化控制流,tosa 方言描述可移植张量算子,linalg 方言描述带有循环结构的线性代数运算,llvm 方言则直接对应 LLVM 指令。每一个方言,都是一套自洽的“词汇表”。
方言机制让 MLIR 变成乐高积木:任何团队可以自定义方言,再声明它与其他方言之间的转换规则,完全不必改动中心本身。以 affine 方言为例,它把数组访问限定为仿射映射——下标与地址之间必须是线性关系,这样编译器才能精确分析依赖、做循环变换。仿射映射的一般形式是:
1. d0…dk-1:循环变量或数组下标,它们是仿射映射的输入参数。
2. α0…αk-1:各下标的系数,对应内存布局中的 stride(步长),例如行优先时行下标系数等于列数。
3. Σ 求和:把每一维的贡献累加,得到一维内存偏移的核心部分。
4. β:常数偏移量,对应数组起始位置的修正(如偏置、padding)。
仓储部往货架上贴“库位层架”,派送部往包裹上贴“街道门牌”,标签虽多,但每套都精确服务于一个环节。MLIR 的高明之处,就是让这些标签在同一个包裹上共存,并且随时可以互相翻译。
核心工具命令
中控室里并排摆着三台机器。调度员每天的工作就围着它们转:第一台是“分拣程序控制器”,负责按顺序执行一道道分拣工序;第二台是“翻译机”,负责把外来单据翻译成内部格式;第三台是“模拟演练台”,在正式发车前先试跑一遍路线。
这三台机器对应 MLIR 工具链的三个核心命令:mlir-opt、mlir-translate、mlir-cpu-runner。mlir-opt 是主将,读取 .mlir 文件,按命令行参数指定的顺序执行 pass,输出优化后的 .mlir;mlir-translate 是翻译官,在 MLIR 内部表示与外部格式(LLVM IR、SPIR-V、Graphviz 文本等)之间互译;mlir-cpu-runner 是演练台,不经过完整编译链也能直接执行 MLIR 模块,适合快速验证。调试时还可以给 mlir-opt 加 --mlir-print-ir-after-all 或 --debug-only 这类调试选项,观察每一道工序之后的 IR 长相。
一条典型的命令形如 mlir-opt file.mlir --tosa-to-linalg --linalg-tile --convert-linalg-to-affine -o out.mlir,每个 flag 就是一个 pass。pass 的执行顺序决定了优化效果,这本质上是一个函数复合:
1. IR0:mlir-opt 读入的初始中间表示,对应传送带起始处的原始包裹。
2. P1…Pn:按顺序注册的 pass,如 --tosa-to-linalg、--linalg-tile。
3. ∘ 复合:表示“先执行 P1,再执行 P2”,相邻 pass 的输入输出首尾相连。
4. IRn:最终产物,所有工序完成后的 IR,可直接交给翻译机或后端。
分拣工序必须“先分大区、再分街道、最后分楼栋”,顺序错了包裹就会迷路。mlir-opt 的存在,就是让你把这道顺序牢牢抓在自己手里。
框架前端集成
转运中心的收货口分成好几条车道:1 号口是顺丰(PyTorch),2 号口是京东(TensorFlow),3 号口是邮政小包(ONNX)。各家车道形状不同,但卸下来的包裹,最后都要先贴上一张行业通用的“统一面单”——TOSA。
这就是框架前端集成做的事。Torch-MLIR 项目负责把 PyTorch 模型经 torch 方言逐步合法化到 MLIR 高层方言;TensorFlow 官方通过 TOSA 或 MLIR-HLO 路径接入;ONNX 模型则借助 importer 导入。TOSA(Tensor Operator Set Architecture)是一套精心设计的“可移植张量算子”方言,像统一面单一样,让不同框架的算子先归一化,再进入 MLIR 的优化流水线。
从数据结构看,前端集成要完成一次“图到模块”的映射:计算图的每个节点(算子)变成一个 dialect 操作,每条边(张量)变成操作数或结果,整张图被包进 func 方言的函数体。可以记作:
1. G=(V,E):计算图,V 是算子节点,E 是张量数据边。
2. module:MLIR 的顶层容器,相当于一个编译单元,容纳多个函数。
3. func:函数方言,把一张子图包装成可调用、可重用的函数。
4. op_v:把图节点逐个映射为方言操作,边则映射为操作数或结果。
面单格式再乱,只要进了统一面单通道,后面的传送带就对它一视同仁。前端集成把“各家快递”变成“统一包裹”,MLIR 工具链的价值,正是从这一刻开始积累。
转换下降机制
面单统一之后,真正的重头戏才开始:把“这是一箱易碎玻璃器皿,送往朝阳区”翻译成“送往 XX 街道 YY 号,轻拿轻放,置 3 号货架”。越接近派送,指令就越具体。方言下降(dialect lowering)做的就是这件事——从“描述做什么”逐级降到“描述怎么做”。
MLIR 的下降由两个核心机制驱动:RewritePattern(重写模式)和 ConversionDriver(转换驱动)。RewritePattern 声明“看到什么样的操作,就替换成什么样的操作序列”;ConversionDriver 则按合法化规则(legality)反复应用模式,直到所有操作都落在目标方言内。合法化规则告诉驱动:转换目标(conversion target)里哪些操作是“天生的好公民”(already legal),哪些必须被转换。
一个高层操作往往展开成多个低层操作,这种一对多映射是下降的基本形态:
1. Op(x1,…,xn):源方言中的一个高层操作,带若干操作数。
2. ⟹:模式重写符号,表示匹配成功后触发整体替换。
3. op1…opm:替换产物,一组合法的低层操作,共享或重排原操作数。
4. m:展开系数,由模式设计者决定,体现“一词多义”的拆分逻辑。
从“送往朝阳区”到“送到 3 号货架”,每一步翻译都在增加可执行性、减少歧义。转换下降机制就是这条翻译流水线的“语法规则”,保证任何一句话都不会翻出歧义。
优化策略
面单落地后,分拣员开始琢磨怎么装车更省。大件拆成小件,方便塞进车厢缝隙——这是 tiling(循环分块);几个小包捆成一捆,少贴几张签——这是 fusion(算子融合);一辆大车一次拉走一条街的货——这是 vectorization(向量化);按派送路线重新码放货架——这是 memory layout(内存布局)。
这些优化大多发生在 linalg/affine 等结构化方言层,因为此时循环和访存还“看得见”。循环分块把一个大循环切成小块,让数据块能留在 CPU 缓存里,避免反复访问内存;算子融合把 element-wise 链合并成一个算子,减少中间张量的写出和读入;向量化让一条 SIMD 指令同时处理多个数;内存布局则选择行优先、列优先或分块布局,匹配硬件的访存习惯。
分块的本质是对循环空间的重参数化。一个双重循环 (i, j) 被改写为四重循环:
1. i, j:原始大循环的循环变量,遍历整个数据空间。
2. i0, j0:外层循环变量,表示当前处理第几个数据块。
3. Ti, Tj:分块尺寸,决定每块处理多少数据,是调优的关键旋钮。
4. i1, j1:内层循环变量,表示块内的局部偏移。
5. 0 ≤ i1 < Ti:约束条件,保证块内偏移不越界,块与块不重叠。
装车方案千变万化,但原则只有一条:让搬运工少跑路、让车厢不空载。MLIR 把优化放在结构化方言层做,正因为此时“车厢和路线”还清晰可见,编译器才有机会施展这些装车智慧。
代码生成链路
最后一公里,包裹被交到末端快递员手里。快递员不关心“易碎”“保鲜”这些业务词,他只认两件事:送到哪个小区,几号楼几单元。LLVM 就是那位快递员——它只认最底层的指令,但胜在路熟、覆盖广。
代码生成链路把 llvm 方言翻译成 LLVM IR,再由 LLVM 后端生成目标机器码,最后链接运行时完成交付。这条链路有两种打开方式:JIT(Just-In-Time,运行时编译再执行)适合快速验证和动态形状场景;AOT(Ahead-Of-Time,预先编译成可执行文件或 .so)适合追求稳定部署的生产环境。mlir-cpu-runner 就是典型的 JIT 路径。交付后还可以借助 perf 或 MLIR 的 timing pass 做性能分析,量化每一步优化到底值多少。
到达 LLVM 层后,所有看似“高级”的数组访问都必须退化为赤裸裸的地址计算。行优先布局下,二维下标 (d0, d1) 的内存地址是:
1. d0, d1:数组的逻辑下标,例如卷积输出特征图的行和列。
2. S0, S1:各维度的 stride,行优先时 S0 = 每行元素数,S1 = 1。
3. size:单个元素的字节数,float32 为 4,int8 为 1。
4. base:数组在内存中的起始地址,由运行时分配或静态指定。
从框架到机器码,包裹终于走完了整条传送带:统一面单、逐级细化、装车优化、最后交给只认门牌的快递员。MLIR 工具链的价值,就是让这一切“对任何快递公司、任何小区”都成立。
AI 应用场景
大模型推理引擎的算子融合优化
阶段:中端优化(IR 层优化)
解决的问题:传统推理引擎按算子逐个调用底层库,add 的结果要写回显存、relu 再读出来,访存开销巨大。借助 MLIR,在 linalg 层把 add+relu 这类 element-wise 链融合成单个算子,减少中间张量落盘,推理延迟可降低 20%~40%。
具体案例:某团队用 Torch-MLIR 把 LLaMA-7B 的 decoder 层导入 MLIR,在 linalg 层对残差连接的 add+LayerNorm+SiLU 链做融合,再下降至 LLVM 生成 .so,最终在 A100 上达到 1.8 倍端到端加速。
芯片公司用 MLIR 构建自家编译工具链
阶段:后端代码生成
解决的问题:传统自研编译器要从零写前端,工作量巨大。MLIR 允许复用前端导入与高层优化,芯片公司只需实现从 linalg/llvm 方言到自身指令集的转换目标与代码生成,工具链交付周期从数年缩短到数月。
具体案例:某 NPU 创业公司基于 MLIR 的 TOSA→linalg→目标方言流水线,为自家 DSP 实现一个 custom 方言与 30 个 conversion pattern,两周内跑通 ResNet-50 的编译与部署。
云厂商统一多框架优化中台
阶段:前端集成
解决的问题:云上同时有 PyTorch、TensorFlow、Paddle 的推理负载,每套都要单独优化,成本高。把三者统一接入 TOSA 中间层,之后共享同一套 linalg 优化与代码生成,一套优化处处受益。
具体案例:某云厂商的推理平台对 ONNX 模型做 ONNX→TOSA→linalg 导入,在该层统一做 tiling 与内存布局优化,使三种框架来源的模型在 CPU 上达到一致的性能水位,运维人力下降 60%。
Python 演示
# -*- coding: utf-8 -*-# 一个极简的 MLIR 工具链模拟器:# 用字符串形式的 IR 演示 方言下降 与 算子融合 两个 pass class Op: def __init__(self, name, args, result): self.name = name self.args = list(args) self.result = result def __repr__(self): return "%" + self.result + " = " + self.name + "(" + ", ".join(self.args) + ")" LOWER_MAP = { "tosa.add": "linalg.add", "tosa.mul": "linalg.mul", "tosa.relu": "linalg.relu",} def lower_tosa(module): lowered = [] for op in module: if op.name in LOWER_MAP: lowered.append(Op(LOWER_MAP[op.name], op.args, op.result)) else: lowered.append(op) return lowered def fuse_elemwise(module): fused = [] i = 0 while i < len(module): op = module[i] if op.name == "linalg.add" and i + 1 < len(module): nxt = module[i + 1] if nxt.name == "linalg.relu" and nxt.args[0] == op.result: fused.append(Op("linalg.add_relu", op.args, nxt.result)) i += 2 continue fused.append(op) i += 1 return fused def run_pipeline(module, passes): current = module for pass_name, pass_fn in passes: current = pass_fn(current) print("---- pass:", pass_name) for op in current: print(op) return current if __name__ == "__main__": src = [ Op("tosa.add", ["a", "b"], "c"), Op("tosa.relu", ["c"], "d"), Op("tosa.mul", ["d", "e"], "f"), ] print("== 原始 TOSA 模块 ==") for op in src: print(op) print() run_pipeline(src, [("lower_tosa", lower_tosa), ("fuse_elemwise", fuse_elemwise)])
class Op:定义 IR 的最小单元:操作名、操作数、结果。LOWER_MAP = {一张“方言映射表”,模拟从 tosa 到 linalg 的下降规则。def lower_tosa(module):第一个 pass:把高层 tosa 算子逐一下降为 linalg 算子。if op.name in LOWER_MAP:模式匹配:命中下降规则的操作被替换,未命中的原样保留。def fuse_elemwise(module):第二个 pass:把 add 与紧随的 relu 合并成 add_relu。if nxt.name == "linalg.relu" and nxt.args[0] == op.result:检查数据依赖,确保后面的 relu 确实消费 add 的结果。def run_pipeline(module, passes):模拟 mlir-opt:按注册顺序逐个执行 pass。if __name__ == "__main__":入口,构建一个小模块并用两张 pass 串联。
常见误区
误区我以为 MLIR 跟 LLVM IR 一样,是一门固定的中间语言,学会它的指令集就会用了。
正解MLIR 是一个“容器框架”,里面能装无数种 IR——每种 dialect 都是一门 IR,LLVM IR 只是其中最低层那一级的方言。学 MLIR 学的不是一套指令,而是“如何定义方言 + 如何下降”的方法论。好比转运中心的核心不是某一种面单,而是“能处理所有面单”的机制。
为何常见:名字里带 IR,直觉上容易和 LLVM IR 类比;实际上它更像“IR 的操作系统”。
误区既然最终都要到 LLVM,那不如跳过 tosa、linalg、affine,直接翻译过去,省时省力。
正解高层方言承载着语义信息:linalg 知道这是个矩阵乘、affine 知道循环边界,这些信息是循环分块、算子融合等优化的依据。一步到位等于把优化空间全部扔掉,生成代码可能慢好几倍。
为何常见:只看“结果等价”而忽略“优化空间”;编译器新手常低估中间表示的信息价值。
误区我把 .mlir 丢给 mlir-opt,期待它吐出一个能在服务器上跑的二进制。
正解mlir-opt 只做 IR→IR 的变换,输入输出都是 .mlir。要得到可执行文件,需要 mlir-translate 翻译成 LLVM IR,再由 LLVM 后端生成机器码,最后链接运行时。各司其职,缺一不可。
为何常见:很多人把它和 gcc 这类“一条命令出可执行文件”的编译器混淆。
误区lower 一个 pass 就是把 `tosa.add` 替换成 `linalg.add`,这不就是字符串替换吗?
正解真正的方言转换要处理三件事:操作数与结果类型的转换(tensor 变 memref)、1:N 展开(一个 op 变多个 op)、合法化检查(确保转换后所有 op 都是目标方言的合法公民)。字符串改名只是最简单情况的一小步。
为何常见:看多了简单的示例,误以为 pattern 只是“改名规则”。
误区我拿到一个 ONNX 模型就觉得万事大吉,剩下的随便找个 runtime 一跑就行。
正解ONNX 只是“外包纸箱”,里面的算子仍是高层语义。要让它跑得又快又好,还要经 ONNX→TOSA→linalg 的导入与下降,再做融合、分块、向量化,最后才生成目标代码。ONNX 是起点,不是终点。
为何常见:ONNX 生态太普及,让人误以为它是“最终标准”,而忘了它只是统一格式的一环。
面试题
Q1. MLIR 中“多级中间表示”最核心的含义是什么?
- A.用多个文件保存程序的不同片段
- B.在不同抽象层级之间分层表达计算← 正确答案
- C.为每种硬件单独编写一套新翻译器
- D.把多种高级语言统一编译成字节码
答案:B。“多级”不是“多个文件”,而是同一程序的多种抽象等级。B 准确描述了 MLIR 的核心设计:高层看语义、低层看实现。A 混淆了“多级”与“多文件”;C 恰恰是 MLIR 想避免的(不用为每个硬件重写前端);D 描述的是统一字节码策略,与 MLIR 的多级表示无关。
Q2. 下列哪个工具负责按顺序执行一系列优化 pass?
- A.mlir-opt 运行 pass 流水线← 正确答案
- B.mlir-run 直接运行调试器
- C.mlir-tune 自动搜索超参数
- D.mlir-debug 逐行跟踪执行
答案:A。mlir-opt 是 MLIR 官方提供的 pass 执行器,专门跑优化与转换流水线,相当于转运中心的中控台。mlir-run、mlir-tune 并不是 MLIR 标准工具名称;调试通常靠 --debug-only 等选项而非 mlir-debug。
Q3. 关于 dialect(方言)的说法,正确的是哪一个?
- A.每种方言都对应一门编程语言
- B.方言定义操作类型与属性的集合← 正确答案
- C.不同方言之间不允许相互转换
- D.方言只能由官方团队新增扩展
答案:B。dialect 由 Operation、Type、Attribute 三要素定义,B 正确。A 混淆“方言”与“编程语言”;C 错误,方言之间正是靠转换规则协作;D 错误,MLIR 的设计目标就是让任何团队都能自定义方言。
Q4. Torch-MLIR 项目的主要职责是什么?
- A.把 PyTorch 模型接入 MLIR 世界← 正确答案
- B.把 MLIR 代码导出回 PyTorch
- C.在 PyTorch 内部重写所有算子
- D.替代 PyTorch 的自动求导引擎
答案:A。Torch-MLIR 把 torch 前端方言逐步合法化到 MLIR 高层方言,是“导入”方向,A 正确。B 方向反了;C 混淆了编译器前端与框架内部实现;D 与自动求导无关。
Q5. 在方言下降中,“合法化(legalization)”指的是什么?
- A.让所有操作通过代码版权审核
- B.确保转换结果符合目标方言规范← 正确答案
- C.过滤掉模型中的非法数值输入
- D.保证多次运行得到完全一致结果
答案:B。合法化是方言下降的验收标准:转换驱动反复应用 pattern,直到所有操作都落在目标方言的许可集合内,B 正确。A、C 是望文生义;D 说的是确定性,与合法化无关。
Q6. 关于 TOSA 方言,下列描述错误的是哪一项?
- A.提供一套稳定且可移植的算子集合
- B.作为框架与 MLIR 之间的中间层
- C.专门描述 GPU 底层线程级指令← 正确答案
- D.位于高层框架图与低层实现之间
答案:C。TOSA 是一套可移植的高层张量算子集合,通常作为框架与 MLIR 的中间层,位于高层图与低层实现之间,A、B、D 都正确。C 错误:TOSA 完全不涉及 GPU 底层线程级指令,那是 llvm 方言或目标后端的职责。
Q7. 循环分块(tiling)提升性能的最主要原因是什么?
- A.减少浮点运算总量提高算术密度
- B.提高数据局部性以利于缓存利用← 正确答案
- C.将循环变量全部变成编译期常量
- D.降低指令流水线中的分支预测失败
答案:B。分块把大循环切成小块,让正在处理的数据块能完整留在缓存中,核心收益是数据局部性,B 正确。A 错误,浮点运算总量没变;C 错误,循环变量仍是运行时变量;D 与分支预测关系不大。
Q8. mlir-translate 可以完成下列哪些任务?
- A.将 MLIR 字节码编译成可执行文件
- B.把外部格式导入为 MLIR 方言← 正确答案
- C.把 MLIR 方言导出为外部格式← 正确答案
- D.对 MLIR 循环结构执行自动分块
答案:BC。mlir-translate 负责 MLIR 与外部格式的双向互译,B(导入)和 C(导出)正确。A 错误,生成可执行文件还需要 LLVM 后端与链接器;D 是 mlir-opt 里 pass 的职责。
Q9. 下列哪些属于 MLIR 中常见的方言?
- A.func 与 arith 等基础计算方言← 正确答案
- B.linalg 结构化线性代数方言← 正确答案
- C.tosa 可移植张量算子方言← 正确答案
- D.numpy 张量库脚本化方言
答案:ABC。func、arith、linalg、tosa 都是 MLIR 内置常用方言,A、B、C 正确。D 的 numpy 方言在 MLIR 中并不存在,MLIR 没有“张量库脚本化方言”这一说。
Q10. 关于框架前端集成,下列说法正确的有哪些?
- A.Torch-MLIR 把 torch 方言逐步合法化← 正确答案
- B.TensorFlow 经 TOSA 或 MHLO 接入 MLIR← 正确答案
- C.ONNX 模型可直接由 mlir-opt 执行优化
- D.IREE 在 IR 层完成融合与内存规划← 正确答案
答案:ABD。Torch-MLIR 把 torch 方言逐步合法化,A 正确;TensorFlow 官方走 TOSA 或 MHLO 接入,B 正确;IREE 在 IR 层做算子融合与内存规划,D 正确。C 错误:ONNX 必须先导入为 MLIR 方言,mlir-opt 只读 .mlir,不能直接消费 ONNX。