CANN ops-nn 算子 aclnnHardsigmoidBackward 接口全解析:两段式调用、参数约束与源码实现

原创2026-09-19 17:49:191,674 阅读
文章标签:人工智能深度学习算子库高性能计算模型量化AscendCANN

CANN ops-nn 算子 aclnnHardsigmoidBackward 接口全解析:两段式调用、参数约束与源码实现

本文基于 CANN ops-nn 开源仓库中 experimental/activation/hard_sigmoid_grad/docs/aclnnHardsigmoidBackward.md 文档,系统讲解 HardSigmoid 反向传播算子(aclnnHardsigmoidBackward)在 NPU 上的使用方式。读者将掌握该算子的产品支持范围、两段式接口调用流程、全部入参约束与返回码语义,并结合仓库源码理解其底层实现原理(数据类型推导、Promote 规则、AICore Kernel 计算逻辑),从而能在自己的工程中正确、高效地完成反向梯度计算。

功能说明:HardSigmoid 的反向传播

aclnnHardsigmoidBackward 是激活函数 HardSigmoid 的反向传播接口,用于计算 aclnnHardsigmoid / aclnnInplaceHardsigmoid 的前向梯度。关于前向算子的完整定义与公式,可参考 activation/hard_sigmoid/docs/aclnnHardsigmoid&aclnnInplaceHardsigmoid.md。

前向 HardSigmoid 的计算公式为:

Hardsigmoid(self)=clip(α×self+β,0,1)Hardsigmoid(self)=clip(\alpha \times self + \beta, 0, 1)

默认取 α=16\alpha=\frac{1}{6},β=12\beta=\frac{1}{2},等价于分段函数:

Hardsigmoid(self)={1,if(self>3)0,if(self≤−3)self6+12,otherwiseHardsigmoid(self)=\begin{cases} 1, & if(self>3) \\ 0, & if(self\le-3) \\ \frac{self}{6} + \frac{1}{2}, & otherwise \end{cases}

由此可推导反向梯度公式(对 self 求导后乘以上游梯度 gradOutput):

grad_input=gradOutput×α×1(0<α×self+β<1)grad\_input = gradOutput \times \alpha \times \mathbb{1}(0 < \alpha \times self + \beta < 1)

即:当 alpha * self + beta 落在开区间 (0, 1) 内时,梯度为 gradOutput * alpha,否则为 0。该梯度计算逻辑在源码中也有直接印证,见下文"Kernel 计算流程"小节。

产品支持情况

aclnnHardsigmoidBackward 在不同产品形态上的支持情况如下表:

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

需要注意,产品支持情况与数据类型支持范围是两个维度:即便同一产品支持该算子,不同系列产品支持的数据类型也可能不同。从文档及源码看,Atlas 推理系列产品、Atlas 训练系列产品上仅支持 FLOAT16、FLOAT 两种数据类型;而 BFLOAT16 的完整支持需要结合源码中的平台判断逻辑理解(详见下文"数据类型支持与推导"小节)。

函数原型:两段式接口

aclnnHardsigmoidBackward 采用 CANN 算子库通用的两段式接口设计(完整机制参见 docs/zh/context/two_phase_api.md):

  1. 先调用 aclnnHardsigmoidBackwardGetWorkspaceSize,获取计算所需 workspace 大小以及包含算子计算流程的执行器(executor);
  2. 再调用 aclnnHardsigmoidBackward 真正执行计算。
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)

两段接口的分工非常清晰:第一段是"规划"阶段,完成入参校验、构图与 workspace 计算;第二段是"执行"阶段,把规划好的执行器提交到指定 stream 上运行。这种设计允许上层框架在真正下发前统一完成内存规划,避免运行期动态分配。

aclnnHardsigmoidBackwardGetWorkspaceSize 参数说明

第一段接口共 5 个参数,其中前三个与算子语义直接相关,后两个是两段式接口的通用输出:

