第 2 课:Dialect、Type、Attribute 与接口

参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。

本课目标与完成标准

学完后你应能:区分 type 与 attribute,理解 dialect 隔离命名和 interface 带来的跨 dialect 泛化。

通过必须同时满足:

  • 独立补齐本课唯一的代码填空题,并通过给定检查;
  • 三个问答题均说明因果链,而不是只报术语;
  • 能指出至少一个正确性边界和一个性能取舍;
  • 总分不低于 8/10,且没有一票否决级概念错误。

前置关系

  • 课程前置:编译原理基础、C++ 阅读能力
  • 本课在路线中的作用:Dialect 是可组合 IR 词汇表;Type 描述 SSA 值的类别,Attribute 是编译期常量/配置,Interface 提供不依赖具体 op 名的能力契约。

核心心智模型

1. 它是什么,解决什么问题

在传统的单一中间表示(如 GCC GIMPLE 或 LLVM IR)中,所有的优化分析都被迫适配在同一套封闭固化的指令集和类型系统上,导致上层领域特定(Domain-specific)语义在过早降级中被永久抹杀。 MLIR 提出了模块化、可无限扩展的Dialect(方言)生态系统。每个 Dialect 是一组语义自洽、命名隔离的 IR 词汇表(如 arith 负责算术、linalg 负责线性代数、memref 负责显式物理内存、llvm 负责底层硬件映射)。 在 Dialect 内部,四大核心正交正规化概念支撑起整个语义大厦:

  • Type(类型):描述 SSA 变量的物理/抽象取值范围与数学类别(如 tensor<2x?xf32>, f32, i64)。
  • Attribute(属性):挂载在 Operation 上的编译期已知静态常量或元数据配置(如张量维度的静态排布、浮点常量数值、轴向索引 {axis = 1 : i64})。
  • Interface(接口):跨 Dialect 的通用行为契约(如 MemoryEffectOpInterface 描述副作用、InferTypeOpInterface 描述形状推导)。 本课解决的核心工程问题是:深刻辨析 Type 与 Attribute 的物理分界,理解 Dialect 命名隔离机制,以及如何利用 Interface 实现跨方言的解耦多态分析与优化。

2. 它如何工作

多方言协同与 OpInterface 动态多态分发流程如图 2-1 与图 2-2 所示:

flowchart TD
    subgraph Dialect["MLIRContext (方言生态)"]
        Builtin["builtin (module, func)"]
        Arith["arith (addf, muli, cmpf)"]
        Linalg["linalg (matmul, generic)"]
        MemRef["memref (alloc, load, store)"]
        LLVM["llvm (ptr, getelementptr)"]
    end
    Linalg -.->|抽象降级| MemRef
    MemRef -.->|指针展开| LLVM
    Arith -.->|指令映射| LLVM

图 2-1:MLIR 多方言协同拓扑与类型系统共存体系

flowchart LR
    Op["Operation 实例"] --> Query{"查询接口 dyn_cast&lt;OpInterface&gt;"}
    Query -->|命中| CallMethod["分发到特定方言 Trait / C++ 虚表实现"]
    Query -->|未实现| Fallback["返回 nullptr / 执行通用保底路径"]
    CallMethod --> Result["返回副作用分析 / 形状推导 / 内存别名结果"]

图 2-2:MLIR OpInterface 动态查询与多态解耦分发执行流程

  1. 方言前缀路由与语法解析: MLIR 源码以 dialect.op(如 arith.constant、linalg.matmul)为唯一标识。解析器 AsmParser 依据方言前缀路由到对应的 C++ 方言定义,构建 Operation 节点。

  2. Type 与 Attribute 的正交约束:

    • SSA 值(Value)严格绑定单一 Type,运行时数据在 SSA 管道中流转;
    • 属性(Attribute)直接嵌入 Operation 的存储字典中,无需任何 SSA 依赖。
    • 例如语句 %c = arith.constant dense<[1.0, 2.0]> : tensor<2xf32> 中,dense<[1.0, 2.0]> 是一个属性实例(DenseElementsAttr),作为常量的字面值载荷;而 tensor<2xf32> 是 Type,约束 %c 的静态形状与元素类型。
  3. 接口动态查询与多态分发(Interface Dispatch): 如图 2-2 所示,优化 Pass 在遍历 IR 时,绝不需要使用硬编码的 if (op->getName() == "xxx")。 Pass 通过 C++ RTTI 模式进行概念建模:if (auto memOp = dyn_cast<MemoryEffectOpInterface>(op))。 若该 Operation 实现了该接口,则直接调用接口方法查询其是否具有内存读写副作用,从而将 DCE(死代码消除)或 CSE(公共子表达式消除)算法与具体 Dialect 的具体 Op 完全解耦。

