CANN ops-math AssignSub 算子深度解析:从算子定义到 aclnn 调用实践
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
导读
AssignSub 是 CANN ops-math 数学算子库中用于计算var - value并将结果写入输出的基础算子,在权重更新(如 SGD 类优化器)等场景中扮演减法赋值的关键角色。本文以 experimental/math/assign_sub/README.md 为骨架,结合算子定义、Shape 推导、Tiling、AscendC Kernel 与单测用例,系统讲解其功能规格、参数约束、两段式 aclnn 调用方式与底层实现原理,帮助你快速上手在 NPU 上调用该算子并理解其性能优化设计。
一、产品支持情况
AssignSub 算子当前支持以下产品形态(详见 README 及 docs/aclnnAssignSub.md):
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | √ |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | √ |
产品形态详细说明请参见昇腾产品形态说明(可查阅 CANN 官方文档的"昇腾产品形态说明"章节)。
从源码实现看,该算子通过 op_host/assign_sub_def.cpp 中的AICore().AddConfig("ascend910b").AddConfig("ascend910_93")注册了对应的 AI Core 编译配置,分别对应 Atlas A2(Ascend 910B)与 Atlas A3(Ascend 910_93)系列芯片,与文档中的产品支持情况一一对应。
二、功能说明
AssignSub 的算子功能为:计算var - value,并将结果写入输出张量var_out。
计算公式如下:
$$ var_out = var - value $$
从语义上看,该算子等价于"原地减法赋值"(类似var -= value),但输入输出通过独立张量传递,适合在反向传播后的参数更新阶段以显式算子形式插入计算图。它的计算模式是**逐元素(element-wise)**运算,两个输入张量在对应位置逐个做减法。
三、参数说明
3.1 算子参数
| 参数名 | 输入/输出 | 说明 |
|---|---|---|
| var | 输入 | 被减数张量,数据类型支持 FLOAT16、INT8、FLOAT、INT32、UINT8、BF16、INT64,数据格式支持 ND。 |
| value | 输入 | 减数张量,数据类型与 var 一致,shape 与 var 一致,数据格式支持 ND。 |
| var_out | 输出 | 输出张量,shape 与 var 一致,数据类型与 var 一致,数据格式支持 ND。 |
上述数据类型与格式约束在 op_host/assign_sub_def.cpp 的算子定义中有完整体现:var、value、var_out三个端口均声明为REQUIRED参数,依次支持DT_FLOAT16、DT_INT8、DT_FLOAT、DT_INT32、DT_UINT8、DT_BF16、DT_INT64七种数据类型,Format 与 UnknownShapeFormat 均限定为FORMAT_ND,并统一调用.AutoContiguous()保证输入在计算前被处理为连续内存布局。
3.2 aclnn 两段式接口参数
在 aclnn(AscendCL NN 算子)调用方式下,每个算子分为两段式接口,必须先调用aclnnAssignSubGetWorkspaceSize获取工作空间大小并完成执行器创建,再调用aclnnAssignSub执行计算:
aclnnStatus aclnnAssignSubGetWorkspaceSize( const aclTensor* var, const aclTensor* value, const aclTensor* varOut, uint64_t* workspaceSize, aclOpExecutor** executor);aclnnStatus aclnnAssignSub( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream);aclnnAssignSubGetWorkspaceSize的参数说明如下:
| 参数名 | 输入/输出 | 说明 |
|---|---|---|
| var | 输入 | 被减数张量,数据类型支持 FLOAT16、INT8、FLOAT、INT32、UINT8、BF16、INT64。 |
| value | 输入 | 减数张量,数据类型与 var 一致,shape 与 var 一致。 |
| varOut | 输出 | 输出张量,shape 与 var 一致。 |
| workspaceSize | 输出 | 返回需要在 Device 侧申请的 workspace 大小。 |
| executor | 输出 | 返回 op 执行器。 |
需要说明的是,从 op_host/assign_sub_tiling.cpp 的GetWorkspaceSize实现看,AssignSub 当前将 workspace 大小置为 0(WS_SYS_SIZE),即正常调用时无需额外申请 workspace 空间;示例代码中workspaceSize > 0才分配内存的写法是 aclnn 调用的通用防御性范式,可覆盖算子内部未来可能引入 workspace 需求的情况。
四、约束与限制
- shape 与数据类型必须完全一致:var 和 value 的 shape 及数据类型必须完全一致,不支持 broadcast。这一点在 op_host/assign_sub_infershape.cpp 的 InferShape 中有强校验:
varShape与valueShape的维度数(GetDimNum)必须相等,逐维(GetDim(i))也必须相等,任一不一致即通过OP_LOGE记录错误日志并返回GRAPH_FAILED。 - 数据格式仅支持 ND。
- int8/uint8 的减法溢出按模 256 环绕处理:即结果按 8 位无符号环绕语义计算,这与 tests/ut/op_kernel/assign_sub_data/gen_data.py 中 golden 数据的生成逻辑一致——int8 结果先以
int16做减法,再按((result + 128) % 256 - 128)回绕到 int8 范围;uint8 则按result % 256回绕。 - int64 类型输入值范围限制在 int32 可表示范围内([-2^31+1, 2^31-1]):这是因为 Kernel 内部使用 int32 作为 int64 的中间计算类型(详见下文 Kernel 实现分析),超出范围会产生截断导致的错误结果。
五、调用说明与完整示例
README 中给出了测试命令的调用方式,可参考 docs/zh/invocation/quick_op_invocation.md 中描述的 build.sh 流程进行算子调用测试。
| 目录 | 描述 |
|---|---|
| examples/test_aclnn_assign_sub.cpp | 通过 aclnn 调用的方式调用 AssignSub 算子。 |
5.1 完整可运行的 aclnn 调用示例
仓库提供的 test_aclnn_assign_sub.cpp 是一个完整的最小可运行示例,其主流程如下:
- 初始化环境:
aclInit(nullptr)初始化 ACL,aclrtSetDevice(deviceId)设置设备(示例中使用设备 0),aclrtCreateStream(&stream)创建流。 - 构造张量:示例构造
{4, 5}形状的 FLOAT 张量,var全部填充 10.0f,value全部填充 3.0f,预期输出为全 7.0f。CreateAclTensor模板函数完成 host 数据到 device 内存的搬运(aclrtMalloc+aclrtMemcpy),并按 ND 格式计算连续 strides 后通过aclCreateTensor创建aclTensor。 - 两段式调用:
uint64_t workspaceSize = 0; aclOpExecutor* executor = nullptr; ret = aclnnAssignSubGetWorkspaceSize(var, value, varOut, &workspaceSize, &executor); CHECK_RET(ret == ACLNN_SUCCESS, LOG_PRINT("aclnnAssignSubGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } ret = aclnnAssignSub(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACLNN_SUCCESS, LOG_PRINT("aclnnAssignSub failed. ERROR: %d\n", ret); return ret); ret = aclrtSynchronizeStream(stream);- 资源释放:依次
aclDestroyTensor销毁张量、aclrtFree释放 device 内存、aclDestroyOpExecutor销毁执行器、aclrtDestroyStream销毁流、aclrtResetDevice复位设备、aclFinalize结束 ACL。
需要包含的头文件为:
#include "acl/acl.h" #include "aclnnop/aclnn_assign_sub.h"5.2 编译与测试
- 算子的 CMake 构建入口位于 experimental/math/assign_sub/CMakeLists.txt,并挂接在 experimental/math/CMakeLists.txt 的算子集合中。
- Tiling 单测位于 tests/ut/op_host/test_assign_sub_tiling.cpp,测试用例
assign_sub_0使用{3, 5, 7, 9}的 INT8 ND 张量、64 核、262144 字节 UB 的模拟环境,断言 Tiling 结果为945 32 32(依次对应 totalNum=945、blockFactor=32、ubFactor=32)且 workspace 为 0。 - Kernel 侧数据生成脚本 tests/ut/op_kernel/assign_sub_data/gen_data.py 使用
{3, 5, 7, 9}形状的全 1 张量,按上述环绕规则生成 golden 数据,用于与 NPU 计算结果对比验证。
六、源码级实现原理
6.1 算子注册(OpDef)
op_host/assign_sub_def.cpp 通过继承OpDef并在构造函数中声明输入输出端口完成算子注册,最后由OP_ADD(AssignSub)注册到算子库。三个端口均声明为REQUIRED,说明 var、value、var_out 在调用时缺一不可。
6.2 Shape 推导(InferShape / InferDataType)
op_host/assign_sub_infershape.cpp 中:
InferShapeAssignSub:校验 var 与 value 的维度数和各维大小完全一致,然后把varShape整体赋给输出,即*outShape = *varShape。这也从图编译层面保证了"输出 shape 与 var 一致"的规格。InferDataTypeAssignSub:context->SetOutputDataType(0, context->GetInputDataType(0)),将输出的数据类型直接继承自输入 var。
6.3 Tiling 策略
op_host/assign_sub_tiling.cpp 实现了 Tiling 函数AssignSubTilingFunc,核心思路如下:
- 获取平台信息:通过
GetPlatformInfo读取 AIV 核数(GetCoreNumAiv)与 UB 内存大小(GetCoreMemSize),用于后续并行度与片上空间的计算。 - 按数据类型选择调度模式:
GetDtypeInfo为七种数据类型分别生成 TilingKey(ASSIGNSUB_TPL_SCH_MODE_0~MODE_6),并给出对应的dtypeSize与perElemBytes。其中 INT8/UINT8 因需在计算中使用 half 中间类型并做模 256 处理,额外计入2 * sizeof(uint16_t)的临时缓冲区开销;BF16 计入2 * sizeof(float);INT64 计入2 * sizeof(int32_t)。 - 计算三级因子(写入 op_kernel/assign_sub_tiling_data.h 定义的
AssignSubTilingData结构):totalNum:输入总元素数(由存储 shape 的元素个数得到);blockFactor:每个核承担的元素块大小,由CeilDiv(totalNum, coreNum)向上对齐到alignNum(由 UB 块大小GetUbBlockSize除以 dtypeSize 得到)算出,并做 512 字节的粗粒度对齐尝试(COARSE_ALIGN_BYTES)以提升 GM 访问效率;ubFactor:单次流水(一次 CopyIn/Compute/CopyOut 循环)处理的元素数,由ubSize / perElemBytes向下对齐到alignNum得到,且不超过 blockFactor。
- 设置执行参数:
SetBlockDim(usedCoreNum)设置实际使用的核数,SetTilingKey(tilingKey)让 Kernel 侧按数据类型模板展开。
从单测 tests/ut/op_host/test_assign_sub_tiling.cpp 的期望值945 32 32可以看到:945 个元素被划分为每个核 32 个元素的块,每个核内再按 32 个元素为一片(ubFactor)进行多轮流水处理。
6.4 AscendC Kernel 实现
op_kernel/assign_sub.cpp 使用if constexpr按 TilingKey(即数据类型)展开模板,实例化NsAssignSub::AssignSub<half/int8_t/float/int32_t/uint8_t/bfloat16_t/int64_t>,并统一调用op.Init(...)与op.Process()。
op_kernel/assign_sub.h 中的核心设计:
- 流水结构:
TPipe配合三个TQue(输入 var、输入 value、输出,各 2 个 buffer)构成双缓冲流水;Process()按ubFactor分片循环,每轮依次执行CopyIn(GM→UB,非对齐场景使用DataCopyPad)、Compute、CopyOut(UB→GM),通过EnQue/DeQue实现生产者-消费者同步。 - DtypeTrait 特化:每种类型定义中间计算类型
ComputeT与策略标志:- FLOAT16/FLOAT/INT32:直接
Sub,无需类型转换; - INT8/UINT8:先
Cast到 half 做减法,再经ShiftLeft/ShiftRight的 8 位算术移位实现模 256 环绕(对应 README 中的溢出约束),最后转回原类型; - BF16:提升到 float 做减法,结果用
CAST_RINT舍入回 BF16; - INT64:使用 int32 作为中间计算类型(源码注释明确说明该设计),因此输入值必须限制在 int32 可表示范围内,超出会产生截断错误——这与 README 的 int64 范围约束完全对应。
- FLOAT16/FLOAT/INT32:直接
6.5 调度模式的模板参数化
op_kernel/assign_sub_tiling_key.h 通过ASCENDC_TPL_ARGS_DECL与ASCENDC_TPL_SEL宏将schMode(0~6,对应七种数据类型)声明为编译期模板参数,使 Kernel 在编译时即可针对每种类型展开出最合适的指令序列,避免了运行时的类型分支开销。
七、贡献说明
| 贡献者 | 贡献方 | 贡献算子 | 贡献时间 | 贡献内容 |
|---|---|---|---|---|
| Xzz | 西工大智能感知交互实验室 | AssignSub | 2026/07/12 | 新增AssignSub算子 |
八、小结
AssignSub 是 ops-math 中结构清晰、规格精简的逐元素减法算子:功能上实现var - value并输出到独立张量;实现上由 assign_sub_def.cpp 完成算子注册、assign_sub_infershape.cpp 完成 shape/数据类型推导、assign_sub_tiling.cpp 完成多核与 UB 双缓冲调度、assign_sub.h 完成按类型的流水计算;使用上通过aclnnAssignSubGetWorkspaceSize+aclnnAssignSub两段式接口即可在 Atlas A2/A3 系列产品上完成调用。需要注意三点规格限制:不支持 broadcast、仅支持 ND 格式、int8/uint8 按模 256 环绕以及 int64 值域受 int32 中间类型限制。掌握该算子的调用范式后,可以类推到 ops-math 中其他结构相近的 element-wise 算子。
【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考