☰
MLIR中文翻译项目:从多级中间表示到Pass管理器的核心指南
2026/10/10 4:57:01 网站建设 项目流程

简介:这是一套面向编译技术研究与开发者的 MLIR 官方文档中文翻译合集,将英官网文档、教程与示例系统转译为中文,覆盖核心概念、多级中间表示基础设施、方言、操作、属性、类型转换、Pattern 重写、Pass 管理器及代码生成等关键模块。压缩包共 316 个文件,约 1.05MB,以 Markdown 文档为主,辅以 C++/Toy 示例源码、MLIR 定义文件、表格及说明文本,便于对照原工程学习概念与实现。资源特别解释了方言如何表达特定领域抽象、Pass 管理器如何编排优化流程,并通过代码生成章节展示从中间表示到目标机器码的路径,适合正在入门或深入 MLIR 的编译器学习者、研究所工程师使用。合集对官方教程做了完整梳理,配有头文件与构建配置,读者可按目录逐章阅读,也可直接检索关键字定位技术细节。目前已有 155 人学习下载,对于小体积高密度的技术资料而言,适合作为常备参考。

1. MLIR官方文档中文翻译:把“基础设施”读成能上手的东西

做编译器后端或者AI框架算子优化的人,大概率都有过这种经历:打开MLIR官方文档,英文单词都认识,但读到第四段就不知道它在说什么。这不是英文水平问题,是论文式文档的表达密度问题。这份MLIR官方文档中文翻译项目,把官方docs里的Rationale、LangRef、Tutorials、Dialect目录、Pass基础设施和代码生成相关章节,按学习路径整理成完整的中文对照版,核心概念如多级中间表示、方言、操作、属性、类型、转换模式、Pass管理器都有覆盖。适合两类人:一是刚开始接触MLIR、想找一份能按顺序读的资料的人;二是已经在写Dialect和Pass、需要快速反查术语或接口含义的人。它解决的是读文档的第一公里——先看懂设计意图,再决定自己该动哪段代码。

2. 拆解MLIR核心概念:多级中间表示、方言与操作的定义边界

2.1 多级中间表示:LLVM IR的“一维”问题

传统编译器的路径里,LLVM IR基本是前后端之间的“终点站”。Clang把C/C++翻译成LLVM IR,优化跑几轮,后端下降成目标指令。这条路的问题是抽象层次单一:循环优化需要知道循环结构和边界,但LLVM IR里的循环已经退化成CFG基本块加跳转;算子融合需要知道张量形状、内存布局和访存模式,但LLVM IR里只有load/store和GEP。于是大家要么在进LLVM IR之前的高层IR里完成优化,要么把语义塞进intrinsic和metadata。时间一长,intrinsic数量爆炸,metadata被各路pass塞满,谁也说不清某个extension到底服务谁。

MLIR的解法是“多级IR”。你可以定义一套从高到低的IR栈,每个层级用一个方言承载:接近前端语义的linalg处理张量计算,affine表达带仿射约束的循环嵌套,scf表示结构化控制流,vector描述向量化后的操作,最后落到llvm方言再交给LLVM IR。Rationale章节反复强调:MLIR不是替代LLVM IR,而是在LLVM IR之上增加一层“可自定义的抽象”。翻译文档这一节的译文质量很关键,它把multi-level和multidimensional做了明确区分——前者指IR栈的纵向层次,后者指张量维度。很多读者只记住“多维”,忘了“多级”,后面理解Dialect之间的关系时就偏了。

文档里有一类示例代码能很好地说明多级的意义。下面这段是affine方言下的循环累加:

#map = affine_map<(i, j) -> (i, j)> func.func @add(%A: memref<64x64xf32>, %B: memref<64x64xf32>) { affine.for %i = 0 to 64 { affine.for %j = 0 to 64 { %a = affine.load %A[%i, %j] : memref<64x64xf32> %b = affine.load %B[%i, %j] : memref<64x64xf32> %sum = arith.addf %a, %b : f32 affine.store %sum, %A[%i, %j] : memref<64x64xf32> } } return }

