静态中间表示为分析提供了结构,但编译器的主要价值在于其转换代码的能力。在 MLIR 中,转换不是单一的(整体的)处理过程,而是模块化、细粒度的重写规则的集合。无论是在 linalg 方言中优化矩阵乘法,还是在 scf(结构化控制流)方言中将张量操作降级为显式循环,其机制都保持一致。两种主要的修改框架是贪婪模式重写驱动器(通常用于优化)和方言转换框架(用于抽象级别之间的降级)。模式匹配与重写方法Pattern 类是 MLIR 转换的核心。与传统编译器可能遍历基本块并手动处理指令不同,MLIR 将其抽象为精确定义的匹配-重写序列。这种方法将转换的定义与其应用策略分离。一个重写模式包含两个不同的阶段:匹配:基础设施检查一个操作,以判断其是否满足特定限制(例如,这是否是一个带常数权重的 linalg.matmul?)。重写:如果匹配成功,重写器会生成新操作并替换原始根操作。对于 C++ 工程师来说,这通过继承 OpRewritePattern<OpType> 来实现。难度不在于逻辑本身,而在于维护底层 IR 的完整性,例如使用-定义链和块参数。digraph G { rankdir=TB; node [shape=box, style=filled, fontname="Helvetica", fontsize=10, color=white]; edge [fontname="Helvetica", fontsize=9, color="#adb5bd"]; subgraph cluster_driver { label="贪婪重写驱动器"; style=dashed; color="#adb5bd"; fontcolor="#495057"; Start [label="从工作列表中取出操作", fillcolor="#e9ecef"]; Match [label="匹配模式?", shape=diamond, fillcolor="#a5d8ff"]; Rewrite [label="应用重写\n(创建新操作)", fillcolor="#b2f2bb"]; Update [label="更新工作列表\n(添加新操作)", fillcolor="#ffec99"]; Next [label="下一个操作", fillcolor="#e9ecef"]; Start -> Match; Match -> Rewrite [label="成功"]; Match -> Next [label="失败"]; Rewrite -> Update; Update -> Start; Next -> Start; } }贪婪模式重写驱动器中单个操作的生命周期涉及迭代匹配和工作列表管理,直至达到收敛。声明式重写规则 (DRR)尽管 C++ 提供了全面的控制,MLIR 鼓励使用 TableGen 以声明方式定义模式。这被称为声明式重写规则 (DRR)。DRR 允许编译器工程师简洁地表达源 DAG 和结果 DAG。TableGen 后端自动生成 C++ 样板代码,确保模式应用安全,并自动验证类型是否符合限制。例如,将转置操作折叠到卷积中纯粹涉及结构化匹配。在 C++ 中,这需要检查属性、操作数类型和结果使用者。在 DRR 中,它表示为直接映射:(transpose (conv $a, $b)) -> (conv_transposed $a, $b)。规范化与优化模式重写最常见的应用是规范化。这包括迭代优化,例如常量折叠、死代码消除和代数简化。MLIR 采用贪婪策略应用这些模式。驱动器重复应用模式,直到 IR 达到一个不动点,即不再有模式匹配。这种“收敛优化”策略很有效,但需要仔细设计模式。如果模式 A 将 $X \to Y$ 转换,而模式 B 将 $Y \to X$ 转换,编译器将进入无限循环。开发人员必须确保模式单调地降低 IR 的复杂程度或成本,或者将它们划分到不同的处理阶段。方言转换框架贪婪驱动器足以在同一抽象级别内进行优化,但它在降级方面存在不足,降级是将高级方言(如 tensor)转换为低级方言(如 memref 或 llvm)的过程。降级通常涉及类型变更(类型转换),并确保生成的 IR 对特定目标是严格合法的。方言转换框架通过引入转换目标的理念来解决此问题。该目标定义了什么是“合法”的。定义合法性合法性并非二元的;它是细粒度的。一个操作可以是:合法:该操作受目标支持,应予保留。非法:该操作必须被转换。如果在处理阶段后仍存在,编译将失败。动态:该操作仅在满足特定条件时才合法(例如,卷积仅在步幅为 1 时合法)。该框架遍历 IR。当它遇到非法操作时,它会寻找一个已注册的 ConversionPattern,该模式能将其降级为一系列合法操作。digraph Lowering { rankdir=LR; node [shape=box, style=filled, fontname="Helvetica", fontsize=10, color=white]; edge [fontname="Helvetica", fontsize=9, color="#868e96"]; Input [label="输入 IR\n(例如张量方言)", fillcolor="#ced4da"]; subgraph cluster_process { label="转换过程"; style=filled; color="#f8f9fa"; Check [label="检查合法性", shape=diamond, fillcolor="#ffc9c9"]; Pattern [label="应用转换模式", fillcolor="#a5d8ff"]; TypeConv [label="类型转换器\n(张量 -> 内存引用)", fillcolor="#b197fc"]; Materialize [label="具体化类型转换", fillcolor="#ffe066"]; } Output [label="目标 IR\n(例如 LLVM 方言)", fillcolor="#69db7c"]; Input -> Check; Check -> Output [label="合法"]; Check -> Pattern [label="非法"]; Pattern -> TypeConv [label="请求类型更新"]; TypeConv -> Pattern [label="新类型"]; Pattern -> Materialize [label="连接类型"]; Materialize -> Check [label="重新验证"]; }合法性检查与类型转换之间的关联推动了降级过程。当连接不同的类型系统时,框架会自动插入类型转换操作。类型转换与具体化降级的一个核心难题是操作数经常变更类型。当将张量上的 linalg 降级为缓冲区上的 linalg 时,tensor<4x4xf32> 会变为 memref<4x4xf32>。如果一个操作已转换但其使用者尚未转换,IR 将变为无效,因为生产者生成的是 memref 而消费者预期的是 tensor。方言转换框架通过允许用户定义源和目标具体化来处理此问题。该框架会暂时插入“类型转换”操作(未实现的转换类型转换)以连接新旧类型系统。一旦转换完成,如果所有类型转换相互抵消,它们就会被移除。如果类型转换仍存在,则认为该转换是部分或不完整的。实现一个降级处理过程要实现一个降级处理过程,例如将自定义 MyDialect 转换为标准 LLVM 方言,您必须协调上述讨论的各个组成部分。类型转换器:定义您的高级类型如何映射到 LLVM 类型。例如,将自定义 ComplexType 映射到由两个浮点数组成的结构体 !llvm.struct<(f32, f32)>。转换目标:明确将 MyDialect 操作标记为非法,并将 LLVM 方言操作标记为合法。模式:实现 ConversionPattern 类。在 matchAndRewrite 方法中,您使用 TypeConverter 获取操作数预期的 LLVM 类型,创建新的 LLVM 指令,并替换根操作。设想一个场景,我们将作用于张量的高级 AddOp 降级为循环嵌套。该模式并非仅仅用一个操作替换另一个操作;它生成一个 scf.for 循环结构。在循环体内部,它插入标量加法。转换框架提供的重写器会跟踪这些插入,确保如果整体转换失败(可能是因为缺少了对其他操作的模式),所有这些变更都会原子性地回滚。模式粒度与合并MLIR 中的一个主要架构决策是偏爱许多小型、可组合的模式,而非大型、复杂的模式。在早期的编译器中,“合并处理过程”可能是一个 5000 行的逻辑块,处理卷积、ReLU 和 BatchNorm 的所有排列组合。在 MLIR 中,这被分解为:一个合并 Conv + Bias 的模式。一个合并 Conv + ReLU 的模式。一个合并 Conv + BatchNorm 的模式。贪婪驱动器会择机应用这些模式。如果出现 Conv + Bias + ReLU,第一个模式会运行,生成 ConvBias。接着,第二个模式识别出 ConvBias(它实现了与 Conv 相同的接口)并合并 ReLU。这种可组合性减轻了维护负担,并允许新操作符通过实现正确的接口或特性来得益于现有优化。通过熟练掌握模式重写和转换框架,您将获得对整个编译流程的掌控,能够将抽象的数学模型转换为为特定硬件加速器量身定制的高度优化机器代码。