CANN ops-nn 算子 aclnnGeluBackward 反向接口详解:GELU 梯度计算的实现与调用实践
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
GELU(Gaussian Error Linear Unit)激活函数在 Transformer 类模型中被广泛使用,其反向传播梯度的高效计算直接影响训练性能。本文以 CANN 神经网络算子库 ops-nn 中 aclnnGeluBackward 接口文档 为主线,结合仓库源码完整讲解该算子的数学原理、两段式 API 用法、参数与返回码约束,并深入 Kernel 层剖析其多项式近似实现与 tiling 调度策略。读者阅读后将掌握在 Atlas A2 训练/推理系列产品上正确调用 aclnnGeluBackward 完成 GELU 反向计算的完整方法,并理解该算子从 aclnn 接口到底层 NPU 核函数调用的全链路实现。
产品支持情况与算子定位
aclnnGeluBackward 用于在昇腾 NPU 上完成 GELU 激活函数的反向传播计算,其产品支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
该算子在 ops-nn 仓库中位于 experimental/activation/gelu_grad 目录,贡献记录显示其为新增的 GeluGrad 算子,对应底层算子类型(OpType)为GeluGrad。算子定义为三输入一输出:dy(反向梯度)、x(正向输入)、y(正向输出)与输出z(输入梯度),详细原型信息见 README.md。
功能说明与数学原理
接口功能
aclnnGeluBackward 的功能是完成 GELU 的反向传播:根据反向梯度gradOutput与正向输入self,计算得到输入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}}) $$
即 GELU 的导数为 $\Phi(x) + x \cdot \varphi(x)$ 的形式,其中 $\Phi$ 为标准正态分布累积分布函数、$\varphi$ 为标准正态分布概率密度函数。
GELU 的 tanh 近似计算公式为:
$$ Gelu(x)=0.5x(1+tanh(\sqrt{2/\pi}(x+0.044715x^3))) $$
注意:上述公式是接口的数学语义定义。实际 Kernel 实现并不直接计算erf,而是采用多项式系数对erf进行逼近(详见下文"Kernel 实现与近似算法"一节),因此在极个别边界输入上可能出现与标准erf实现的微小数值差异。
两段式函数原型
与 CANN 其他 aclnn 算子一致,aclnnGeluBackward 采用两段式接口设计:必须先调用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,头文件注释中给出了完整的 Mermaid 计算流程:gradOutput/self/gradInput先分别经过l0op::Contiguous转为连续 Tensor,再送入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(即不包含 BFLOAT16)。
从源码看,上述约束在 aclnn_gelu_backward.cpp 中通过CheckParams逐一落实:
- 空指针检查:
CheckNotNull对gradOutput、self、gradInput三个入参执行OP_CHECK_NULL; - 数据类型推导检查:
CheckPromoteType调用CheckPromoteTypeGeluBackward,支持列表为DT_FLOAT、DT_FLOAT16、DT_BF16(见源码第 29-30 行的DTYPE_SUPPORT_LIST); - shape 与 broadcast 检查:
CheckShape中OP_CHECK_MAX_DIM限制最大维度(对应文档的 0-8 维),并通过OP_CHECK_BROADCAST_AND_INFER_SHAPE校验gradOutput与self的可广播性,且要求gradInput的 shape 恰好等于二者 broadcast 后的 shape。
返回值
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 的数据类型不满足数据类型推导规则 |
从实现来看,CheckParams的返回顺序与文档描述一致:先判空(返回ACLNN_ERR_PARAM_NULLPTR),再做 dtype 推导与 shape 校验(返回ACLNN_ERR_PARAM_INVALID)。
aclnnGeluBackward 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnGeluBackwardGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值:aclnnStatus,返回状态码,具体参见 aclnn返回码。
第二段接口的实现非常精简(见 aclnn_gelu_backward.cpp 第 205-211 行):调用框架统一的CommonOpExecutorRun(workspace, workspace_size, executor, stream)完成计算调度。
约束说明
- 确定性计算:aclnnGeluBackward 默认确定性实现,即相同输入在多次运行中产生一致的计算结果,利于调试与结果复现。
调用示例
以下示例代码摘自接口文档,演示了完整的调用流程。具体编译和执行过程请参考编译与运行样例。仓库中另有可直接运行的 test_aclnn_gelu_grad.cpp 示例,以及通过bash build.sh --run_example gelu_grad eager cust --vendor_name=custom --experimental命令一键运行的方式(见 README.md)。
#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<int> gradOutputHostData = {1, 1, 1, 1, 1, 1, 1, 1}; std::vector<int> gradInputHostData = {0, 0, 0, 0, 0, 0, 0, 0}; ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor( gradOutputHostData, gradOutputShape, &gradOutputDeviceAddr, aclDataType::ACL_INT32, &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("aclnnGeluBackward 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; }示例代码关键点解读
- 资源初始化(固定写法):
aclInit完成 ACL 运行时初始化,aclrtSetDevice绑定设备,aclrtCreateStream创建执行流。示例中deviceId取 0,实际使用时需按目标设备填写。 - Tensor 构造:
CreateAclTensor模板函数完成 host 数据到 device 内存的搬运(aclrtMalloc+aclrtMemcpy),按连续 Tensor 规则计算 strides,并通过aclCreateTensor创建aclTensor。本示例中gradOutput与gradInput的 host 数据使用int类型初始化,而gradInput以ACL_FLOAT类型创建,实际接入真实模型时请保证三者数据类型满足接口的互推导约束(FLOAT/FLOAT16/BFLOAT16)。 - 两段式调用:先调用
aclnnGeluBackwardGetWorkspaceSize拿到workspaceSize与executor;若workspaceSize > 0则在 device 侧aclrtMalloc申请 workspace;再调用aclnnGeluBackward(workspaceAddr, workspaceSize, executor, stream)真正执行计算。 - 结果回收与资源释放:
aclrtSynchronizeStream等待计算完成,aclrtMemcpy将结果回拷 host 侧;最后依次释放aclTensor、device 内存、workspace、stream,并aclrtResetDevice+aclFinalize。
从源码看 aclnnGeluBackward 的实现细节
第一段接口的计算流程组装
aclnn_gelu_backward.cpp 中aclnnGeluBackwardGetWorkspaceSize的核心逻辑为:
- 创建执行器:通过
CREATE_EXECUTOR()创建aclOpExecutor; - 参数校验:调用上文所述的
CheckParams; - 空 Tensor 短路:若
self或gradOutput为空 Tensor(IsEmpty()),直接返回workspaceSize = 0与执行器,不做实际计算; - 形状推断:
BroadcastInferShape得到gradOutput与self的 broadcast 后 shape; - 隐式类型提升:
op::PromoteType(gradOutput, self)推导两个输入提升后的公共数据类型promoteType; - 连续性处理:三个输入各自经过
l0op::Contiguous转为连续 Tensor; - 平台分支:在 ASCEND910 ~ ASCEND910E 平台走不广播分支,其余平台(含 Atlas A2 系列)走 broadcast 分支——
BroadcastTensor在 shape 不一致时插入l0op::BroadcastTo将gradOutput/self扩展到 broadcast 后的 shape; - 类型转换与计算:
l0op::Cast将输入转到promoteType,再调用l0op::GeluGrad完成梯度计算,输出再Cast回gradInput的原 dtype,最终经l0op::ViewCopy写回gradInput(非连续输出场景由 ViewCopy 完成数据搬移); - workspace 计算:
*workspaceSize = uniqueExecutor->GetWorkspaceSize()汇总整条 l0 计算链所需的 workspace,并通过ReleaseTo(executor)移交执行器。
值得说明的是,头文件注释中标注的接口输入格式支持FRACTAL_NZ、NC1HWC0、ND三种,而接口文档的参数表与底层 GeluGrad 算子定义(见下文)均限定为 ND 格式,从源码看实际计算路径中Contiguous会把各格式统一整理为连续内存视图,用户按 ND 构造 Tensor 即可。
底层 GeluGrad 算子定义与 shape 推导
底层算子 GeluGrad 的定义位于 gelu_grad_def.cpp:三个输入dy、x、y与一个输出z,数据类型均支持DT_FLOAT、DT_FLOAT16、DT_BF16,格式为 ND,并注册了ascend910b与ascend310b两种 AICore 配置。
其 shape 推导逻辑位于 gelu_grad_infershape.cpp:输出z的 shape 直接取输入 0(dy)的 shape,即输出梯度与输入梯度同 shape。
tiling 调度策略
gelu_grad_tiling.cpp 实现了基于数据量的多核切分策略,要点如下:
- 依据 SOC 版本选择计算方式:
ASCEND310B使用 tiling key 1(versionNum = 1),其余平台使用 tiling key 0(对应ASCEND910B);且 BF16 输入仅支持ASCEND910B/ASCEND310B两个平台; - 以 UB 内存(
ubSize)、block 大小(blockSize)与数据类型长度推导单次搬运的数据个数tileDataNum; - 当数据量小到可被单核容纳时(
tileDataNum >= inputNum)仅用 1 个核;否则按"每个核至少 32B 数据"的原则确定参与计算的核数coreNum; - 将输入按 32B 对齐后均匀切分到各核,区分"大核/小核"(前
tailBlockNum个核多分一块),并为每个核计算完整的 tile 搬运次数与尾块数据个数,写入GeluGradTilingData; - 通过
context->SetBlockDim(coreNum)设置核数,并为l0计算链申请系统级 workspace。
Kernel 实现与近似算法
Kernel 层入口在 gelu_grad.cpp,根据 tiling key 实例化KernelGeluGrad<TYPE_DY, TYPE_X, TYPE_Z, Is0versionNum>(gelu_grad.h),其核心计算流程为CopyIn → Compute → CopyOut流水循环。
从源码看,Kernel 并未直接调用erf,而是用多项式系数对误差函数做逼近,再组合出完整梯度公式。以 tiling key 0(Is0versionNum = true,ASCEND910B)为例,系数与计算步骤为:
- 系数:
COEFF0 = -0.0713548162726002527220f、COEFF1 = -1.595769121605730711759f、COEFF2 = 0.2140644488178007f、COEFF3 = 1.595769121605730711759f、COEFF4 = 1.0f; - 计算过程:先求
x^2并分别构造两路多项式exp((COEFF0·x² + COEFF1)·x)与(COEFF2·x² + COEFF3)·x,再通过Adds(…, 1.0)、Duplicate、Div组合出1/(exp(…)+1)形式的 sigmoid 结构,经若干Mul组合后与dy相乘,最后Cast回输出类型(CAST_RINT舍入)写入z; - 对于 FLOAT16/BFLOAT16 输入,先
Cast到 float 计算中间量以提升精度,计算完成后再Cast回原类型,避免低精度中间累积误差。
Kernel 通过BUFFER_NUM = 2的双缓冲队列隐藏搬移开销,并依据 tiling 数据区分大小核与尾块处理,保证整段数据被完整计算。
单元测试验证
仓库提供了基于 gtest 的 op_api 单测 test_aclnn_gelu_grad.cpp,覆盖 float32、float16(以及 bf16 等)场景:构造gradOutput为全 1、self取值于 [-1, 1] 的 ND Tensor,校验aclnnGeluBackwardGetWorkspaceSize返回ACL_SUCCESS,并对输出gradInput设置了 0.001/0.01 量级的精度阈值,验证了接口参数校验与数值正确性;同时包含 Ascend950 平台用例以及 shape 满足 broadcast 关系(如{1,5}与{2,5})的用例。另有 infershape 单测 验证 shape 推导。
使用注意事项小结
- 必须两段式调用:先
aclnnGeluBackwardGetWorkspaceSize后aclnnGeluBackward,workspace 仅当workspaceSize > 0时才需申请; - shape 约束:
gradOutput与self需满足 broadcast 关系,gradInput的 shape 必须等于二者 broadcast 后的 shape; - dtype 约束:三者数据类型需满足互推导/互转换规则,Atlas 推理与训练系列产品上仅支持 FLOAT、FLOAT16;
- 支持空 Tensor 与非连续 Tensor:空输入直接短路返回,非连续输入由框架自动转连续并回写;
- 确定性计算:默认确定性实现,可放心用于需要结果可复现的训练调试场景。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考