CANN ops-math Sign 符号算子解析:功能、参数约束与两段式 aclnnSign 调用实践
2026/9/19 18:16:53 网站建设 项目流程

CANN ops-math Sign 符号算子解析:功能、参数约束与两段式 aclnnSign 调用实践

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

Sign(符号函数)算子按元素提取 Tensor 的符号信息,将输入压缩为{1, 0, -1}三值集合,是网络量化、梯度裁剪(clip)、比较逻辑与数据分布统计中高频使用的基础算子。本文基于 CANN ops-math 仓库中 experimental/math/sign/README.md 及其配套源码,完整讲解 Sign 算子的功能定义、产品支持范围、参数约束、两段式 aclnnSign 接口调用流程,并结合 op_host / op_kernel / op_api 源码说明算子从图编译到 NPU 上执行的完整链路。读完本文,你将能够独立编写、编译并验证一个可运行的 aclnnSign 单算子调用程序,并理解其底层 tiling 与 kernel 实现原理。

产品支持情况

Sign 算子已在以下昇腾产品上验证可用(详见 experimental/math/sign/README.md 的产品支持情况表格):

产品是否支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品
Atlas A2 训练系列产品 / Atlas A2 推理系列产品

与之对应,算子定义文件 op_host/sign_def.cpp 中通过AICore().AddConfig("ascend910b")AICore().AddConfig("ascend910_93")注册了 AICore 侧的适配配置,从编译侧印证了上述产品的支持范围。

功能说明

Sign 算子对输入 Tensor 逐元素取符号:正数输出 1,负数输出 −1,0 输出 0。计算公式如下:

$$ \text{out}{i}= \begin{cases} 1 & \text{if } \text{input}{i}>0 \ 0 & \text{if } \text{input}{i}=0 \ -1 & \text{if } \text{input}{i}<0 \end{cases} $$

接口文档 docs/aclnnSign.md 中对该功能有相同定义(其中输入记为self,输出记为result),op_api 侧的实现(op_api/aclnn_sign.cpp)最终会调用 l0 层算子l0op::Sign完成逐元素计算,与 README 中的公式一一对应。

参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
input输入待取符号的Tensor支持空TensorFLOAT、FLOAT16、BFLOAT16、INT32、INT16ND1-8
output输出与input同形状的符号结果数据类型、shape需与input一致FLOAT、FLOAT16、BFLOAT16、INT32、INT16ND同input

几点需要特别说明:

  • 数据类型与 shape 一致性:输入与输出的数据类型、shape 必须完全一致,这一点同时体现在 host 侧 sign_infershape.cpp 的*yShape = *xShape推导逻辑,以及 op_api 侧 aclnn_sign.cpp 的CheckShape校验中。
  • 非连续 Tensor 支持:op_api 在计算前会通过l0op::Contiguous将非连续输入转连续,计算完成后通过l0op::ViewCopy把结果写回非连续视图,因此 API 层面可以直接接收非连续 Tensor,无需用户手动做 contiguous。
  • 空 Tensor 支持:aclnn_sign.cpp 中对空 Tensor 提前返回,workspaceSize 置 0,不再下发 kernel。

说明:API 层 aclnn_sign.cpp 的DTYPE_SUPPORT_LIST中还包含 DOUBLE、INT64、COMPLEX64、COMPLEX128、BOOL 等类型——其中 BOOL 会先 Cast 成 INT32 计算后再转回 BOOL 输出(见同文件 L157-L177),其余超集类型需视具体昇腾架构(Ascend910 / Ascend910B / Ascend310P 等)与平台配置决定是否可用。面向 Atlas A2/A3 系列产品使用时,请以 README 表格中列出的 FLOAT、FLOAT16、BFLOAT16、INT32、INT16 为准。

约束说明

输入input和输出output的 Shape 必须严格保持一致。该约束由两条路径共同保证:

  1. infershape 推导:sign_infershape.cpp 直接执行*yShape = *xShape,输出 shape 与输入完全一致;
  2. API 参数校验:aclnn_sign.cpp 中的CheckShape通过OP_CHECK_SHAPE_NOT_EQUAL宏检查 self 与 result 的 shape,不一致时返回ACLNN_ERR_PARAM_INVALID(错误码 161002)。

调用说明

README 中给出了 Sign 算子的调用方式:通过aclnnSign接口调用,调用样例为 examples/test_aclnn_sign.cpp,接口定义详见 docs/aclnnSign.md。

调用方式调用样例说明
aclnn调用test_aclnn_sign.cpp通过 aclnnSign 接口方式调用Sign算子

两段式接口原型

aclnnSign遵循 CANN 单算子 API 的两段式接口规范:必须先调用第一段接口aclnnSignGetWorkspaceSize获取计算所需 workspace 大小及执行器,再调用第二段接口aclnnSign真正执行计算。两段接口的原型如下:

