CANN ops-nn 算子开发指南:aclnnHardsigmoid 与 aclnnInplaceHardsigmoid 两段式接口详解
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
HardSigmoid 是神经网络中常用的分段线性激活函数,用于把输入张量逐元素映射到 [0, 1] 区间,同时保留线性过渡带。本文以 CANN ops-nn 仓库中 HardSigmoid 算子文档 为骨架,结合该算子在仓库内的源码实现(op_api、op_host、op_kernel、tests),系统讲解aclnnHardsigmoid与aclnnInplaceHardsigmoid两个 aclnn 接口的产品支持情况、计算公式、函数原型、参数约束、返回码、调用示例,以及两段式接口背后的执行流程与 kernel 实现原理,帮助你直接在昇腾 NPU 上完成 HardSigmoid 算子的接入与调优。
一、接口概览与使用场景
aclnnHardsigmoid与aclnnInplaceHardsigmoid是 CANN 算子库(ops-nn)为 HardSigmoid 激活函数提供的两个 aclnn 接口,二者实现相同的数学功能,区别仅在于计算结果的落点:
- aclnnHardsigmoid:需要调用方新建一个输出张量对象(out),算子将计算结果写入 out 指向的 device 内存,原始输入 self 保持不变。
- aclnnInplaceHardsigmoid:无需新建输出张量,算子直接在输入 self 的 device 内存上原地改写,结果覆盖原输入。
两者的选择依据是业务场景:如果后续计算不再需要原始输入,推荐使用 inplace 版本以省去一份输出张量的内存申请;如果原始输入还需复用,则使用非 inplace 版本。从源码实现看,inplace 版本本质上是把 out 直接指向 self:在 op_api/aclnn_hardsigmoid.cpp 中,aclnnInplaceHardsigmoidGetWorkspaceSize内部通过auto out = const_cast<aclTensor*>(self);复用 self 作为输出,再委托给aclnnHardsigmoidGetWorkspaceSize完成工作区计算,因此两个接口的计算流程完全一致。
二、产品支持情况
根据算子文档,HardSigmoid 的 aclnn 接口在以下昇腾产品上的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 支持(数据类型仅 FLOAT、FLOAT16、INT32,不支持 BFLOAT16) |
仓库中 HardSigmoid 算子 README 给出的支持面与此一致,并补充了 Ascend 950、Atlas 200I/500 A2 等产品同样支持该算子。需要留意的是,Atlas 训练系列产品(910)在 aclnn 接口层面不支持 BFLOAT16,这一差异在 op_api/aclnn_hardsigmoid.cpp 中也有明确体现:源码分别定义了ASCEND910_DTYPE_SUPPORT_LIST(FLOAT、FLOAT16、INT32)与ASCEND910B_DTYPE_SUPPORT_LIST(FLOAT、FLOAT16、INT32、BF16)两张数据类型支持表,运行时根据具体芯片平台选择对应支持列表。
三、功能说明与计算公式
HardSigmoid 属于激活函数变种,对输入张量 self 逐元素计算,输出张量与输入形状相同。核心计算公式为:
$$ Hardsigmoid(self)=clip(\alpha \times self + \beta, 0, 1) $$
默认 $\alpha=\frac{1}{6}$,$\beta=\frac{1}{2}$,等价于如下分段函数:
$$ Hardsigmoid(self)=\begin{cases} 1, & if(self\gt3) \ 0, & if(self\le-3) \ \frac{self}{6} + \frac{1}{2}, & otherwise \end{cases} $$
即:大于 3 的元素输出恒为 1,小于等于 -3 的元素输出恒为 0,中间的输入做一次仿射变换后截断到 [0,1]。当输入为 INT32 时,中间计算结果会被截断(truncate)后再转换为 INT32 输出,因此 INT32 下结果只有 0 和 1 两个可能取值(x/6 + 1/2在整数域内落在 (0,1) 之外时被截断)。这一行为在测试基准 tests/assets/golden.py 中通过torch.trunc复现,与 kernel 的实现保持一致。
图模式下的算子原型 op_graph/hard_sigmoid_proto.h 同样声明了可选的alpha(默认 0.16666666)与beta(默认 0.5)两个属性,与 aclnn 接口的默认参数完全对应;该原型在注释中明确指出与 PyTorch 的torch.nn.Hardsigmoid兼容。
四、两段式接口架构与函数原型
与 CANN 其他 aclnn 算子一致,HardSigmoid 采用两段式接口(详见仓库文档 两段式接口说明):先调用第一段GetWorkspaceSize接口完成入参校验、计算流程构建并返回所需 workspace 大小与执行器,再调用第二段执行接口真正在 NPU 上启动计算。完整流程为:
aclnnHardsigmoidGetWorkspaceSize(self, out, &workspaceSize, &executor) ↓ 根据 workspaceSize 在 Device 侧 aclrtMalloc 申请工作区 aclnnHardsigmoid(workspace, workspaceSize, executor, stream)4.1 aclnnHardsigmoid 系列原型
aclnnStatus aclnnHardsigmoidGetWorkspaceSize( const aclTensor* self, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnHardsigmoid( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)4.2 aclnnInplaceHardsigmoid 系列原型
aclnnStatus aclnnInplaceHardsigmoidGetWorkspaceSize( const aclTensor* self, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnInplaceHardsigmoid( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)从 op_api/aclnn_hardsigmoid.h 的头文件声明可以看到,四个接口均以ACLNN_API导出,@domain标注为aclnn_ops_infer,头文件中还以 mermaid 图形式给出了 API 的基本计算路径:self → L0::Contiguous → L0::Hardsigmoid → L0::ViewCopy → out,即先把非连续输入规整为连续张量,执行 HardSigmoid 计算,再把结果拷回可能非连续的输出张量。
五、GetWorkspaceSize 参数详解与校验逻辑
5.1 aclnnHardsigmoidGetWorkspaceSize 参数表
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 激活函数输入,公式中的 self | self 与 out 的 shape 一致;支持空 Tensor | FLOAT、FLOAT16、INT32、BFLOAT16 | ND | 0-8 | √ |
| out(aclTensor*) | 输出 | 激活函数输出,公式中的 Hardsigmoid(self) | self 与 out 的数据类型和 shape 一致 | FLOAT、FLOAT16、INT32、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
注:Atlas 训练系列产品上仅支持 FLOAT、FLOAT16、INT32 三种数据类型(不支持 BFLOAT16)。
5.2 入参校验与返回码
第一段接口会完成入参校验,校验逻辑在 op_api/aclnn_hardsigmoid.cpp 的CheckParams中实现,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self 或 out 为空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 或 out 的数据类型和数据格式不在支持范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的数据类型与输出 out 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的 shape 与输出 out 的 shape 不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 或 out 的 shape 超过 8 维 |
源码中的校验链路与文档一一对应:CheckNotNull2Tensor负责空指针检查(对应 161001);CheckDtypeValid按芯片平台选择ASCEND910_DTYPE_SUPPORT_LIST或ASCEND910B_DTYPE_SUPPORT_LIST校验数据类型(对应 161002 第一行);CheckShape通过OP_CHECK_MAX_DIM限制最大 8 维,并通过OP_CHECK_SHAPE_NOT_EQUAL校验 self/out shape 一致(对应 161002 后两行);随后再逐项比对 self 与 out 的数据类型、存储格式是否一致。
5.3 aclnnInplaceHardsigmoidGetWorkspaceSize 参数表
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 公式中的输入 self 和输出 Hardsigmoid(self)(原地读写) | 支持空 Tensor | FLOAT、FLOAT16、INT32、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
注:Atlas 训练系列产品上仅支持 FLOAT、FLOAT16、INT32。
inplace 版本的校验错误场景:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self 为空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的数据类型和数据格式不在支持范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的 shape 超过 8 维 |
由于 inplace 版本没有独立的 out 张量,因此校验项比非 inplace 版本少了“类型/格式/shape 不一致”的检查,从源码看它完全复用了aclnnHardsigmoidGetWorkspaceSize的校验流程(self 同时充当 out)。
5.4 第二段执行接口参数
aclnnHardsigmoid与aclnnInplaceHardsigmoid的四个参数含义相同:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段 GetWorkspaceSize 接口获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
第二段接口内部调用框架统一的CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成计算(见 op_api/aclnn_hardsigmoid.cpp),返回值同样是aclnnStatus,具体返回码含义参见仓库文档 aclnn 返回码说明。
六、空 Tensor 与确定性约束
- 空 Tensor 支持:当 self 为空张量时,第一段接口直接返回
workspaceSize = 0并跳过实际计算,无需申请工作区。这一逻辑在 op_api/aclnn_hardsigmoid.cpp 中以self->IsEmpty()分支实现。 - 确定性计算:
aclnnHardsigmoid与aclnnInplaceHardsigmoid默认采用确定性实现,即相同输入在多次执行中得到逐位一致的输出,便于调试与结果复现。
七、完整调用示例(C++)
以下示例代码摘自算子文档,展示了从设备初始化、张量构造、两段式调用到结果回拷与资源释放的完整流程,编译与运行方法参考仓库文档 编译与运行样例。
7.1 aclnnHardsigmoid 调用示例
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_hardsigmoid.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根据自己的需要处理 CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 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<float> selfHostData = {-4, -3, -2, 0, 1, 2, 4, 5}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API // 调用aclnnHardsigmoid第一段接口 uint64_t workspaceSize = 0; aclOpExecutor* executor; ret = aclnnHardsigmoidGetWorkspaceSize(self, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardsigmoidGetWorkspaceSize 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;); } // 调用aclnnHardsigmoid第二段接口 ret = aclnnHardsigmoid(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardsigmoid 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侧,需要根据具体API的接口定义修改 auto size = GetShapeSize(outShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float), 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: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }以文档中的输入{-4, -3, -2, 0, 1, 2, 4, 5}为例,代入公式可得输出依次为{0, 0, 1/6, 1/2, 2/3, 5/6, 1, 1},其中 -4 与 -3 落在截断区间输出 0,4 与 5 大于 3 输出 1,可以据此在本地验证算子行为。
7.2 aclnnInplaceHardsigmoid 调用示例
inplace 版本与 7.1 的主要差异在于:不需要创建 out 张量,第一段接口只传 self,第二段执行后结果直接写回 self 的 device 内存,回拷时读取的也是selfDeviceAddr,最终只释放 self 相关资源。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_hardsigmoid.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根据自己的需要处理 CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 std::vector<int64_t> selfShape = {4, 2}; void* selfDeviceAddr = nullptr; aclTensor* self = nullptr; std::vector<float> selfHostData = {-4, -3, -2, 0, 1, 2, 4, 5}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API // 调用aclnnInplaceHardsigmoid第一段接口 uint64_t inplaceWorkspaceSize = 0; aclOpExecutor* inplaceExecutor; ret = aclnnInplaceHardsigmoidGetWorkspaceSize(self, &inplaceWorkspaceSize, &inplaceExecutor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceHardsigmoidGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* inplaceWorkspaceAddr = nullptr; if (inplaceWorkspaceSize > 0) { ret = aclrtMalloc(&inplaceWorkspaceAddr, inplaceWorkspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;); } // 调用aclnnInplaceHardsigmoid第二段接口 ret = aclnnInplaceHardsigmoid(inplaceWorkspaceAddr, inplaceWorkspaceSize, inplaceExecutor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceHardsigmoid 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侧,需要根据具体API的接口定义修改 auto size = GetShapeSize(selfShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), selfDeviceAddr, size * sizeof(float), 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: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); if (inplaceWorkspaceSize > 0) { aclrtFree(inplaceWorkspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }7.3 示例代码执行要点
- 两段式接口缺一不可:必须先调用 GetWorkspaceSize 拿到
workspaceSize与executor,再申请工作区内存、调用执行接口;workspaceSize为 0 时无需申请工作区(空 Tensor 场景即为 0)。 aclrtMalloc使用ACL_MEM_MALLOC_HUGE_FIRST申请大页内存;aclrtMemcpy分别承担 host→device 的输入搬入与 device→host 的结果搬出。aclCreateTensor以 ND 格式创建张量并显式传入 strides,支持构造非连续张量;示例代码计算了连续张量的 strides,若构造切片视图则需按实际 stride 填写。- 资源释放顺序与申请顺序相反:先
aclDestroyTensor,再aclrtFree各 device 内存,最后销毁 stream、复位 device 并aclFinalize。 - 仓库 examples 目录 提供了可直接运行的对应样例(
test_aclnn_hard_sigmoid.cpp、test_aclnn_inplace_hard_sigmoid.cpp),另有图模式构图样例arch35/test_geir_hard_sigmoid.cpp可供参考。
八、源码级原理:从 aclnn 到底层 Kernel
8.1 Host 侧算子定义与 shape 推导
在 op_host/hard_sigmoid_def.cpp 中注册了名为HardSigmoid的算子定义:输入input_x与输出output_y均支持 FLOAT、FLOAT16、BF16、INT32 四种类型与 ND 格式,并声明了可选的alpha(默认 1/6)与beta(默认 0.5)属性。算子配置开启动态 shape 支持(DynamicShapeSupportFlag(true))与动态 rank 支持(DynamicRankSupportFlag(true)),同时通过DynamicCompileStaticFlag(true)支持静态编译,为昇腾 950(ascend950)平台配置了 AICore 后端。
shape/类型推导在 op_host/hard_sigmoid_infershape.cpp 中实现:InferShape4HardSigmoid直接将输入 shape 拷贝为输出 shape,InferDataType4HardSigmoid将输入数据类型直接赋给输出,印证了文档中“输出与输入 shape、数据类型一致”的约束。
8.2 第一段接口的计算图构建
aclnnHardsigmoidGetWorkspaceSize并非只做参数校验,它同时完成了计算图的构建与 workspace 大小估算(op_api/aclnn_hardsigmoid.cpp):
- 创建
OpExecutor(CREATE_EXECUTOR()),用于承载后续 L0 算子节点; - 执行
CheckParams完成入参校验; - 空 Tensor 直接返回
workspaceSize=0; - 调用
l0op::Contiguous(self)把可能非连续的 self 规整为连续张量; - 调用
l0op::HardSigmoid(selfContiguous)构造 HardSigmoid 计算节点; - 调用
l0op::ViewCopy(hardsigmoidOpOut, out)将计算结果写回用户提供的 out(支持非连续 out); - 从 executor 汇总得到
workspaceSize并ReleaseTo释放给调用方。
第二段接口则通过框架统一的CommonOpExecutorRun把这张图在指定 stream 上真正下发执行。这也解释了为什么“非连续 Tensor”被文档标注为支持:非连续输入在内部被 Contiguous 归一化,非连续输出由 ViewCopy 承接。
8.3 AICore Kernel 计算实现
kernel 侧实现在 op_kernel/arch35/hard_sigmoid.cpp 中,采用 EnQue/DeQue 三级流水(CopyIn / Compute / CopyOut)架构,跨管道同步由 Queue 自动管理,并按数据类型分三条计算路径:
- FLOAT:原生 fp32 计算,
y = Muls(x, alpha) → Adds(y, beta) → Mins(y, 1.0) → Maxs(y, 0.0),即 affine 变换后按上下界截断; - FLOAT16/BFLOAT16:先 Cast 升到 fp32 计算 affine 变换,再按目标类型舍入降回(bf16 用
CAST_ROUND,fp16 用CAST_RINT),最后在原生 dtype 域做 clamp; - INT32:先用
CAST_RINT升 fp32 计算,再经 affine + clamp,最后用CAST_TRUNC截断转回 INT32——与文档“INT32 计算结果截断后转换”的描述一致。
tiling 参数在 op_kernel/arch35/hard_sigmoid_tiling_data.h 中定义,包括totalElements(元素总数)、blockFactor(单核元素数)、ubFactor(单次 UB 搬运/计算元素数)以及alpha、beta,其中缓冲深度HARD_SIGMOID_BUFFER_NUM = 2(双缓冲)由 Host/Kernel 共享头约束,确保 tiling 推导的 UB 预算与 kernel 实际占用一致。
8.4 测试与正确性验证
仓库为 HardSigmoid 提供了完整的测试支撑:
- 单测:tests/ut/op_host/op_api/test_aclnn_hardsigmoid.cpp 覆盖 aclnn 接口层的调用与返回码;tests/ut/op_host/test_hard_sigmoid_infershape.cpp 覆盖 shape/类型推导;tests/ut/op_kernel/test_hard_sigmoid.cpp 覆盖 kernel 计算。
- ST 用例:tests/st/aclnnHardsigmoid/atk_aclnnHardsigmoid.json 与 executor_aclnnHardsigmoid.py 提供端到端测试入口。
- golden 基准:tests/assets/golden.py 用 PyTorch 实现参考计算:浮点类型用
torch.where按 0/1 边界截断仿射结果,INT32 额外torch.trunc后转回,与 kernel 语义对齐;其输入注入逻辑还专门构造了-3、3、±inf、nan、±tiny等边界值与特殊值用例(如hard_sigmoid_fp32_special),用于验证分段边界与特殊输入的处理。
九、与其他调用方式的关系
除了 aclnn 两段式接口,HardSigmoid 算子还支持图模式调用:
- 图模式算子 IR:op_graph/hard_sigmoid_proto.h 以
REG_OP注册了HardSigmoid算子的输入input_x、输出output_y以及alpha、beta两个属性,图模式下可通过算子 IR 直接构图;op_graph/hard_sigmoid_graph_infer.cpp 提供了图模式的数据类型推导。调用示例见 examples/arch35/test_geir_hard_sigmoid.cpp。 - ONNX 前端适配:framework/hard_sigmoid_onnx_plugin.cpp 提供 ONNX 框架的算子插件映射,可将 ONNX 图中的 HardSigmoid 节点转换为本算子。
无论哪种调用方式,最终都落到 op_kernel/arch35/hard_sigmoid.cpp 中的 AICore kernel 执行,保证计算结果的一致性。
十、常见问题与使用建议
- 报 161001(ACLNN_ERR_PARAM_NULLPTR):检查 self(以及非 inplace 版本的 out)是否为有效指针,通常是在调用前未创建 aclTensor。
- 报 161002(ACLNN_ERR_PARAM_INVALID):依次排查数据类型是否在支持列表内(Atlas 训练系列不支持 BF16)、self 与 out 的数据类型/格式/shape 是否一致、张量维度是否超过 8 维。
- 是否需要申请 workspace:以第一段接口返回的
workspaceSize为准,为 0 时无需申请;空 Tensor 输入时返回 0,属于正常情况。 - inplace 版本的内存语义:执行后 self 的原数据被覆盖,如果调用方仍持有对原输入的引用,需自行保证后续不再使用旧值。
- INT32 输入的语义:结果会先截断再转 INT32,最终输出仅包含 0/1,与浮点版本的连续输出行为不同,精度对齐测试请参考 tests/assets/golden.py 中的
binary_equal校验标准。
参考文档与源码索引
- 算子接口文档:activation/hard_sigmoid/docs/aclnnHardsigmoid&aclnnInplaceHardsigmoid.md
- 算子 README:activation/hard_sigmoid/README.md
- aclnn 接口声明与实现:op_api/aclnn_hardsigmoid.h、op_api/aclnn_hardsigmoid.cpp
- Host 算子定义与推导:op_host/hard_sigmoid_def.cpp、op_host/hard_sigmoid_infershape.cpp
- Kernel 实现与 tiling 定义:op_kernel/arch35/hard_sigmoid.cpp、op_kernel/arch35/hard_sigmoid_tiling_data.h
- 图模式算子 IR:op_graph/hard_sigmoid_proto.h
- 调用样例:examples/test_aclnn_hard_sigmoid.cpp、examples/test_aclnn_inplace_hard_sigmoid.cpp
- 测试基准与单测:tests/assets/golden.py、tests/ut/op_host/op_api/test_aclnn_hardsigmoid.cpp
- 通用机制文档:两段式接口说明、aclnn 返回码说明、编译与运行样例
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考