拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

MLIR中文资料:从多级中间表示到Pass开发的学习路径

MLIR中文资料:从多级中间表示到Pass开发的学习路径 简介MLIR官方文档中文翻译项目为中文开发者提供了一套完整的MLIR学习资料覆盖多级中间表示的基本概念、方言、操作、属性、类型转换、模式、Pass管理器及代码生成等核心主题尤其适合编译器研究人员、学生及希望深入理解LLVM生态的读者。资源包共316个文件其中MD文档89个承载主要教程与概念讲解H头文件、CPP源码与MLIR描述文件提供实现示例和语法参考Toy示例演示典型转换流程TXT说明、SVG配图与配置文件辅助理解整体结构。压缩包大小仅1.05MB内容精炼但体系完整便于快速查阅。目前已有155人学习/下载属于较新的翻译整理类资源。读者可以直接阅读中文版MLIR教程了解方言设计与Pass管理机制并利用Toy示例框架动手实践从而系统地掌握MLIR的编译流程与代码生成方法提升编译器开发与优化能力。1. 这份中文 MLIR 资料到底能帮你省下多少时间做过编译器或深度学习算子开发的人大概率都有过这样的经历打开 MLIR 官方文档发现内容确实丰富但每篇都要在英文术语和抽象概念之间来回切换一个 Pass 的运作机制读上三遍还是觉得隔了一层。这个中文翻译项目就是把这些官方文档和教程统一翻译成中文按照 MLIR 的核心概念、多级中间表示、基础设施、方言、操作、属性、类型、转换模式、Pass 管理器到代码生成这条主线重新组织了学习路径。它的价值不在于“翻译完了”而在于把散落在官方文档各处的知识点按照一条从基础到落地的顺序串了起来。适合正在做 AI 编译器、算子优化、硬件后端接入的工程师也适合刚入门 MLIR 想建立整体认知的研究生。如果你只是偶尔看一眼 MLIR 的新闻这份资料对你来说可能过深但如果你需要在一个月内从零开始上手 Pass 编写或方言定义这份资料能明显缩短你“看不懂官方文档”的黑暗期。接下来我会从核心概念讲起再到怎么用这份资料落地学习最后给出踩坑记录。2. 先把 MLIR 的核心概念吃透多级中间表示、方言与操作定义的逻辑2.1 多级中间表示为什么我们不再满足于单一 IRMLIR 的全称是 Multi-Level Intermediate Representation直译就是“多级中间表示”。传统编译器通常只设计一种 IR比如 LLVM IR从前端到后端所有优化都在这一层完成。它的好处是统一坏处是把抽象层级不同的优化强行压在一个层级里要么信息不够用要么冗余信息太多。MLIR 的思路则是允许你在同一个编译框架里同时存在多个抽象层级的 IR从接近前端的高级表示一路降到接近硬件的低级表示。这个翻译项目对“多级”这个词的处理值得留意它把官方文档中关于“为什么需要多级”“每一级负责什么”的论述单独整理了篇幅而不是一笔带过。阅读时我建议你把重点放在第二节的 dialect 层级关系图上这张图解释了输入模型如何从 tensor 级别逐步下降到 memref 描述、再到硬件指令级描述。// 一个简单的高层 IR 示例tensor 级别的加法 func.func add(%arg0: tensor4x4xf32, %arg1: tensor4x4xf32) - tensor4x4xf32 { %0 arith.addf %arg0, %arg1 : tensor4x4xf32 return %0 : tensor4x4xf32 }这段 IR 看起来直观但它还是“高层”的因为它操作的是 tensor这是不带布局、内存空间等硬件信息的抽象类型。MLIR 允许这种 IR 存在的同时也允许同一函数体内部出现更底层的表示整个转换过程由 Conversion 和 Pass 驱动。理解“多级”的关键在于接受一个事实同一份代码可以存在多份不同抽象层级的 IR 表示它们之间通过转换模式衔接。2.2 方言DialectMLIR 可扩展性的根基方言是理解 MLIR 绕不开的第二个概念。一个方言就是一组操作、属性、类型等元素的集合类似于命名空间。不同方言描述不同抽象层级的语义例如 arith 方言描述算术运算linalg 方言描述结构化线性代数计算而 amdgpu 方言直接贴近 AMD GPU 指令。翻译项目中关于方言的章节会反复强调一个观点不需要把所有计算都压进同一个方言里而是让每个方言各管一段通过显式的转换过程串联。方言之间的转换是 MLIR 开发中最日常的工作也是这份中文文档里比较重的部分。它包含模式匹配和重写机制负责把一个方言的操作转换成另一个方言的操作。考虑一个实际的降级例子// 把 linalg 的 add 操作转换为 arith 和 memref 的等价描述 // 这个转换由模式Pattern驱动匹配 linalg.add 并使用 memref.load / arith.addf / memref.store 重写 func.func linalg_add(%arg0: memref4x4xf32, %arg1: memref4x4xf32) { linalg.add ins(%arg0, %arg1 : memref4x4xf32) outs(%arg1 : memref4x4xf32) return }这段代码中的 linalg.add 是一个结构化描述它说“把两个 memref 按元素相加”但没说按什么顺序遍历、是否向量化。当转换到 arith 和 memref.load/store 时循环结构才被显式展开。写转换模式时要注意条件匹配时要确认操作所在方言、操作数类型是否匹配目标方言的约束否则模式匹配会静默失败且不会报错只会留下未转换的操作。翻译文档在模式编写这节给出过一条建议先写测试再写模式实现因为模式匹配的错误通常在测试里才暴露。2.3 操作、属性、类型三者之间的关系MLIR 的核心围绕操作Operation、属性Attribute、类型Type三要素展开。操作是 IR 的基本单元类似 LLVM 中的指令但比指令更抽象属性是操作上携带的编译期常量信息例如数组形状、常量值类型描述操作数的数据种类和形状。你可以把类型和属性都理解为操作的“元数据”区别在于类型存在于类型系统内参与类型推导和匹配而属性不参与这些。这份中文文档在处理这三者关系时没有把它们拆成三个孤立章节而是放在同一个上下文里讲这对新手特别友好。实际写方言时你往往要同时定义它们先定义类型再为类型定义操作最后为操作挂上必要属性。下面是一个自定义方言的最小结构帮助你理解三者的协同关系// 自定义方言 mydsl 中定义一个操作mysdsl.quantize // 该操作带一个属性 scale表示量化缩放因子和一个输出类型 quant.f32 my_dsl.quantize %arg0 { scale 0.25 : f32 } : (tensor8xf32) - tensor8x!quant.f32属性写在花括号里类型写在冒号后方。操作匹配时属性影响语义但不影响类型推导而类型则参与操作数约束检查。若你的方言操作需要携带编译期常量信息用属性需要参与类型系统推导或约束匹配用类型。这条边界在很多自定义方言案例中被混淆是后续很多 bug 的种子后面避坑章我会单独展开。3. 按这份翻译项目组织起你的 MLIR 学习路径从基础到 Pass 管理器与代码生成3.1 面对庞大的翻译资源怎么读才不迷路很多读者拿到这类中文资料包后第一反应是打开文件列表想从第一个文件开始顺序读结果在翻译的表格和术语对照中消耗了大量精力两周后还在“核心概念”里打转。我的建议是反着来先用一小时把“核心概念”和“多级中间表示”两篇快速扫完然后直接跳到 Pass 管理器和代码生成相关章节的引言部分最后再回来精读方言和转换模式。之所以建议反着读是因为 MLIR 的学习有一个“先见森林再见树木”的规律只有先知道一个 Pass 是怎么被创建、加入流水线、执行、产生 IR 变更的全链路再回头看操作定义、属性约束、类型系统你才知道这些概念为何存在。翻译项目中 Pass 管理器的章节对 Pipeline 的执行顺序讲得比官方版本更顺因为它在中文语境里补充了每一步的“输入输出状态说明”这是官方文档默认读者已经知道的部分。3.2 一个 Pass 的一生从注册到执行到验证Pass 管理器章节是这份资料最核心的部分之一。MLIR 的 Pass 类似 LLVM 的 Pass但机制更灵活它允许你定义任意粒度的 IR 变换从单个操作的局部改写到整个函数或模块的重写。Pass 的执行由 PassManager 控制注册 Pass 后你需要把它添加到执行流水线Pipeline中MLIR 按照你给定的顺序依次执行每个 Pass。// 注册一个简单 Pass把输出信息打印出来 // 这个 Pass 继承 PassWrapper并实现 runOnOperation 接口 struct MyPrintPass : public PassWrapperMyPrintPass, OperationPassModuleOp { MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(MyPrintPass) void runOnOperation() final { getOperation().walk([](Operation *op) { llvm::errs() Found operation: op-getName() \n; }); } };上面的代码里runOnOperation 是 Pass 的核心入口getOperation() 返回当前 Pass 作用域的顶层操作。这里用 OperationPass 说明该 Pass 作用于整个模块。实际运行这个 Pass 还需要注册到 mlir-opt 工具中常见做法是在 main 函数里调用 registerPass然后在命令行用mlir-opt -my-print-pass input.mlir触发。参数含义很直接Pass 作用于 ModuleOpwalk 遍历该模块内所有嵌套操作并打印名称。对一个新手而言这段代码就是你进入 Pass 开发的第一扇门。3.3 代码生成与转换模式当 MLIR 走向硬件后端MLIR 的最终价值在于代码生成也就是把 IR 逐步下降到目标硬件可执行的指令序列。这个翻译项目把代码生成相关章节放在最后但它绝不是“附加内容”而是整个学习路径的终点。你需要理解的关键点是MLIR 的代码生成不是一步完成的而是多次部分转换的叠加。一个典型的降级路径是tosa 方言表示张量运算语义→ linalg 方言结构化循环计算→ affine 方言显式循环与访存→ memref 方言内存布局和引用→ LLVM 方言 → LLVM IR。每一步都由一组转换模式完成。转换模式RewritePattern就是“匹配 重写”的规则集合MLIR 会不断尝试匹配操作的子图匹配成功则执行重写直到没有新模式可匹配为止。// 一个把 mysdsl.quantize 简化为普通算术运算的转换模式 // 匹配条件操作属于 my_dsl 方言且名称是 quantize // 重写逻辑替换为该方言的 dequantize arithmetic 操作组合 struct QuantizeSimplify : public OpRewritePatternQuantizeOp { QuantizeSimplify(MLIRContext *ctx) : OpRewritePatternQuantizeOp(ctx) {} LogicalResult matchAndRewrite(QuantizeOp op, PatternRewriter rewriter) const override { auto input op.getInput(); if (!input.getType().isaQuantType()) return failure(); // 实际重写逻辑生成等价算术运算 rewriter.replaceOpWithNewOpDequantizeOp(op, input, op.getScale()); return success(); } };这段模式代码的关键在 matchAndRewritematch 部分通过操作类型的限定QuantizeOp和输入类型检查isa 完成rewrite 部分则把原操作替换为一个新的操作。返回 failure() 表示本次匹配失败框架会尝试其他模式返回 success() 则结束该操作的匹配。转换模式能写好前提是理解重写器PatternRewriter的方法集比如 replaceOp, replaceOpWithNewOp, create 等。翻译文档中关于这部分往往会标注每个方法的使用边界这是官方文档里语焉不详的部分。4. 用这份资料落地实操从零搭建本地实验环境并验证文档示例4.1 搭建最小可复现环境LLVM/MLIR 源码编译与配置看再多的文档不如上手跑一个 Pass。我建议你按照翻译项目里“环境搭建”部分的指引从源码编译 LLVM/MLIR 项目。不要直接下载预编译二进制包因为 MLIR 开发中经常需要修改 MLIR 自身源码或添加新的方言源码编译是后续开发不踩坑的基础。# 获取并编译 LLVM/MLIR以 LLVM 18 为例 # 这里只做一次后续增量编译时间通常在几分钟以内 git clone https://github.com/llvm/llvm-project.git cd llvm-project git checkout llvmorg-18.1.0 # 固定版本便于复现 cmake -S llvm -B build -G Ninja \ -DLLVM_ENABLE_PROJECTSmlir \ -DLLVM_TARGETS_TO_BUILDhost \ -DCMAKE_BUILD_TYPERelease \ -DLLVM_ENABLE_ASSERTIONSON ninja -C build check-mlir这段命令中的几个参数值得解释LLVM_ENABLE_PROJECTS 必须包含 mlir否则不会构建 MLIR 子项目LLVM_TARGETS_TO_BUILD 设为 host 只编译本机目标节省时间和磁盘LLVM_ENABLE_ASSERTIONS 建议开启MLIR 的很多 type 和操作约束检查依赖断言机制关闭后可能掩盖 IR 非法问题。check-mlir 会跑完整个 MLIR 的测试套件通常需要一小时左右但它能确认你的构建环境完整可用。如果你用的是 Apple Silicon 或 ARM 机器可能还要加 -DLLVM_TARGET_ARCH 相关参数否则测试会因架构不一致而失败。4.2 在本地跑通文档中的例子mlir-opt 与自定义 Pass 的联调环境搭建完成后先运行一遍文档示例确保你的前端输入和期望输出一致。假设翻译项目中有一个 Pass 实例章节演示如何把一个 arith.addf 操作替换为 mulf 操作实际语义不变只是为了演示重写机制。你需要做的是写一个 .mlir 输入文件注册自定义 Pass编译成 mlir-opt 插件然后执行转换观察输出。// test.mlir输入文件一个简单的 addf 操作 func.func add_scalar(%a: f32, %b: f32) - f32 { %0 arith.addf %a, %b : f32 return %0 : f32 }如果翻译文档里给出的 Pass 是 AddToMulPass那么它的作用就是把 addf 替换成 mulf。编译插件的方式是把你的 Pass 代码放在一个单独的目录里通过一个共享库加载到 mlir-opt 中# 编译并加载自定义 Pass 到 mlir-opt # 先把你写的 pass 编译为动态库 cmake --build build --target MyPass # 在 mlir-opt 中注册并执行 ./build/bin/mlir-opt -load ./build/lib/MyPass.so -my-add-to-mul test.mlir参数说明-load 参数指定要加载的插件库-my-add-to-mul 是你在 Pass 注册时命令行暴露的名称两者缺一不可。-load 不加不会报错但你的 Pass 不会被识别-my-add-to-mul 名称与注册代码不一致时mlir-opt 会提示未知参数。跑通这一步后你就可以在翻译文档的辅助下系统性地对每个 Pass 例子做修改、跑通、确认 IR 输出变化这个过程能极大巩固文档中关于 Pass 管理器与转换模式的描述。4.3 如何验证翻译文档中的示例与本地行为一致最有价值的学习方式是拿文档中的 IR 输入和输出与本地执行结果对拍。翻译项目中代码生成章节会展示从高层方言降级到 LLVM 方言的完整 IR 变化你可以在本地手动构造输入并执行对应的转换流水线。常见做法是使用 mlir-opt 的命令行组合把多个转换串成一条流水线# 把 tosa 方言降级到 linalg 方言再降级到 affine 方言 ./build/bin/mlir-opt -tosa-to-linalg -linalg-to-affine input.mlir output.mlir这条命令展示了 MLIR 的一个关键特性转换流水线是命令行可组合的。每个 -xxx 都对应一个 Pass它们按顺序执行前一个 Pass 的输出作为后一个 Pass 的输入。你可以把翻译文档中展示的中间 IR 与 output.mlir 里的 IR 做 diff凡是出现不一致的地方往往就是版本差异或者需要额外配置某个转换选项的地方。这是我验证文档内容最常用的方法也是排查自己写的 Pass 是否有预期行为的最直接手段。5. 避开这 5 个坑方言定义、Pass 执行顺序与类型匹配中的常见翻车现场5.1 Pass 执行顺序不对IR 转换结果莫名错误现象多个 Pass 串成流水线时单独跑每个 Pass 都能通过但组合起来结果错误甚至产出非法的 IR。原因MLIR 的 Pass 默认对操作的范围限定不同有些 Pass 作用于 ModuleOp有些作用于 FuncOp在错误的层级作用域修改 IR 会导致后续 Pass 匹配失败。翻译文档中有一个典型的错误案例一个作用域限定为 FuncOp 的转换模式被放到 ModuleOp 层级流水线里后walk 遍历不到预期的操作。解决打印Pass执行前后的IR差异用 mlir-opt 提供的-debug-onlydialect-conversion或-print-ir-after-all观察每个 Pass 执行后的 IR 快照确认每个 Pass 的作用域是否正确。我一般还会在开发初期把流水线拆成单条命令逐步执行而不是一次性写好完整 pipeline。5.2 模式匹配静默失败操作没被转换但也没有报错现象流水线跑完了输出结果和输入一样没有警告。原因RewritePattern 的 match 条件没有完全匹配可能因为操作类型约束不匹配或方言版本差异。框架不会为 match 失败打印日志所以这种现象极容易让人怀疑代码有问题但其实只是条件过严。解决在模式里加上调试打印或用-debug-onlypattern-rewrite开启模式重写日志观察哪些操作被匹配了、哪些没有。文档在这方面给了很多调试参数示例建议把-debug系列选项写在环境变量或脚本里随时能开启。5.3 属性与类型选择错误该用属性用了类型或者反过来现象自定义方言开发中试图往类型上加编译期常量信息结果类型推导冲突频繁IR 不断报错。原因把本该属于属性层面的信息放进了类型系统。类型参与统一化和约束匹配包含过具体的信息会导致两个同类操作因属性值不同被判定为不同类型。解决先明确这条信息的生命周期编译期常量且不影响类型推导的全部放进属性里只有在类型推导中必须参与的才放到类型中。一个常见做法是用 Attribute 存储量化 scale 值用 Type 存储量化后的数据类型形状。5.4 翻译文档和你当前版本不一致导致示例跑不通现象文档示例中的 Pass 名称在本地 mlir-opt 中提示 not found。原因MLIR 在不断演进很多 Pass 被重命名或拆分。文档翻译的是一个相对稳定的版本但你的本地构建可能是更新或更旧的版本。解决用mlir-opt --help查看当前版本所有可用 Pass 名称遇到名称不一致时搜索本地源码确认新名称。不要强行凑命令参数否则会浪费大量时间。我在实践中会把翻译文档对应的 LLVM 版本号标注在阅读笔记里作为参考基线。5.5 代码生成后运行时行为异常根源却在 IR 降级阶段现象一个算子最终生成的代码在硬件上跑结果不对但你检查了每个 Pass 的 IR 输出都“看起来是对的”。原因某个降级步骤里丢失了精度信息或溢出处理语义数值上的差异在 IR 文本里很难觉察。解决对代码生成结果使用逐层数值对比验证用同样的输入在原始高层 IR 和目标低层 IR 上运行对比输出值。这个建议在翻译项目的代码生成章节有强调IR 的合法性不代表数值等价性尤其是 float 运算重新关联后结果会有差异。不要跳过这一步直接上硬件验证。6. 验证学习成果的三个进阶技巧用 -print-ir-after-all 养成 IR 观察习惯到了这个阶段你不应该再逐字阅读文档而应进入“用文档当参考手册”的节奏。我自己的习惯是把-print-ir-after-all作为默认参数写进开发脚本每次跑流水线都输出每个 Pass 执行后的 IR 变化。这样你会逐渐形成对 MLIR Pass 行为的直觉哪些 Pass 会改动操作结构哪些只是修改附加信息哪些会触发额外的子转换。这种直觉无法从文档里直接获得只能通过反复观察 IR 快照积累。# 把 IR 快照输出保存到文件后续用 diff 比对两次修改的影响 ./build/bin/mlir-opt -print-ir-after-all -o /dev/null input.mlir ir_after.log这个命令里的-o /dev/null是必要的print-ir-after-all 会把快照打到 stderr而最终的 IR 输出到 stdout如果你不把 stdout 丢到 /dev/null日志文件里会混入最终 IR干扰 diff。整个文件会很大建议用-print-ir-after-only和-filter-dialect搭配使用只保留你关心的 Pass 和方言。最后一个技巧是用mlir-opt --help的分类输出对照文档。很多人在自定义 Pass 时找不到合适的选项其实 MLIR 文档已经按作用域、转换模式、方言支持、重写器选项做了分类。我建议你每周花一点时间浏览一遍 help 文本这个动作看似基础但能让你发现大量未曾注意的 Pass 和选项比漫无目的地翻阅文档效率高得多。我从接触 MLIR 到现在最大的教训是文档只解决“这是什么”真正解决“怎么用”的是你对 IR 变化的敏感度而这份中文翻译项目的价值恰好就是把你从语言和术语的障碍中拉出来集中精力去建立这种敏感度。希望帮到你。本文还有配套的精品资源点击获取
返回列表