土法炼钢兴趣小组的算法知识备份

【编译器与 MLIR】表驱动定义:ODS 与声明式编程

文章导航

分类入口
compilerarchitecture
标签入口
#mlir#llvm#compiler#tablegen#ods#dialect#code-generation#declarative

目录

表驱动定义: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 的基本概念:

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++ 代码包括:

  1. 类定义mlir::my::AddOp,继承自 Op<AddOp, Pure, SameOperandsAndResultType>
  2. 构建器(Builder):自动生成的 build() 方法,接受 ValueType 参数。
  3. 访问器(Accessor)getLhs()getRhs()getResult() 方法。
  4. 验证器(Verifier)骨架:如果 hasVerifier = 1,生成 static LogicalResult verify() 声明。
  5. Parser/Printer:根据 assemblyFormat 自动生成 .mlir 的解析和打印逻辑。
  6. 折叠接口:如果 hasFolder = 1,生成 OpFoldResult fold() 声明。

三、argumentsresultsassemblyFormat 详解

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 任意整数(i1i32i64等)
AnyFloat 任意浮点(f16f32f64等)
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

语法元素:

在 90% 的场景中,assemblyFormat 的声明式写法就够了。只有当 Op 的语法非常特殊时才需要手写 parse()print()

四、Traits 与 Interfaces

Traits 和 Interfaces 都提供跨 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.inc

C++ 文件中 #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++ 更合理:

MLIR 对此的哲学是”渐进采用”:实验阶段手写 C++,方言稳定后将重复的样板代码转为 ODS。scf(结构化控制流)方言就混合了手写和 TableGen。

七、本篇后续

ODS 解决了”定义 Op”的效率问题。完整可构建工程请继续阅读 MLIR Toy 教程。下一篇讲 Region 和 Block 在控制流表示中的角色——这是 MLIR 与扁平 IR 最关键的差异之一,也是自定义方言可以表示的抽象层级范围所在。

参考资料

官方文档(A 级)

源码(A 级)

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。

2026-06-09 · compiler / architecture

【编译器与 MLIR】MLIR 全景图与设计哲学

从 Module-Operation-Region-Block 四层结构出发,系统讲解 MLIR 的三条核心设计原则:渐进降阶、方言可组合性、基础设施复用,配合 IREE、CIRCT、Torch-MLIR 等实际案例建立心智模型。

2026-06-09 · compiler / architecture

【编译器与 MLIR】类型系统与属性

解析 MLIR 的类型体系:内建类型(Integer、Float、Tensor、MemRef)与自定义方言类型的注册机制;区分 Type 与 Attribute 的设计意图;通过 OpBuilder 理解类型和属性在 IR 构造中的实际角色。


By .