flowchart TD
subgraph ODS["TableGen 定义 (.td 文件)"]
Def["def My_MatmulOp : My_Op<'matmul', [Pure, SameOperandsAndResultType]>"]
Args["arguments = (ins AnyTensor:$lhs, AnyTensor:$rhs)"]
Res["results = (outs AnyTensor:$output)"]
Asm["assemblyFormat = '$lhs `,` $rhs attr-dict `:` type($output)'"]
end
ODS --> GenCode["mlir-tblgen 驱动代码生成器"]
第 3 课:ODS/TableGen 与自定义 Operation
参考答案版:包含完整代码实现与问答题深度参考解析。建议先独立完成练习题后再展开核对。
本课目标与完成标准
学完后你应能:用 ODS 声明 op 的 operands/results/traits,并解释生成代码与手写语义钩子的边界。
通过必须同时满足:
- 独立补齐本课唯一的代码填空题,并通过给定检查;
- 三个问答题均说明因果链,而不是只报术语;
- 能指出至少一个正确性边界和一个性能取舍;
- 总分不低于 8/10,且没有一票否决级概念错误。
前置关系
- 课程前置:编译原理基础、C++ 阅读能力
- 本课在路线中的作用:ODS 用 TableGen 描述 dialect/op 的结构约束并生成 C++ 声明、parser/printer 框架和文档,减少重复样板。
核心心智模型
1. 它是什么,解决什么问题
在 C++ 中手动编写一个工业级 MLIR Operation 需要耗费数以百行的样板代码(Boilerplate):手写类定义、构造函数、访问器(Accessors)、Operand/Result 数量校验、Parser/Printer 文本解析序列化、类型检查 Verifier 以及文档。这极其冗长且极易出错。 为了消除样板代码并实现单一事实来源(Single Source of Truth),MLIR 提供了基于 LLVM TableGen 语言的领域特定元编程框架——ODS(Operation Definition Specification)。 开发者在声明式的 .td 文件中定义算子的输入、输出、属性、Traits 与文本语法格式,由专用代码生成工具 mlir-tblgen 在编译期全自动生成强类型的 C++ 声明与实现。 本课解决的核心工程问题是:掌握基于 ODS 的声明式算子建模生命周期,理解 Trait 的物理语义承诺,以及划分编译期代码生成(ODS)与手动 C++ 语义钩子(Verifier / Fold / Canonicalize)的清晰职责边界。
2. 它如何工作
ODS 声明规范与全自动构建生命周期如图 3-1 与图 3-2 所示:
图 3-1:ODS 声明式算子规范 (Operation Definition Specification) 元模型
flowchart LR
TD["算子定义规范 (*.td)"] -->|mlir-tblgen -gen-op-decls| IncH["算子头文件声明 (*.h.inc)"]
TD -->|mlir-tblgen -gen-op-defs| IncCpp["算子实现代码 (*.cpp.inc)"]
IncH & IncCpp --> CxxCompile["C++ 编译期集成"]
CxxCompile --> DialectReg["MLIRContext 注册并供编译器调用"]
图 3-2:基于 mlir-tblgen 的自定义算子全自动构建与接入生命周期
- 元模型定义(
.td描述): 在 TableGen 脚本中,继承基础模板定义算子:arguments:使用(ins ...)声明输入操作数与属性;results:使用(outs ...)声明返回值;traits:挂载编译期语义特征(如Pure表示无副作用、Commutative表示可交换、SameOperandsAndResultType表示输入输出类型同构);assemblyFormat:声明声明式自定义打印与解析规则,免写手写 Parser。
- 代码生成(
mlir-tblgen驱动): 如图 3-2 所示,在项目 CMake 构建阶段:- 运行
mlir-tblgen -gen-op-decls生成*.h.inc(包含强类型 C++ 包装类、getLhs()/getRhs()访问器、Trait 继承声明); - 运行
mlir-tblgen -gen-op-defs生成*.cpp.inc(包含操作数偏移行内解析、自动类型校验骨架、build()工厂方法与 Parser/Printer 逻辑)。
- 运行
- C++ 语义钩子接入: 对于超越声明式表达能力的复杂业务逻辑(如多维张量沿某一轴求和的形状推导、常量折叠),开发者在自定义 C++ 文件中实现声明的语义钩子函数:
verify():执行复杂的深层跨字段数值校验;fold():执行编译期常量计算(Constant Folding);getCanonicalizationPatterns():注册代数化简等价重写规则。
3. 正确性条件与常见误区
Trait 是严肃的语义承诺,而非非绑定的注释: Trait(如
Pure、Commutative、SameOperandsAndResultType)会直接被下游优化 Pass(如 DCE、CSE、规范化重写)作为无条件成立的前提公理使用。若给实际向显存写入数据或改变全局状态的 Op(如memref.store)错误标记了Pure,下游 DCE Pass 会在发现其返回值未被使用时直接将其整条物理消除,导致严重的状态静默损坏。类型系统与 ODS Constraint 匹配: 若在
arguments声明了AnyTensor:$lhs``,但未在 Trait 中约束类型相等,则 `$lhs可以是tensor<4xf32>而 ``$rhs`` 可以是tensor<8xi32>。若要确保逐元素相加合法,必须同时施加SameOperandsAndResultType或显式的AllTypesMatch` 约束。生成头文件与手写代码的循环依赖: 包含生成的
*.h.inc时必须置于正确的命名空间内,否则会导致编译期类名未定名错误。
4. 性能与工程取舍
声明式约束 vs 手写 C++ Verifier: 能用 ODS 声明式表达的约束(如
SameTypeOperands、TypeConstraint)尽量在.td中声明。ODS 能自动生成直观易读的报错提示,并能自动导出至 Markdown 语法手册;过度依赖手写 C++verify()会降低算子文档的自解释性。自动 AssemblyFormat vs 手写 Parser: 声明式
assemblyFormat覆盖了 95% 的自定义语法格式,并能自动保证 Parser 与 Printer 的完全对称可逆(Round-trip Safety);仅当涉及极特殊的复杂动态语法解析时才需要开启let hasCustomAssemblyFormat = 1;。
具体演示
以二元加法算子 My_AddOp 为例:
def My_AddOp : My_Op<"add", [Pure, SameOperandsAndResultType]> {
let summary = "elementwise add";
let arguments = (ins AnyTensor:$lhs, AnyTensor:$rhs);
let results = (outs AnyTensor:$result);
}
arguments指定必须输入两个张量$lhs与$rhs;results指定输出张量$result;[Pure]承诺无任何内存副作用,供 DCE 安全消除;[SameOperandsAndResultType]自动生成校验代码,若用户传入%a: tensor<2xf32>与%b: tensor<3xf32>,Verifier 在解析阶段会直接报错中断。
请在阅读后先合上这一节,用自己的语言复述“输入状态 → 中间状态 → 输出状态”,再做练习。
实践任务:唯一代码填空题
补齐 ODS 中两个输入和一个输出。
规则:只能修改 TODO/______ 所在位置;不要删除断言或放宽误差。代码注释说明了每个边界条件。
Code
%%writefile /tmp/MyOps.td
def My_AddOp : My_Op<"add", [Pure, SameOperandsAndResultType]> {
let summary = "elementwise add";
let arguments = (ins AnyTensor:$lhs, ______:$rhs);
let results = (outs ______:$result);
}检查方法
有源码构建环境时交给 mlir-tblgen;否则检查约束是否足以拒绝标量和不同类型 operands。
提交时请给出:补齐后的代码、实际运行输出(环境不可用时注明“仅静态审查”)以及对失败用例的解释。
Q1
不要背定义:请从输入、状态变化和输出三个阶段解释“ODS/TableGen 与自定义 Operation”的工作机制。
你的答案:
Q2
错误地给有副作用 op 标 Pure,DCE 可能做出什么错误?
你的答案:
Q3
shape 相等这种跨 operand 约束应写 trait、ODS constraint 还是 verifier?
你的答案:
评分与通过规则
- 代码 4 分:正常输入 2 分,边界输入 1 分,解释实现 1 分;
- Q1~Q3 各 2 分;
- 一票否决:结果碰巧正确但核心因果链错误、删除边界检查、把未运行结果说成实测。
需要提示时按四级机制请求:概念区域 → 具体方向 → 关键局部 → 完整答案。