ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

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

2026/9/19 18:17:19 拓冰建站 浏览量
CANN ops-math Sign 符号算子解析:功能、参数约束与两段式 aclnnSign 调用实践 CANN ops-math Sign 符号算子解析功能、参数约束与两段式 aclnnSign 调用实践【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-mathSign符号函数算子按元素提取 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负数输出 −10 输出 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输出记为resultop_api 侧的实现op_api/aclnn_sign.cpp最终会调用 l0 层算子l0op::Sign完成逐元素计算与 README 中的公式一一对应。参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensorinput输入待取符号的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 必须严格保持一致。该约束由两条路径共同保证infershape 推导sign_infershape.cpp 直接执行*yShape *xShape输出 shape 与输入完全一致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.mdworkspace是除输入/输出外算子在 NPU 上完成计算所需的临时内存workspaceSize表示其大小第一段接口负责入参校验、构建算子执行器并计算出 workspace 大小第二段接口负责将任务下发到指定 stream 上执行第二段接口aclnnSign(...)不能重复调用同一个 executor 只能执行一次重复调用会出现异常。aclnnSignGetWorkspaceSize 参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensorself输入待进行sign计算的入参公式中的self无FLOAT、FLOAT16、INT32、INT16、BFLOAT16ND1-8√result输出待进行sign计算的出参公式中的result无FLOAT、FLOAT16、INT32、INT16、BFLOAT16ND1-8√workspaceSize输出返回需要在Device侧申请的workspace大小-----executor输出返回op执行器包含了算子计算流程-----第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的tensor是空指针ACLNN_ERR_PARAM_INVALID161002self 和 result 的数据类型、数据格式不在支持范围之内或数据维度超过 8 维或数据形状不一致上述校验逻辑在源码中有明确对应CheckParamsaclnn_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::vectorint64_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::vectorT hostData, const std::vectorint64_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::vectorint64_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::vectorint64_t selfShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* out nullptr; std::vectorint16_t selfHostData {-3, -1, 0, 1, 2, -5, 0, 6}; std::vectorint16_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::vectorint16_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_castint32_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; }示例代码的运行流程可归纳为七个步骤初始化aclInit→aclrtSetDevice→aclrtCreateStream构造 Tensor在 Device 侧申请内存、拷入 Host 数据用aclCreateTensor创建self与out两个 aclTensor注意输出 Tensor 的数据类型与 shape 必须与输入一致两段式调用先aclnnSignGetWorkspaceSize拿到 workspaceSize 与 executor按需aclrtMalloc申请 workspace再aclnnSign下发计算同步aclrtSynchronizeStream等待任务完成取回结果aclrtMemcpyDEVICE_TO_HOST把输出拷回 Host 并打印释放 TensoraclDestroyTensor资源回收释放 Device 内存、销毁 stream、aclrtResetDevice、aclFinalize。若想使用 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 实现aclnnSignGetWorkspaceSize与aclnnSign两个对外接口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.cppInferShapeSign将输入 shape 直接赋给输出*yShape *xShape保证两者一致op_host/sign_tiling.cppSignTilingFunc负责切分计算任务核心思路是通过PlatformAscendC获取当前平台的 UB 内存大小与 AICore 核数依据输入数据类型BF16 与其他类型分别使用 9 / 5 个 UB 分块配额计算出每个核可承载的tileDataNum按 32B 对齐的输入长度在多个核之间均衡分配数据块区分大核/小核以及尾部残块tailBlockNum结果写入SignTilingDataop_kernel/sign_tiling_data.h最后通过SetTilingKey与SetBlockDim设置 kernel 的分发参数。kernel 层逐元素符号计算op_kernel/sign.h 中的KernelSignTYPE_X使用 AscendC 编程框架实现采用经典的 CopyIn → Compute → CopyOut 流水线双 buffer、队列深度 1逐 tile 处理数据Compute 阶段按类型分支FLOAT16 / FLOAT直接调用向量指令AscendC::SignINT32 / 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 仿真tikicpulibICPU_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),仅供参考