# 自定义算子 ## 概述 AscendNPU IR已为上游模型支持丰富的算子集合。但部分业务场景下用户仍需自定义算子实现专属计算逻辑,典型场景包括: - 现有算子的组合无法满足所需的计算需求。 - 厂商希望自定义算子保持私有。 - 多个算子的组合无法达到最优性能。 自定义算子允许用户自由使用AscendNPU IR提供的接口,提供能与其他算子一起编译的自有算子。 ## 接口说明 **参数说明**: | 参数 | 描述 | | --------- | ------------------------------------------------------------ | | `name` | 唯一算子名称
注意:部分名称为内置算子预留,命名多以`__builtin`为前缀。编译器会自动将此类内置算子链接到`bishengir-compile`配套内置模板库,无需用户额外配置。
若使用自定义算子名称,用户需指定实现位置、编译命令及所有必要的信息。 | | `inputs` | 输入参数 | | `outputs` | 输出结果,可指定为`init`操作数,用作操作结果的初始值,或操作结果将写入的初始位置。 | **属性说明**: | 属性名 | 说明 | 备注/示例 | |--------|------|----------| | `CoreType` | 在哪种核类型上执行 | 参见`TCoreTypeAttr` | | `Pipe` | 在哪个Pipe上执行(用于`hivm.hir.custom`) | 参见`PipeAttr` | | `InPipe` | Macro自定义算子的输入Pipe | 参见`PipeAttr`(`hivm.pipe_in`) | | `OutPipe` | Macro自定义算子的输出Pipe | 参见`PipeAttr`(`hivm.pipe_out`) | | `VFMode` | 在向量单元上的运行模式 | 参见`VFModeAttr`。当核类型为Cube时此属性被忽略。注意:内置算子可指定或不指定,编译器会检查正确性并规范化 | | `Symbol` | 实现函数的名称 | - | | `sync_event_slots` | Macro自定义算子的同步槽位元信息。`GraphSyncSolver`会据此填充`sync_related_args`,并在macro前后注入`set/wait flag`。 | 列表长度必须与macro实现体内、同一Pipe对上的`set_flag`/`wait_flag`对数一致。详见[Macro sync_event_slots](#macro算子同步槽位sync_event_slots) | | `iterator_types` | 逐操作数的迭代器语义,供结构化降级与flatten类Pass使用(`HIVM_IteratorTypeAttr`) | 可选;设置时长度应覆盖参与tiling的输入与输出 | | `indexing_map` | 逐操作数的仿射索引映射(与Linalg的`indexing_maps`作用相同) | 可选,类型为`ArrayAttr` | | `max_rank` | flatten/布局类Pass支持的最大张量秩 | 可选`i64`属性,默认值5 | | `align_dim` | 逐操作数的维度对齐提示 | 通过`arg_attrs`挂在对应操作数上 | | `arg_attrs` | 逐操作数的字典属性数组 | 类型为`ArrayAttr`(例如操作数2上的`{align_dim = 1 : i64}`)。Triton前端根据注册类上的`align_dim`自动生成 | | `extra_buffers_types` | 临时buffer的元素类型 | 由`hivm-alloc-extra-buffer` Pass分配`memref`并追加到`temp_buffers`(汇编中为`tmps`) | | `extra_buffers_sizes` | 临时buffer的一维大小(元素个数) | 同上 | | `temp_buffers` | 传给设备实现的临时`memref`(`tmps`操作数段) | 通常由`extra_buffers_*`属性自动填充,无需手动设置 | | `no_side_effect` | 表示算子无副作用 | - | | `bitcode` / `source` / `compile` | 实现产物路径及可选编译命令 | 通常由Triton前端设置 | ## 降级流程 ```text ┌─────────────────────────────────────────────────────────────────┐ │ CustomOp │ │ hivm.hir.custom "name" { attrs... } ins(..) outs(...) │ └─────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────┐ │ HIVMToStandard │ │ ───────────────────────────────────────────────────────────────│ │ • 内置算子 │ │ -> 调用内置库 │ │ • 用户提供的实现 -> │ | -> 调用用户提供的函数名 | | -> bishengir-compile 使用用户提供的链接命令进行链接 | └─────────────────────────────────────────────────────────────────┘ │ ▼ 毕昇编译器将其编译为 Object Files ``` ## 支持能力 | 特性 | 说明 | | ------------------------------- | ------------------------------------------------------------ | | CoreType | 自定义算子执行核 | | Pipe | 自定义算子执行pipe(`hivm.hir.custom`) | | InPipe / OutPipe | Macro自定义算子的输入/输出pipe(`hivm.hir.custom_macro`) | | VFMode | 自定义算子在向量核上的运行模式:SIMT/SIMD/MIX | | Symbol | 使用者提供的函数名称 | | sync_event_slots | Macro同步槽位声明,用于GraphSyncSolver集成 | | iterator_types | Tiling / flatten的逐操作数迭代器语义 | | indexing_map | 结构化降级的逐操作数仿射映射 | | max_rank | 布局Pass支持的最大张量秩(默认5) | | align_dim / arg_attrs | 对齐调整Pass的逐操作数对齐提示 | | extra_buffers_* / tmps | 临时buffer声明与分配 | | no_side_effect | 纯算子标记,便于优化 | | 内置算子 | 一组内置算子(名称预留) | ## 限制约束 当前自定义算子体系存在两项待完善能力,具体如下: - **用户侧实现适配不完善** 支持将自定义算子降级为用户提供的实现,相关流程包含:HIVM IR链接到用户提供的源码或目标文件、向bishengir-compile注册特定链接命令,目前该功能仍在开发迭代中。 - **Pass交互适配不完善** 各类自定义算子的变换Pass,包含Flatten优化、对齐调整、内存规划、布局变换等,仍有多项适配逻辑待补充,相关工作正在推进。 ## MLIR示例 ### 算子声明示例 **内置算子**: ```mlir %0 = hivm.hir.custom "__builtin_gather_load" ins(%arg0, %arg1, %c4_i64, %c0_i32, %c2_i64, %c1_i64, %c2_i32, %c2_i32, %c0_i32, %c0_i32 : memref, tensor<3x3xi64>, i64, i32, i64, i64, i32, i32, i32, i32) outs(%empty : tensor<3x3xf32>) -> tensor<3x3xf32> ``` **自定义算子**: ```mlir %0 = hivm.hir.custom { hivm.tcore_type = #hivm.tcore_type, hivm.pipe = #hivm.pipe, hivm.vf_mode = #hivm.vf_mode, symbol = "my_custom" } "my_custom_op" ins(%arg0, %arg1, %c4_i64, %c0_i32, %c2_i64, %c1_i64, %c2_i32, %c2_i32, %c0_i32, %c0_i32 : memref, tensor<3x3xi64>, i64, i32, i64, i64, i32, i32, i32, i32) outs(%empty : tensor<3x3xf32>) -> tensor<3x3xf32> ``` **自定义Macro算子**: ```mlir %0 = hivm.hir.custom_macro { hivm.tcore_type = #hivm.tcore_type, hivm.pipe_in = #hivm.pipe, hivm.pipe_out = #hivm.pipe, hivm.vf_mode = #hivm.vf_mode, symbol = "custom_macro_add_int32", sync_event_slots = [ #hivm.sync_event_slot<#hivm.pipe, #hivm.pipe> ] } "macro_add" ins(%arg0, %arg1 : memref, memref) outs(%dst : memref<32xi32, #hivm.address_space>) ``` ### 核心属性用法示例 #### Macro算子同步槽位(sync_event_slots) 对于在InPipe(如`PIPE_MTE2`)搬运数据,在OutPipe(如`PIPE_V`)执行计算的Macro自定义算子,通常需要在设备实现中插入`set_flag`与`wait_flag`操作,确保MTE2将数据写入UB完成后,向量Pipe再开始读取。 在Triton侧通过`sync_event_slots`与`SYNC_HINT.INTERNAL`声明这些宏内部的同步关系。 GraphSyncSolver会为每个槽位分配独立的event ID,并在需要时在macro算子前后补充set/wait操作,保证其与上下游HIVM算子正确衔接。 **规则**:macro实现体内,每一对同方向Pipe的`set_flag`/`wait_flag`,需要在Python/MLIR中对应声明一个`sync_event_slots`条目。 | 模式 | Python / MLIR槽位数 | 典型设备代码 | 适用场景 | | ------ | ----------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | | 单槽位 | 1 × `(PIPE_MTE2, PIPE_V, INTERNAL)` | 两次GM→UB搬运完成后,一次`set_flag` + `wait_flag`,再执行向量计算 | 默认场景:两次load连续执行,向量计算开始前只需一次MTE2到V的同步 | | 双槽位 | 2 × `(PIPE_MTE2, PIPE_V, INTERNAL)` | `load` → `set_flag(0)` → `load` → `set_flag(1)` → `wait_flag(0)` → `wait_flag(1)` → 向量计算 | 每次GM→UB需要独立event的场景,如流水重叠、或需按次确认搬运完成 | **单槽位说明**: 两个操作数都通过MTE2搬入UB后,向量Pipe才开始计算。单个event即可标识本macro所需的MTE2输入搬运已全部完成,PIPE_V可安全读取两块UB暂存空间,对应伪代码如下: ```text load_gm_to_ubuf(src0) load_gm_to_ubuf(src1) set_flag(MTE2 → V, event 0) wait_flag(MTE2 → V, event 0) vector_vadd(...) ``` **双槽位说明**: 第一次load后立即执行`set_flag`,可在等待首次搬运完成的同时启动第二次GM到UB的搬运。每个槽位对应GraphSyncSolver分配的一个独立event ID,对应伪代码如下: ```text load_gm_to_ubuf(src0) set_flag(MTE2 → V, event 0) load_gm_to_ubuf(src1) set_flag(MTE2 → V, event 1) wait_flag(MTE2 → V, event 0) wait_flag(MTE2 → V, event 1) vector_vadd(...) ``` 双槽位对应的MLIR: ```mlir sync_event_slots = [ #hivm.sync_event_slot<#hivm.pipe, #hivm.pipe>, #hivm.sync_event_slot<#hivm.pipe, #hivm.pipe> ] ``` 若Python/MLIR中声明的槽位数量,与C++ 实现中`set_flag`/`wait_flag`的配对数量不一致,会导致GraphSyncSolver分配的event ID与kernel不匹配,这是自定义Macro算子集成时的常见错误。 #### Tiling属性(iterator_types、indexing_map、max_rank) 通过配置这些属性,CustomOp可实现HIVM结构化算子接口(`getIteratorTypesArray`、`getIndexingMaps`),使flatten、broadcast及布局Pass能够像处理原生结构化算子一样,对自定义算子的操作数进行分析与优化。 `iterator_types`支持的取值包括:`parallel`、`broadcast`、`transpose`、`reduction`、`interleave`、`deinterleave`、`inverse`、`pad`、`concat`、`gather`、`cumulative`、`opaque`。 MLIR示例(摘自`custom-op-attrs.mlir`): ```mlir #map2d = affine_map<(d0, d1) -> (d0, d1)> %0 = hivm.hir.custom {hivm.tcore_type = #hivm.tcore_type, hivm.pipe = #hivm.pipe, hivm.vf_mode = #hivm.vf_mode, symbol = "k_named_maps", max_rank = 5 : i64, iterator_types = [#hivm.iterator_type, #hivm.iterator_type], indexing_map = [#map2d, #map2d, #map2d]} "user.named_maps" ins(%arg0, %arg1 : memref<2x2xf32>, tensor<2x2xf32>) outs(%empty : tensor<2x2xf32>) -> tensor<2x2xf32> ``` Triton注册示例: ```python @al.register_custom_op class tiled_custom_op: core = al.CORE.VECTOR pipe = al.PIPE.PIPE_V mode = al.MODE.SIMT symbol = "my_tiled_func" bitcode = "/path/to/kernel.bc" iterator_types = [ al.IteratorType.Parallel, al.IteratorType.Broadcast, ] def __init__(self, x, y, out=None): # 每个结构化操作数(输入 + 输出)对应一个仿射映射。 self.indexing_map = [ al.affine_map.get_identity(2), al.affine_map.get_identity(2), al.affine_map.get_identity(2), ] ``` #### 操作数对齐属性(align_dim、arg_attrs) `align_dim`用于标记指定操作数需要对齐的维度。在MLIR中,该属性通过`arg_attrs`挂载到对应索引的操作数上,示例如下: ```mlir %0 = hivm.hir.custom { ... } "my_custom_op" ins(%arg0 {align_dim = 1 : i64}, %arg1 {align_dim = 0 : i64} : memref, memref) outs(%dst : tensor) -> tensor ``` Triton示例(在`__init__`中设置,键为参数名或位置索引): ```python def __init__(self, x, ptr1, ptr2, out=None): self.align_dim = {"ptr2": 1, 1: 0} # ptr2 第 1 维;第 2 个参数第 0 维 ``` #### 临时缓冲区属性(extra_buffers_*、temp_buffers) 通过相关属性可在算子上声明临时缓冲区的类型与大小,`hivm-alloc-extra-buffer`会自动分配一维memref,并接入算子的`tmps`操作数段,示例如下: ```mlir // 经 hivm-alloc-extra-buffer 之后: %alloc0 = memref.alloc() : memref<512xf32> %alloc1 = memref.alloc() : memref<128xf16> %0 = hivm.hir.custom { extra_buffers_types = [f32, f16], extra_buffers_sizes = [512 : i64, 128 : i64], ... } "my_custom_op" ins(%arg0 : memref<2x2xf32>) tmps(%alloc0, %alloc1 : memref<512xf32>, memref<128xf16>) outs(%empty : tensor<2x2xf32>) -> tensor<2x2xf32> ``` Triton示例(单个buffer用元组,多个用`(dtype, size)`列表): ```python @al.register_custom_op class my_custom_op_extra_buf: core = al.CORE.VECTOR pipe = al.PIPE.PIPE_V mode = al.MODE.SIMT symbol = "my_extra_buf_func" bitcode = "/path/to/kernel.bc" def __init__(self, x, out=None): self.extra_buffers = [ (tl.bfloat16, 256), (tl.float32, 512), ] # 或单个 buffer:self.extra_buffers = (tl.float16, 128) ``` 设备kernel会按照`extra_buffers`的声明顺序,通过`tmps`操作数接收分配的scratch memref。 #### no_side_effect 该属性用于标记仅读取输入、仅写入声明输出的纯算子,示例如下: ```mlir %0 = hivm.hir.custom {no_side_effect, symbol = "pure_kernel", ...} "pure_op" ins(...) outs(...) -> ... ``` ### Triton自定义算子示例 #### 标准自定义算子示例 Python脚本:`test_custom_op.py` ```python # 更多关于 Triton 自定义算子设计的详情,请参考 # https://gitcode.com/Ascend/triton-ascend/pull/988 import triton import triton.language as tl import triton.language.extra.cann.extension as al import torch import torch_npu import pytest def torch_add(a, b): return a + b @al.register_custom_op class add: core = al.CORE.VECTOR pipe = al.PIPE.PIPE_V mode = al.MODE.SIMD def __init__(self, a, b, out=None): assert out, "out is required" self.symbol = "custom_add_" + str(a.dtype) self.bitcode = 'add.bc' @triton.jit def triton_custom_add( output_ptr, a_ptr, b_ptr, L: tl.constexpr ): idx = tl.arange(0, L) a = tl.load(a_ptr + idx) b = tl.load(b_ptr + idx) buf = tl.full([L], 0, a.dtype) res = al.custom("add", a, b, out=buf) tl.store(output_ptr + idx, res) testlist = [ (32) ] typelist = [torch.int32] @pytest.mark.parametrize("DT", typelist) @pytest.mark.parametrize("L", testlist) def test_custom(DT, L): a = torch.ones(L, dtype=DT).npu() b = torch.ones(L, dtype=DT).npu() ref = torch_add(a, b) out = torch.zeros(L, dtype=DT).npu() triton_custom_add[1, 1, 1](out, a, b, L) torch.testing.assert_close(out, ref) ``` C++ API定义:`add.cpp` ```cpp #define __aiv__ [aicore] #define INTRINSIC_NO_ARGS(NAME) NAME() #define INTRINSIC(NAME, ...) NAME(__VA_ARGS__) template struct memref_t { T *allocated; T *aligned; int64_t offset; int64_t sizes[Dim]; int64_t strides[Dim]; }; template struct intrin_args { __ubuf__ DST_T *dst; __ubuf__ SRC_T *src[OPERANUM]; SRC_T scalar; uint64_t repeat; uint16_t dst_block_stride; uint16_t src_block_stride[OPERANUM]; uint16_t dst_repeat_stride; uint16_t src_repeat_stride[OPERANUM]; }; template __aiv__ __attribute__((always_inline)) void vector_eltwise_vadd_intrin(intrin_args<2, SRC_TYPE, DST_TYPE> args) { #define ELTWISE_VV_ARGS \ args.dst, args.src[0], args.src[1], args.repeat, args.dst_block_stride, \ args.src_block_stride[0], args.src_block_stride[1], \ args.dst_repeat_stride, args.src_repeat_stride[0], \ args.src_repeat_stride[1] INTRINSIC(vadd, ELTWISE_VV_ARGS); } extern "C" { __aiv__ __attribute__((always_inline)) void _mlir_ciface_custom_add_int32( memref_t<__ubuf__ int32_t, 1> *src0, memref_t<__ubuf__ int32_t, 1> *src1, memref_t<__ubuf__ int32_t, 1> *dst) { uint16_t src0_block_stride = 1; uint16_t src1_block_stride = 1; uint16_t src0_repeat_stride = 8; uint16_t src1_repeat_stride = 8; auto new_src0_ptr = src0->aligned + src0->offset; auto new_src1_ptr = src1->aligned + src1->offset; auto dst_ptr = dst->aligned + dst->offset; INTRINSIC_NO_ARGS(set_mask_count); const int64_t n = dst->sizes[0]; INTRINSIC(set_vector_mask, 0, n); vector_eltwise_vadd_intrin( intrin_args<2, int32_t>{dst_ptr, {new_src0_ptr, new_src1_ptr}, 0, 1, 1, {src0_block_stride, src1_block_stride}, 8, {src0_repeat_stride, src1_repeat_stride}}); INTRINSIC_NO_ARGS(set_mask_norm); } } ``` 编译`.bc`文件指令: ```bash ccec -x cce --cce-aicore-arch=dav-c220-vec --cce-aicore-only -c -emit-llvm ./add.cpp -o ./add.bc ``` Python脚本执行命令: ```bash python -m pytest -sv test_custom_op.py ``` #### Macro自定义算子示例 Python脚本:`test_custom_macro_op.py` ```python import triton import triton.language as tl import triton.language.extra.cann.extension as al import torch import torch_npu import pytest @al.register_custom_op class macro_add: core = al.CORE.VECTOR pipe = (al.PIPE.PIPE_MTE2, al.PIPE.PIPE_V) mode = al.MODE.SIMD sync_event_slots = [ (al.PIPE.PIPE_MTE2, al.PIPE.PIPE_V, al.SYNC_HINT.INTERNAL), ] def __init__(self, a, b, out=None): assert out is not None, "out is required" self.symbol = "custom_macro_add_int32" self.bitcode = "macro_add.bc" @triton.jit def triton_custom_macro_add(output_ptr, a_ptr, b_ptr, L: tl.constexpr): idx = tl.arange(0, L) a = tl.load(a_ptr + idx) b = tl.load(b_ptr + idx) buf = tl.full([L], 0, tl.int32) res = al.custom("macro_add", a, b, out=buf) tl.store(output_ptr + idx, res) @pytest.mark.parametrize("L", [32]) def test_custom_macro(L): a = torch.ones(L, dtype=torch.int32).npu() b = torch.ones(L, dtype=torch.int32).npu() out = torch.zeros(L, dtype=torch.int32).npu() triton_custom_macro_add[(1,)](out, a, b, L) torch.testing.assert_close(out.cpu(), (a + b).cpu()) ``` C++ API定义:`macro_add.cpp` ```cpp #define __aiv__ [aicore] #define INTRINSIC_NO_ARGS(NAME) NAME() #define INTRINSIC(NAME, ...) NAME(__VA_ARGS__) constexpr int64_t UB_SCRATCH_SRC0_BYTES = 256; constexpr int64_t UB_SCRATCH_SRC1_BYTES = 384; constexpr int64_t kGmToUbChunkElems = 16; template struct memref_t { T *allocated; T *aligned; int64_t offset; int64_t sizes[Dim]; int64_t strides[Dim]; }; extern "C" { __aiv__ __attribute__((always_inline)) void _mlir_ciface_custom_macro_add_int32( memref_t<__gm__ int32_t, 1> *src0, memref_t<__gm__ int32_t, 1> *src1, memref_t<__ubuf__ int32_t, 1> *dst) { const int64_t n = dst->sizes[0]; auto ub_src0 = ub_scratch_at_bytes(UB_SCRATCH_SRC0_BYTES, n); auto ub_src1 = ub_scratch_at_bytes(UB_SCRATCH_SRC1_BYTES, n); memref_t<__gm__ int32_t, 1> gm_src0 = {src0->allocated, src0->aligned, src0->offset, {n}, {1}}; memref_t<__gm__ int32_t, 1> gm_src1 = {src1->allocated, src1->aligned, src1->offset, {n}, {1}}; // 单内部 event:两次 GM→UB 后一次 MTE2→V 握手。 load_gm_to_ubuf_1d(&gm_src0, &ub_src0); load_gm_to_ubuf_1d(&gm_src1, &ub_src1); INTRINSIC(set_flag, PIPE_MTE2, PIPE_V, 0); INTRINSIC(wait_flag, PIPE_MTE2, PIPE_V, 0); vector_vadd_ub(&ub_src0, &ub_src1, dst); } } ``` 编译`.bc`文件指令: ```bash ccec -x cce --cce-aicore-arch=dav-c220-vec --cce-aicore-only -c -emit-llvm ./macro_add.cpp -o ./macro_add.bc ``` Python脚本执行命令: ```bash python -m pytest -sv test_custom_macro_op.py ``` 降级为MLIR: ```mlir module attributes {hacc.target = #hacc.target<"Ascend910B3">} { func.func @triton_custom_add(%arg0: memref, %arg1: memref, %arg2: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 1 : i32}, %arg3: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg4: memref {tt.divisibility = 16 : i32, tt.tensor_kind = 0 : i32}, %arg5: i32, %arg6: i32, %arg7: i32, %arg8: i32, %arg9: i32, %arg10: i32) attributes {SyncBlockLockArgIdx = 0 : i64, WorkspaceArgIdx = 1 : i64, global_kernel = "local", mix_mode = "aiv", parallel_mode = "simd"} { %c0_i32 = arith.constant 0 : i32 %0 = tensor.empty() : tensor<32xi32> %1 = linalg.fill ins(%c0_i32 : i32) outs(%0 : tensor<32xi32>) -> tensor<32xi32> %reinterpret_cast = memref.reinterpret_cast %arg3 to offset: [0], sizes: [32], strides: [1] : memref to memref<32xi32, strided<[1]>> %alloc = memref.alloc() : memref<32xi32> memref.copy %reinterpret_cast, %alloc : memref<32xi32, strided<[1]>> to memref<32xi32> %2 = bufferization.to_tensor %alloc restrict writable : memref<32xi32> %reinterpret_cast_0 = memref.reinterpret_cast %arg4 to offset: [0], sizes: [32], strides: [1] : memref to memref<32xi32, strided<[1]>> %alloc_1 = memref.alloc() : memref<32xi32> memref.copy %reinterpret_cast_0, %alloc_1 : memref<32xi32, strided<[1]>> to memref<32xi32> %3 = bufferization.to_tensor %alloc_1 restrict writable : memref<32xi32> %4 = hivm.hir.custom {bitcode = "/home/test/add.bc", hivm.pipe = #hivm.pipe, hivm.tcore_type = #hivm.tcore_type, hivm.vf_mode = #hivm.vf_mode, symbol = "custom_add_int32"} "add" ins(%2, %3 : tensor<32xi32>, tensor<32xi32>) outs(%1 : tensor<32xi32>) -> tensor<32xi32> %reinterpret_cast_2 = memref.reinterpret_cast %arg2 to offset: [0], sizes: [32], strides: [1] : memref to memref<32xi32, strided<[1]>> bufferization.materialize_in_destination %4 in writable %reinterpret_cast_2 : (tensor<32xi32>, memref<32xi32, strided<[1]>>) -> () return } } ```