表驱动定义:ODS 与声明式编程
前面五篇建立了 MLIR 的概念框架和数据结构理解。这一篇进入工程实践的第一个核心——如何定义一个方言和它的 Op。
答案是 ODS(Operation Definition Specification)+
TableGen。它的核心思想是:用声明式语言描述 Op
的接口和行为,让工具自动生成 C++ 样板代码。对比 MLIR Toy
教程 中手写 C++ 与 ODS
生成的代码,构建器、访问器、解析/打印器等样板逻辑可由
TableGen 自动产出,方言作者主要编写语义相关的
verify() 和 fold()。
一、TableGen 是什么
TableGen 是 LLVM 项目中的一个声明式 DSL——专为生成结构化 C++ 代码而设计。它最早用于描述 LLVM 的目标指令集(如 x86 的数千条指令),后来被 MLIR 用于描述方言和 Op。
TableGen 的基本概念:
- 记录(Record):TableGen 的基本单位,类似一个具有命名字段的 struct。每个 Op 定义最终会编译成一个 Record。
- 类(Class):Record 的模板,类似 C++ 的类——定义字段名和默认值,Records 可以继承 Class 并覆盖字段。
- 多类(Multiclass):参数化的 Record 生成器,一次定义生成多个 Record。用于”相同结构、不同参数”的 Op 族(如所有二元算术运算)。
TableGen 源文件以 .td 为后缀。LLVM 的
llvm-tblgen 和 MLIR 的 mlir-tblgen
分别解析 .td 文件并生成 .inc
文件(需被 C++ 代码
#include),或直接生成完整的 .cpp
文件。
二、ODS 的层级结构
定义一个方言需要三层 .td 定义:
Dialect.td // 方言定义
└── DialectOps.td // Op 定义(引用 Dialect.td)
└── DialectTypes.td // Type 定义(可选,引用 Dialect.td)
2.1 方言定义
// include/MyDialect/IR/MyDialect.td
#ifndef MY_DIALECT_TD
#define MY_DIALECT_TD
include "mlir/IR/OpBase.td"
def MyDialect : Dialect {
let name = "my";
let summary = "A custom dialect for learning MLIR";
let description = [{
MyDialect is a minimal example dialect that demonstrates
how to define a new dialect using ODS and TableGen.
}];
let cppNamespace = "::mlir::my";
}
#endif
Dialect 是 MLIR 预定义的 TableGen
基类。关键字段:
| 字段 | 含义 |
|---|---|
name |
方言名称,出现在 .mlir
文件中的操作名前缀(my.add) |
summary |
单行摘要 |
description |
多行说明([{...}] 是 TableGen
的多行字符串语法) |
cppNamespace |
C++ 命名空间,生成的代码放在此命名空间下 |
2.2 Op 定义
// include/MyDialect/IR/MyDialectOps.td
#ifndef MY_DIALECT_OPS_TD
#define MY_DIALECT_OPS_TD
include "MyDialect.td"
include "mlir/Interfaces/SideEffectInterfaces.td"
// 定义类型约束(可复用的 predicate)
def I32 : Type<CPred<"$_self.isInteger(32)">, "32-bit integer">;
def F32OrF64 : Type<Or<[F32.predicate, F64.predicate]>, "f32 or f64">;
// 二元加法 Op
def My_AddOp : MyDialect_Op<"add", [Pure, SameOperandsAndResultType]> {
let summary = "integer or float addition";
let description = [{
The `my.add` operation performs element-wise addition of two
scalar values. Both operands must be of the same type.
}];
let arguments = (ins
SignlessIntegerOrFloatLike:$lhs,
SignlessIntegerOrFloatLike:$rhs
);
let results = (outs
SignlessIntegerOrFloatLike:$result
);
let assemblyFormat = "$lhs `,` $rhs attr-dict `:` type($result)";
let hasFolder = 1; // 支持常量折叠
let hasVerifier = 1; // 支持额外验证逻辑
}
#endif
这段定义生成的 C++ 代码包括:
- 类定义:
mlir::my::AddOp,继承自Op<AddOp, Pure, SameOperandsAndResultType>。 - 构建器(Builder):自动生成的
build()方法,接受Value和Type参数。 - 访问器(Accessor):
getLhs()、getRhs()、getResult()方法。 - 验证器(Verifier)骨架:如果
hasVerifier = 1,生成static LogicalResult verify()声明。 - Parser/Printer:根据
assemblyFormat自动生成.mlir的解析和打印逻辑。 - 折叠接口:如果
hasFolder = 1,生成OpFoldResult fold()声明。
三、arguments、results
与 assemblyFormat 详解
3.1 参数和结果的描述 DSL
(ins ...) 和 (outs ...) 是 MLIR
TableGen 的专用 DSL(domain-specific language within a
DSL)——不是通用的 TableGen 语法。
let arguments = (ins
TypeConstraint:$operandName, // 必选操作数
Optional<TypeConstraint>:$optionalOperand, // 可选操作数
Variadic<TypeConstraint>:$variadicOperands, // 变长操作数(0 或多个)
DefaultValuedAttr<StrAttr, "\"default\"">:$attr, // 带默认值的属性
ConfinedAttr<I64Attr, [IntMinValue<0>]>:$attr2 // 带约束的属性
);
操作数绑定到
Value——它们是运行时数据流。属性绑定到
Attribute——它们是编译期元数据。ODS 在
(ins ...) 中不区分操作数和属性:两者都定义为
arguments,ODS 根据类型约束判断是 operands(类型约束来自
Type)还是 attributes(类型约束来自 Attr)。
3.2 常用类型约束
| 约束 | 含义 |
|---|---|
AnyType |
任意类型 |
AnyInteger |
任意整数(i1、i32、i64等) |
AnyFloat |
任意浮点(f16、f32、f64等) |
SignlessIntegerLike |
无符号区分整数 |
F32 |
仅 f32 |
I64 |
仅 i64 |
AnyTensor |
任意 tensor |
AnyMemRef |
任意 memref |
Variadic<...> |
0 或多个,编译为 ValueRange |
Optional<...> |
0 或 1 个,编译为 Value(缺失时为
null) |
自定义类型约束可以用 predicate 组合:
def F32OrF64 : Type<Or<[F32.predicate, F64.predicate]>, "f32 or f64">;
def NonNegativeI64 : ConfinedAttr<I64Attr, [IntMinValue<0>]>;
3.3 assemblyFormat:声明式打印/解析
assemblyFormat 是最容易被低估的 ODS
特性——它用一行字符串自动生成完整的 Parser 和 Printer。
let assemblyFormat = "$lhs `,` $rhs attr-dict `:` type($result)";
这会生成的 parser 能解析:
%0 = my.add %a, %b : i32
语法元素:
$operandName:按定义顺序的操作数占位符。`literal`:字面量 token(逗号、冒号、括号等)。attr-dict:属性的自动字典打印(默认出现在 Op 末尾)。attr-dict-with-keyword:带attributes关键字的属性字典。type($result):打印第$result操作数的类型。functional-type($inputs, $results):打印函数式类型签名((i32) -> i32)。
在 90% 的场景中,assemblyFormat
的声明式写法就够了。只有当 Op 的语法非常特殊时才需要手写
parse() 和 print()。
四、Traits 与 Interfaces
Traits 和 Interfaces 都提供跨 Op 的通用行为,但机制不同:
- Trait:编译期混入(mixin),在 Op 的 C++ 类继承链上添加行为。不涉及虚函数调用。
- Interface:运行时多态,通过
OpInterface定义虚方法表。允许跨方言、不依赖具体 Op 类型地调用能力。
4.1 常用内建 Traits
def My_Op : MyDialect_Op<"op", [
Pure, // 无副作用(可消除死代码)
SameOperandsAndResultType, // 输入输出类型相同
SameOperandsAndResultShape, // 输入输出形状相同(对 tensor)
Commutative, // 操作数的顺序可交换
NoMemoryEffect, // 不读不写内存(更强于 Pure)
Elementwise, // 逐元素操作(便于向量化)
IsTerminator // 是 Block 的结束 Op
]> { ... }
4.2 自定义 Interface
// MyDialectInterfaces.td
def MyCustomInterface : OpInterface<"MyCustomInterface"> {
let description = [{ Interface for operations that support custom behavior. }];
let methods = [
InterfaceMethod<
"Get the custom value for this op",
"int64_t", "getCustomValue"
>,
];
}
在 Op 中实现:
def My_Op : MyDialect_Op<"op", [MyCustomInterface]> { ... }
// MyDialectOps.cpp
int64_t MyOp::getCustomValue() {
// 具体实现
return 42;
}4.3 Verifier:自定义验证逻辑
hasVerifier = 1 生成验证器声明。Op 作者实现
LogicalResult verify() 方法:
LogicalResult MyOp::verify() {
// 检查操作数约束
if (getLhs().getType() != getRhs().getType())
return emitOpError("operand types must match");
// 使用 emitOpError() 报告诊断
return success();
}ODS 自动生成的验证器在调用 verify()
之前已经检查了:操作数数量、结果数量、类型约束(TypeConstraint)。verify()
只处理声明式约束无法表达的语义约束。
4.4 Canonicalization:声明式折叠
hasCanonicalizer = 1 告诉 ODS
此类型需要规范化支持。用于声明静态的
getCanonicalizationPatterns() 方法:
void MyOp::getCanonicalizationPatterns(RewritePatternSet &patterns,
MLIRContext *context) {
patterns.add<SimplifyRedundantAdd>(context);
patterns.add<FoldAddWithZero>(context);
}每个 Pattern 是一个 RewritePattern
子类,实现
matchAndRewrite——这是下一章(模式重写)的主题。
五、从 .td 到 C++:mlir-tblgen 的生成流程
TableGen 生成的是 .inc 文件(C++
片段),通过 #include 嵌入到 .cpp
中:
# 生成方言和 Op
mlir-tblgen -gen-dialect-decls MyDialect.td -I$INCLUDE_PATH -o MyDialect.h.inc
mlir-tblgen -gen-dialect-defs MyDialect.td -I$INCLUDE_PATH -o MyDialect.cpp.inc
mlir-tblgen -gen-op-decls MyDialectOps.td -I$INCLUDE_PATH -o MyDialectOps.h.inc
mlir-tblgen -gen-op-defs MyDialectOps.td -I$INCLUDE_PATH -o MyDialectOps.cpp.inc
# 生成 Type 和 Attribute
mlir-tblgen -gen-typedef-decls MyDialectTypes.td -I$INCLUDE_PATH -o MyDialectTypes.h.inc
mlir-tblgen -gen-typedef-defs MyDialectTypes.td -I$INCLUDE_PATH -o MyDialectTypes.cpp.incC++ 文件中 #include 这些生成文件:
// MyDialect.h
#include "mlir/IR/Dialect.h"
namespace mlir { namespace my { class MyDialect; } }
#define GET_DIALECT_CLASSES
#include "MyDialect.h.inc"
// MyDialectOps.h
#define GET_OP_CLASSES
#include "MyDialectOps.h.inc"通常这些生成步骤放在 CMake 的
mlir_tablegen() 宏中:
# CMakeLists.txt
set(LLVM_TARGET_DEFINITIONS MyDialectOps.td)
mlir_tablegen(MyDialectOps.h.inc -gen-op-decls)
mlir_tablegen(MyDialectOps.cpp.inc -gen-op-defs)
add_public_tablegen_target(MLIRMyDialectOpsIncGen)CMake 负责自动检测 .td 文件的变化并重新生成
.inc 文件。
六、何时用 TableGen,何时手写 C++
TableGen 的收益在 Op 数量多的时候最明显——一个方言有 20-50 个 Op,手写每个 Op 的构建器、验证器、解析/打印逻辑是纯粹的重复劳动。
但有些场景手写 C++ 更合理:
- Op 非常少(1-2 个)。TableGen 的初始设置成本(CMake 集成、include 守卫、.inc 路径配置)比手写更贵。
- Op
的语义极其复杂、表示不规则。
assemblyFormat覆盖不了的语法,TableGen 的 ROI 急剧下降。 - 需要深度定制
build()逻辑。自动生成的构建器按参数顺序构造,但如果构建逻辑涉及类型推导或非平凡属性计算,需要手写额外的build()。 - 原型和实验。在探索方言语义的阶段,手写 C++ 的迭代速度远比配置 TableGen 快。
MLIR 对此的哲学是”渐进采用”:实验阶段手写
C++,方言稳定后将重复的样板代码转为
ODS。scf(结构化控制流)方言就混合了手写和
TableGen。
七、本篇后续
ODS 解决了”定义 Op”的效率问题。完整可构建工程请继续阅读 MLIR Toy 教程。下一篇讲 Region 和 Block 在控制流表示中的角色——这是 MLIR 与扁平 IR 最关键的差异之一,也是自定义方言可以表示的抽象层级范围所在。
参考资料
官方文档(A 级)
- MLIR Operation Definition Specification (ODS) — https://mlir.llvm.org/docs/DefiningDialects/Operations/
- MLIR TableGen Overview — https://mlir.llvm.org/docs/DefiningDialects/
- TableGen Language Reference — https://llvm.org/docs/TableGen/
源码(A 级)
mlir/include/mlir/IR/OpBase.td— ODS 基类定义mlir/include/mlir/IR/CommonTypeConstraints.td— 类型约束定义mlir/tools/mlir-tblgen/— mlir-tblgen 源码
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【编译器与 MLIR】AI 时代的编译器基础设施
从三阶段编译器局限出发,系统讲解 MLIR 方言、渐进降阶与 Pass 基础设施,覆盖 Tensor/Linalg/Affine/GPU 到框架桥接的完整编译链。
【编译器与 MLIR】MLIR 全景图与设计哲学
从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。
【编译器与 MLIR】类型系统与属性
解析 MLIR 的类型体系:内建类型(Integer、Float、Tensor、MemRef)与自定义方言类型的注册机制;区分 Type 与 Attribute 的设计意图;通过 OpBuilder 理解类型和属性在 IR 构造中的实际角色。
【编译器与 MLIR】从零构建一个微型 Tensor DSL
手把手构建微型 Tensor DSL:ODS 定义方言、写 tiny-to-linalg 降阶 Pass,经标准管线生成 LLVM IR,走完编译链闭环(参考 MLIR Toy 教程)。