一文搞懂 HIVM 的 hir.custom_macro:跨 Pipe 宏操作完整解析与速查
【免费下载链接】BiliBiliToolProB 站(bilibili)自动任务工具,支持docker、青龙、k8s等多种部署方式。全面拥抱AI。敏感肌也能用。项目地址: https://gitcode.com/GitHub_Trending/bi/BiliBiliToolPro
hir.custom_macro是 AscendNPU-IR 项目中 HIVM 方言的跨 Pipe 宏操作:把「数据加载 + 向量计算 + 后处理」这类横跨多条流水线的活打包成一个 IR 操作,再由编译器根据你声明的pipe_in/pipe_out自动补齐跨 Pipe 同步。如果你之前只用过单流水线的hir.custom,遇到"进出的管道不一样"的场景就会卡住——这篇就是为此准备的。
场景速览:什么时候需要跨 Pipeline 的宏操作
典型的融合核里,数据往往先由 MTE2 流水线从 GM(全局内存)取回,再交给 V 流水线(Vector Core)做向量运算,最后把结果写回。用hir.custom描述这条链路会立刻碰壁——它只能绑定一条流水线。hir.custom_macro就是为这种"进门管道 ≠ 出门管道"的融合而生:同一个操作上声明入口与出口,中间的握手交给编译器。
先看 IR,再拆细节:两个完整示例与 TableGen 定义
先上手感。下面是一个手写的自定义宏操作,注意属性区里那对pipe_in/pipe_out,它们分别记录了数据进来和结果出去的流水线:
%empty = tensor.empty() : tensor<3x3xf32> %0 = hivm.hir.custom_macro { hivm.tcore_type = #hivm.tcore_type<VECTOR>, hivm.vf_mode = #hivm.vf_mode<SIMD>, hivm.pipe_in = #hivm.pipe<PIPE_MTE2>, hivm.pipe_out = #hivm.pipe<PIPE_V> } "my_custom_op" ins(%arg0, %arg1, %c4_i64, %c0_i32, %c2_i64, %c1_i64, %c2_i32, %c2_i32, %c0_i32, %c0_i32 : memref<?xf32>, tensor<3x3xi64>, i64, i32, i64, i64, i32, i32, i32, i32) outs(%empty : tensor<3x3xf32>) -> tensor<3x3xf32>对比一个内置宏操作(名字以__builtin_开头):IR 文本里不再手写属性区,核类型、管道、VF 模式全部由编译器在构造时补全:
%empty = tensor.empty() : tensor<3x3xf32> %0 = hivm.hir.custom_macro "__builtin_gather_load" ins(%arg0, %arg1, %c4_i64, %c0_i32, %c2_i64, %c1_i64, %c2_i32, %c2_i32, %c0_i32, %c0_i32 : memref<?xf32>, tensor<3x3xi64>, i64, i32, i64, i64, i32, i32, i32, i32) outs(%empty : tensor<3x3xf32>) -> tensor<3x3xf32>这两个写法背后的 TableGen 定义位于bishengir/include/bishengir/Dialect/HIVM/IR/HIVMOps.td(CustomMacroOp段约 L1105–L1167):
def CustomMacroOp : HIVM_CustomOp<"custom_macro", [MacroOpTrait]> { let arguments = (ins StrAttr:$name, Variadic<AnyType>:$inputs, Variadic<AnyType>:$outputs); let results = (outs Variadic<AnyType>:$results); }怎么读这段定义?操作在方言里登记的名字是custom_macro,继承自定义操作族的公共基类HIVM_CustomOp;与hir.custom最本质的差别是它挂着MacroOpTrait——等于提前向编译器报备:这是个要跨多条流水线跑的宏,同步分析时别拿单步操作那套规则套我。arguments段声明了三个输入操作数,results段声明了可变长的结果。
操作拆解:输入、输出与属性的完整清单
操作数一侧,ins段一共有三个:$name是StrAttr类型的字符串,也就是操作名(如"my_custom_op");$inputs是可变长的任意类型参数,承担真正的计算输入;$outputs同样是可变长任意类型,扮演输出缓冲区的角色(DPS init)。outs段则以可变长$results返回结果。
必需属性按职责分成两组:一组回答"在哪儿跑",另一组回答"绑到哪条流水线上"。
执行环境
| 属性 | 类型 | 用途 |
|---|---|---|
hivm.tcore_type | TCoreTypeAttr | 指定运行的核类型,如VECTOR |
hivm.vf_mode | VFModeAttr | 向量执行模式,如SIMD |
流水线绑定
| 属性 | 类型 | 用途 |
|---|---|---|
hivm.pipe_in | PipeAttr | 入口流水线:数据从哪条 Pipe 进入宏 |
hivm.pipe_out | PipeAttr | 出口流水线:结果从哪条 Pipe 离开宏 |
可选属性
| 属性 | 类型 | 用途 |
|---|---|---|
gm_addr_args_indices | DenseI32ArrayAttr | 索引列表,标记哪些参数携带 GM(全局内存)地址 |
⚡️ 跨 Pipe 同步:编译器如何自动补齐握手
单 Pipe 的hir.custom只需在自己那条流水线内部做同步;而宏操作横跨两条管道,生产者写过的数据,消费者不能立刻读,中间必须有一次标志位握手。MacroOpTrait就是编译器做这件事的钩子:InjectSync与GraphSyncSolver这两个同步分析 Pass 在构建依赖图时,会读取你声明的hivm.pipe_in/hivm.pipe_out,自动在宏操作前后插上一对set_flag/wait_flag——入口管道写完数据后抬旗,出口管道等旗落下再开工。换句话说:跨哪两条 Pipe 由你在 IR 里声明,什么时机同步由 Pass 推导,整条链路不需要手写任何同步代码。
💡 选型对比:hir.custom 还是 hir.custom_macro
| 维度 | hir.custom | hir.custom_macro |
|---|---|---|
| Trait | SinglePipeOpTrait | MacroOpTrait |
| Pipe 属性 | 单个hivm.pipe | hivm.pipe_in+hivm.pipe_out一对 |
| 涉及流水线数 | 1 条 | 2 条(入口 + 出口) |
| 同步方式 | 仅限 Pipe 内同步 | 需要跨 Pipe 同步,由 Pass 自动插入 |
| BuiltinInfo 字段 | coreType、pipe、vfMode | coreType、inPipe、outPipe、vfMode |
一句话选型建议:整个操作能塞进一条流水线,就用custom;数据要从一条管道进(比如 MTE2 从 GM 取数)、从另一条管道出(比如 V 管道做向量计算),就换custom_macro。
📌 API 速查:CustomMacroOp 访问器与 Pipe 名称常量
C++ 侧,CustomMacroOp在基类接口之外还提供了一组专门访问器,方便 Pass 与工具链读写:
| 方法 | 返回类型 | 作用 |
|---|---|---|
getInPipe() | PIPE | 读取入口管道 |
setInPipe(PIPE) | void | 写入入口管道 |
getOutPipe() | PIPE | 读取出口管道 |
setOutPipe(PIPE) | void | 写入出口管道 |
getCoreType() | optional<TCoreType> | 读取核类型 |
setCoreType(TCoreType) | void | 写入核类型 |
getVFMode() | optional<VFMode> | 读取 VF 模式 |
setVFMode(VFMode) | void | 写入 VF 模式 |
isBuiltin() | bool | 判断是否为内置宏操作 |
另外,两个 Pipe 属性名还以类内常量的形式给出,编译器在 IR 上查找属性时用的就是它们:
static constexpr StringLiteral inPipeName = "hivm.pipe_in"; static constexpr StringLiteral outPipeName = "hivm.pipe_out";编译器会校验什么:五条硬性规则
custom_macro进入编译流程后,以下几条规则会被逐一检查:
- MacroOpTrait 语义——操作会被识别为宏操作,同步分析阶段走专门的处理路径,而不是按单流水线 custom 对待。
- pipe_in / pipe_out 必须齐备——两个属性缺一不可,编译器靠它们推导该插入哪一对跨 Pipe 同步。
- 跨 Pipe 同步插入——
InjectSync/GraphSyncSolver依据声明的入口/出口管道,在操作前后自动补上set_flag/wait_flag。 - DPS 语义——操作实现了
DestinationStyleOpInterface,$outputs操作数承担目的地缓冲(DPS init)职责,结果直接写进你给的 buffer。 - 内置宏额外校验——当
isBuiltin()返回 true(即名字是__builtin_前缀的内置宏)时,编译器还会对它的参数做正确性检查。
❓ 你可能想问:三个高频问题
Q:pipe_in 和 pipe_out 到底怎么定?A:顺着数据流方向看。数据"进"宏时走的那条管道是pipe_in——例如从 GM 取数就填PIPE_MTE2;结果"出"宏时走的那条是pipe_out——向量计算落在 V 管道,就填PIPE_V。只要你能说清数据从哪儿来、往哪儿去,选型就定了。
Q:我的操作其实只涉及一条流水线,还要写 custom_macro 吗?A:不用。入口和出口在同一条管道上的话,hir.custom就够了;custom_macro专治入口、出口管道不同的情况,比如 MTE2 加载 + V 管道计算的融合。
Q:set_flag / wait_flag 需要我在 IR 里手写吗?A:不需要。你只负责声明pipe_in/pipe_out,剩下的由InjectSync/GraphSyncSolver两个 Pass 推导跨管道依赖,并自动在宏操作周围插入标志位同步。
延伸阅读
- TableGen 定义:HIVMOps.td(
CustomMacroOp段约 L1105–L1167) - 测试用例:custom-op.mlir
- 关联文档:单 Pipe 版
hir.custom解析——01-custom-op.md
【免费下载链接】BiliBiliToolProB 站(bilibili)自动任务工具,支持docker、青龙、k8s等多种部署方式。全面拥抱AI。敏感肌也能用。项目地址: https://gitcode.com/GitHub_Trending/bi/BiliBiliToolPro
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考