affine.for表达的是“带仿射边界的循环”,affine.load/store里的数组下标是仿射表达式,循环结构和访存模式都是显式的。到LLVM IR里,这段逻辑会被展平成一堆基本块、跳转、load和store,循环边界与访问模式需要额外分析才能恢复。而在这里,后续的循环融合、分块、向量化直接基于结构做就行。多级的意义就在这句话里:信息在合适的层级表达,优化在能使用该信息的层级做。我最早从LLVM进入编译器团队,第一次看MLIR文档时满脑子都是“为什么要有这些奇怪概念”,把Rationale读完才明白,LLVM IR给前后端划了一条分界线,MLIR给的是从线到线之间可编程的填充空间。

2.2 方言、操作、属性、类型:四块基石怎么配合

方言(Dialect)是组织一切的基本单位。它本质上是一个命名空间,下面聚合操作、类型、属性和接口。翻译文档里高频出现的内建方言包括:builtin(提供module、func、基本属性类型)、arith(算术运算)、func(函数定义)、affine、linalg、scf、vector、llvm。每个方言解决的问题不同,但它们共享同一套Operation/Attribute/Type框架——这是“基础设施”这个词的由来。

操作(Operation)是执行计算的最小单元,比LLVM指令更通用。一个典型操作包含操作名、操作数、结果、区域和属性。区域(Region)是MLIR最特殊的设计:操作内部可以包含子区域,子区域里又是一串操作。scf.for的区域里可以放affine操作,linalg.generic的区域里可以放arith操作——这种嵌套性支撑了多级IR的“同一程序内多级共存”。

下面是一段TableGen定义操作的片段,翻译文档会保留原文语法并给出行内中文注释:

def AddFOp : Arith_Op<"addf"> { let summary = "浮点加法操作"; let arguments = (ins AnyFloat:$lhs, AnyFloat:$rhs); let results = (outs AnyFloat:$result); let hasFolder = 1; }

arguments声明两个浮点操作数,results声明一个浮点结果,hasFolder表示该操作参与常量折叠。TableGen是MLIR定义方言的声明式语言,看懂了arguments/results这两行,再看Pass里对操作匹配的代码就轻松。Dialect目录下的TD文件解释基本都沿这套结构展开,翻译版在这部分的注释质量直接决定了自定义方言的入门成本。

属性与类型容易被混为一谈,翻译文档的术语表给了明确对照:

属性(Attribute)职责类型(Type)职责
IntegerAttr编译期常量整数IntegerType整数值的位宽与符号性
FloatAttr编译期常量浮点FloatType浮点值的位宽与格式
ArrayAttr操作数/结果数组的静态描述VectorType向量形状与元素类型
DenseElementsAttr张量稠密数据值MemRefType内存缓冲描述

一句话区分:属性属于编译期的“元数据”,附加在操作上;类型属于值的分类,约束参与运算的每个值。一个操作可以有属性,它的每个操作数和结果都有自己的类型。“属性”不一定参与运行,“类型”直接决定寄存器分配、类型转换与合法性检查。读canonicalize和shape inference时,把DenseElementsAttr当成类型去理解张量,后面所有shape推断都会卡住——这是新手最常走偏的一步。

2.3 中文文档里这个概念网的阅读顺序

翻译项目把官方docs按学习路径重新排了序。我建议用“三遍读法”过概念网。

第一遍只读Rationale和Glossary。Rationale回答“为什么需要MLIR”,Glossary统一术语。这一遍结束时,你要能用一两句话回答“Dialect是什么”“多级是什么意思”。第二遍精读LangRef里的Dialect、Operation、Attribute、Type四个章节,对照上面表格看示例代码。第三遍带着具体问题反查方言文档——比如手上有个算子融合的活,就去linalg和affine的Dialect页面找Transformation章节。

官方Dialect页面很长,翻译版也对齐了这种长结构。不要从头到尾硬读,按“定义与语法→操作列表→转换与优化”三段扫,前两段快读,第三段精读。我刚接触MLIR时从头啃过一次Dialect目录,结果是满屏op名变成字典,一个也没记住。后来先明确“这个方言解决什么问题”,再回头看操作列表,效率完全不一样。这里还有一个容易内化的技巧:把概念网映射到“代码生成流水线”上——前端→高层方言→中低层方言→LLVM方言→LLVM IR,整条链路每一步都有自己的操作、属性和类型。你在文档里看到一个新API时,先问“它在流水线哪一层”,几乎百试百灵。

