MLIR专题4:常用数据结构

发布时间:2026/6/16 19:58:42

MLIR专题4:常用数据结构 MLIR::Dialect在 MLIR 的大厦中,如果说Operation是砖瓦,那么Dialect(方言)就是划定领地的“逻辑军营”与“元信息大本营”。在物理内存中,每一个自定义方言在整个编译器的运行生命周期里,在全局有且仅有一个实例(单例模式)。它被死死注册在全局的MLIRContext大表里。我们来看mlir::Dialect核心类体内最关键的几个物理成员:class Dialect { private: MLIRContext *context; // 1. 指向全局上下文的指针(共享大库) StringRef name; // 2. 方言的唯一前缀名字(如 "npu" 或 "arith") TypeID dialectID; // 3. 编译期静态确定的唯一身份标识 // 核心大表:该方言领地内派生出的所有具体组件 std::vectorAbstractOperation* registeredOps; // 4. 注册在该方言下的所有算子元信息 // 动态接口矩阵 DenseMapTypeID, void* dialectInterfaces; // 5. 方言级别的横向扩展接口表 };为了消灭编译期的指针寻址延迟,MLIR 把所有的方言实例统一收纳。当我们把视角拉高到整个编译器内存时,它的排布长这样:┌────────────────────────────────────────────────────────┐ │ MLIRContext (全局上下文) │ │ ┌──────────────────────────────────────────────────┐ │ │ │ DialectRegistry (方言注册大哈希表) │ │ │ │ "arith" ───► Pointer to ArithDialect 实例 │ │ │ │ "npu" ───► Pointer to NPUDialect 实例 │ │ │ └──────────────────────────────────────────────────┘ │ └─────────────────────────┬──────────────────────────────┘ │ ▼ 顺着指针点进去 ┌────────────────────────────────────────────────────────┐ │ NPUDialect 物理实例 │ │ │ │ ├─ name: "npu" │ │ ├─ dialectID: 0x7fff3a2b (全局唯一地址) │ │ │ │ ├─ registeredOps (算子元信息大池) │ │ │ ├─ "npu.cmac" ──► AbstractOperation 实例 │ │ │ └─ "npu.vpu" ──► AbstractOperation 实例 │ │ │ │ │ └─ dialectInterfaces (方言级接口表) │ │ └─ [InterfaceID] ──► AutotuningInterfaceModel │ └────────────────────────────────────────────────────────┘核心解耦机制:字符串前缀路由:当你从前端解析进一段文本npu.cmac %a, %b时,文本解析器(Parser)会先点进MLIRContext,拆出前缀"npu",瞬间就路由到了NPUDialect实例。AbstractOperation 的真正归宿:我们在前面聊过,具体的AddOp或CmacOp的外壳只有 8 字节,它们的肉体Operation极其扁平。那么,算子特有的verify()函数指针、fold()函数指针,全都在方言初始化时,被整整齐齐地码放在了方言内部的registeredOps(AbstractOperation)大表里。声明阶段(.h.inc):CRTP 再次降维打击和OpState类似,你的自定义方言并没有走沉重的运行时多态,它继承自一个带有CRTP属性的方言模板:namespace mlir { namespace toy { // 子类把自己作为模板参数喂给基类 DialectImpl class ToyDialect : public ::mlir::Dialect { explicit ToyDialect(::mlir::MLIRContext *context); static ::llvm::StringRef getDialectNamespace() { return "toy"; } // 初始化入口:方言被激活时,负责向肚子里装填所有算子 void initialize(); }; } // namespace toy } // namespace mlir初始化阶段(.cpp.inc):算子的批量“落户”当你在 Pass 或者是main函数里呼叫context.loadDialectToyDialect()时,方言物理实例在堆上被创建,并立刻触发initialize()函数。在这个函数里,方言会完成最壮观的元信息注册大熔炉:void ToyDialect::initialize() { // 终极闭环:一次性把你这个方言领地内定义的几百个算子的静态元信息(AbstractOperation), // 全部实例化,并整齐地塞进当前的 registeredOps 向量中! addOperations ConstantOp, AddOp, MulOp, ReshapeOp (); // 如果你有自定义的底层 Type(如 toy::TensorType)或 Attribute,也在这里完成物理落户 addTypesToyTensorType(); }主要接口addOperations()—— 算子元信息的“排队落户”addOperationsConstantOp, AddOp, MulOp();核心作用:把我们在 ODS(TableGen)中定义、C++ 展开的具体算子类,批量注入到方言的元信息向量大池(registeredOps)中。底层大闭环:还记得我们前面聊过的8 字节 CRTP 视图包装器(AddOp)和底层的AbstractOperation(全局元信息大表)吗? 当你调用addOperationsAddOp()时,MLIR 会顺着AddOp这个类,现场把它肚子里的verifyOp()、fold()、getCanonicalizationPatterns()等静态函数指针全部抽出来,打包实例化成一个统一的AbstractOperation结构体,然后整齐地码放在方言的大表里。大白话:它负责告诉编译器:“世界上多了一个叫toy.constant的算子,它的规矩是 XXX,以后看到它请按这个规矩在连续内存里给它 1 次malloc出物理肉体。”addTypes()—— 独门数据类型的“海关备案”核心作用:向全局上下文注册你的方言私有的、独一无二的数据类型(Custom Type)。工业级应用场景:做 AI 芯片(NPU)编译器时,官方自带的tensor...或memref...往往不够用。因为你的硬件有特殊的存储层级(比如SRAM、HBM、Weight_Buffer)。你通常会自己定义一个!npu.tensor类型。 当你调用addTypesToyTensorType()后,MLIR 的文本解析器(Parser)在看到文本!toy.struct...时,就知道该转手交给谁去解析了。幕后的去重(Uniqueing)奇迹:MLIR 的类型系统为了压榨性能,在内存里是绝对唯一(Unique)的。也就是说,无论你的计算图里诞生了几万个!toy.struct变量,它们在底层指针上都指向同一个物理内存单例。addTypes就是在全局为这个类型单例开辟初始化的海关绿色通道。addAttributes()—— 编译期常量的“格式定义”核心作用:向全局注册你方言独有的编译期常量/配置标签格式(Custom Attribute)。工业级应用场景:在为 NPU 算子编写硬件指令发射 Pass 时,算子往往带有很多专有的静态配置。比如一个卷积算子,它的padding策略是SAME还是VALID?它的硬件流水线切片模式(Tiling Strategy)是按 X轴切还是按 Y轴切?你可以把这些复杂的配置信息定义为方言特有的Attribute(如!npu.tiling_config{x=2, y=4})。通过addAttributes注册后,这个属性格式就被框架正式承认,并开始享受和原生属性一样的编译期哈希去重待遇。三者的终极数据协同美学这三个接口在initialize()里批量执行完后,就完成了整个方言在逻辑视图层与物理存储层的终极闭环。当我们用一条高层的builder.create语句,来看这三剑客在底层的完美大汇合:// 你的 Pass 代码 builder.createtoy::AddOp(loc, toyTensorType, lhs, rhs, customConfigAttr);底层发生的故事是:addOperations

相关新闻