CANN ops-nn 算子 aclnnHardsigmoidBackward 接口全解析:两段式调用、参数约束与源码实现
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 的计算公式为:
默认取 ,,等价于分段函数:
由此可推导反向梯度公式(对 self 求导后乘以上游梯度 gradOutput):
即:当 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):
- 先调用
aclnnHardsigmoidBackwardGetWorkspaceSize,获取计算所需 workspace 大小以及包含算子计算流程的执行器(executor); - 再调用
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):
l0op::Contiguous:将非连续输入的gradOutput、self归一化为连续 Tensor(对应"非连续 Tensor √"的支持能力);l0op::Cast:将归一化后的输入转换到 Promote 推导出的计算类型;l0op::HardSigmoidGrad:以alpha = 0.16666666、beta = 0.5为属性执行核心梯度计算;l0op::Cast:把计算结果转回out声明的数据类型;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"的方式实现梯度公式:
tmp = alpha * self + beta(Muls+Adds);result = grad_output * alpha;- 第一次比较:
mask = (tmp > 0),用Select将tmp <= 0的位置清零; - 第二次比较:
mask = (tmp < 1),用Select将tmp >= 1的位置清零。
最终 result 即满足 时为 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 推理/训练系列产品上正确集成该算子的反向传播逻辑。