3. 从翻译文档搭学习路径:目录结构、术语表与三遍阅读法

3.1 翻译目录结构与官方docs的映射关系

这份翻译项目不是简单把英文文档逐页复制,而是按官方docs的目录骨架组织,方便与英文原版对照。核心映射如下:

官方docs路径翻译目录内容范围
Rationale设计动机MLIR为何存在、与LLVM的关系
LangRef语言参考Dialect/Operation/Attribute/Type定义
Tutorials/ToyToy教程从零实现一个方言和Pass
Dialects方言参考builtin、arith、affine、linalg、scf等
Pass InfrastructurePass基础设施Pass注册、分析、管理器、流水线
Codegen代码生成方言下降与LLVM方言对接

有这个映射表,英文原文更新到新版本时,你可以按对应目录去diff,而不是全量重读。我在实际用的时候,会在翻译版看到一段“官方原版新增了某接口说明”,直接切到官网路径核对接口签名。翻译版保留了与官方一致的章节目录层级,这一点比许多社区二手总结做得更好——二手总结往往只讲结论,丢了文档的论证链条。

拿资料后的第一步,我建议先花二十分钟把目录整体扫一遍。你会发现Tutorials部分占了较大比重,这其实是好事:MLIR官方把Toy语言教程当作最重要的入门材料,翻译版保留了这个结构,说明它没有变成偷工减料的“速读版”。扫目录时顺手标记与当前工作相关的章节,比如做GPU算子就标SPIR-V和GPU dialect,做CPU调度就标affine和scf,先把阅读边界圈出来。

3.2 术语表:Operation译“操作”还是“算子”

翻译文档里术语选词直接影响检索效率。Operation在MLIR社区有“操作”和“算子”两种译法,这份文档统一为“操作”,并且在Glossary里明确与“算子(Operator)”区分:MLIR的Operation是语法层概念,表示IR中的一个节点;“算子”在AI框架语境里更偏向数学语义。我认可这个取舍。如果你把Operation当成算子,读“模式重写”时会把“匹配操作结构”误解成“匹配计算语义”,差一整个层面。

其他几个关键术语的统一口径:

英文中文注释
Dialect方言保留直译,强调命名空间
Attribute属性编译期元数据
Type类型值分类约束
Rewrite Pattern重写模式匹配-替换规则
Lowering下降从高层抽象向低层抽象转换
Conversion转换带类型转换的下降过程

这些术语不是拍脑袋定的,基本沿着LLVM中文社区常用译法走,同时避开了与深度学习框架术语的冲突。比如operand译“操作数”、result译“结果”、region译“区域”,在LangRef里都是一一对照的。后续自己看源码注释或写技术博客时沿用这套术语,团队交流成本会低很多。

有个容易被忽略的点:翻译文档会在某些术语后面保留英文原词,比如“方言(Dialect)”。这不是冗余,而是官方文档正文里经常混用Dialect与dialect,大小写不同,含义就有区别——前者指具体的某个方言实例,后者指方言这一抽象机制。保留英文原词能避免这层歧义。反查官网时直接用英文词做关键词,比用中文高效得多。

3.3 三遍阅读法:精读、跳读与反查

第一步是目录通读。花二十分钟把翻译文档的一级和二级标题扫一遍,标记出与你工作相关的章节。我在做算子融合时,标记的是linalg、affine、vector、pattern rewrite,而把SPIR-V和GPU章节先跳过。先圈出边界,再往里填内容。

第二步是按需精读。把标记章节里的概念定义和示例代码完完整整读一遍,代码块里的每个操作名都查一遍含义。精读时不求记住全部API,只求理解“这一层在流水线的哪一段、解决什么问题”。这一步会花掉大部分时间,也是翻译文档价值最高的环节——官方原文的句子结构复杂,中文对照能帮你把“这句到底在说什么”从半小时压缩到五分钟。