3. 正确性条件与常见误区

  • 运行时数据与编译期属性的混淆(Attribute vs SSA Value): Attribute 的值必须在编译期是完全已知且不可变的字面常量。若张量的维度或步长是由输入数据的运行时计算决定的(例如根据网络包大小动态决定的 batch_size),该数值必须以 SSA Value(index 或 i64 类型操作数)传递,绝对不能试图塞入编译期的 Attribute 中。

  • 动态维度标记(Dynamic Dimension ?): 在静态类型中,未知维度用 ? 表示(如 tensor<?x128xf32>)。动态维度的真实尺寸必须在运行时通过 tensor.dim 等操作动态获取为 SSA 值。

  • 未注册方言与语义空洞(Unregistered Dialect Trap): 若在 mlir-opt 中开启 -allow-unregistered-dialect,编译器允许解析任何任意前缀的未知 Op(如 foo.bar)。但此时该 Op 不具备任何 Verifier 校验规则、无法执行 Interface 查询,只能作为黑盒语法树节点存在。

4. 性能与工程取舍

  • 高层抽象类型保留 vs 过早降级(Lowering Timing): 保留高层类型(如 tensor<4x16xf32>)能够完整携带张量的高阶数学代数语义,使得切片融合、转置消除、分块 Tiling 等高层图优化极易进行; 若过早将 tensor 降级为带物理步长的 memref 或底层 llvm.ptr 指针,指针别名(Aliasing)与扁平物理跨步会使编译器无法再恢复高维几何形状,造成严重的优化阻断。

  • Interface 虚表分发开销: Interface 底层通过类似 C++ Concept 或轻量级虚表分发实现。虽然有极轻微的寻址开销,但相比于硬编码匹配,它换来了整个编译器架构极高的可维护性与正交扩展性。

具体演示

以 %c = arith.constant dense<[1.0, 2.0]> : tensor<2xf32> 为例:

  • arith:方言命名空间,避免与其它自定义方言的 constant 命名冲突。
  • dense<[1.0, 2.0]>:DenseElementsAttr 属性,在编译器内存中以紧凑字节数组高效存储常量数据。
  • tensor<2xf32>:RankedTensorType,静态声明该张量阶数为 1,长度为 2,元素为 32 位浮点数。
  • 若将返回值错误声明为 tensor<3xf32>,MLIR Verifier 在构建期会立即捕获类型与属性形状不匹配的断言,从编译前端杜绝非法张量。

请在阅读后先合上这一节,用自己的语言复述“输入状态 → 中间状态 → 输出状态”,再做练习。

实践任务:唯一代码填空题

补齐 tensor 常量的类型和 dense attribute。

规则:只能修改 TODO/______ 所在位置;不要删除断言或放宽误差。代码注释说明了每个边界条件。

Code
%%writefile /tmp/lesson02.mlir
module {
  func.func @constant() -> tensor<2xf32> {
    %c = arith.constant ______ : ______
    func.return %c : tensor<2xf32>
  }
}

检查方法

用 mlir-opt 解析;再把返回类型故意改为 tensor<3xf32>,确认 verifier 拒绝。

提交时请给出:补齐后的代码、实际运行输出(环境不可用时注明“仅静态审查”)以及对失败用例的解释。

Q1

不要背定义:请从输入、状态变化和输出三个阶段解释“Dialect、Type、Attribute 与接口”的工作机制。

你的答案:

Q2

为什么动态 shape 的实际数值不能只写成 attribute?

你的答案:

Q3

设计新 dialect 时,何时复用 builtin type,何时创建自定义 type?

你的答案:

