CANN ops-nn 中 aclnnSoftplusBackward 算子接口详解:SoftplusV2Grad 反向传播的两段式调用实战
2026/9/19 23:31:49 网站建设 项目流程

CANN ops-nn 中 aclnnSoftplusBackward 算子接口详解:SoftplusV2Grad 反向传播的两段式调用实战

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

本篇技术指南聚焦 CANN 神经网络算子库(ops-nn)中aclnnSoftplusBackward反向传播算子接口,系统讲解其数学原理、产品支持范围、两段式接口(GetWorkspaceSize+ 执行)的完整参数语义、错误码约束与可运行的调用示例,并结合activation/softplus_v2_grad目录下的 op_api、op_host(tiling)与 op_kernel 源码,剖析该算子在 NPU 上的实现链路。读者读完可掌握在 Ascend 平台上编写、编译并运行 Softplus 反向梯度计算的完整方案。

功能说明:Softplus 前向算子的反向传播

aclnnSoftplusBackward是 aclnnSoftplus 前向算子的反向传播接口。它根据前向传播的原始输入self与上游传入的梯度gradOutput,计算对前向输入的梯度gradInput,属于元素级(element-wise)反向算子。

计算公式如下:

$$ gradInput = gradOutput \cdot \begin{cases} \dfrac{1}{1+e^{(-\beta \cdot self)}}, & \beta \cdot self \le threshold \ 1, & \beta \cdot self > threshold \end{cases} $$

其中:

  • self为前向传播输入,gradOutput为上游梯度,gradInput为输出梯度;
  • beta用于控制 softplus 曲线的陡峭程度,beta越大,softplus 越接近 ReLU;
  • threshold为数值稳定性阈值。当 $\beta \cdot self > threshold$ 时,梯度直接退化为1.0(即gradInput = gradOutput),避免对极大负数求exp造成溢出,保证数值稳定性。

从算子注册代码可以印证这一语义:softplus_v2_grad_def.cpp 中定义了 2 个输入(input_gradientsinput_features)、1 个输出(output_backprops)以及两个标量属性beta(默认1.0f)与threshold(默认20.0f),并通过OP_ADD(SoftplusV2Grad)完成算子注册。

产品支持情况

aclnnSoftplusBackward在当前仓库中登记的产品支持情况如下:

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

需要说明的是,不同产品在数据类型支持上存在差异:在 Atlas 训练系列产品上,gradOutputgradInput仅支持 FLOAT、FLOAT16、DOUBLE 三种类型(详见接口文档);而 aclnn_softplus_backward.cpp 中按 SoC 版本区分了ASCEND910_DTYPE_SUPPORT_LISTASCEND910B_DTYPE_SUPPORT_LIST(后者额外支持 BFLOAT16),说明 dtype 支持矩阵随平台演进。

两段式接口与函数原型

与 CANN 其他算子接口一致,aclnnSoftplusBackward遵循两段式接口设计:必须先调用aclnnSoftplusBackwardGetWorkspaceSize获取计算所需 workspace 大小以及包含算子计算流程的执行器(executor),再调用aclnnSoftplusBackward执行计算。