第三步是反查。回到实际工程里,遇到报错信息或接口签名时,反向回到文档对应章节。举个例子:写了一个自定义pass,注册后跑mlir-opt,报错“operation name is not registered in dialect”。第一反应通常是检查注册代码。更快的方法是到LangRef里找“操作名与方言前缀”那一节:MLIR要求操作名必须包含方言前缀,格式是dialect.opname。报错里的opname大概率是少了前缀,或者TD文件里没加dialect声明。我按这个思路,一次就把问题定位到了TD里少写了MyDialect_前缀。这种反查路径在文档里是显式给出的,关键是你要记得“有这条规则”,而不是靠猜。

我一般会在第一遍通读结束后,把每个章节的标题复制到临时笔记里当成书签。之后每解决一个问题,就在对应标题下补一行“问题+答案”。一个月下来,那份笔记的价值比文档本身还高。

4. 转换与模式:从Rewrite Pattern到Pass管理器的完整路径

4.1 为什么MLIR把优化称为“模式重写”

MLIR的优化核心不是“遍历IR然后修改”,而是“定义匹配-替换规则”。一个Rewrite Pattern包含三部分:匹配条件、替换生成、收益判定。匹配条件描述“什么样的IR结构可以被重写”,替换生成描述“匹配成功后产出什么结构”,收益判定决定这个重写是否值得应用。翻译文档把这三部分对应到TD里的三个字段:根操作名、生成函数、benefit数值。

用一段TD伪代码说明:

def CombineAddMul : RewritePattern { let root = "arith.mulf"; let arguments = (ins AnyFloat:$a, AnyFloat:$b, AnyFloat:$c); let result = "mul(add(a, b), c)"; let benefit = 1; }

root声明匹配哪个操作,arguments描述操作数绑定,result描述替换后的结构,benefit是收益权重。实际实现会用C++写replace函数,但TD里这套声明式写法足够表达意图。参数意义:benefit数字越大优先级越高,多个pattern同时匹配同一种IR结构时,rewrite driver按benefit排序决定先应用哪个。

这种设计的优势在于可组合性。一个转换是几十个pattern的列表,你可以选择把某些pattern关掉、调权重、或者追加自定义pattern,而不需要动整个pass框架。文档里反复强调“pattern是重写的最小单位”,意思是它不限定在某个pass内部,也可以跨pass复用。MLIR把模式重写规范化成了可复用、可独立测试的组件,而且每个模式可以跨dialect匹配——这一点与LLVM里的instcombine有本质区别。instcombine也是一堆模式的集合,但它的模式绑定在特定pass内,MLIR的模式是独立实体。

4.2 一个完整的下降路径:linalg → affine/scf → vector → llvm

翻译文档的Codegen部分会展示这样一条下降链:linalg.generic表达的张量运算 → affine/scf表达的控制流与访存 → vector表达的数据并行 → llvm方言 → LLVM IR。

以linalg.generic为例,它用region描述“一个张量元素怎么算”,但没限定循环顺序。下降到scf.for之后,循环边界、步长、迭代顺序全部显式化。再往下,vector方言把标量操作组合成向量操作。最后llvm方言提供llvm.load、llvm.add等与LLVM IR一一对应的操作。每一步都是“匹配-替换”,但每一步的约束条件不一样。

常见误区是把“下降”理解成“重写”。下降确实用Rewrite Pattern实现,但下降的核心约束是:源方言和目标方言可能都不同,中间需要类型映射。比如linalg的tensor类型要变成memref,再变成llvm.ptr。所以MLIR把这类转换单独叫Conversion,它比普通pattern多了一套type converter机制。这套机制不是可选的——没有type converter,操作即使匹配上,也无法构造出目标类型的中间值。

翻译文档在“Conversion”章节里花了大篇幅讲类型转换器,这部分推荐精读,因为它的抽象层次介于“懂pattern”和“写pass”之间。很多自定义pass写不好,不是匹配逻辑有问题,而是类型转换没走对。文档里有句描述很准确:conversion的目标不是“把代码变成另一种写法”,而是“把程序换成一套新类型体系下的等价实现”。理解这句,再去看ConversionTarget里的合法性回调,思路会顺很多。

4.3 Pass管理器:Pipeline、分析与调度机制

