CANN ops-nn 算子开发实战:aclnnGeluBackward 两段式接口实现 Gelu 反向传播
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
本篇技术指南以 CANN ops-nn 开源仓库中 aclnnGeluBackward 算子文档 为核心,系统讲解 Gelu 激活函数反向传播算子的数学原理、两段式 API 调用流程、参数约束与完整调用示例。结合 activation/gelu_grad 目录下的源码、配置与测试用例,读者可以掌握在 Ascend NPU 上通过 aclnn 接口完成 Gelu 梯度计算的完整实战方案。
产品支持情况
aclnnGeluBackward 算子在不同硬件产品上的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 支持 |
| Atlas 训练系列产品 | 支持 |
从源码看,算子注册文件 gelu_grad_def.cpp 中通过this->AICore().AddConfig("ascend950", aicoreConfig)与AddConfig("ascend350", aicoreConfig)为 Ascend 950 与 Ascend 350 两个 SoC 版本注册了 AICore 计算配置;op_host/config 目录下同时提供了ascend950与ascend350两个平台的二进制算子描述文件,与文档中"Atlas A3 系列(对应 350 平台)支持、Atlas 200I/500 A2 推理产品不支持"的产品支持矩阵一致。
功能说明
aclnnGeluBackward 接口的功能是完成 aclnnGelu 的反向传播计算,即根据输出侧梯度 gradOutput 与正向输入 self,计算输入侧梯度 gradInput。
数学原理
Gelu 正向计算公式(其中 x 可以为标量或者 Tensor):
$$ Gelu(x)=x \cdot \Phi(x)=x/2 \cdot [1+erf(x/\sqrt{2})] $$
其中 erf 的计算公式为:
$$ erf(x)=\frac{2}{\sqrt \pi}\sum^{\infty}_{n=0}{\frac{(-1)^n \cdot x^{2n+1}}{n! \cdot (2n+1)}} $$
gradInput 和 gradOutput 的关系可以表示为:
$$ gradInput = gradOutput \cdot (\frac{1}{2}+\frac{1}{2} \cdot erf(\frac{x}{\sqrt2})+\frac{x}{\sqrt{2\pi}} \cdot e^{-\frac{x^2}{2}}) $$
即梯度由三部分组成:常量项1/2、误差函数项erf(x/√2)/2以及高斯密度项x·e^(-x²/2)/√(2π)。
Gelu 的近似计算公式为:
$$ Gelu(x)=0.5x(1+tanh(\sqrt{2/\pi}(x+0.044715x^3))) $$
实现层面的公式印证
在 gelu_grad_dag.h 中可以看到算子内核(Kernel)对上述公式的逐指令实现:内核定义了一组多项式拟合常量BETAN = -1.595769121605730711759f、AN = -0.0713548162726002527220f、A3 = 0.2140644488178007f、BETA = 1.595769121605730711759f,通过Mul(平方)、Axpy(乘加)、Exp(指数)、Adds、Div(除法)等矢量指令完成exp(x²·AN + BETAN·x)形式的高斯核近似计算,再与dy逐元素相乘得到梯度输出。整个计算在向量寄存器中按loopNum = (count + vl - 1) / vl分块循环执行,其中vl为向量寄存器一次可容纳的元素数。
而在算子API层,gelu_grad.cpp 中的IsAiCoreSupport检查gradOutput、self、unused、gradInput四个 Tensor 的数据类型是否都属于DT_FLOAT / DT_FLOAT16 / DT_BF16支持列表,随后通过ADD_TO_LAUNCHER_LIST_AICORE(GeluGrad, OP_INPUT(gradOutput, self, unused), OP_OUTPUT(gradInput))将计算任务下发到 AICore 执行(当前无 AICPU 实现,默认走 AICore 路径)。
函数原型
每个算子分为两段式接口(详见 两段式接口说明):必须先调用aclnnGeluBackwardGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器,再调用aclnnGeluBackward接口执行计算。
aclnnStatus aclnnGeluBackwardGetWorkspaceSize( const aclTensor *gradOutput, const aclTensor *self, const aclTensor *gradInput, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnGeluBackward( void *workspace, uint64_t workspace_size, aclOpExecutor *executor, const aclrtStream stream)两段式接口的头文件声明位于 aclnn_gelu_backward.h,其中还给出了 API 内部的计算基本路径(DAG):gradOutput与self先分别经过l0op::Contiguous转为连续布局,与gradInput一起进入l0op::GeluGrad核心计算,输出再经l0op::ViewCopy写回gradInput。
aclnnGeluBackwardGetWorkspaceSize(第一段接口)
参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| gradOutput(aclTensor*) | 输入 | 求梯度时的权重,即为了将正向输出的 tensor 变为标量所相乘的权重 tensor | shape 需要和正向 self 的 shape 满足 broadcast 关系;dtype 与 self 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 Tensor | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| self(aclTensor*) | 输入 | Gelu 的正向输入值 | shape 需要和 gradOutput 的 shape 满足 broadcast 关系;dtype 与 gradOutput 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 Tensor | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| gradInput(aclTensor*) | 输出 | backward 计算的输出,为 GELU 正向入参的梯度值,即对输入进行求导后的结果 | dtype 与 self 和 gradOutput 进行数据类型推导后的可转换的数据类型(参见互转换关系)一致;shape 与 gradOutput 和 self 进行 broadcast 后的 shape 一致 | FLOAT、FLOAT16、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
补充说明:
- 对于Atlas 推理系列产品、Atlas 训练系列产品:数据类型仅支持 FLOAT、FLOAT16。
- 三个 Tensor 的数据格式均要求为 ND 格式。不过从 API 层实现看,算子内部会自动对非连续输入执行
Contiguous化,因此三个 Tensor 均支持传入非连续(strided)Tensor,这与文档中"非连续 Tensor:√"的标注一致。
返回值
aclnnStatus:返回状态码,具体参见 aclnn 返回码。
第一段接口会完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 gradOutput、self、gradInput 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | gradOutput、self、gradInput 的数据类型和数据格式不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | gradOutput、self、gradInput 的维度关系不满足可 broadcast 原则 |
| ACLNN_ERR_PARAM_INVALID | 161002 | gradOutput、self、gradInput 的数据类型不满足数据类型推导规则 |
形状推导的源码依据
shape 校验与推导逻辑可以在 gelu_grad_infershape.cpp 中找到:
- 对于 Ascend950 / Ascend350 平台,算子按三个输入(
dy、x、gelu)做BroadcastShape,三者必须满足可 broadcast 关系,输出 shape 为广播后的 shape,否则返回GRAPH_FAILED并提示 "The shapes of dy, x and gelu must be broadcastable"; - 其他平台则将输入
x的 shape 直接拷贝给输出。
这解释了文档中"gradOutput 与 self 的 shape 满足 broadcast 关系、gradInput 的 shape 为 broadcast 后的 shape"的参数约束。与之对应,op_host/config/ascend950/gelu_grad_binary.json 中三个输入dy、x、y与输出z的 shape 均标记为-2(动态 shape),并分别按bfloat16、float16、float32三种 dtype 生成了三个二进制算子描述,format_match_mode为FormatAgnostic,即算子不关心输入的具体布局,统一按 ND 语义处理。
aclnnGeluBackward(第二段接口)
参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnGeluBackwardGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值
aclnnStatus:返回状态码,具体参见 aclnn 返回码。
约束说明
- 确定性计算:aclnnGeluBackward 默认确定性实现,即相同输入多次执行结果一致,便于调试与结果复现(可进一步参考 确定性计算 说明)。
调用示例
示例代码如下(取自文档,完整可编译工程可参考 examples/test_aclnn_gelu_backward.cpp,编译与执行过程详见 编译与运行样例):
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_gelu_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_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 std::vector<int64_t> selfShape = {4, 2}; std::vector<int64_t> gradOutputShape = {4, 2}; std::vector<int64_t> gradInputShape = {4, 2}; void* selfDeviceAddr = nullptr; void* gradOutputDeviceAddr = nullptr; void* gradInputDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* gradOutput = nullptr; aclTensor* gradInput = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> gradOutputHostData = {1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f}; std::vector<float> gradInputHostData = {0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f}; ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor( gradOutputHostData, gradOutputShape, &gradOutputDeviceAddr, aclDataType::ACL_FLOAT, &gradOutput); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(gradInputHostData, gradInputShape, &gradInputDeviceAddr, aclDataType::ACL_FLOAT, &gradInput); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API,需要修改为具体的API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnGeluBackward第一段接口 ret = aclnnGeluBackwardGetWorkspaceSize(gradOutput, self, gradInput, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnGeluBackwardGetWorkspaceSize 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); } // 调用aclnnGeluBackward第二段接口 ret = aclnnGeluBackward(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnGeluBackward 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和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(gradOutput); aclDestroyTensor(self); aclDestroyTensor(gradInput); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(gradOutputDeviceAddr); aclrtFree(gradInputDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例代码流程解读
示例完整覆盖了一次算子调用的标准生命周期,共分为 7 个步骤:
- 资源初始化:
aclInit→aclrtSetDevice→aclrtCreateStream,建立 Device/Stream 运行环境; - 构造输入与输出:通过
aclrtMalloc在 Device 侧申请内存,aclrtMemcpy将 Host 数据拷入,再由aclCreateTensor以 ND 格式创建self、gradOutput、gradInput三个aclTensor(示例 shape 均为{4, 2},即 8 个元素); - 两段式调用:先调用
aclnnGeluBackwardGetWorkspaceSize获得workspaceSize与executor,当workspaceSize > 0时用aclrtMalloc申请 workspace,再调用aclnnGeluBackward提交计算; - 同步等待:
aclrtSynchronizeStream阻塞等待流中任务执行完毕; - 取回结果:用
aclrtMemcpy将 Device 侧gradInput数据拷回 Host 并逐元素打印; - 释放 Tensor:
aclDestroyTensor释放三个aclTensor; - 释放设备资源:依次释放 device 内存、workspace、Stream,并执行
aclrtResetDevice与aclFinalize。
示例中的gradOutput全部取1.0f,此时输出即为 Gelu 函数对输入 x 的导数本身;将其代入梯度公式可快速验证结果。
测试与验证
仓库为 aclnnGeluBackward 提供了完整的测试体系,可用于验证算子正确性:
- ST 测试:executor_aclnnGeluBackward.py 以
torch.ops.aten.gelu_backward(grad_output, self, approximate="tanh")作为基准实现,与 NPU 上的 aclnn 调用结果对比;atk_aclnnGeluBackward.json 提供用例配置。由此可以确认,本算子的近似模式(approximate="tanh")与 PyTorch 的 tanh 近似 GELU 梯度语义保持一致; - UT 测试:包括 op_api 单测、op_host 的 infershape 单测、tiling 单测 以及 op_kernel 单测(配套数据生成脚本 gen_data.py);
- 算子底层:内核入口 gelu_grad_apt.cpp 中
gelu_grad内核接收dy、x、y、z、workspace、tiling六个参数,基于BroadcastSch调度器配合 DAG 描述完成元素级计算,并通过KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY)指定纯向量核(AIV)执行。
总结
aclnnGeluBackward 是 CANN ops-nn 中 Gelu 激活函数的反向传播算子,采用"先查 workspace、后执行计算"的两段式接口设计。本文从数学公式出发,结合 API 声明、算子定义、形状推导、内核实现 与 调用示例 等仓库资源,完整梳理了该接口的调用流程、参数约束与底层执行链路。开发者可依据本文的示例代码,在支持的产品(如 Ascend 950、Atlas A2/A3 系列等)上快速接入 Gelu 梯度计算,并借助仓库内置的 ST/UT 测试框架进行正确性验证。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考