aclnnStatus aclnnSoftplusBackwardGetWorkspaceSize( const aclTensor* gradOutput, const aclTensor* self, const aclScalar* beta, const aclScalar* threshold, aclTensor* gradInput, uint64_t* workspaceSize, aclOpExecutor** executor)
aclnnStatus aclnnSoftplusBackward( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

从实现看,第一段接口 aclnnSoftplusBackwardGetWorkspaceSize 内部完成了参数校验、输入连续性整理(l0op::Contiguous)、必要时 dtype 提升(l0op::Cast到 FLOAT)、构图(l0op::SoftplusV2Grad)并返回executor->GetWorkspaceSize();第二段接口则直接通过CommonOpExecutorRun提交执行。

aclnnSoftplusBackwardGetWorkspaceSize 参数说明

第一段接口的完整参数语义如下表:

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
gradOutput(aclTensor*)输入输入的梯度。公式中的 gradOutput。支持空 Tensor;shape 需与 self、gradInput 相同;需与 self 满足 broadcast 关系,且 Broadcast 后 shape 与 self 的 shape 相等。FLOAT、FLOAT16、DOUBLE、BFLOAT16ND0-8
self(aclTensor*)输入输入数据。公式中的 self。支持空 Tensor;为 softplus 的正向输入值;shape 需与 gradOutput、gradInput 相同。INT8、INT16、INT32、INT64、UINT8、BOOL、FLOAT16、FLOAT、DOUBLE、BFLOAT16ND0-8
beta(aclScalar*)输入公式中的 beta,可表示与 ReLU 的近似程度。数据类型满足数据类型互推导关系;支持可转换为 FLOAT 的类型。----
threshold(aclScalar*)输入公式中的 threshold,表示阈值,大于此值时恢复为线性函数。数据类型满足数据类型互推导关系;支持可转换为 FLOAT 的类型。----
gradInput(aclTensor*)输出公式中的 gradInput。支持空 Tensor;shape 需与 gradOutput、self 相同;gradInput 的 shape 和数据类型与 self 相同。FLOAT、FLOAT16、DOUBLE、BFLOAT16ND0-8
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小。-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程。-----

几点补充说明:

  • dtype 的"宽松输入 + 内部 cast"策略self支持的类型范围明显宽于gradOutput/gradInput(如 INT8、INT64、BOOL 等)。其原因是 aclnn_softplus_backward.cpp 中的CheckDtypeValid会先校验输入在支持列表内,随后判断 dtype 是否落在 Kernel 支持的KERNEL_SUPPORT_LIST(FLOAT、FLOAT16、BF16)中,若不在则通过l0op::Cast提升为 FLOAT 计算,最后再把结果 cast 回gradInput的原 dtype 写入,从而扩大接口可用性;
  • broadcast 支持gradOutput的 shape 可与self满足 broadcast 关系,Broadcast 后的 shape 需与self相等,输出gradInput的 shape 与self相同。这一语义在 softplus_v2_grad_infershape.cpp 中通过InferShape4Broadcast(context, 2)(对 input_gradients 与 input_features 做广播推理)落地;
  • 空 Tensor 支持:当任一输入为空 Tensor 时,第一段接口直接返回workspaceSize = 0并提前结束,无需下发计算。

返回值与错误码

两段接口均返回aclnnStatus状态码,具体可参见 aclnn返回码。

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

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 gradOutput、self、beta、threshold、gradInput 是空指针时。
ACLNN_ERR_PARAM_INVALID161002gradOutput、self 和 gradInput 的数据类型不在支持的范围之内。
ACLNN_ERR_PARAM_INVALID161002gradOutput、self 和 gradInput 的 shape 不同。

对照源码,CheckParams 的校验顺序为:空指针检查(CheckNotNull)→ 数据类型合法性检查(CheckDtypeValid)→ shape 检查(CheckShape,含最大维度MAX_SUPPORT_DIMS_NUMS与 shape 一致性),任一失败即返回对应错误码。此外,betathreshold为空指针时同样报ACLNN_ERR_PARAM_NULLPTR

aclnnSoftplusBackward 参数说明

第二段接口的 4 个参数全部为输入,语义如下:

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

返回值为aclnnStatus,具体参见 aclnn返回码。

约束说明

  • 确定性计算aclnnSoftplusBackward默认确定性实现(即相同输入在任何运行条件下得到逐位一致的结果),便于调试与结果比对;
  • dtype 一致性input_gradientsinput_features的 dtype 必须一致,不支持混合精度输入(见 README.md);
  • 标量属性betathreshold是 Host 侧标量属性而非 Tensor,分别默认1.020.0,该默认值与 softplus_v2_grad_def.cpp 中的 Attr 注册值、softplus_v2_grad_tiling_struct.h 中的kDefaultBeta/kDefaultThreshold三处保持一致,是 tiling 侧运行期的兜底默认值。

调用示例(可编译运行)

下面给出完整示例代码(仅供参考,具体编译与执行过程请参考编译与运行样例)。该示例与仓库 examples/test_aclnn_softplus_v2_grad.cpp 保持一致:

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_softplus_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> gradOutputShape = {4, 2}; std::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> gradInputShape = {4, 2}; void* gradOutputDeviceAddr = nullptr; void* selfDeviceAddr = nullptr; void* gradInputDeviceAddr = nullptr; aclTensor* gradOutput = nullptr; aclTensor* self = nullptr; aclTensor* gradInput = nullptr; aclScalar* beta = nullptr; aclScalar* threshold = nullptr; std::vector<float> gradOutputHostData = {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8}; std::vector<float> selfHostData = {0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8}; std::vector<float> gradInputHostData = {0, 0, 0, 0, 0, 0, 0, 0}; float betaValue = 1.1f; float thresholdValue = 1.2f; // 创建 gradOutput, self, gradInput aclTensor ret = CreateAclTensor(gradOutputHostData, gradOutputShape, &gradOutputDeviceAddr, aclDataType::ACL_FLOAT, &gradOutput); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(gradInputHostData, gradInputShape, &gradInputDeviceAddr, aclDataType::ACL_FLOAT, &gradInput); CHECK_RET(ret == ACL_SUCCESS, return ret); beta = aclCreateScalar(&betaValue, aclDataType::ACL_FLOAT); CHECK_RET(beta != nullptr, return ret); threshold = aclCreateScalar(&thresholdValue, aclDataType::ACL_FLOAT); CHECK_RET(threshold != nullptr, return ret); // 3. 调用CANN算子库API,需要修改为具体的API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnSoftplusBackward第一段接口 ret = aclnnSoftplusBackwardGetWorkspaceSize(gradOutput, self, beta, threshold, gradInput, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSoftplusBackwardGetWorkspaceSize 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); } // 调用aclnnSoftplusBackward第二段接口 ret = aclnnSoftplusBackward(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSoftplusBackward 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(gradInputShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), gradInputDeviceAddr, 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,需要根据具体API的接口定义 aclDestroyTensor(gradOutput); aclDestroyTensor(self); aclDestroyTensor(gradInput); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(gradOutputDeviceAddr); aclrtFree(selfDeviceAddr); aclrtFree(gradInputDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例代码的关键流程可归纳为:aclInit初始化 → 构造aclTensor/aclScalar→ 第一段接口取 workspace 与 executor → 申请 workspace 内存 → 第二段接口执行 →aclrtSynchronizeStream同步 → 结果回拷 → 资源释放。示例中beta = 1.1threshold = 1.2,输入 shape 为{4, 2},输出结果即每个元素按公式计算得到的gradInput

源码级实现剖析:从 Host 到 Kernel 的完整链路

aclnnSoftplusBackward并非孤立接口,其背后是activation/softplus_v2_grad目录下完整的"op_api → op_host(tiling) → op_kernel"三层实现。理解这条链路有助于排查性能与精度问题。

op_api 层:构图与数据类型适配

aclnn_softplus_backward.cpp 在第一段接口中完成如下工作:

  1. 通过CREATE_EXECUTOR()创建执行器,随后执行CheckParams参数校验;
  2. gradOutputself执行l0op::Contiguous保证输入连续;
  3. 若输入 dtype 不在 Kernel 支持列表(FLOAT/FLOAT16/BF16)或两者 dtype 不一致,则l0op::Cast提升为 FLOAT;
  4. betathreshold通过ToFloat()转为 float 标量,调用 softplus_v2_grad.h 中声明的l0op::SoftplusV2Grad(grad_output, self, beta, threshold, executor)构图;
  5. 结果再 Cast 回gradInput的原始 dtype,并通过l0op::ViewCopy写入输出,最后返回executor->GetWorkspaceSize()

op_host 层:shape 推导与 tiling 切分

  • shape 推导:softplus_v2_grad_infershape.cpp 通过InferShape4Broadcast对两个输入做 broadcast 推理,产出output_backprops的 shape;
  • tiling:softplus_v2_grad_tiling_arch35.cpp 实现了 arch35(Ascend 950 等)下的切分策略:先用PadAndSqueeze将不同 rank 的输入/输出归一化到最大广播坐标系,再用FindSplitAxis依据 UB 容量确定切分轴与 tile 大小(a_i/a_o/a_i_tail),随后MultiCoreSplit按 AIV 核数将总 tile 数均分到多核(含tiles_main/cores_tail尾块处理),最后把归一化 shape/stride 与betathreshold写入SoftplusV2GradTilingData并通过ctx_->SetTilingKey区分 RANK4/RANK8 两档模板实例化。此外,该算子在 tiling 阶段声明 workspace 大小为 0(TilingFuncSoftplusV2Grad 中workspaces[0] = 0)。

op_kernel 层:向量流水与数值实现

Kernel 入口 softplus_v2_grad.cpp 按 RANK 模板实例化(SOFTPLUS_V2_GRAD_RANK_4/SOFTPLUS_V2_GRAD_RANK_8),调用SoftplusV2GradKernel<T, RANK>。核心计算在 softplus_v2_grad_kernel.h 中:

  • FP32 路径(P=3 流水)CopyInBrc(NDDMA 多维广播搬运)→ 向量函数SoftplusV2GradVFCopyOutOne搬出,通过MTE2→V→MTE3→MTE2事件对做流水同步;
  • FP16/BF16 路径(P=4 流水):多出两次AscendC::Cast,输入先扩精度为 FP32 计算,结果再以CAST_RINT缩回原精度输出;
  • 向量指令序列(S1~S9)Muls(beta*self)NegExpAdds(+1)Div(gradOutput/(1+e^{-β·self}))→ 构造阈值寄存器 →Compare(GT)生成比较掩码 →SelectgradOutputDiv结果间选择。这与接口文档中的数学公式逐条对应,且Compare/Select恰好实现了"β·self > threshold时梯度退化为 1"的分支逻辑。

测试与验证

仓库为aclnnSoftplusBackward提供了多级测试资产,可作为自行验证的参考:

  • UT(单元测试):tests/ut/op_api/test_aclnn_softplus_backward.cpp 覆盖 op_api 层接口调用;tests/ut/op_host/arch35/test_softplus_v2_grad_tiling.cpp 覆盖 tiling 切分结果;tests/ut/op_kernel/test_softplus_v2_grad.cpp 配合gen_data.py/compare_data.py完成 Kernel 级数据比对;tests/ut/op_host/test_softplus_v2_grad_infershape.cpp 覆盖 shape 推导;
  • ST(系统测试):tests/st/aclnnSoftplusBackward 提供 ATK 测试配置atk_aclnnSoftplusBackward.jsonexecutor_aclnnSoftplusBackward.py,可在真实 NPU 环境验证精度与确定性;
  • 调用方式对照:除 aclnn 单算子调用外,test_geir_softplus_v2_grad.cpp 演示了通过算子 IR(softplus_v2_grad_proto.h)构图调用 SoftplusV2Grad 的图模式路径,适合在图编译场景下使用。

总结

aclnnSoftplusBackward是 CANN ops-nn 中 Softplus 前向算子的标准反向接口:数学上通过threshold阈值将梯度分段为 sigmoid 形式与线性形式以保证数值稳定;接口上采用两段式设计,第一段完成校验、构图与 workspace 计算,第二段提交执行;实现上由 op_api 层做 dtype 适配与广播处理、op_host 层完成 shape 推导与多核 tiling、op_kernel 层以向量指令流水实现核心公式。无论是 PyTorch 等上层框架接入反向传播,还是在昇腾平台手写推理/训练算子,本文给出的参数语义、错误码表与可运行示例均可直接指导开发与调试。

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

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

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

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

立即咨询