CANN ops-nn 融合激活算子 aclnnGeluMul 使用指南:GELU 与逐元素乘法的两段式接口实战
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
本文面向在 Ascend NPU 上开发或移植 Transformer/大模型推理代码的开发者,系统讲解 CANN ops-nn 算子库中 GeluMul 融合算子的 aclnn 接口aclnnGeluMul。文章以 activation/gelu_mul/docs/aclnnGeluMul.md 为核心骨架,结合 activation/gelu_mul 目录下的算子定义、Tiling、Kernel 与测试源码,覆盖产品支持矩阵、数学定义、两段式接口用法、完整参数语义、约束条件与可编译运行的调用示例。读完本文,你将能够独立完成 GeluMul 算子的 workspace 计算、内存申请与算子下发执行。
功能说明:一次 Kernel 完成“分割 → GELU → 乘”
GeluMul 是一个将"切分 + 激活 + 逐元素乘"三个操作融合进单个算子下发的激活类算子,常见于 GLU 类(Gated Linear Unit)门控结构的变体实现中。其接口功能为:将输入 Tensor 按照最后一个维度平均分为左右两个 Tensorx1与x2,对左侧的x1执行 GELU 激活计算,再将激活结果与右侧的x2逐元素相乘,输出 shape 的最后一维变为输入的一半。
数学定义
给定输入张量input,其最后一维长度为2d,GeluMul 的计算过程如下:
- 沿最后一维将输入切分为两个部分:
$$ x_1 = \text{input}[..., :d], \quad x_2 = \text{input}[..., d:] $$
- 对
x1应用 GELU 激活函数,支持两种近似模式:
- tanh 模式(
approximate="tanh"):
$$ \text{GELU}(x) = 0.5 \cdot x \cdot \left( 1 + \tanh\left( \sqrt{\frac{2}{\pi}} \cdot \left( x + 0.044715 x^3 \right) \right) \right) $$
- erf 模式(
approximate="none"):
$$ \text{GELU}(x) = 0.5 \cdot x \left( 1 + \text{erf}\left( \frac{x}{\sqrt{2}} \right) \right) $$
即先计算:
$$ x_1 = \text{GELU}(x_1) $$
- 最终输出为二者逐元素乘积:
$$ \text{out} = x_1 \times x_2
### 模式参数的真实语义 `approximate` 属性对应公式中的 GELU 实现模式。从 [算子定义源码](https://link.gitcode.com/i/67163d5e4eb60993b6cdc2c26965eb0f) 与 [IR 原型](https://link.gitcode.com/i/71a4c93ec11e17798d36d9c9d54e4064) 可见,它被定义为可选字符串属性,**默认值为 `"none"`**,即默认走 erf 精确模式。在 [Tiling 源码](https://link.gitcode.com/i/aa631cb42b82c5d67ce8bfa2490f5c03) 中,该字符串会被解析为整数模式: - `"none"` → `approximateMode = 0`,对应 erf 模式; - `"tanh"` → `approximateMode = 1`,对应 tanh 模式; - 其它取值直接返回 `GRAPH_FAILED` 报错。 ### Kernel 层的计算实现 在 [Kernel 实现](https://link.gitcode.com/i/ee11accf08ab5de4eb22eafc92835ee1) 中可以看到两种模式的真实硬件实现路径: - **tanh 模式**(`ComputeGeluTanh`):利用恒等式 `tanh(z) = (e^{2z} - 1) / (e^{2z} + 1)`,将 `x / (1 + exp(-sqrt(8/pi)(x + 0.044715x³)))` 通过 `Muls → Exp → Adds → Div` 一系列向量指令完成,其中 `ALPHA = -1.5957691f` 正是 `-sqrt(8/pi)` 的近似值,`BETA = 0.044715f` 对应 tanh 公式中的常数; - **erf 模式**(`ComputeGeluErf`):非 330 AICore 平台使用分段多项式逼近 erf 的 8 个多项式系数(`ERF_PARAM1` ~ `ERF_PARAM7`)并结合 `Exp/Div` 完成;`__CCE_AICORE__ == 330` 平台则直接调用 AscendC 的硬件 `Erf` 指令(`ErfAlgo::SUBSECTION_POLYNOMIAL_APPROXIMATION`)后计算 `0.5 * (1 + erf(x / sqrt(2)))`。 无论哪种模式,输入都会被先 `Cast` 到 FP32 进行计算以保证精度,FP16/BF16 输入在写回前再 `Cast` 回原类型,最终通过 `Mul` 指令完成 `x1 * x2`。这正是该算子被设计为"融合算子"的意义——避免 GELU 与乘法之间多次读写 Global Memory。 ## 产品支持情况 aclnnGeluMul 在不同 Ascend 产品上的支持情况如下(与 [算子 README](https://link.gitcode.com/i/544fd412d8e39e41069312b3e97122ec) 一致): | 产品 | 是否支持 | | :--- | :--- | | Ascend 950PR / Ascend 950DT | √ | | Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | √ | | Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ | | Atlas 200I/500 A2 推理产品 | × | | Atlas 推理系列产品 | × | | Atlas 训练系列产品 | × | | Atlas Kirin X90 处理器系列产品 | √ | | Atlas Kirin 9030 处理器系列产品 | √ | 从 [算子定义源码](https://link.gitcode.com/i/67163d5e4eb60993b6cdc2c26965eb0f) 的 `AICore().AddConfig` 注册信息同样可以印证上述支持范围:`ascend910b`、`ascend910_93`、`ascend950` 以及 `kirinx90`/`kirin9030`(使用独立的 Kirin Core 配置)。另外注意一个产品差异: > Atlas Kirin X90 / Atlas Kirin 9030 处理器系列产品**不支持 BFLOAT16**,其 Kirin 配置仅注册了 FLOAT16 与 FLOAT 两种数据类型(见 [gelu_mul_def.cpp](https://link.gitcode.com/i/67163d5e4eb60993b6cdc2c26965eb0f#L59-L72))。 同时,[config 目录](https://link.gitcode.com/i/711d2c38d9d8a1622f6e94cf33063e51) 下为各平台分别提供了算子二进制配置文件(如 [ascend910b/gelu_mul_binary.json](https://link.gitcode.com/i/805a5e9bc2c9850ac272d73747faf34a)),其中 float32/bfloat16/float16 三种 dtype 各对应一个 `GeluMul_xxx` 二进制,`approximate` 属性默认取 `"none"`。 ## 函数原型:两段式接口(GetWorkspaceSize + 执行) 与 CANN 其它 aclnn 算子一致,GeluMul 采用**两段式接口**设计:必须先调用 `aclnnGeluMulGetWorkspaceSize` 获取入参校验结果、根据计算流程计算所需的 workspace 大小并完成算子执行器(executor)的构建,再调用 `aclnnGeluMul` 真正执行计算。两段式接口的通用背景可参考 [两段式接口说明](https://link.gitcode.com/i/5022a6ca166908ad6c8de0107bfacdf3)。 ### 第一段:aclnnGeluMulGetWorkspaceSize ```Cpp aclnnStatus aclnnGeluMulGetWorkspaceSize( const aclTensor *input, char *approximateOptional, const aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)第二段:aclnnGeluMul
aclnnStatus aclnnGeluMul( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)第一段接口参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| input(aclTensor*) | 输入 | 输入的张量,公式中的 input | 支持空 Tensor;最后一维值为偶数且小于等于 1024;其他维度的乘积小于等于 200000 | BFLOAT16、FLOAT16、FLOAT | ND | 2-8 | √ |
| approximateOptional(char*) | 输入 | Gelu 计算的模式 | 只支持"none"和"tanh",分别对应 erf 模式和 tanh 模式,输入为空指针时为"none" | - | - | - | - |
| out(aclTensor*) | 输出 | 输出的张量,公式中的 out | 输出数据类型与输入保持一致;输出 shape 除最后一维外与输入一致;最后一维的值为输入最后一维值的二分之一 | BFLOAT16、FLOAT16、FLOAT | ND | 2-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
关于 shape 约束,可以从 Tiling 源码 中的ShapeCheck()得到源码级印证:lastDimSize必须为偶数且不超过 1024、batchSize(即除最后一维外所有维度的乘积)不超过 200000;infershape 源码 中输出 shape 的推导逻辑即为yShape->SetDim(xDimNum - 1, xShape->GetDim(xDimNum - 1) / 2),即最后一维直接除以 2,其余维度原样复制,输入输出数据类型保持一致。
第一段接口返回值与错误码
返回aclnnStatus状态码,通用返回码定义可参考 aclnn返回码。第一段接口会完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 input 或 out 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | input 的数据类型不在支持的范围之内 |
第二段接口参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnGeluMulGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
第二段接口同样返回aclnnStatus状态码,具体参见 aclnn返回码。
约束说明
- 确定性计算:
aclnnGeluMul默认为确定性实现(对相同的输入与配置,多次运行结果一致,便于精度对比与调试)。 - 典型场景尾轴为 16 的倍数:当尾轴(最后一维)为非 32 Byte 对齐时,建议走小算子拼接逻辑。这一点在 Kernel 实现 中体现为
Process()内部的BigTailProcess(d > PPMaxCalNum时按行循环处理)与SmallTailProcess(d较小时做 32 Byte 对齐、多行合并计算)两条路径,并由 Tiling 源码 的GetNeedCoreNum()根据d与PPMaxCalNum的关系决定多核切分策略。 - 平台差异:Atlas Kirin X90 / Kirin 9030 不支持 BFLOAT16(见上文产品支持情况)。
调用示例
以下示例来自 示例源码(与文档示例一致),演示了从资源初始化、Tensor 构造、两段式接口调用到结果回读、资源释放的完整流程。完整编译与执行过程请参考 编译与运行样例。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_gelu_mul.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 shapeSize = 1; for (auto i : shape) { shapeSize *= i; } return shapeSize; } void PrintOutResult(std::vector<int64_t> &shape, void** deviceAddr) { auto size = GetShapeSize(shape); std::vector<float> resultData(size, 0); auto ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), *deviceAddr, size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return); for (int64_t i = 0; i < size; i++) { LOG_PRINT("aclnnGeluMul result[%ld] is: %f\n", i, resultData[i]); } } 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 == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 std::vector<int64_t> inputShape = {2, 4}; std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7}; void* inputDeviceAddr = nullptr; aclTensor* input = nullptr; // 创建input aclTensor ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input); CHECK_RET(ret == ACL_SUCCESS, return ret); char approximate[] = "tanh"; std::vector<int64_t> outShape = {2, 2}; std::vector<float> outHostData(2 * 2, 1); aclTensor* out = nullptr; void* outDeviceAddr = nullptr; // 创建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 = 16 * 1024 * 1024; aclOpExecutor* executor; // 调用aclnnGeluMul第一段接口 ret = aclnnGeluMulGetWorkspaceSize(input, approximate, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnGeluMulGetWorkspaceSize 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); } // 调用aclnnGeluMul第二段接口 ret = aclnnGeluMul(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnGeluMul 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的接口定义修改 PrintOutResult(outShape, &outDeviceAddr); // 6. 释放aclTensor,需要根据具体API的接口定义修改 aclDestroyTensor(input); aclDestroyTensor(out); // 7.释放device资源,需要根据具体API的接口定义修改 aclrtFree(inputDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例要点解读
- 输入/输出 shape 关系:示例中
inputShape = {2, 4}(最后一维2d = 4,即d = 2),outShape = {2, 2},最后一维恰为输入的一半。前两行数据的计算过程为:x1 = {0, 1, 2, 3}(前 4 个元素的前半{0, 1}与后半{2, 3}分别为第一行的 x1、x2)……依此类推,out的最后一维从 4 缩减为 2。 - approximate 传参:
char approximate[] = "tanh"直接以 C 字符串形式传入第一段接口,若希望使用默认 erf 模式,可传入nullptr或"none"。 - workspace 生命周期:第一段接口输出的
workspaceSize决定第二段接口前需要申请多大的 Device 侧内存;当workspaceSize == 0时无需申请。示例中初始化为16 * 1024 * 1024仅为占位值,实际值以第一段接口返回为准。从 Tiling 源码 看,workspaceSize_被固定设置为16 * 1024 * 1024(16MB),并通过tilingContext->GetWorkspaceSizes(1)返回给框架。
图模式(IR)调用方式
除 aclnn 单算子调用外,GeluMul 还支持通过算子 IR 在计算图中构图调用(见 算子 README 的"调用说明")。图模式下使用的是ge::op::GeluMul这一算子原型,其定义位于 gelu_mul_proto.h:输入x支持DT_BF16/DT_FLOAT16/DT_FLOAT、ND 格式、2~8 维;属性approximate为 String 类型、默认"none";输出y与输入同类型同格式,shape 除最后一维减半外保持一致。
测试与验证
仓库为 GeluMul 提供了较为完整的测试覆盖,可作为二次开发或移植时的验证参考:
- UT(单元测试):
- infershape 测试:例如以
{4, 1, 1280}的 FP16 输入验证 InferShape 与 InferDataType 均返回GRAPH_SUCCESS; - tiling 测试:验证不同 shape/dtype 组合下的 Tiling 结果;
- kernel 测试:配合 gen_data.py 生成测试数据、compare_data.py 比对结果。
- infershape 测试:例如以
- ST(系统测试):atk_aclnnGeluMul.json 定义了接口级测试用例配置,executor_aclnnGeluMul.py 为对应的执行脚本。
总结
aclnnGeluMul将"沿最后一维分割 → GELU 激活(erf/tanh 两种模式)→ 逐元素相乘"融合为一次 NPU 算子下发,输入最后一维要求为偶数且不超过 1024,其余维度乘积不超过 200000,支持 2~8 维 ND 格式的 BFLOAT16/FLOAT16/FLOAT 数据(Kirin 平台除外不支持 BFLOAT16)。使用上遵循 CANN 两段式接口规范:先通过aclnnGeluMulGetWorkspaceSize完成参数校验与 workspace 计算,再以aclnnGeluMul下发执行。结合 示例源码 与 Kernel 实现 阅读,可以快速掌握该融合算子的调用范式与底层计算路径,并据此在自有工程中正确接入。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考