评分与通过规则

  • 代码 4 分:正常输入 2 分,边界输入 1 分,解释实现 1 分;
  • Q1~Q3 各 2 分;
  • 一票否决:结果碰巧正确但核心因果链错误、删除边界检查、把未运行结果说成实测。

需要提示时按四级机制请求:概念区域 → 具体方向 → 关键局部 → 完整答案。

参考答案(仅 answer 分支)

先完成题目再核对。即使代码一致,也要能解释关键步骤,并尝试更换一个输入规模。

Code
%%writefile /tmp/lesson02.mlir
module {
  func.func @constant() -> tensor<2xf32> {
    %c = arith.constant dense<[1.0, 2.0]> : tensor<2xf32>
    func.return %c : tensor<2xf32>
  }
}

Q1 参考答案

从三个阶段拆解其运行机制:

  1. 输入阶段(方言命名空间解析与静态属性绑定):Parser 遇到以命名空间隔离的 Operation 字符串(如 arith.constant),将其路由至已在 MLIRContext 注册的方言模块。解析器解析挂载的编译期常量属性字典(如 dense<[1.0, 2.0]>),验证属性语法并存入 Operation 内部。

  2. 状态变化阶段(类型验证与接口多态判定):Operation 实例化后调用自身的 verify() 逻辑,严格验证操作数类型、属性类型与结果类型三者间的静态约束契约(例如检查 DenseElementsAttr 的元素个数是否严格等于目标 RankedTensorType 的形状容量)。在此期间,Pass 框架可通过 dyn_cast<OpInterface> 动态探测并调用该 Operation 实现的能力,完成类型推导或副作用标记。

  3. 输出阶段(SSA 强类型赋值与 Def-Use 节点注入):Operation 生成绑定了严格 Type(如 tensor<2xf32>)的全新 SSA 变量 %c。该 SSA 变量被正式注册进编译器的 Def-Use 依赖链条中,作为强类型操作数供给下游 Operation 安全使用。

Q2 参考答案

在编译原理与系统体系结构中,“编译期元数据(Attribute)”与“运行时数据(SSA Value)”具有本质不可逾越的边界:

  1. 执行时机的根本矛盾:Attribute 的生命周期是在编译器运行阶段(Compile-Time),其值必须由编译器在编译生成二进制机器码时就完全知晓并固化在指令或只读数据段中。而动态 Shape(如变长 Prompt 的序列长度、动态 Batch 大小)的真实数值只有当程序部署在真实硬件上被具体数据触发执行时(Runtime)才会诞生。

  2. 内存分配与参数依赖:如果动态 Shape 的具体大小是基于前序算子的计算结果(例如 torch.nonzero 或动态掩码过滤出的有效元素个数),该尺寸本身就是一个必须通过 GPU 寄存器或显存传递的 SSA Value。试图将运行时才能确定的值写成编译期静态 Attribute,在因果律上无法实现。因此在 MLIR 中,动态维度在 Type 中标注为 ?,而其具体的整数维度必须以 SSA Value 形式通过操作数动态传递。

Q3 参考答案

在构建新领域 Dialect 时,复用 Builtin Type 还是创建 Custom Type 的决策矩阵如下:

  1. 必须复用 Builtin Type(如 f32, i32, tensor, memref)的场景:
    • 当数据的底层物理语义、位宽或多维数组排布与标准体系完全兼容时;
    • 当希望无缝复用 MLIR 上游已有的海量标准 Pass(如 arith 基础代数优化、tensor-bufferization 内存分配、linalg 分块融合)时。如果自定义了私有 Tensor 类型,上游的所有标准化优化管道将全部失效。
  2. 必须创建自定义 Custom Type 的场景:
    • 领域特定抽象(Domain Invariants):当类型携带现有 Builtin 无法表达的核心不变式时(例如量化模型中的非均匀量化标尺类型、通信拓扑中的分布式通信器句柄 !mpi.comm、硬件专有的寄存器片段布局 !nvgpu.warp_matrix);
    • 语义隔离与类型安全:防止类型混用导致的静默错误,通过自定义 Type 强制要求调用方使用专用的转换算子显式转换,在编译前端拦截非法数据流。

参考资料

资料用于建立事实基线;面试回答仍需用自己的语言组织。