aclnnStatus aclnnSignGetWorkspaceSize( const aclTensor *self, const aclTensor *result, uint64_t *workspaceSize, aclOpExecutor **executor)
aclnnStatus aclnnSign( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)

关于两段式接口需要记住的关键点(详见 two_phase_api.md):

  • workspace是除输入/输出外,算子在 NPU 上完成计算所需的临时内存,workspaceSize表示其大小;
  • 第一段接口负责入参校验、构建算子执行器并计算出 workspace 大小;第二段接口负责将任务下发到指定 stream 上执行;
  • 第二段接口aclnnSign(...)不能重复调用,同一个 executor 只能执行一次,重复调用会出现异常。

aclnnSignGetWorkspaceSize 参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
self输入待进行sign计算的入参,公式中的selfFLOAT、FLOAT16、INT32、INT16、BFLOAT16ND1-8
result输出待进行sign计算的出参,公式中的resultFLOAT、FLOAT16、INT32、INT16、BFLOAT16ND1-8
workspaceSize输出返回需要在Device侧申请的workspace大小-----
executor输出返回op执行器,包含了算子计算流程-----

第一段接口会完成入参校验,出现以下场景时报错:

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的tensor是空指针
ACLNN_ERR_PARAM_INVALID161002self 和 result 的数据类型、数据格式不在支持范围之内;或数据维度超过 8 维;或数据形状不一致

上述校验逻辑在源码中有明确对应:CheckParams(aclnn_sign.cpp)依次完成空指针检查(ACLNN_ERR_PARAM_NULLPTR)、数据类型检查(CheckDtypeValid)与 shape 一致性检查(CheckShape),失败时返回ACLNN_ERR_PARAM_INVALID。更多返回码说明可参见 aclnn_return_code.md。

aclnnSign 参数说明

参数名输入/输出描述
workspace输入在Device侧申请的workspace内存地址
workspaceSize输入在Device侧申请的workspace大小,由第一段接口aclnnSignGetWorkspaceSize获取
executor输入op执行器,包含了算子计算流程
stream输入指定执行任务的Stream

调用示例代码

完整可运行的示例见 examples/test_aclnn_sign.cpp。下面给出核心调用流程(以 INT16 数据为例,示例中的输入{-3, -1, 0, 1, 2, -5, 0, 6}对应输出应为{-1, -1, 0, 1, 1, -1, 0, 1}):

#include <iostream> #include <vector> #include <cstdint> #include "acl/acl.h" #include "aclnnop/aclnn_sign.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shape_size = 1; for (auto i : shape) { shape_size *= i; } return shape_size; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor( const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor( shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. (固定写法)device/stream初始化,参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出 std::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> outShape = {4, 2}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; std::vector<int16_t> selfHostData = {-3, -1, 0, 1, 2, -5, 0, 6}; std::vector<int16_t> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_INT16, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_INT16, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnSign第一段接口 ret = aclnnSignGetWorkspaceSize(self, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSignGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } // 调用aclnnSign第二段接口 ret = aclnnSign(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSign failed. ERROR: %d\n", ret); return ret); // 4. (固定写法)同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 5. 获取输出的值,将device侧内存上的结果拷贝至host侧 auto size = GetShapeSize(outShape); std::vector<int16_t> resultData(size, 0); ret = aclrtMemcpy( resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %d\n", i, static_cast<int32_t>(resultData[i])); } // 6. 释放aclTensor aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例代码的运行流程可归纳为七个步骤:

  1. 初始化aclInitaclrtSetDeviceaclrtCreateStream
  2. 构造 Tensor:在 Device 侧申请内存、拷入 Host 数据,用aclCreateTensor创建selfout两个 aclTensor(注意输出 Tensor 的数据类型与 shape 必须与输入一致);
  3. 两段式调用:先aclnnSignGetWorkspaceSize拿到 workspaceSize 与 executor,按需aclrtMalloc申请 workspace,再aclnnSign下发计算;
  4. 同步aclrtSynchronizeStream等待任务完成;
  5. 取回结果aclrtMemcpy(DEVICE_TO_HOST)把输出拷回 Host 并打印;
  6. 释放 TensoraclDestroyTensor
  7. 资源回收:释放 Device 内存、销毁 stream、aclrtResetDeviceaclFinalize

若想使用 FLOAT 数据类型,只需把CreateAclTensor的模板参数改为float、数据改为{1, 2, 3, 4, 5, 6, 7, 8},并将aclDataType::ACL_INT16换成aclDataType::ACL_FLOAT即可(op_api 单测 tests/ut/op_api/test_aclnn_sign.cpp 即使用{-1.023, 2023.08, 3.14, 10987654321.0}这类浮点输入验证)。具体编译与运行样例的方法,请参考 compile_and_run_sample.md。

