想象一家大型餐厅的后厨。客人点的是精致的创意菜——“红酒炖牛腩”“芒果千层蛋糕”,这些写在菜单上的菜谱,就像编译器里的“高层面板”。但后厨里的帮厨只认一套最基础的动作:切、煮、炸、烤、称量。无论菜谱多花哨,最终都得翻译成这些基础动作,炉火才能执行。MLIR 的“标准方言”就是那张全行业统一的基础动作清单:它规定了内置的算术运算、比较逻辑、张量操作和基础类型系统,是所有高层方言与底层硬件之间的通用语言。任何自定义方言,都要经过“降级”(Lowering),一步步把花哨菜谱拆成白纸黑字的基础动作。本文就用这个“后厨”视角,拆解标准方言的定位、类型系统、核心操作,以及它在编译流水线中承上启下的角色。
理解 MLIR 标准方言,是进入现代 AI 编译器(Torch-MLIR、Triton 等)的钥匙:它决定你能否读懂计算图从“高层算子”到“机器码”的全过程,也是为 AI 芯片编写编译后端的必备功底。
目录
可视化演示
展示MLIR Lowering降级流水线、标准方言操作集及张量内存布局,支持敏感度调节。
本节配有交互动效(进入视口后自动加载,约 3KB)。
MLIR Lowering降级流水线 蓝色高层框架算子如torch.matmul 绿色标准方言底层算子arith.mulf High IR → Std Dialect → LLVM IR → Machine 逐步降级从高层抽象到低层机器指令
标准方言定位:后厨的基础动作清单
餐厅后厨墙上贴着两张表:一张是当季创意菜谱,天天更新;另一张是基础动作清单——切、煮、炸、烤、调味,十年不变,所有帮厨都要背熟。MLIR 的世界里,高层面板就是那份创意菜谱,而标准方言就是那张基础动作清单。它是框架内置的“公地”,不归任何一家自定义方言所有。
标准方言(Standard Dialect)是 MLIR 自带的基础操作与类型集合。现代 MLIR 把它细分为 arith(算术)、tensor、memref 等子方言,但思路一脉相承:无论上层方言多花哨,最终都要能翻译成这套基础操作。它解决了“不同方言之间如何对话”的问题。
从角色看,标准方言是“承上启下”的枢纽。向上,它为新方言提供语义参照系,新算子可以先用标准方言实现再逐步优化;向下,它有经过千锤百炼的 LLVM 降级路径。新方言作者只需把自己算子重写为标准方言,就搭上了通往机器码的高速路。
就像后厨只要把创意菜谱翻译成基础动作清单,就再也不用担心炉火怎么烧——标准方言把“方言到机器码”最难的一段变成了公共成熟通路。打牢这张清单的底子,是理解编译流水线的第一步。
- Toy 方言代表用户自定义的高层方言,描述贴合业务的复合操作,如矩阵乘、卷积。
- Lowering 箭头方言转换过程,把高层操作改写为低层操作的序列,这是第一次降级。
- 标准方言降级的目标,由算术、比较、张量操作与基础类型构成,是所有方言的公共底层。
- 到达 LLVM IR第二次降级,把标准方言逐条映射为 LLVM 指令,最终交给后端生成机器码。
基础类型系统:给食材贴标签
帮厨最怕什么?把盐当成糖。为了避免事故,后厨给每种食材贴统一标签:面粉、糖、盐、水。MLIR 的类型系统就是标准方言的“食材标签”,它决定了每个值是什么类型、能参与什么运算。
标准方言提供一组基础类型。index 表示索引和位置(像盘子的编号);i32、f64 表示整数与浮点标量;tensor 表示不可变的多维数据结构;memref 表示可读写内存区域的引用。标签附着在每个 SSA 值上,像食材标签一样跟着数据走。
类型的严谨性是编译器的底气。tensor<2x4xf32> 读作“形状为 2 行 4 列、元素为 32 位浮点的张量”。编译器靠这些标签做一致性检查;memref 还会记录内存布局与地址空间。与脚本语言不同,MLIR 在编译期就确定每个操作的类型,这是优化与代码生成的前提。
标签系统让整个后厨有条不紊:打荷的看到 f32 就不会把整数操作塞给它,传菜的看到 memref 就知道这块食材占着灶台内存。理解了类型,就掌握了标准方言的语法底层。
- tensor 关键字声明这是张量类型,表示不可变的多维数据容器。
- 形状列表尖括号内
2 × 4表示两个维度的长度,总共 8 个元素。 - 元素类型 f32每个元素为 32 位浮点数,决定了数值精度与运算规则。
算术运算指令:后厨的刀工基础
刀工是后厨的根基——切丁、切片、剁泥、拍蒜。没有这些基础动作,任何精致摆盘都无从谈起。标准方言的算术指令就是这样的“刀工”。
MLIR 把算术操作放在 arith 系列:addi、subi、muli 是整数加减乘,addf、subf、mulf 是浮点版本;divsi/divui 表示有符号/无符号整数除法,divf 是浮点除法,remi 求余数。后缀 i 代表 integer,f 代表 float——这就是命名里的“刀型标签”。
算术指令最重要的约束是类型一致性:arith.addi 要求两个操作数和结果同为整数类型,比如 (i32, i32) → i32;把 i32 和 f32 直接相加是违法的,必须先做类型转换。这种严格约束让编译器在编译期就能捕获大量“食材放错位置”的 bug。整数除法还要区分有符号与无符号,它们决定负数除法时的舍入方向。
和刀工一样,算术指令看似简单,却是优化最常下手的地方:编译器可以把 a × 2 改成 a + a,把 a × 1 直接删掉。刀法越标准,优化的空间越透明。
- 操作名 arith.addi标准方言的整数加法指令,
i后缀表示整数操作。 - 操作数 lhs, rhs两个输入操作数,必须是同一整数类型,否则非法。
- 类型签名括号内是输入类型,箭头右侧是结果类型,三者同属
i32。
比较逻辑运算:尝味道与做选择
大厨的日常是“尝一口,再决定”:汤咸了加水,淡了加盐。这个“判断 + 选择”的过程,对应标准方言里的比较与逻辑指令。
cmpi 是比较指令,带一个“谓词”参数:eq(相等)、ne(不等)、slt(有符号小于)、sgt(有符号大于)、ult(无符号小于)等,输出是 i1 布尔值。逻辑层面有 and、or、xor 组合布尔值;select 指令则像三元表达式,按布尔条件从两个值中挑一个。
为什么需要这些指令?因为 SSA 形式里值一旦定义就不可变,没有传统语言那种“if 语句改变量”的写法,控制流通过分支加 select 实现数据选择。有符号比较与无符号比较在负数场景下结果完全不同:0xFFFFFFFF 作为有符号数是 -1,作为无符号数是 4294967295,选错谓词程序就错了。
尝味和选择相辅相成:cmpi 尝出咸淡,select 决定加盐还是加水。标准方言用这套小工具,在低层搭建起高层面板里"if-else"的样子。
- cmpi 与谓词 slt
cmpi发起比较,slt指定“有符号小于”这一判断规则。 - 两个操作数
lhs与rhs为同类型整数,参与大小比较。 - 结果 i1输出单比特布尔,1 表示条件成立,0 表示不成立。
张量操作:重新摆盘的艺术
一条烤盘上摆 2 行 4 列共 8 块饼干,后厨为省烤箱空间,把它们重新排成 1 行 8 个。张量操作就是这种“重新摆盘”的技术。
标准方言的张量操作关心“结构”而非“数值数学”:tensor.from_elements 把一组标量组装成张量;tensor.reshape、expand_shape、collapse_shape 改变形状;tensor.extract / tensor.insert 按索引访问和写入元素。真正的矩阵乘法等高密度数学,属于 linalg、math 这类更高语义的方言。
形状变换遵循“元素数守恒”:tensor<2×4×f32> 重排成 tensor<8×f32> 只改形状不改数据个数,所以 reshape 往往只是“换个视角”。当 tensor.extract 的索引全是编译期常量时,求值可以立即折叠成一次常量读取。这一层结构操作,是后续缓冲区分配(bufferization)的铺垫。
摆盘技巧的价值在于为“上菜”做准备:张量结构理顺了,才能谈分配内存、优化访存。一盘菜怎么摆,决定了客人(硬件)吃得顺不顺。
- 操作名 collapse_shape折叠形状操作,属于张量结构变换家族。
- 输入形状
2 × 4的二维张量,共 8 个元素。 - 输出形状摊平为长度 8 的一维张量,元素类型保持不变。
Lowering机制:菜谱拆解成步骤
主厨收到“芒果千层蛋糕”的订单,不会把蛋糕直接扔进烤箱,而是拆成摊皮、打奶油、叠层、冷藏一系列基础步骤。这个“菜谱拆解”就是编译器里的 Lowering(降级)。
MLIR 提供 DialectConversion 方言转换框架:开发者为高层面板的操作编写重写模式(rewrite pattern),把复合操作改写为标准方言操作序列。转换由模式匹配驱动:左边匹配到某种操作形态,右边生成目标方言的操作。
降级通常是“渐进式”的:toy 方言先降到 arith、tensor、memref 等标准方言,再降到 LLVM。每次只降低一层抽象,既保留中间优化机会,又便于分步调试。标准方言恰好是这条链上最高的“通用站”——它通往 LLVM 的路径是社区反复打磨过的。
拆解菜谱的学问在于“拆到什么程度”:拆太粗,炉火看不懂;拆太细,浪费灶台。渐进降级让每一步都恰到好处,而标准方言就是那条被验证过千万次的“基准拆法”。
- 左侧被匹配模式
toy.multiply是高层面板中一个自定义乘法算子。 - 重写箭头表示模式匹配成功后的替换,对应编译器里的一条 rewrite rule。
- 右侧生成操作
arith.mulf是标准方言浮点乘法,语义完全等价。
流水线角色:后厨的完整流程
后厨完整流程是:接单、翻译菜谱、备料、烹饪、出餐。编译也是如此:前端生成高层 IR,经过规范化、降级、优化,最后代码生成。标准方言在流水线中担任枢纽。
第一个动作是规范化(canonicalization):把冗余表达化简,比如 x + 0 直接替换为 x,x × 1 直接删除。第二个动作是多级降级:所有上层方言统一翻译成标准方言后,后续优化只需面对一种 IR——编译器团队写的 pass 一次编写,适用于所有前端。
标准方言的“统一表示”带来两个直接收益。一是可扩展性:新方言不需要写完整后端,只要会降级到标准方言即可。二是代码生成质量:LLVM 后端看到的是一份经过规范化的、语义清晰的 IR,能应用更多优化,减少资源浪费。
一张干净的标准方言 IR,就像一份工整的备料单:名厨看一眼知道菜怎么炒,LLVM 看一眼知道代码怎么生成。规范,是质量的第一道保障。
- 左侧操作
arith.addi(x, 0)表示对x加上常数 0。 - 规范化规则0 是加法单位元,整个操作退化为
x,冗余计算被删除。 - 化简结果仅保留
x,减少一次无意义运算。
AI 应用场景
推理引擎的算子降级
阶段:模型编译与部署阶段
解决的问题:PyTorch/TensorFlow 导出的高层算子(如卷积、LayerNorm)无法被硬件直接执行;传统做法是手写每类算子的底层实现,工作量大且不可复用。Torch-MLIR 等工具先把 torch 方言降级为 arith、tensor 等标准方言操作,再落到 LLVM,一条路径服务所有模型。
具体案例:某公司将 ResNet 的 ONNX 模型接入自研推理引擎:前端把 onnx.Conv 翻译成 tensor.extract、arith.mulf、arith.addf 等标准操作序列,经规范化和降级后生成 LLVM IR,最终在 CPU 上跑通 200 帧/秒。
自研 AI 芯片的编译器接入
阶段:硬件后端开发阶段
解决的问题:新芯片没有现成编译器,若从头写“算子到硬件指令”的翻译器成本极高。借助标准方言,团队只需编写少量从自定义方言到标准方言的重写模式,社区已有的标准方言→LLVM 后端可直接复用。
具体案例:NPU 团队为自家 tpu.dispatch 算子编写重写规则,分解为 arith.addi、tensor.extract 等标准操作,再经标准流水线生成可执行代码,工具链开发周期从一年缩短到三个月。
调试数值不一致的算子
阶段:性能调优与 Bug 排查阶段
解决的问题:训练与推理结果不一致时,高层面板语义太厚难以定位;把 IR 打印到标准方言粒度后,可逐条核对算术操作和类型转换。
具体案例:工程师发现复现性报错源于一处有符号/无符号除法混用,查看降级后的 cmpi 与 divsi 指令后定位到取整方向错误,修复后对齐了 cuDNN 的数值结果。
Python 演示
"""mini_mlir_lower.py: 模拟 MLIR 渐进式降级的小工具. 把高层方言(Toy)的复合操作逐条翻译成标准方言(Arith/Tensor)指令,并打印降级后的指令流, 帮助理解标准方言在编译流水线中的角色.""" # 高层方言到标准方言的"菜谱拆解表"LOWER_TABLE = { "toy.reshape": ["tensor.collapse_shape", "std.memref_cast"], "toy.multiply": ["arith.mulf"], "toy.relu": ["arith.maxf", "std.select"], "toy.conv": ["tensor.extract", "arith.mulf", "arith.addf"],} def lower_one(op_name): """把一个高层操作拆解为标准方言操作序列.""" if op_name in LOWER_TABLE: return LOWER_TABLE[op_name] return [op_name] # 已是标准方言则原样保留 def lower_pipeline(ops): """对整个流水线做渐进降级, 返回标准方言指令列表.""" lowered = [] for op in ops: lowered.extend(lower_one(op)) return lowered def main(): pipeline = ["toy.conv", "toy.relu", "toy.reshape"] result = lower_pipeline(pipeline) print("=== 标准方言指令流 ===") for i, inst in enumerate(result, 1): print(f"{i:2d}. {inst}") print(f"共 {len(result)} 条指令, 已消除全部高层自定义算子") if __name__ == "__main__": main()
LOWER_TABLE = {定义“菜谱拆解表”:每种高层面板操作对应一串标准方言指令。def lower_one(op_name):单个操作降级入口,从拆解表查规则。if op_name in LOWER_TABLE:命中已注册的重写模式,完成模式匹配。lowered.extend(lower_one(op))用 extend 拼接多步指令,保持执行顺序。for i, inst in enumerate(result, 1):编号打印降级后的指令流。if __name__ == "__main__":保证模块可被导入而不自动执行主流程。
常见误区
误区我刚学 MLIR 时以为标准方言就是最上面一层的 IR,新功能直接写成标准方言操作最省事,反正它叫“标准”。
正解恰恰相反,标准方言位于抽象阶梯的底部通用层,语义非常朴素(标量算术、结构操作)。把卷积这种复合语义直接拆成标准方言,会变成一大串指令,丢失可优化结构。正确做法是先在自定义高层面板表达丰富语义,最后再渐进降级到标准方言,就像先用创意菜谱沟通、再翻译成基础动作。
为何常见:“标准/内置”听起来像正统顶层 API,容易和“用户编程语言库”混淆。
误区我以为类型就是整数和浮点两个标签,写代码够用了,反正示例都是 `addi(i32, i32)`。
正解标准方言提供 index、i1、i8-i64、f16/f32/f64、complex、tensor、memref、vector 等一整套基础类型。index 专门描述索引,tensor 描述不可变数据,memref 描述可变内存。类型系统是完整的食材标签体系,远不止两种。
为何常见:入门教程示例多用 addi 的 i32 签名,容易以偏概全。
误区`tensor.extract`、`tensor.reshape` 这些名字听起来就是线性代数库函数,矩阵乘应该也在里面。
正解标准方言的张量操作只负责结构——构建、取元素、改形状,不含矩阵乘这类密集数学运算。矩阵乘在 linalg/math 方言,或经 bufferization 后由循环加标量算术表达。把结构操作当成数学计算,会误解整个 IR 分层设计。
为何常见:把 PyTorch 的 tensor API 印象直接套到了编译器 IR 上。
误区一次重写从高层面板直接翻译到机器码最省时间,为什么还要渐进多步?
正解一步降级会同时丢失大量中间语义,优化 pass 无处下手、Bug 难以定位。渐进降级像后厨先拆菜谱再做菜:方言→标准方言→LLVM,每步只降一级抽象,每一步都可验证,社区复用度也高。
为何常见:编译过程不可见,容易高估单步重写的可行性。
误区标准方言操作最后不都变成 LLVM 指令吗?干脆当成同一层。
正解标准方言仍有较高语义:有类型丰富的 tensor/memref、有运行时分配语义;LLVM IR 是更底层的 SSA + 基本块,类型是指针、向量、标量的机器精确表示。标准方言必须经过逐条映射和内存降级(memref→指针)才到达 LLVM。
为何常见:两者都叫“中间表示”,且降级界线看起来模糊。
面试题
Q1. MLIR 标准方言在编译框架中的核心定位是?
- A.面向用户的编程高级语言
- B.高低层之间的公共基础层← 正确答案
- C.直接执行运算的硬件引擎
- D.AI 专用算子数学库
答案:B。标准方言不是给用户写程序用的(那是前端语言的事),也不是硬件执行引擎。它像后厨的基础动作清单,位于高层面板与 LLVM 之间,是承上启下的公共基础层。A/C/D 都把层级搞错了。
Q2. 以下哪个是标准方言中的基础类型?
- A.索引位置使用的 index 类型← 正确答案
- B.网页超文本标记类型
- C.操作系统文件句柄类型
- D.数据库事务日志类型
答案:A。index 是标准方言用于表达索引与地址的基础类型,降级时会转换为指针宽度的具体整数。B/C/D 分别是网页、操作系统、数据库领域的概念,均不属于 MLIR 的类型体系。
Q3. arith.addi 与 arith.addf 的核心区别是?
- A.前者是整数加法后者浮点加法← 正确答案
- B.前者是浮点加法后者整数加法
- C.两者语义完全相同没区别
- D.前者是张量加法后者是标量加法
答案:A。命名后缀就是刀型标签:i 代表 integer,f 代表 float,所以 addi 做整数加法、addf 做浮点加法,两者类型签名不同。C 忽略后缀差异,D 混淆了“标量/张量”与“整数/浮点”两组正交维度。
Q4. cmpi 指令比较后的输出类型通常为?
- A.i1 布尔标量类型← 正确答案
- B.任意宽度的整数类型
- C.与输入相同的类型
- D.浮点张量结果类型
答案:A。cmpi 是一条“味觉判断”指令,只回答真/假,所以输出固定为单比特 i1。B 把输入宽度误当作输出,C 忽略了比较与算术的差异,D 混淆了数值运算与比较运算的分工。
Q5. tensor 类型与 memref 类型的核心区别是?
- A.张量不可变而 memref 引用可变内存← 正确答案
- B.张量描述内存布局而 memref 保存数值
- C.两者完全相同没有实质差别
- D.memref 类型不包含形状信息
答案:A。tensor 像摆好的拼盘(不可变),memref 像可随时加菜的大锅(可变内存引用)。二者在降级中通过 bufferization 衔接:先用 tensor 表达计算,再落到 memref 分配内存。C 与事实矛盾,B 张冠李戴,D 错误——memref 同样携带形状信息。
Q6. 渐进式降级(progressive lowering)指的是?
- A.逐层降低抽象直到低层表示← 正确答案
- B.一次性直接翻译到机器码
- C.从 LLVM 反向推导高层面板
- D.跳过中间表示直接生成目标代码
答案:A。渐进降级像把菜谱先拆成基础步骤再执行:每层只降一级抽象(方言→标准方言→LLVM),保留中间优化机会、便于调试。B/D 一步到位会丢失中间语义,C 方向完全反了。
Q7. select 指令在 SSA 形式中的作用是?
- A.按条件从两个值中挑选一个← 正确答案
- B.修改条件变量的内存内容
- C.完整执行两个分支的全部逻辑
- D.无条件地把两个数组连接起来
答案:A。SSA 中值不可变,没有传统 if 变量修改的能力,select 像大厨尝味后“选加盐还是加水”,按布尔条件输出两值之一。B 违反 SSA 不可变原则,C 属于控制流合并,D 是拼接操作,均非 select 语义。
Q8. 下列选项中属于标准方言操作的是?
- A.arith.addi 整数加法← 正确答案
- B.cmpi 整数比较指令← 正确答案
- C.torch.nn 卷积模块
- D.tensor.extract 张量取值← 正确答案
答案:ABD。arith.addi、cmpi、tensor.extract 分别是算术、比较、张量结构操作,属于标准方言。torch.nn 是 PyTorch 的高层语义,属于深度学习框架层面,不在标准方言范围内。
Q9. 关于标准方言降级到 LLVM,下列说法正确的是?
- A.index 类型需转换为具体整数← 正确答案
- B.标准方言逐条映射为 LLVM 指令← 正确答案
- C.张量直接映射为机器寄存器
- D.memref 最终转为指针访问形式← 正确答案
答案:ABD。index 在目标机上有具体宽度,必须显式转换;标准方言逐条映射为 LLVM 指令;memref 在 bufferization 后变成指针加形状描述,最终以指针访存。张量不能直接映射寄存器,必须先完成布局与缓冲分配,C 错误。
Q10. 标准方言提升编译器可扩展性的方式包括?
- A.提供统一稳定的基础语义← 正确答案
- B.新方言可直接复用降级路径← 正确答案
- C.禁止新方言重写基础操作
- D.强制用户从头编写编译后端
答案:AB。标准方言提供共同的基础语义和成熟的 LLVM 降级路径,新方言像新菜谱只需翻译成基础动作,不必自己造炉子。C 与事实不符——新方言完全可以把复杂算子重写为标准操作序列;D 违背可扩展性设计初衷。