Pass管理器负责调度pass的执行顺序。MLIR里每个pass在PassManager里按nest结构组织,nest的意思是一个pass可以作用在特定dialect的操作上,也可以作用于操作的region内部。比如pass的anchor op是func.func,那它对每个func都会执行一遍;如果anchor是scf.for,它就会递归到每个for循环体里去执行。同一个pass放在不同的anchor层级,实际执行范围和频率完全不同。

流水线(pipeline)是pass的线性列表,用mlir-opt命令行可以这样指定:

mlir-opt -linalg-to-affine -affine-to-scf -convert-scf-to-cf -convert-cf-to-llvm

这条命令把一条linalg程序逐级下降到LLVM方言。顺序不能乱:linalg必须先到affine,然后affine的循环结构才能转成scf,scf再转成cf控制流,最后落到llvm。翻译文档的Pass基础设施章节里,把“pass顺序与方言依赖”的关系单独列了表,标记安全依赖和冲突。实际工程里,顺序注入比pass编写更常成为问题来源——mirror pass顺序写反导致的“operation not found”比比皆是,而且报错点往往离真正问题点隔了好几个pass。

关于分析(Analysis),Pass管理器会缓存并复用分析结果。同一个函数被多个pass处理时,如果前一个pass的分析结果仍有效,就不会重复计算。翻译文档用“依赖树”形容分析与pass之间的关系:pass声明它需要哪些分析(如DominanceInfo、LoopInfo),管理器按声明去构建结果。自定义pass时漏声明依赖,运行时往往会看到未初始化的分析结果——现象诡异,原因简单。

还有一个实操经验:能把能合并的pass合成一个,就把能合并的合成一个,比如把一系列affine优化用pipeline串起来,比拆成单个pass省掉分析重建时间。至于具体收益多少,拿自己的IR实测,文档不会给通用答案。

5. 避坑:读MLIR中文翻译文档最容易踩的五个理解偏差

5.1 偏差一:把“多级中间表示”理解成“多种中间表示”

现象:读完第一章Rationale后,认为MLIR就是“把LLVM IR换成好几种IR”,觉得linalg、affine、scf各自是一套独立IR,彼此之间靠转换连接,类似“IR1→IR2→IR3”。

原因:multi-level强调同一套基础设施在不同抽象层级上的实例化。这些方言共享同一套Operation/Attribute/Type框架,只是操作集合和约束不同。翻译文档如果没有反复强调“框架统一、实例分层”,读者很容易滑向“多种IR”的直觉,进而把Dialect的转换想象成IR之间的翻译器。

解决:把方言看成同一坐标系上的不同高度,而不是不同坐标系。自检方式:手动画一条下降链,标出每一步“同一段逻辑”是怎么换了一层表示。能画出“同一个for循环在不同方言里的三种写法”就说明理解了。画不出来的话,回到LangRef的Operation章节重读一遍,别急着往下走。

5.2 偏差二:把“下降”和“转换”当成同义词

现象:看到lowering和conversion中文都译作“转换”,就认为conversion pass只是一般的pattern应用,不需要额外配置。随后自己在写一个跨方言pass时报出一堆类型不匹配的错误。

原因:conversion在MLIR语境里有特定含义,它通常伴随类型系统跃迁,比如tensor型变成memref型再变llvm.ptr。普通rewriting可以只匹配结构并替换,不需要处理类型体系变化。中文文档把conversion统一译成“转换”,如果没加限定,确实容易混。

解决:以后见到“转换”这个词时先问一句:“这个转换涉及类型系统变化吗?”涉及,去读Conversion章节;不涉及,去读Pattern Rewrite章节。这是笨办法,但能避免后续查代码时心态崩掉。

5.3 偏差三:翻译术语与官方英文对不上,反查时搜不到

现象:在翻译版里看到某个概念的解释后,回官网搜对应英文,搜不到。比如在中文目录里看到“操作折叠”一节,去官网页面找“operation folding”找不到,搜“op folding”才对。

原因:官方文档本身存在术语的“缩写混用”。标题里常用op缩写,正文里用operation全称,翻译版又对这两种情况分别译成“Op”和“操作”,于是同一个概念有两个中文形式,反过来检索英文时就容易对不上。

