CANN ops-nn 中 aclnnHardsigmoidBackward 接口详解:NPU 上 HardSigmoid 激活反向梯度计算
2026/9/18 8:39:19 网站建设 项目流程

CANN ops-nn 中 aclnnHardsigmoidBackward 接口详解:NPU 上 HardSigmoid 激活反向梯度计算

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

本文围绕 CANN ops-nn 算子库中aclnnHardsigmoidBackward两段式单算子接口展开,完整覆盖其产品支持范围、函数原型、参数约束与错误码语义,并结合activation/hard_sigmoid_grad目录下的真实源码,剖析该接口从入参校验、执行器构建到 AICore Kernel 计算流水的完整实现链路,帮助读者在自研训练中正确接入 HardSigmoid 反向算子,并理解其精度设计与确定性计算的底层保障。

算子定位:HardSigmoid 的反向传播

aclnnHardsigmoidBackward是激活函数 aclnnHardsigmoid 的反向传播接口,用于在反向传播阶段计算输入梯度grad_input

结合前向算子定义(见 HardSigmoid 前向说明),前向公式为:

$$ HardSigmoid(self)=clip(\alpha \times self + \beta, 0, 1), \quad \alpha=\frac{1}{6},\ \beta=\frac{1}{2} $$

对应的反向公式(见 HardSigmoidGrad 算子说明)为分段形式:

$$ HardsigmoidBackward(self, grad_output)= \begin{cases} grad_output \ast \alpha, & if(0 < \alpha \ast self + \beta < 1) \ 0, & otherwise \end{cases} $$

即:仅当前向落在线性区间(self大约处于 (-3, 3) 开区间内)时,梯度按常数斜率alpha=1/6透传上游梯度;在前向被裁剪饱和的区域,梯度置零。公式中的 $\alpha$ 固定为 $1/6$、$\beta$ 固定为 $0.5$,与 PyTorchhardsigmoid默认语义保持一致。

该算子接口实现位于 activation/hard_sigmoid_grad,头文件与实现分别见 aclnn_hardsigmoid_backward.h 和 aclnn_hardsigmoid_backward.cpp。

产品支持情况与数据类型范围

产品是否支持
Ascend 950PR/Ascend 950DT
Atlas A3 训练系列产品/Atlas A3 推理系列产品
Atlas A2 训练系列产品/Atlas A2 推理系列产品
Atlas 200I/500 A2 推理产品×
Atlas 推理系列产品
Atlas 训练系列产品

数据类型方面,gradOutputselfout均支持 BFLOAT16、FLOAT16、FLOAT,数据格式为 ND,shape 维度 0~8,且支持非连续 Tensor 与空 Tensor。需要特别注意的一个平台差异:Atlas 推理系列产品、Atlas 训练系列产品仅支持 FLOAT16 与 FLOAT,不支持 BFLOAT16。

这一点可以直接从源码印证。aclnn_hardsigmoid_backward.cpp 中按平台区分了两套数据类型白名单:

static const std::initializer_list<op::DataType> ASCEND910_DTYPE_SUPPORT_LIST = {op::DataType::DT_FLOAT, op::DataType::DT_FLOAT16}; static const std::initializer_list<op::DataType> ASCEND910B_DTYPE_SUPPORT_LIST = { op::DataType::DT_FLOAT, op::DataType::DT_BF16, op::DataType::DT_FLOAT16};

判断逻辑是:SocVersion 落在ASCEND910BASCEND910E区间内、或运行于 regbase(注册式)架构时返回支持 BF16 的 910B 列表,否则(如传统 Atlas 推理/训练系列)回退为仅 FLOAT/FLOAT16 的列表。因此文档中“Atlas 推理/训练系列仅支持 FLOAT16、FLOAT”的约束,在GetDtypeSupportList()中有明确的代码依据。

此外,out的数据类型不要求与输入完全相同,但必须是gradOutputself类型推导(op::PromoteType)之后可以转换的目标类型,即遵循 互推导关系 中的数据类型转换规则。例如输入为 FLOAT16,输出可以选 FLOAT(升精度可转),反向则不允许。

函数原型:两段式接口