参数名 输入/输出 描述 使用说明 数据类型 数据格式 维度(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 执行器,包含了算子计算流程。 - - - - -

参数细节要点如下:

  • 输入输出对齐:三个 aclTensor 的 shape 必须完全一致,维度支持 0-8 维,数据格式统一为 ND。
  • 空 Tensor 支持:gradOutput、self 均支持空 Tensor(0 元素)。源码在 op_api/aclnn_hardsigmoid_backward.cpp 中明确处理了这一分支:当任一输入为空时,workspaceSize 直接置 0 并提前返回成功,无需真正构图执行。
  • 非连续 Tensor:三个张量均支持非连续存储(strides 非紧密排列)。这一能力由第一段接口内部的 Contiguous 归一化步骤保证,见下文"源码实现"。
  • 数据类型差异:对于 Atlas 推理系列产品、Atlas 训练系列产品,数据类型仅支持 FLOAT16、FLOAT。

返回值与入参校验

第一段接口返回 aclnnStatus 状态码,具体定义参见 aclnn返回码。第一段接口会完成入参校验,出现以下场景时报错:

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

这套校验规则与源码中 CheckParams 的三步检查一一对应(见 op_api/aclnn_hardsigmoid_backward.cpp):先查空指针(CheckNotNull),再查数据类型合法性(CheckDtypeValid),最后查 shape 支持性(CheckShape)。其中 CheckShape 还会额外检查维度不超过 MAX_SUPPORT_DIMS_NUMS(对应文档中的 0-8 维约束)。

aclnnHardsigmoidBackward 参数说明

第二段接口的参数全部由第一段接口产出或外部传入,语义如下:

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

注意:若第一段接口返回的 workspaceSize 为 0,则无需申请 workspace 内存,第二段接口传入空指针即可(调用示例中对此做了条件判断)。第二段接口本身不参与入参校验,它只是将 executor 携带的计算图提交到指定 stream 上执行。

约束说明

  • 确定性计算:aclnnHardsigmoidBackward 默认即为确定性实现,即相同输入在多次运行中产生一致结果,不会引入随机性。确定性计算的通用背景可参考 docs/zh/context/determinism_compute.md。

源码实现:从 API 到 AICore Kernel 的完整调用链

本节结合仓库源码还原 aclnnHardsigmoidBackward 的底层实现,帮助你理解接口行为背后的设计。

1. 数据类型支持与 Promote 推导

源码在 op_api/aclnn_hardsigmoid_backward.cpp 中定义了两组数据类型支持列表:

  • ASCEND910_DTYPE_SUPPORT_LIST:仅 FLOAT、FLOAT16;
  • ASCEND910B_DTYPE_SUPPORT_LIST:FLOAT、BF16、FLOAT16(即文档表格中的 BFLOAT16、FLOAT16、FLOAT 全集)。

平台判断逻辑为:SocVersion 处于 ASCEND910B ~ ASCEND910E 区间,或当前为 Regbase(注册制)运行模式时,使用完整列表;否则使用仅含 FLOAT/FLOAT16 的列表。这就是文档中"Atlas 推理系列产品、Atlas 训练系列产品仅支持 FLOAT16、FLOAT"这一限制的来源。

在校验通过后,第一段接口还会执行类型提升(Promote):

auto promoteType = op::PromoteType(gradOutput->GetDataType(), self->GetDataType());

即 gradOutput 与 self 先按 Promote 规则推导出中间计算类型,Kernel 以该类型计算,最后再 Cast 回 out 声明的数据类型。这也解释了文档中对 out 的约束:"数据类型需要是 self 与 gradOutput 推导之后可转换的数据类型"。

2. 构图执行:Contiguous → Cast → HardSigmoidGrad → ViewCopy

第一段接口并非直接调用 Kernel,而是通过 l0 算子构图(op_api/aclnn_hardsigmoid_backward.cpp):

  1. l0op::Contiguous:将非连续输入的 gradOutput、self 归一化为连续 Tensor(对应"非连续 Tensor √"的支持能力);
  2. l0op::Cast:将归一化后的输入转换到 Promote 推导出的计算类型;
  3. l0op::HardSigmoidGrad:以 alpha = 0.16666666、beta = 0.5 为属性执行核心梯度计算;
  4. l0op::Cast:把计算结果转回 out 声明的数据类型;
  5. l0op::ViewCopy:将结果写入 out 对应的内存视图。

其中 l0op::HardSigmoidGrad 的定义位于 op_api/hardsigmoid_grad.cpp,它通过 ADD_TO_LAUNCHER_LIST_AICORE 把算子注册进 AICore 启动器列表,并透传 alpha、beta 两个属性。整个构图完成后,uniqueExecutor->GetWorkspaceSize() 汇总出总 workspace 大小返回给调用方。

3. 算子定义与 Shape 推导

算子在 Host 侧的注册信息见 op_host/hard_sigmoid_grad_def.cpp:

  • 输入 grad_output、self 与输出 grad_input 均声明为 REQUIRED,数据类型支持 ge::DT_FLOAT / DT_FLOAT16 / DT_BF16,格式为 ND,并设置 AutoContiguous();
  • 属性 alpha(默认 0.16666666f)与 beta(默认 0.5f)均为 OPTIONAL,与上文 l0op 调用中传入的常量一致;
  • 配置了 ascend950 的 AICore 配置项,并开启动态 shape、动态 rank、静态编译等能力。

Shape 推导逻辑见 op_host/hard_sigmoid_grad_infershape.cpp:输出 shape 直接等于输入 shape(*output_shape = *input_shape),与文档中"gradOutput 与 self、out 的 shape 一致"的约束一致。

4. Tiling 与多核切分

Host 侧 Tiling 实现在 op_host/arch35/hard_sigmoid_grad_tiling.cpp:

  • 按 CeilDiv(totalLength, coreNum) 计算每核 blockFactor 做多核切分,并以 blockFactor * GetBlockIdx() 定位各核的全局内存偏移;
  • UB(统一缓冲区)切分按数据类型区分:bf16 模式在 fp32 下计算(bufferNum=7,typeSize=4),fp16 与 fp32 模式 bufferNum=8;
  • 空 Tensor 场景(totalLength==0)直接置零 tiling 数据并设置单核,与第一段接口的空 Tensor 提前返回相呼应。

5. AICore Kernel 计算流程

Kernel 端实现见 op_kernel/arch35/hard_sigmoid_grad.h,采用"两趟 CompareScalar + Select"的方式实现梯度公式:

  1. tmp = alpha * self + beta(Muls + Adds);
  2. result = grad_output * alpha;
  3. 第一次比较:mask = (tmp > 0),用 Select 将 tmp <= 0 的位置清零;
  4. 第二次比较:mask = (tmp < 1),用 Select 将 tmp >= 1 的位置清零。

最终 result 即满足 0<α⋅self+β<10 < \alpha \cdot self + \beta < 1 时为 gradOutput * alpha、否则为 0 的梯度输出,与文档功能说明完全一致。该 Kernel 使用双缓冲(BUFFER_NUM=2)流水,通过 CopyIn → Compute → CopyOut 三段式循环处理分块数据;bf16 场景还有独立的 op_kernel/arch35/hard_sigmoid_grad_bf16.h 变体。

调用示例

下面给出完整可运行示例,实际编译与执行过程请参考 编译与运行样例。示例代码与仓库中 experimental/activation/hard_sigmoid_grad/examples/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手册
  // 根据自己的实际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,需要修改为具体的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侧,需要根据具体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,需要根据具体API的接口定义修改
  aclDestroyTensor(self);
  aclDestroyTensor(gradOut);
  aclDestroyTensor(out);

  // 7. 释放device资源,需要根据具体API的接口定义修改
  aclrtFree(selfDeviceAddr);
  aclrtFree(gradOutDeviceAddr);
  aclrtFree(outDeviceAddr);
  if (workspaceSize > 0) {
    aclrtFree(workspaceAddr);
  }
  aclrtDestroyStream(stream);
  aclrtResetDevice(deviceId);
  aclFinalize();
  return 0;
}