从 API 到 Kernel 的实现链路

了解 Sign 算子在仓库中的实现层次,有助于理解上面的调用流程背后发生了什么。

op_api 层:参数校验与计算编排

op_api/aclnn_sign.cpp 实现aclnnSignGetWorkspaceSizeaclnnSign两个对外接口:

  • aclnnSignGetWorkspaceSize依次执行:创建 OpExecutor →CheckParams参数校验(空指针、dtype、shape)→ 空 Tensor 短路处理 → 非连续输入转连续(l0op::Contiguous)→ 调用 l0 算子l0op::Sign构建计算图 → 输出非连续时用l0op::ViewCopy回写 → 通过GetWorkspaceSize汇总临时内存需求;
  • aclnnSign通过CommonOpExecutorRun把已编排好的执行器任务下发到指定 stream。

l0 算子层:AICore / AICPU 双路径调度

op_api/sign.cpp 中注册了l0op::Sign

  • 对于 AICore 支持的数据类型(FLOAT、FLOAT16、INT16、INT32、BF16 等),走SignAiCore,通过ADD_TO_LAUNCHER_LIST_AICORE下发到 AICore 执行;
  • 其余情况走SignAiCpu,通过ADD_TO_LAUNCHER_LIST_AICPU使用 AICPU 执行器兜底,保证算子在不同平台上的可用性。

host 侧:shape 推导与 tiling 计算

  • op_host/sign_infershape.cpp:InferShapeSign将输入 shape 直接赋给输出(*yShape = *xShape),保证两者一致;
  • op_host/sign_tiling.cpp:SignTilingFunc负责切分计算任务,核心思路是:
    • 通过PlatformAscendC获取当前平台的 UB 内存大小与 AICore 核数;
    • 依据输入数据类型(BF16 与其他类型分别使用 9 / 5 个 UB 分块配额)计算出每个核可承载的tileDataNum
    • 按 32B 对齐的输入长度在多个核之间均衡分配数据块(区分"大核/小核"以及尾部残块tailBlockNum),结果写入SignTilingData(op_kernel/sign_tiling_data.h);
    • 最后通过SetTilingKeySetBlockDim设置 kernel 的分发参数。

kernel 层:逐元素符号计算

op_kernel/sign.h 中的KernelSign<TYPE_X>使用 AscendC 编程框架实现,采用经典的 CopyIn → Compute → CopyOut 流水线(双 buffer、队列深度 1)逐 tile 处理数据,Compute 阶段按类型分支:

  • FLOAT16 / FLOAT:直接调用向量指令AscendC::Sign
  • INT32 / INT16:用Maxs(x, -1)后接Mins(y, 1)的组合把数据钳制到[-1, 1]区间,等价实现符号提取;
  • BFLOAT16:先 Cast 成 FLOAT 做Sign,再 Cast 回 BFLOAT16 输出,规避 BF16 向量指令的精度限制。

入口 kernel 定义在 op_kernel/sign.cpp,通过REGISTER_TILING_DEFAULT(SignTilingData)读取 tiling 数据后调用op.Init(...)op.Process()完成计算。

测试验证

仓库为 Sign 算子提供了完整的三层测试:

  • op_api 单测:tests/ut/op_api/test_aclnn_sign.cpp 验证aclnnSignGetWorkspaceSize的参数校验与 workspace 计算;
  • host 侧 tiling 单测:tests/ut/op_host/test_sign_tiling.cpp 直接构造TilingContextPara验证 tiling 计算正确性;
  • kernel 侧测试:tests/ut/op_kernel/test_sign.cpp 在 CPU 仿真(tikicpulib+ICPU_RUN_KF)环境下运行 kernel,并用 gen_data.py 生成输入、compare_data.py 比对输出,覆盖 float16、float32 等典型场景。

贡献说明

该算子在开源仓库中的演进记录如下(来自 README 的贡献说明):

贡献者贡献方贡献算子贡献时间贡献内容
hth810个人开发者Sign2025/12/12Sign算子适配开源仓
hth810个人开发者Sign2026/5/12Sign算子添加int16支持

INT16 支持同时体现在算子定义(sign_def.cpp 中ge::DT_INT16的声明)、kernel 的int16_t分支(sign.h)以及示例程序的ACL_INT16用法中,三者相互印证。

小结

Sign 算子功能简单但链路完整:从 README 定义的逐元素符号语义,到 op_api 的参数校验与编排、host 侧 infershape 与 tiling 计算、kernel 侧多类型分支实现,再到三层测试用例,构成了 CANN ops-math 仓库中一个典型单算子从声明到落地的全流程范例。实际开发中,只需牢记两点:输入输出 shape 与 dtype 必须一致,以及aclnnSign 必须按"先 GetWorkspaceSize、后执行"的两段式顺序调用,即可快速完成该算子的接入与验证。

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询