按照 两段式接口 的通用规范,aclnnHardsigmoidBackward分为两段,必须先调用第一段接口获取 workspace 大小与执行器,再调用第二段接口执行计算:

aclnnStatus aclnnHardsigmoidBackwardGetWorkspaceSize( const aclTensor* gradOutput, const aclTensor* self, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnHardsigmoidBackward( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

两段接口的头文件注释(aclnn_hardsigmoid_backward.h)明确标注了参数方向:第一段接口中gradOutputself为入参,outworkspaceSizeexecutor为出参;第二段接口全部为入参。函数声明带有ACLNN_API导出宏并以extern "C"包裹,因此该接口同时可被 C/C++ 程序调用。

aclnnHardsigmoidBackwardGetWorkspaceSize 参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
gradOutput(aclTensor*)输入计算入参,反向传播上游梯度支持空Tensor;与 self、out 的 shape 一致BFLOAT16、FLOAT16、FLOATND0-8
self(aclTensor*)输入计算入参,前向 HardSigmoid 的输入支持空Tensor;与 gradOutput、out 的 shape 一致BFLOAT16、FLOAT16、FLOATND0-8
out(aclTensor*)输出self 的梯度值,公式中的 grad_inputshape 与输入一致;类型需满足与输入推导结果的互推导关系BFLOAT16、FLOAT16、FLOATND0-8
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor(aclOpExecutor**)输出返回 op 执行器,包含算子计算流程-----

第一段接口返回码与入参校验

返回值类型为aclnnStatus,完整取值参见 aclnn返回码。第一段接口会完成入参校验,出现以下场景时报错:

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 gradOutput、self、out 是空指针时
ACLNN_ERR_PARAM_INVALID161002gradOutput 和 self 的数据类型不在支持范围之内;gradOutput、self、out 的 shape 不同;gradOutput 和 self 推导后的数据类型不能转换成 out 的数据类型

从源码看,这些校验与 aclnn_hardsigmoid_backward.cpp 中CheckParams的三步检查一一对应:

  1. CheckNotNull:三个 Tensor 指针判空,失败返回ACLNN_ERR_PARAM_NULLPTR
  2. CheckDtypeValid:按平台白名单检查gradOutput/self类型,并用op::PromoteType推导后校验能否转换为out类型,失败返回ACLNN_ERR_PARAM_INVALID
  3. CheckShape:通过OP_CHECK_MAX_DIM限制维度不超过MAX_SUPPORT_DIMS_NUMS(即 8 维),并用OP_CHECK_SHAPE_NOT_EQUAL强制三者 shape 完全一致。

这些分支均有单元测试覆盖,tests/ut/op_host/op_api/test_aclnn_hardsigmoid_backward.cpp 中分别构造了空指针(grad_nullptr/self_nullptr/out_nullptr)、非法数据类型(INT32、BOOL 输入)、9 维 shape 等用例,断言返回ACLNN_ERR_PARAM_NULLPTRACLNN_ERR_PARAM_INVALID,与上表语义一致。

aclnnHardsigmoidBackward 参数说明

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

返回值同样是aclnnStatus,参见 aclnn返回码。

源码透视:第一段接口的执行器构建流程

aclnnHardsigmoidBackwardGetWorkspaceSize的内部流程(aclnn_hardsigmoid_backward.cpp)比“单纯算个 workspace 大小”更丰富,值得逐段理解:

  1. 公共出参判空OP_CHECK_COMM_INPUT(workspaceSize, executor)校验两个出参指针非空。
  2. 入参校验:调用前述CheckParams,任一环节失败即短路返回对应错误码。
  3. 空 Tensor 快速路径:若gradOutputselfout任一为空 Tensor,直接置*workspaceSize = 0、释放执行器并成功返回。这解释了参数表“支持空Tensor”的实现方式——空张量不触发任何 Device 计算。
  4. 构建算子计算流程:以op::PromoteType(gradOutput, self)得到推导后的公共计算类型promoteType,依次向执行器追加一串 Level0 原子算子:
auto gradOutputContiguous = l0op::Contiguous(gradOutput, uniqueExecutor.get()); auto gradOutputContiguousCasted = l0op::Cast(gradOutputContiguous, promoteType, uniqueExecutor.get()); auto selfContiguous = l0op::Contiguous(self, uniqueExecutor.get()); auto selfContiguousCasted = l0op::Cast(selfContiguous, promoteType, uniqueExecutor.get()); // Use 1.0f/6.0f instead of 0.16666666f literal to match PyTorch hardsigmoid // semantics exactly at boundary x=-3.0 ... float alpha = 1.0f / 6.0f; float beta = 0.5f; auto hardsigmoidGradOut = l0op::HardSigmoidGrad(gradOutputContiguousCasted, selfContiguousCasted, alpha, beta, uniqueExecutor.get()); auto hardsigmoidGradOutCast = l0op::Cast(hardsigmoidGradOut, out->GetDataType(), uniqueExecutor.get()); auto viewCopyResult = l0op::ViewCopy(hardsigmoidGradOutCast, out, uniqueExecutor.get()); *workspaceSize = uniqueExecutor->GetWorkspaceSize();

这条调用链揭示了两个设计细节:

  • 非连续 Tensor 的支持来自最外层的Contiguous:先转连续再参与计算,最后由ViewCopyout的原始视图写回,因此接口天然兼容非连续输入输出;
  • alpha 常量的取法在注释中专门说明:使用1.0f/6.0f运行时除法而非字面量0.16666666f,是为了在边界x = -3.0处与 PyTorch 的hardsigmoid语义严格一致(fp32 精度下1/6*(-3)+0.5严格等于 0,而字面量近似会产生约 2.98e-08 的偏差,使边界点误入mask=1区域)。这个边界敏感性也解释了为什么 Kernel 对精度如此敏感(见下文)。

其中核心的梯度计算由 l0op::HardSigmoidGrad 完成,该函数注册了HardSigmoidGrad算子类型(OP_TYPE_REGISTER),并通过ADD_TO_LAUNCHER_LIST_AICORE将其加入 AICore 执行序列,alphabeta作为算子属性下发。算子注册定义(hard_sigmoid_grad_def.cpp)中同样给出了属性默认值alpha = 1.0f/6.0fbeta = 0.5f,输入输出均声明为 ND 格式并开启AutoContiguous()

第二段接口aclnnHardsigmoidBackward的实现则非常薄:记录 DFX 阶段后直接调用CommonOpExecutorRun(workspace, workspaceSize, executor, stream)驱动执行器按第一段构建好的流程执行,这也是所有两段式单算子 API 的统一执行方式。

Kernel 实现:双精度路径与确定性设计

Device 侧 Kernel 入口位于 hard_sigmoid_grad_apt.cpp,通过模板参数schMode区分三种计算模式(0=fp32、1=fp16、2=bf16),统一实例化NsHardSigmoidGrad::HardSigmoidGrad<T>模板类。

真正的计算逻辑在 arch35/hard_sigmoid_grad.h,其中最有价值的实现细节是按数据类型分叉的两条计算路径

  • fp32 原生路径(T=float:直接在输入队列缓冲区上做tmp = alpha*self + beta,再计算result = grad_output * alpha,省去两次 Cast,避免占用额外的 fp32 UB 空间;
  • half/bf16 转换路径:先将grad_outputself无损加宽 Cast 到 fp32,在 fp32 下完成全部乘加与比较,最后按类型回 Cast(half 用CAST_RINT、bf16 用CAST_ROUND舍入模式)。

文件头注释说明了为何 half/bf16 必须升到 fp32 计算:fp16 的机器精度约为ε≈9.77e-4,在x≈±3这一 HardSigmoid 拐点附近,alpha*self+beta的结果恰好逼近 0 或 1 的边界,直接用 fp16 比较会导致区间判定出错,因此选择加宽到 fp32 消除边界精度风险。

公式到算子指令的映射在Compute中清晰可见,以 fp32 路径为例:

// Step 1: tmp = alpha * self + beta. Muls(tmpLocal, selfLocal, static_cast<T>(alpha_), currentNum); Adds(tmpLocal, tmpLocal, static_cast<T>(beta_), currentNum); // Step 2: result = grad_output * alpha. Muls(resultLocal, gradLocal, static_cast<T>(alpha_), currentNum); // Step 3: mask = (tmp > 0), select. CompareScalar(maskLocal, tmpLocal, static_cast<T>(0), CMPMODE::GT, alignedCount); Select(resultLocal, maskLocal, resultLocal, static_cast<T>(0), SELMODE::VSEL_TENSOR_SCALAR_MODE, currentNum); // Step 4: mask = (tmp < 1), select. CompareScalar(maskLocal, tmpLocal, static_cast<T>(1), CMPMODE::LT, alignedCount); Select(resultLocal, maskLocal, resultLocal, static_cast<T>(0), SELMODE::VSEL_TENSOR_SCALAR_MODE, currentNum);

即先用CompareScalar生成tmp > 0的掩码,把不满足条件的梯度置 0,再用tmp < 1的掩码做第二次筛选,最终等价于分段公式中“仅在0 < alpha*self + beta < 1时保留grad_output * alpha”。

其他值得注意的工程细节:

  • 数据布局与分块:hard_sigmoid_grad_tiling_data.h 定义了 Kernel 侧的分块参数totalLength(总元素数)、blockFactor(每核元素数)、ubFactor(每 UB tile 元素数)以及alphabeta。Kernel 通过ComputePerCoreLength将总数据量按 AICore 数切分,每个核处理自己的切片;
  • 双缓冲流水gradOutputQueueselfQueuegradInputQueue均使用BUFFER_NUM = 2的双缓冲队列,RunTileLoop驱动CopyIn → Compute → CopyOut三阶段流水;
  • 确定性保障CompareScalar需要按 256 字节(64 个 fp32 元素)对齐读窗口,而实际有效元素数currentNum通常不满对齐边界。为保证对齐填充区[currentNum, alignedCount)的掩码位确定,代码在计算前用Duplicate(tmp, -1.0f, alignedCount)将整窗预填为 -1.0(必然落在 (0,1) 区间之外),从而让尾部比较结果恒为“不通过”。这与文档“确定性计算”约束相呼应——实现上从数据预填到比较窗口都刻意消除了不确定性来源;
  • 精度降低标记:算子注册中的PrecisionReduceFlag(true)表明该算子允许在精度损失可接受的前提下进行优化计算,这也是其采用“fp16 输入升 fp32 计算”这类策略的注册层体现。

约束说明

  • 确定性计算:aclnnHardsigmoidBackward默认确定性实现。即相同输入下多次执行得到完全一致的输出,便于结果复现与调试,这一点由上文所述的边界预填与固定对齐比较策略支撑。

调用示例

以下为完整可运行的调用示例(来自文档,编译与运行环境准备参见 编译与运行样例):

#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手册 // 根据自己的实际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> 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 aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建gradOut aclTensor ret = CreateAclTensor(gradOutHostData, gradOutShape, &gradOutDeviceAddr, aclDataType::ACL_FLOAT, &gradOut); 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 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnHardsigmoidBackward第一段接口 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;); } // 调用aclnnHardsigmoidBackward第二段接口 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; }

从示例数据本身也可以验证算子语义:self取值 0~7,对应的alpha*self + beta分别落在 (0, 1) 开区间内(0.5、2/3、5/6、2/3+… 均在区间内),因此 8 个元素的输出应全部等于gradOutput * 1/6,即0, 1/6, 2/6, 3/6, 4/6, 5/6, 6/6, 7/6,可直接作为运行后的结果校验基准。

延伸阅读

  • 前向算子接口说明:aclnnHardsigmoid 文档
  • 算子级功能说明与公式定义:HardSigmoidGrad README
  • 可运行的完整调用样例:test_aclnn_hard_sigmoid_grad.cpp
  • 参数校验行为验证:aclnnHardsigmoidBackward 单元测试
  • 两段式接口通用规范:two_phase_api.md
  • 数据类型互推导规则:deduction_relationship.md
  • 返回码全集:aclnn_return_code.md
  • 编译与运行样例流程:compile_and_run_sample.md

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

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

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

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

立即咨询