news 2026/9/18 6:26:37

CANN ops-nn 算子开发实战:aclnnGeluBackward 两段式接口实现 Gelu 反向传播

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-nn 算子开发实战:aclnnGeluBackward 两段式接口实现 Gelu 反向传播

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 目录下同时提供了ascend950ascend350两个平台的二进制算子描述文件,与文档中"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.595769121605730711759fAN = -0.0713548162726002527220fA3 = 0.2140644488178007fBETA = 1.595769121605730711759f,通过Mul(平方)、Axpy(乘加)、Exp(指数)、AddsDiv(除法)等矢量指令完成exp(x²·AN + BETAN·x)形式的高斯核近似计算,再与dy逐元素相乘得到梯度输出。整个计算在向量寄存器中按loopNum = (count + vl - 1) / vl分块循环执行,其中vl为向量寄存器一次可容纳的元素数。

而在算子API层,gelu_grad.cpp 中的IsAiCoreSupport检查gradOutputselfunusedgradInput四个 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):gradOutputself先分别经过l0op::Contiguous转为连续布局,与gradInput一起进入l0op::GeluGrad核心计算,输出再经l0op::ViewCopy写回gradInput

aclnnGeluBackwardGetWorkspaceSize(第一段接口)

参数说明

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
gradOutput(aclTensor*)输入求梯度时的权重,即为了将正向输出的 tensor 变为标量所相乘的权重 tensorshape 需要和正向 self 的 shape 满足 broadcast 关系;dtype 与 self 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8
self(aclTensor*)输入Gelu 的正向输入值shape 需要和 gradOutput 的 shape 满足 broadcast 关系;dtype 与 gradOutput 的 dtype 满足数据类型推导规则(参见互推导关系);支持空 TensorFLOAT、FLOAT16、BFLOAT16ND0-8
gradInput(aclTensor*)输出backward 计算的输出,为 GELU 正向入参的梯度值,即对输入进行求导后的结果dtype 与 self 和 gradOutput 进行数据类型推导后的可转换的数据类型(参见互转换关系)一致;shape 与 gradOutput 和 self 进行 broadcast 后的 shape 一致FLOAT、FLOAT16、BFLOAT16ND0-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_NULLPTR161001传入的 gradOutput、self、gradInput 是空指针
ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的数据类型和数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的维度关系不满足可 broadcast 原则
ACLNN_ERR_PARAM_INVALID161002gradOutput、self、gradInput 的数据类型不满足数据类型推导规则

形状推导的源码依据

shape 校验与推导逻辑可以在 gelu_grad_infershape.cpp 中找到:

  • 对于 Ascend950 / Ascend350 平台,算子按三个输入(dyxgelu)做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 中三个输入dyxy与输出z的 shape 均标记为-2(动态 shape),并分别按bfloat16float16float32三种 dtype 生成了三个二进制算子描述,format_match_modeFormatAgnostic,即算子不关心输入的具体布局,统一按 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 个步骤:

  1. 资源初始化aclInitaclrtSetDeviceaclrtCreateStream,建立 Device/Stream 运行环境;
  2. 构造输入与输出:通过aclrtMalloc在 Device 侧申请内存,aclrtMemcpy将 Host 数据拷入,再由aclCreateTensor以 ND 格式创建selfgradOutputgradInput三个aclTensor(示例 shape 均为{4, 2},即 8 个元素);
  3. 两段式调用:先调用aclnnGeluBackwardGetWorkspaceSize获得workspaceSizeexecutor,当workspaceSize > 0时用aclrtMalloc申请 workspace,再调用aclnnGeluBackward提交计算;
  4. 同步等待aclrtSynchronizeStream阻塞等待流中任务执行完毕;
  5. 取回结果:用aclrtMemcpy将 Device 侧gradInput数据拷回 Host 并逐元素打印;
  6. 释放 TensoraclDestroyTensor释放三个aclTensor
  7. 释放设备资源:依次释放 device 内存、workspace、Stream,并执行aclrtResetDeviceaclFinalize

示例中的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内核接收dyxyzworkspacetiling六个参数,基于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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 6:26:07

微信聊天记录导出WeChatMsg使用指南:从0到1跑通完整流程

微信聊天记录导出WeChatMsg使用指南&#xff1a;从0到1跑通完整流程 【免费下载链接】WeChatMsg 提取微信聊天记录&#xff0c;将其导出成HTML、Word、CSV文档永久保存&#xff0c;对聊天记录进行分析生成年度聊天报告 项目地址: https://gitcode.com/GitHub_Trending/we/WeC…

作者头像 李华
网站建设 2026/9/18 6:24:57

机器人集群协同与编队控制实战:从算法到落地关键问题

先想象一个画面&#xff1a;场地中央二十几台小型机器人以三角形阵列推进&#xff0c;队形像被一双看不见的手捏着。侧面突然出现一个障碍物&#xff0c;阵列没有停顿&#xff0c;前端的机器人略微转向、拉开间距&#xff0c;像鱼群绕过礁石一样自然分流&#xff0c;越过障碍物…

作者头像 李华
网站建设 2026/9/18 6:24:57

COMSOL多孔介质热湿耦合仿真建模与应用

1. 项目背景与核心价值多孔介质材料在建筑保温、农业大棚、工业干燥等领域应用广泛&#xff0c;其温湿度变化直接影响使用效果。传统实验方法存在周期长、成本高、难以获取内部数据等问题。COMSOL Multiphysics作为一款多物理场仿真软件&#xff0c;能够完美模拟热风作用下多孔…

作者头像 李华
网站建设 2026/9/18 6:22:15

Python构建起点小说网大数据分析系统实战

1. 项目概述&#xff1a;当Python遇上起点小说网大数据去年接手一个网络文学数据分析项目时&#xff0c;我花了三周时间手动整理Excel表格&#xff0c;直到某天凌晨三点发现分类标签全部错位。这次惨痛经历让我意识到&#xff0c;面对起点中文网这类日均产生数万章节更新的平台…

作者头像 李华
网站建设 2026/9/18 6:21:51

三日冲刺法:决胜DAY 3的高效收尾执行指南

1. 三天冲刺法&#xff1a;为什么要把“第三天”单独拎出来讲DAY 3&#xff0c;听起来就是个平平无奇的日期标记。但如果你把它当成一个“冲刺计划”的最后一天&#xff0c;那这24小时的分量就完全不一样了。最近我在实践一种自己的工作节奏&#xff0c;叫作“三日冲刺法”。不…

作者头像 李华