ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

MLIR官方文档中文翻译:多级中间表示与方言体系学习指南

MLIR官方文档中文翻译:多级中间表示与方言体系学习指南 简介MLIR多级中间表示官方文档的中文翻译项目面向编译器研究者、底层开发者以及希望深入理解MLIR架构的中高级程序员。内容系统覆盖MLIR核心概念包括多级中间表示基础设施、方言dialects、操作operations、属性attributes、类型转换机制、模式patterns、Pass管理器与代码生成框架等为中文用户扫清官方英文文档的阅读障碍。压缩包共316个文件约1.05MB以89个md文档为主体辅以41个h头文件、40个cpp源码、36个mlir示例、td定义文件、svg架构图及py辅助脚本等md与txt构成理论学习主线源码文件对应教程中的具体实现便于对照研读。已有155人学习使用。无论是研究MLIR的设计理念还是动手实践Pass编写与代码生成这套资料都能提供从理论到实践的完整指引适合作为系统入门和日常查阅的首选中文参考。1. MLIR官方文档中文翻译项目把“下下来也读不动”的官方资料变成一张中文地图为什么说MLIR官方文档中文翻译项目解决的问题不只是几个术语的翻译而是“拿到资料也不知道从哪读起”MLIR 不是一条 IR而是由 Dialect、Operation、Attribute、Type 共同组成的体系。官方文档里的词都能查到但原文散落在不同目录第一次接触多级中间表示的工程师很难把“方言、转换、Pass 管理器、代码生成”串成一条能落地的路径。这个翻译项目的价值是把官方文档与教程按“核心概念、多级中间表示、基础设施、方言、操作、属性、类型、转换、Pass 管理器、代码生成”的线索整理成一套完整中文版学习资料让阅读顺序跟着使用场景走而不是跟着目录走。资料适合两类人一类是给 AI 芯片或异构后端做工具链的工程师想快速搞清楚一个算子从高层方言一路降到 LLVM 方言要多远另一类是刚接触编译原理的学生还在纠结为什么不直接用 LLVM IR。下面按“概念怎么立、命令怎么跑、坑在哪、往哪个方向深入”的顺序展开最后给一个自己验证学习成果的具体技巧。2. 多级中间表示与方言体系先给 MLIR 建立三个阅读坐标2.1 多级中间表示为什么“多”抽象层次不是一条线而是一组方言传统编译器里的“中间表示”通常是单层前端生成一份接近指令集的 IR后面所有优化都在同一抽象级别上做。MLIR 的思路不同它承认编译一个复杂算子时不同阶段需要不同粒度的信息前端需要结构清晰的张量运算中端需要能体现循环、内存布局的表示后端需要贴近机器指令的表达。如果把所有信息塞进同一层 IR要么丢失语义要么被细节淹没。MLIR 用“方言”来解决这个问题。一个方言就是一组 Operation、Type、Attribute 的集合给同一份程序提供一套专门的词汇。比如tensor方言描述张量值linalg方言描述可融合的线性代数算子scf方言描述带结构控制的循环llvm方言直接对应 LLVM IR 的指令。程序在不同编译阶段以不同方言呈现层次之间通过“转换”衔接。读官方文档时最容易犯的错是想把 MLIR 当成“一条具体的 IR 语法”去背。实际上你拿到手的是一个基础设施它提供统一的 IR 数据结构、Pass 管理框架、模式重写引擎而“业务逻辑”全部由方言承担。理解这一点再去看翻译资料里反复出现的“方言”“操作”“属性”“类型”就不会再把它们当成并列的零碎概念。2.2 Operation、Type、Attribute 的分工官方文档高频词的查表用法官方文档中Dialect、Operation、Type、Attribute 四个词出现频率极高但它们不是同一个层面的东西。简单地说Operation 是程序里的一个“动作”Type 描述这个动作操作的数据形状Attribute 描述编译期就能确定、不随运行变化的参数。Dialect 则是给这些动作、类型、属性提供的命名空间。下面这张表可以作为读任何一篇 MLIR 文档时的手边索引术语官方文档中的角色排查报错时先问自己Dialect一组 Operation/Type/Attribute 的集合通常带前缀如tensor.、arith.、toy.报错里的 op 归属哪个方言目标前端有没有导入这个方言OperationIR 中的最小语义单元包含操作数、结果、区域和属性op 写在 .mlir 文件里语法是否与当前版本匹配Type对值的静态约束如tensor2x3xf32、memref?x?xf32转换目标是否允许这个类型出现报错是否提到 “type is not legal”Attribute编译期常量如dense[1.0, 2.0]、仿射映射、布局信息属性是否属于当前方言拼写是否混淆了 type 与 attribute官方文档里大量报错示例的根因最后都能落到这张表上。比如addf op requires all operands and results to have the same type说明是 Type 层面的约束没满足而attribute value not found说明是 Attribute 的拼写或归属问题。中文翻译资料的价值在于把这些散落在不同页面的诊断信息归并到同一个概念表里报错时能少翻几十页原文。另一个容易忽略的点MLIR 的 Operation 是可以嵌套的。一个 op 里可以带 regionregion 里又能放一串 op所以“最小语义单元”并不是“一行指令”而是“一层语义表达”。读转换相关章节时遇到region这个词不要绕过去它才是结构化控制和高级抽象能落地的关键。2.3 官方文档的“三层入口”LangRef、Tutorials、Rationale 怎么搭配读官方仓库里的文档很多但阅读入口大致分三类语言参考、教程、设计说明。它们的用途完全不同翻译资料也是按这个思路组织的。语言参考定位词法、语法、类型系统、Pass 基础设施它适合当作字典不适合从头读。教程则按 Toy 示例逐步从零建一个方言、注册一个 Pass、实现转换它适合照做但行号容易被新版本带偏。设计说明解释“为什么把 IR 设计成这样”如Rationale.md中关于方言为何不能随意转换的讨论它适合在遇到理解障碍时回头补课。实践中我建议按需要选入口而不是按目录顺序读想看懂toy.transpose的定义去语言参考里查toy方言想复现一轮完整转换去教程里照 Ch2 到 Ch6 的顺序搭想搞明白为什么linalg比直接写循环好再去读 Rationale 里的张量抽象说明。中文资料如果只是一层层翻译原文价值不大它真正有用的地方是把这三类内容按“知识、动手、为什么”的标签重新标注让读者知道自己此刻踩在哪类内容上。如果手里只有一份完整的中文版学习资料使用顺序建议是先用教程建立手感再用语言参考精读一个方言最后用 Rationale 解决“为什么”。不要反着来反着来很容易在第一天就被术语淹没。3. Pass 管理器与转换用一个 .mlir 文件跑通一遍“下降”链路3.1 Pass 管理器的角色Pipeline、Pass 和三种作用域MLIR 官方文档里Pass 管理器是最容易被“看懂了但用不起来”的部分。它的作用很简单把一系列 Pass 按顺序组成一个 Pipeline然后让它在某个 IR 单元上执行。难点在于一个 Pass 到底作用在什么范围上。文档里常见的三种范围是 module、function、loop。module 级别的 Pass 能看到整个编译单元适合做全局符号清理、内联决策function 级别的 Pass 在函数体内部做优化比如死代码消除loop 级别则针对结构化循环做变换比如循环展开、交换。新版 MLIR 已经把若干 Pass 细化到 region 级别但概念没有变先确定你要处理的最小单位是什么再决定 Pass 挂在哪一层。另一个关键概念是“重复执行”。同样的 Pass 在 Pipeline 中出现两次很正常因为目标 IR 经过前面的转换后可能又出现新的可优化点。官方文档里说 Pass 管理器会负责调度但这不代表你可以乱序执行。比如--canonicalize尽量放在一些转换之后跑因为新生成的 op 需要规范化后才能被下游 Pattern 匹配。看中文翻译文档时不要只记“Pipeline 是 Pass 的列表”这句话。我一般会先把一个 Pass 想成两个问题它从哪些 op 进入它把哪些 op 变成什么。只要这两个问题能回答Pass 在 Pipeline 里的位置就自然出来了。3.2 最小复现写一个两行 .mlir 文件跑 round-trip 和 canonicalize在开始之前先保证本机有可用的mlir-opt。如果是自己编译 LLVM/MLIR工具一般生成在build/bin目录下如果使用包管理器安装版本可能偏旧命令参数会有差异。下面的例子非常小只要 MLIR 能运行就能复现。cat /tmp/simple.mlir EOF func.func simple_add(%a: i32, %b: i32) - i32 { %0 arith.addi %a, %b : i32 func.return %0 : i32 } EOF # 1. 什么都不做把文件读进来再原样输出 mlir-opt /tmp/simple.mlir # 2. 跑 canonicalize观察 IR 是否发生变化 mlir-opt /tmp/simple.mlir --canonicalize # 3. 跑 canonicalize cse并打印每次 Pass 执行后的 IR mlir-opt /tmp/simple.mlir --canonicalize --cse --mlir-print-ir-after-all 21 | less这段命令有三个要点。第一mlir-opt默认会做一次 parse 和打印这一步通过也就意味着文件语法至少是当前版本能认的。很多人报错“unexpected token”其实是在写 op 时把内置方言词法和自定义方言词法混用了。第二--canonicalize是通用规范化 Pass它内部由无数 pattern 组成功能随版本变化很大但它能告诉你“规范化”不是某个固定操作而是“尽可能把 IR 变简单”。第三--mlir-print-ir-after-all会把每次 Pass 跑完后的 IR 打出来调试转换流程时几乎是必开选项。这个最小复现的真正目的不是验证 canonicalize 本身而是让你建立起“IR 状态机”的感觉每一步转换之前先看清输入执行完再看输出。翻译文档里那些转换章节本质上就是在描述“输入 IR 长什么样、匹配到什么 pattern、输出成什么”。能对着这个命令把过程看一遍比把教程读十遍都管用。3.3 转换和下降官方文档把 conversion 与 lowering 分开讲是有原因的官方文档里会频繁出现 “conversion” 和 “lowering”。翻译成中文都接近“转换”但含义不同中文资料里如果混译很容易让读者误判。conversion 强调“两个方言之间映射关系”。比如把tensor方言的tensor.extract转换成memref方言的memref.load两者语义上有对应关系但目标方言的抽象级别不一定更低。lowering 强调的是“向更靠近机器的方向下降”。比如scf.for转换成cf.br加cf.cond_br这就是一次实质性下降因为它把结构化控制流拆成了底层分支。实际操作中两者往往同时发生。一个 ConversionPass 往往既做方言映射又把抽象级别降低。但理解这个区分仍然有意义写转换规则时你要分清哪些 op 是“直接一一对应”哪些 op 是“被拆分重写”。前者通常简单后者要处理操作数和控制流的变化是踩坑最多的位置。看翻译文档时如果某一段描述里出现“转换到 X 方言”先问一句“这个转换是不是同时包含了重构”。只做替换的转换任何方言都能写涉及重新组织控制流的转换才是能否跑通的关键。手头能复现的验证方式还是mlir-opt --mlir-print-ir-after-all把每一步转换拆开看。4. 避坑版本不一致、Pass 未注册、代码生成链路对不上时的排查路径4.1 现象一mlir-opt 提示 “pass not registered”照文档执行mlir-opt --convert-toy-to-affine /tmp/test.mlir结果报pass not registered。这不是因为文档写错而是 MLIR 的 Pass 注册机制比想象的严格一个 Pass 要在mlir-opt的命令行中可见必须经过PassPipelineRegistration注册并且mlir-opt可执行文件要链接包含该 Pass 的库。文档示例通常假设读者在用官方构建目录里的mlir-opt而自建工具链很容易缺链接。解决方法是先查注册表不猜名字。跑mlir-opt --list-passes | grep -i toy如果没结果说明这个名字不存在或二进制没链接对应库跑mlir-opt --help-hidden | grep -i convert能看到被隐藏的开发者选项。还有一个常见陷阱中文文档里把 Pass 名称翻译成中文是合理的但命令行参数必须保留英文原名不要用译名直接去敲。真正定位时从两个方向入手一是确认这个名字在当前分支中是否被改名MLIR 上游经常给 Pass 加前缀或拆分二是确认注册代码是否在mlir/lib/Conversion/对应目录下构建时有没有把它编进mlir-opt。把这两个问题查清这类报错基本十分钟内能解决。4.2 现象二教程示例代码里的文件路径和行号对不上拿着官方 Toy 教程复现发现它指向的examples/toy/Ch1/mlir/toy.cpp在当前克隆的仓库里根本不存在或者文件在但行号不对。原因是官方文档往往跟着某个 release 或特定 commit 写而 LLVM 仓库主干一直在变。中文翻译保留了原始路径信息时在新版仓库里就很容易出现这种错位。解决的思路不是“去网上找新版代码”而是把文档和源码锁在同一时刻。先用git log --oneline --all -- mlir/examples/toy/Ch1/mlir/toy.cpp看这个文件经过哪些提交再用git log -S toy.transpose -- mlir/examples/toy找包含关键算子字符的 commit。找到后把仓库切到相应提交或者只查看那个历史版本的文件内容。这个坑最迷惑人的地方在于MLIR 的基础概念没变但 API、文件路径、Pass 名称都在变。教程里的思路能参考代码细节不能直接照抄。如果条件允许直接看当前仓库里mlir/test/Examples/Toy/下的测试文件它们和当前版本完全同步。4.3 现象三转换时报 “unsupported operation” 或 “type is not legal”代码生成流程进行到一半报failed to legalize operation tensor.extract并且附带一个或者一串 “unable to legalize” 的错误。这通常是 ConversionTarget 没把目标方言里的某个 op 或 type 设置为合法。很多新手以为转换就是把源方言的所有 op 一一映射但 MLIR 的转换框架要求你先明确“哪些 op 保持原样、哪些 op 必须被消除”。原因在于转换目标说的“合法”不只是允许这些 op 存在而是要求它们符合目标方言的约束。比如从tensor降到memref之前要保证所有tensor类型都已消失如果出现一个tensor?x?xf32的动态形状没有被处理后续的memref算子就没法接住。报错可能出现在很久以后的 Pass 里但根因在前一步类型没清干净。解决方法是把转换链路拆成两步验证。第一步用--mlir-print-ir-after-all找第一个出现非法 op 的 Pass第二步回到中文版相关章节看它对 ConversionTarget 合法类型列表的描述确认这是类型问题还是 pattern 缺失。很多时候只需要在转换参数里补充声明一个 type 为 legal问题就过了。4.4 现象四文档里的 Pipeline 顺序和实际编译流程差很远官方文档给出的示例 Pipeline 往往是教学用法比如直接--convert-scf-to-cf --convert-cf-to-llvm。但实际工程里代码生成之前要处理 bufferization、内存布局、并行调度Pipeline 会长得多。新手按教学 Pipeline 跑通后就以为这是代码生成的全部结果接入自己的方言时到处碰壁。这个坑的本质是没分清“教学链路”和“生产链路”。中文手册里覆盖的转换实例目的是展示机制不是提供完整后端。解决方法是把文档里给出的--pass-pipeline当成骨架对照实际编译流程的必经阶段去补bufferization 是否完成函数签名类型是否已经变成memref或llvm.ptr全局变量是否已经落到合适的内存区域。我通常在验证时会故意把 Pipeline 拉长一点先把每一步输出存成文件备份。这样一旦最后一步挂了可以直接二分定位从中间某个输出继续跑而不是从头重跑整条链路。这个思路和 MLIR 本身的设计一致Dialect 边界就是天然的“断点检查点”。5. 代码生成专题顺着“下降”把官方资源完整地用一遍5.1 先看清 MLIR 语境里“代码生成”指的是哪一段很多人以为“代码生成”是从源语言直接生成目标机器码。MLIR 语境下的代码生成通常指“把高层方言逐步下降到 LLVM 方言”之后由 LLVM 后端完成寄存器分配、指令选择。也就是说MLIR 的职责到 LLVM IR 为止不是一路管到底。这个边界非常重要它意味着你在写转换规则时只需要保证输出能被 LLVM 方言表达不需要关心最终汇编长什么样。中文资料里如果混入“直接生成汇编”的表述读的时候要打一个问号确认作者说的究竟是 MLIR 代码生成还是完整编译器后端。常见下降链可以画成一条路径高层领域方言 → 张量/循环方言 → 结构化控制流方言 → 底层分支指令方言 → LLVM 方言。中间每一段都对应文档里的一组转换 Pass。理解这条链的价值在于你能快速定位某个 Pass 在整个代码生成流程里的位置而不是孤立地记命令。以官方 Toy 示例为例前端生成toy方言的 op经过函数内联和形状推断后toy.transpose这类算子会先转换为affine或linalg的张量操作再逐步变为循环和分支最后进入 LLVM 方言。每一步都保持“上一步的输出 下一步的输入”。5.2 用打印 IR 的方式跟踪每一步方言切换代码生成的调试本质上就是“观测方言边界上的 IR 变化”。MLIR 为此提供了非常顺手的工具能力在 Pipeline 执行过程中打印每个 Pass 前后的 IR。这一点比很多传统编译器都好用因为它把方言边界显式暴露出来了。mlir-opt /tmp/toy.mlir \ --canonicalize \ --mlir-print-ir-before-all \ --mlir-print-ir-after-all \ 21 | grep ^// -------- IR Dump -A 30 | less这条命令会把每个 Pass 执行前后的 IR 都输出然后通过 grep 把关键节点的 IR 片段过滤出来。参数说明--mlir-print-ir-before-all和--mlir-print-ir-after-all不必同时开先开 after 即可输出量很大一定要接less或重定向到文件不要直接往终端扔。最常见的用法是用它验证一个转换到底发生在哪个 Pass。比如你预期toy.transpose会转成linalg.generic但找不到转换点就把输出重定向到文件然后搜索linalg第一次出现的位置再往前看是哪个 Pass 的 IR Dump。这个搜索过程基本替代了逐行读文档。5.3 把翻译资料里的转换章节变成一张“三栏跟踪表”读完整份中文资料后如果没有留下可检索的记录学习效果会打折扣。我习惯把每次转换按“入口方言/出口方言/关键 op”记成一张三栏跟踪表后续查问题直接跳转。转换阶段入口方言出口方言关键 op 示例高层方言规范化toytoytoy.transpose保留张量转内存tensormemreftensor.extract→memref.load结构化循环scfcfscf.for→cf.brcf.cond_br进入 LLVM 方言cfllvmcf.br→llvm.br这张表不需要一开始就填完整每读完一个转换章节补一行。填表的过程会强迫你回答几个问题这个转换的输入 op 是什么输出 op 是什么有没有丢掉控制流信息。如果三个问题都能答上来说明这个转换已经是你的了而不是“看懂了”。代码生成方向的进阶阅读我一般以mlir/lib/Conversion/目录下的源码为索引而不是只看文档。文档说清意图源码说清边界条件。中文翻译资料的价值是帮你理解意图但真正写 Pattern 时还是要回到当前版本的源码里确认接口签名。6. 用“单算子跟踪”自查这份 MLIR 资料有没有真的吃透一个非常有效的小技巧是选一个算子从它出现在.mlir文件里开始一路跟踪到它进入 LLVM 方言为止。这个过程能同时检验你对方言、Pass 管理器、转换、代码生成四个方面的理解。做法分三步第一步挑一个不复杂的算子比如toy.transpose第二步在资料里分别找到它对应的 op 定义、verifier 校验规则、转换 pattern、以及它进入 LLVM 方言前最后一步的几个位置第三步用mlir-opt --mlir-print-ir-after-all实际跑一遍把纸上查到的转换点和实际 IR Dump 的位置对上。可以按下面这张表来组织你的跟踪记录检查点要能回答的问题验收方式Op 定义这个 op 的操作数和结果是什么在源码或文档译文里翻到Toy_TransposeOp相关定义约束校验什么情况下 verifier 会拒绝这个 op故意写一个类型不一致的.mlir文件看报错转换模式它先转换成哪个方言的哪个 op在 IR Dump 里搜索目标 op 名Pass 触发这个转换由哪个 Pass 拉起找到对应 Pass 的注册名字和所在目录方言切换它最终以什么形式进入 LLVM 方言跟踪最后一轮 IR Dump如果你能把中间两行的答案直接写出来说明你已经超过“能跑通教程”的阶段开始真正理解转换链。如果卡在某一行对应的中文资料章节就是你需要回头重读的部分而不是从头再看一遍。我自己在后端开发里一直保留这个习惯新接触一个领域 DSL 时先把一个最小算子走完整个链路再谈优化。这个方法让我避开好几次“文档好像懂了一动手就翻车”的情况希望帮到你。本文还有配套的精品资源点击获取
返回列表