示例代码关键点解读

  • 两段式调用顺序:第 3 步中必须先执行 GetWorkspaceSize 拿到 workspaceSize 与 executor,再据此申请 Device 内存并执行第二段接口;workspaceSize 为 0 时跳过 aclrtMalloc,避免无意义的申请。
  • 数据构造:示例中 gradOutShape、selfShape、outShape 均为 {4, 2},满足三个 Tensor shape 一致的约束;数据均为 FLOAT 类型,满足各类产品的基础支持范围。
  • 结果回读:第 5 步通过 aclrtMemcpy(ACL_MEMCPY_DEVICE_TO_HOST) 将 Device 侧梯度结果拷回 Host 并打印,用于验证计算正确性。
  • 资源回收:第 6、7 步按与申请相反的顺序释放 aclTensor、Device 内存、workspace、stream,并调用 aclrtResetDevice 与 aclFinalize 完成资源收尾。

总结

aclnnHardsigmoidBackward 是 CANN ops-nn 中 HardSigmoid 激活函数的反向传播接口,采用两段式 API 设计,支持 0-8 维 ND 格式、非连续 Tensor 与空 Tensor,覆盖 BFLOAT16/FLOAT16/FLOAT 三种数据类型(部分产品仅支持后两种)。从源码看,其实现由 Host 侧构图(Contiguous → Cast → HardSigmoidGrad → Cast → ViewCopy)与 AICore Kernel(两趟 CompareScalar + Select)协同完成,并通过 Promote 类型推导与多核 Tiling 保证不同输入条件下的正确性与执行效率。掌握本文的接口约束与调用链,即可在 Atlas A3/A2、Atlas 推理/训练系列产品上正确集成该算子的反向传播逻辑。

如需继续深入,可进一步阅读仓库内相关资源:算子目录总览、两段式接口机制、aclnn 返回码、编译与运行样例。

登录后查看全文
ops-nn