解决:读翻译版时,重要术语旁记一下英文原词。推荐直接拿英文标题做索引关键词,翻译只用来理解,不用于检索。我自己的习惯是在笔记里写“方言(Dialect)”,不用纯中文记概念,反查官网时直接Ctrl+F“Dialect”即中。

5.4 偏差四:把Pass管理器当成“顺序执行器”

现象:认为mlir-opt命令里的pass就是按顺序扫一遍,跟脚本一样简单。往pipeline里加两个pass,觉得“先跑A再跑B就行”,结果输出IR出现异常,或者某个pass报“operation not found”。

原因:忽略了pass的nest层级和分析依赖。nest决定pass作用在哪个操作层级,分析依赖决定pass运行前需要哪些已计算结果。顺序不只是执行先后,还受“操作在哪个方言、region嵌套多深”影响。同样的两个pass,anchor设置在func上和设置在scf.for上,行为可能不同。

解决:读Pass基础设施章节时把“anchor op”和“analysis”两个词圈出来。看到任何pipeline示例,先问:每个pass的anchor是什么?需要用哪些分析?这比背命令参数有用得多。

5.5 偏差五:只读翻译版,不看官方更新公告

现象:按翻译版说明写了个pass,跑mlir-opt时报错说找不到该pass名字。检查命令拼写没有错,文档里的名字也是这个,本地版本就是不认。

原因:MLIR迭代快,接口改名、pass改名在同一年内就会发生。翻译是快照性的,官方更新后,老的pass名字可能过期。尤其是一些带版本号的pass,比如convert-gpu-to-nvvm,在不同版本里参数格式都变过。

解决:把翻译版当作入门理解材料,写代码前用本地安装的MLIR版本确认pass名是否存在:

mlir-opt --help | grep "convert-scf-to-cf"

如果输出为空,说明本地MLIR版本里这个pass名已变。此时看翻译版标注的“对应官方源码版本”,按这个版本去筛本地MLIR,或者直接换用本地版本的pass名。版本不匹配时,先升文档认知,再动代码。

6. 验证方法:用Toy教程和mlir-opt自检学习效果

Toy教程是MLIR官方文档里最适合做验证的章节,翻译项目也完整覆盖了它。Toy本身是一门玩具语言,教程从定义AST、写parser开始,一路做到dialect、pass、codegen。把它当阅读理解题来做,比反复看Rationale有效得多。

我的自检路径是五步:第一步读Toy第1到第3章,确认能说出“AST怎么变成MLIR操作”。第二步读第4章,理解“为什么Toy的transpose需要专门的方言和操作”。第三步读第5章,亲手写一个fold pattern,把多余的reshape折叠掉。第四步读第6章,把自定义pass注册进mlir-opt。第五步读第7章,走一遍到LLVM的下降链。

动手验证时可以拿mlir-opt直接跑文档里的Toy示例:

mlir-opt toy.mlir -toy-combine

toy-combine是教程里注册的优化pass。跑通后加一个参数:

mlir-opt toy.mlir -toy-combine -mlir-print-ir-after-all

-mlir-print-ir-after-all会在每个pass执行后打印IR。这一步能直观看到IR怎么变、变化发生在哪个层级。我看IR时习惯做两个对照:对照翻译版的示例输出,确认自己的结果与文档一致;对照官网原版的输出,确认翻译版没有把示例IR写错。版本差异导致IR细节不一致时,以本地实际输出为准。

这套自检带给我最大的收获不是“会跑Toy”,而是建立了一个判断标准:任何知识点,如果不能在一个具体示例里指出“它出现在哪个操作、哪个pass、哪条命令的哪段输出”,就说明还没真正懂。

从那以后,我每读一章翻译文档,都强制自己在三天内找一个小例子跑一遍,要么改文档里的示例,要么把自己工程里的一个pass换种写法验证。这个过程翻过车,第5.5节说的pass名称过期我就踩过,但反而因此把本地MLIR版本与文档的对应关系摸清了。希望这套方法也能帮到你完整消化这份中文翻译资源,而不是只把它当成字典放着落灰。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询