CANN ops-nn HardSigmoidGrad 反向算子深度解析:ACLNN 接口、数学原理与 NPU 实现
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
HardSigmoidGrad 是 CANN 神经网络算子库 ops-nn 中 HardSigmoid 激活函数的反向算子,用于在 NPU 上计算反向传播梯度。本文基于 HardSigmoidGrad README 与仓库源码,完整讲解其产品支持范围、数学公式、入参规格、aclnnHardsigmoidBackward 两段式调用流程及 Kernel 层的数值精度设计,帮助读者在 Ascend 平台上正确、高效地使用与验证该算子。
产品支持情况
HardSigmoidGrad 算子在不同 Ascend 产品上的支持情况如下表所示(数据来源:activation/hard_sigmoid_grad/README.md):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | √ |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | √ |
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | √ |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | √ |
| Atlas 训练系列产品 | √ |
需要说明的是,在不同产品上支持的数据类型存在差异:其中Atlas 推理系列产品、Atlas 训练系列产品(即 910/310P 平台)的数据类型仅支持FLOAT16、FLOAT;而 Ascend 950 系列、Atlas A2/A3 系列在接口层同时支持BFLOAT16、FLOAT16、FLOAT(对应实现见 aclnn_hardsigmoid_backward.cpp 中的ASCEND910_DTYPE_SUPPORT_LIST与ASCEND910B_DTYPE_SUPPORT_LIST两组支持列表)。
功能说明与计算公式
HardSigmoidGrad 是 HardSigmoid 前向算子的反向(backward)算子,其功能是计算反向传播的梯度 grad_input。
前向 HardSigmoid 的数学定义为(参见 HardSigmoid README):
$$ 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} $$
由于分段函数在中间线性区间的导数为 $\alpha=\frac{1}{6}$、在饱和区间导数为 0,因此反向算子 HardSigmoidGrad 的计算公式为(README 原文):
$$ HardsigmoidBackward(self, grad_output)= \begin{cases} grad_output \ast \alpha, & if(0 < \alpha \ast self + \beta < 1) \ 0, & otherwise \end{cases} $$
即:仅当 $\alpha \times self + \beta$ 落在开区间 $(0, 1)$ 内时,梯度才等于 grad_output 乘以 $\alpha$;否则梯度为 0。其中 $\alpha$ 固定为 $1/6$、$\beta$ 固定为 $0.5$,不对外暴露为可配置属性。
从实现层面看,该常量在算子定义(OpDef)中登记为可选属性并赋予默认值:alpha = 1.0f / 6.0f、beta = 0.5f,见 hard_sigmoid_grad_def.cpp。接口层在组装算子图时同样显式传入alpha = 1.0f / 6.0f、beta = 0.5f,并且源码注释特别强调:使用1.0f/6.0f而非字面量0.16666666f,是为了在边界点 x=-3.0 处与 PyTorch 的 hardsigmoid 语义严格对齐——1/6*(-3)+0.5 == 0严格成立,而0.16666666f*(-3)+0.5 ≈ 2.98e-08会"泄漏"进 mask=1 的区域导致边界处梯度错误(见 aclnn_hardsigmoid_backward.cpp)。
参数说明
HardSigmoidGrad 算子共包含两个输入和一个输出(README 参数表):
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| grads | 输入 | 反向传播过程中上一步输出的梯度,作为本反向算子的输入,公式中的 grad_output | FLOAT、FLOAT16、BFLOAT16 | ND |
| input_x | 输入 | 表示 HardSigmoid 前向算子的输入 Tensor,公式中的 self | FLOAT、FLOAT16、BFLOAT16 | ND |
| y | 输出 | 反向传播梯度输出,公式中的 HardsigmoidBackward(self, grad_output) | FLOAT、FLOAT16、BFLOAT16 | ND |
以上类型与格式约束可以在算子注册定义 hard_sigmoid_grad_def.cpp 中得到印证:grads、input_x两个输入与y输出均为 REQUIRED 参数,数据类型限定为DT_FLOAT / DT_FLOAT16 / DT_BF16,数据格式统一为FORMAT_ND,且声明了AutoContiguous()(输入会自动做连续性处理)。
从推理侧看,输出 shape 与输入 shape 完全一致——hard_sigmoid_grad_infershape.cpp 中的InferShape4HardSigmoidGrad直接将输出 shape 赋值为输入 shape(*output_shape = *input_shape),这也是逐元素反向算子的通用行为。
约束说明
README 明确标注约束说明:无。不过在接口层(GetWorkspaceSize 阶段)仍存在如下入参校验约束,违反时会返回对应的 aclnn 返回码:
| 返回码 | 错误码 | 触发场景 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | gradOutput、self、out 传入空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | gradOutput/self 数据类型不在支持范围内;或三个 Tensor 的 shape 不一致;或 gradOutput 与 self 互推导后的数据类型无法转换成 out 的数据类型 |
这些校验逻辑对应 aclnn_hardsigmoid_backward.cpp 中的CheckParams函数,依次完成空指针检查(CheckNotNull)、数据类型检查(CheckDtypeValid,其中out的数据类型需满足PromoteType(gradOutput, self)的互推导可转换关系)与 shape 检查(CheckShape,限制最大维度MAX_SUPPORT_DIMS_NUMS且三个 Tensor 形状必须相等)。
接口层还支持空 Tensor 与 0-8 维 shape:当任意输入为空 Tensor 时,第一段接口直接返回workspaceSize = 0并跳过计算(见 aclnn_hardsigmoid_backward.cpp)。
调用说明:aclnnHardsigmoidBackward 两段式接口
调用方式总览
HardSigmoidGrad 算子通过aclnn 接口方式调用,完整可编译样例见 test_aclnn_hard_sigmoid_grad.cpp,接口详细定义见 aclnnHardsigmoidBackward 文档。
与 CANN 所有算子一致,该接口采用两段式调用(参见两段式接口说明):必须先调用aclnnHardsigmoidBackwardGetWorkspaceSize获取计算所需 workspace 大小以及包含算子计算流程的执行器,再调用aclnnHardsigmoidBackward执行计算。
函数原型
// 第一段:获取 workspace 大小与执行器 aclnnStatus aclnnHardsigmoidBackwardGetWorkspaceSize( const aclTensor* gradOutput, // 输入:上一层梯度,公式中 grad_output const aclTensor* self, // 输入:前向算子输入,公式中 self aclTensor* out, // 输出:self 的梯度 grad_input uint64_t* workspaceSize,// 输出:Device 侧需申请的 workspace 大小 aclOpExecutor** executor) // 输出:op 执行器 // 第二段:执行计算 aclnnStatus aclnnHardsigmoidBackward( void* workspace, // Device 侧 workspace 内存地址 uint64_t workspaceSize, // 由第一段接口获取 aclOpExecutor* executor, // op 执行器 aclrtStream stream) // 执行任务的 Stream第一段接口 GetWorkspaceSize 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续 Tensor |
|---|---|---|---|---|---|---|---|
| gradOutput(aclTensor*) | 输入 | 计算入参 | 支持空 Tensor;gradOutput 与 self、out 的 shape 一致 | BFLOAT16、FLOAT16、FLOAT | ND | 0-8 | √ |
| self(aclTensor*) | 输入 | 计算入参 | 支持空 Tensor;gradOutput 与 self、out 的 shape 一致 | BFLOAT16、FLOAT16、FLOAT | ND | 0-8 | √ |
| out(aclTensor*) | 输出 | 为 self 的梯度值,公式中的 grad_input | gradOutput 与 self、out 的 shape 一致;数据类型需是 self 与 gradOutput 推导之后可转换的数据类型(见互推导关系) | BFLOAT16、FLOAT16、FLOAT | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
两个输入 Tensor 均支持**非连续(Non-Contiguous)**布局,接口内部会自动调用Contiguous补齐连续性;同时支持空 Tensor(空 Tensor 时跳过实际计算)。
第二段接口执行说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | workspace 大小,由第一段接口aclnnHardsigmoidBackwardGetWorkspaceSize获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
接口内部执行链路
从 aclnn_hardsigmoid_backward.cpp 可以看到,GetWorkspaceSize阶段会完成一层"通用算子编排",随后真正计算由 HardSigmoidGrad(l0op 层算子)执行,整体链路为:
- 入参校验:空指针、数据类型、shape 检查;
- 空 Tensor 短路:任一输入为空时
workspaceSize = 0直接返回; - 连续性处理与类型提升:对 gradOutput、self 分别执行
Contiguous→Cast(提升到PromoteType(gradOutput, self)的公共类型); - 组装算子图:调用
l0op::HardSigmoidGrad(grads, x, alpha, beta, executor),其中输出 Tensory通过executor->AllocTensor(x->GetViewShape(), x->GetDataType())自动按输入 shape 与数据类型分配(见 hardsigmoid_grad.cpp),并通过ADD_TO_LAUNCHER_LIST_AICORE挂载到 AiCore 执行器列表; - 结果回写:对计算结果再次
Cast回 out 的目标数据类型,并用ViewCopy写入 out; - 汇总 workspace:
uniqueExecutor->GetWorkspaceSize()返回整个链路(Contiguous/Cast/HardSigmoidGrad/ViewCopy)所需的 workspace 总大小。
第二段接口aclnnHardsigmoidBackward则通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)统一驱动执行器在指定 stream 上运行。
完整调用示例
以下为可直接编译运行的最小示例(节选自 test_aclnn_hard_sigmoid_grad.cpp,完整编译执行流程参考编译与运行样例):
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_hardsigmoid_backward.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 手册,按实际设备填写 deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,gradOutput/self/out 的 shape 必须一致 std::vector<int64_t> gradOutShape = {4, 2}; std::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> outShape = {4, 2}; void* selfDeviceAddr = nullptr; void* gradOutDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* gradOut = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> gradOutHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; // 创建 self / gradOut / out 三个 aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(gradOutHostData, gradOutShape, &gradOutDeviceAddr, aclDataType::ACL_FLOAT, &gradOut); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 两段式调用 CANN 算子库 API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 第一段:获取 workspace 大小与执行器 ret = aclnnHardsigmoidBackwardGetWorkspaceSize(gradOut, self, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardsigmoidBackwardGetWorkspaceSize 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;); } // 第二段:执行计算 ret = aclnnHardsigmoidBackward(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnHardsigmoidBackward 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<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 aclDestroyTensor(self); aclDestroyTensor(gradOut); aclDestroyTensor(out); // 7. 释放 device 资源 aclrtFree(selfDeviceAddr); aclrtFree(gradOutDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例的调用流程可归纳为 7 步:初始化(aclInit/aclrtSetDevice/aclrtCreateStream)→构造 Tensor(aclrtMalloc+aclrtMemcpy后aclCreateTensor创建 ND 格式 aclTensor)→两段式接口调用→同步等待(aclrtSynchronizeStream)→结果回拷(aclrtMemcpyDEVICE_TO_HOST)→释放 Tensor(aclDestroyTensor)→释放资源(aclrtFree/aclrtDestroyStream/aclrtResetDevice/aclFinalize)。
以示例数据self = {0,1,...,7}、gradOut = {0,1,...,7}为例,由于 $\alpha \times self + \beta = self/6 + 0.5$ 在self ∈ (-3, 3)时位于 $(0,1)$ 区间,因此输出result[i] = gradOut[i] / 6;超出该区间的元素(self=4、5、6、7 对应 index 4-7)输出为 0。
确定性计算
接口文档明确说明:aclnnHardsigmoidBackward 默认确定性实现,即相同输入在多次运行中产生逐位一致的输出,满足确定性计算要求(参见 aclnnHardsigmoidBackward 文档 的约束说明章节)。
Kernel 层实现原理与数值精度设计
在算子定义 hard_sigmoid_grad_def.cpp 中,HardSigmoidGrad 声明为AiCore 动态算子(DynamicCompileStaticFlag(true)、DynamicShapeSupportFlag(true)、DynamicRankSupportFlag(true)),并针对 arch35(Ascend 950 系列)注册了hard_sigmoid_grad_apt的 kernel 入口。
Kernel 入口 hard_sigmoid_grad_apt.cpp 按 tiling 生成的schMode模板参数分发到三种类型实例:
schMode == HARD_SIGMOID_GRAD_MODE_FLOAT32→HardSigmoidGrad<float>schMode == HARD_SIGMOID_GRAD_MODE_FLOAT16→HardSigmoidGrad<half>schMode == HARD_SIGMOID_GRAD_MODE_BFLOAT16→HardSigmoidGrad<bfloat16_t>
三个数据类型共用同一个模板类(见 hard_sigmoid_grad.h),通过if constexpr (std::is_same_v<T, float>)在编译期拆分出两条计算流水线:
fp32 原生流水线(T=float):直接在输入队列缓冲上完成Muls(tmp, self, alpha)→Adds(tmp, tmp, beta)→Muls(result, gradOut, alpha),再通过两次CompareScalar(分别与 0 和 1 比较)生成 mask 后Select选择保留或置 0,全程无类型转换,UB 占用最少。
Cast→fp32→Cast 回写流水线(half/bf16):先将 half/bf16 无损拓宽为 fp32(Cast ... CAST_NONE),在 fp32 精度下完成同样的乘加与比较选择,最后按类型专属的舍入模式转回原类型(half 使用CAST_RINT就近偶数舍入,bf16 使用CAST_ROUND)。kernel 头文件注释明确说明了这样做的原因:避免 fp16 在边界点 x≈±3(HardSigmoid 拐点)处ε≈9.77e-4的边界精度风险(见 hard_sigmoid_grad.h)。
此外,kernel 在计算前会先把临时缓冲预填充为-1.0f(位于 (0,1) 区间之外),保证CompareScalar按 256B 对齐窗口读取的尾部位(tail bits)结果确定,这是算子"确定性实现"在数据面(DataCopy/Compare 对齐)上的落地保障。Tiling 阶段(见 hard_sigmoid_grad_tiling_arch35.cpp)则按数据类型核算 UB 缓冲占用(fp32 路径约 7 个 fp32 槽位、fp16/bf16 路径约 6 个 fp32 槽位),并将ubFactor对齐到max(ubBlockSize, 256B),从而保证 kernel 层 256B 对齐的读取窗口始终不会越出InitBuffer边界。
整体上,HardSigmoidGrad 采用了"host 端 tiling 切分 + 核内双缓冲(BUFFER_NUM=2)Tile 流水(CopyIn→Compute→CopyOut)+ 类型感知的统一模板 Kernel"的典型 AiCore 算子架构,兼顾了三种浮点数据类型的精度正确性与 UB 资源效率。
相关资源索引
- 算子说明文档:activation/hard_sigmoid_grad/README.md
- 接口参考文档:docs/aclnnHardsigmoidBackward.md
- 调用样例:examples/test_aclnn_hard_sigmoid_grad.cpp(arch35 版本位于 examples/arch35)
- 接口实现:op_api/aclnn_hardsigmoid_backward.cpp、op_api/hardsigmoid_grad.cpp
- 算子定义与 shape 推导:op_host/hard_sigmoid_grad_def.cpp、op_host/hard_sigmoid_grad_infershape.cpp
- Kernel 实现:op_kernel/hard_sigmoid_grad_apt.cpp、op_kernel/arch35/hard_sigmoid_grad.h
- Tiling 实现:op_host/arch35/hard_sigmoid_grad_tiling_arch35.cpp
- 前向算子:activation/hard_sigmoid/README.md 与 aclnnHardsigmoid 接口文档
- 单元测试:tests/ut/op_host/op_api/test_aclnn_hardsigmoid_backward.cpp
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考