CANN ops-nn 算子库 aclnnElu / aclnnInplaceElu 接口详解:ELU 激活函数的两段式调用与源码实现
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
本篇文章围绕 CANN 神经网络算子库(ops-nn)中的 ELU(Exponential Linear Unit,指数线性单元)激活算子展开,系统讲解aclnnElu与aclnnInplaceElu两套单算子 API 的功能定义、函数原型、参数约束、返回码、两段式调用流程及完整可运行的调用示例,并结合仓库内 op_api/aclnn_elu.cpp、op_api/elu.cpp、op_kernel/arch35/elu_dag.h 等源码,深入剖析其参数校验、计算图构造与 NPU 内核实现。读完本文,你将能够独立完成 ELU 算子(含就地写入版本)的 NPU 侧编程调用,并理解其底层执行链路。
一、产品支持情况
aclnnElu与aclnnInplaceElu接口在 CANN ops-nn 中的产品支持情况如下(以关联文档 activation/elu/docs/aclnnElu&aclnnInplaceElu.md 为准):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 支持 |
| Atlas 训练系列产品 | 支持 |
说明:仓库中 activation/elu/README.md 的产品支持表格将 Atlas 200I/500 A2 推理产品记录为支持,与本文档描述存在差异;实际部署时请以目标产品实测结果为准。
二、功能说明与计算公式
接口功能:对输入张量self中的每个元素x调用指数线性单元激活函数 ELU,并将计算结果写入输出张量out中。
计算公式如下:
$$ ELU(x) = \begin{cases} scale \ast x, \quad x > 0\ \alpha \ast scale \ast (exp(x \ast inputScale)-1), \quad x \leq 0 \end{cases} $$
与标准 ELU 定义相比,该接口引入了两个额外的标量参数,用于增强表达力:
alpha(α):负半轴的激活系数,控制负区间输出的饱和值;scale:整体缩放系数,作用于正负两个区间;inputScale:输入缩放系数,作用于指数内部的自变量x。
当三者均取默认值 1.0 时,公式退化为经典的 ELU 形式ELU(x) = x (x>0)、α·(eˣ−1) (x≤0)。在 op_host/elu_def.cpp 的算子定义中,alpha、scale、input_scale三个属性均为OPTIONAL类型,默认值为1.0,与公式语义一致。
三、两段式接口与函数原型
aclnnElu与aclnnInplaceElu实现相同的功能,区别仅在于输出结果的存放方式,请根据实际场景选择:
- aclnnElu:需要调用方新建一个输出张量对象
out来存储计算结果; - aclnnInplaceElu:无需新建输出张量对象,直接在输入张量
selfRef的内存中原地写入计算结果,节省一份输出张量的 Device 侧内存。
两者都遵循 CANN 单算子 API 的两段式接口调用范式(详见 docs/zh/context/two_phase_api.md):必须先调用第一段接口aclnnEluGetWorkspaceSize/aclnnInplaceEluGetWorkspaceSize完成入参校验、算子计算流程编排,并返回计算所需 workspace 大小以及包含算子计算流程的执行器;随后按返回的workspaceSize在 Device 侧申请 workspace 内存,再调用第二段接口aclnnElu/aclnnInplaceElu真正执行计算。
四个接口的函数原型如下:
aclnnStatus aclnnEluGetWorkspaceSize( const aclTensor *self, const aclScalar *alpha, const aclScalar *scale, const aclScalar *inputScale, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnElu( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)aclnnStatus aclnnInplaceEluGetWorkspaceSize( aclTensor *selfRef, const aclScalar *alpha, const aclScalar *scale, const aclScalar *inputScale, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnInplaceElu( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)注意:第二段接口不能重复调用。即
GetWorkspaceSize → Execute → Execute的连续两次执行会产生异常,每次计算都应重新走一遍两段式流程。
四、aclnnEluGetWorkspaceSize 参数与返回码
4.1 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 表示ELU激活函数的输入,公式中的x | 支持空Tensor | FLOAT、FLOAT16、DOUBLE、BFLOAT16 | ND | 0-8 | √ |
| alpha(aclScalar*) | 输入 | 表示ELU激活函数的激活系数,公式中的α | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| scale(aclScalar*) | 输入 | 表示ELU激活函数的缩放系数,公式中的scale | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| inputScale(aclScalar*) | 输入 | 表示ELU激活函数的输入的缩放系数,公式中的inputScale | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| out(aclTensor*) | 输出 | 表示ELU激活函数的输出 | shape需要与self一致 | FLOAT、FLOAT16、DOUBLE、BFLOAT16 | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在Device侧申请的workspace大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回op执行器,包含了算子计算流程 | - | - | - | - | - |
说明:在 Atlas 训练系列产品(910 架构)上,
self与out的数据类型仅支持 FLOAT、FLOAT16、DOUBLE(不含 BFLOAT16)。
对照 op_api/aclnn_elu.cpp 中的DTYPE_SUPPORT_LIST,接口层支持DT_FLOAT、DT_FLOAT16、DT_DOUBLE、DT_BF16四种类型,与上表一致。三个aclScalar参数均要求能安全转换为 FLOAT(CanCast校验),转换失败将直接返回ACLNN_ERR_PARAM_INVALID。
4.2 返回值
返回aclnnStatus状态码,具体定义参见 docs/zh/context/aclnn_return_code.md。第一段接口会完成入参校验,出现以下场景时返回对应错误:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 参数self、alpha、scale、inputScale、out是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数self的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数alpha、scale、inputScale的数据类型不可转换为FLOAT |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数out的数据类型不是self可转换的 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数self、out的shape不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数self、out的维度大于8 |
这些校验逻辑与源码一一对应:CheckNotNull负责空指针检查,CheckDtypeValid负责数据类型支持范围、CanCast可转换性以及self/out间的可转换关系检查,CheckShape负责 shape 一致性与最大维度(MAX_DIM_LEN = 8)检查,具体见 op_api/aclnn_elu.cpp。
五、aclnnElu 参数说明
第二段接口aclnnElu用于在 Stream 上真正执行算子计算,其参数如下:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在Device侧申请的workspace内存地址 |
| workspaceSize | 输入 | 在Device侧申请的workspace大小,由第一段接口aclnnEluGetWorkspaceSize获取 |
| executor | 输入 | op执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的Stream |
返回值同样为aclnnStatus,参见 docs/zh/context/aclnn_return_code.md。从源码看,第二段接口的实现非常精简,直接调用框架能力CommonOpExecutorRun(workspace, workspaceSize, executor, stream)完成计算(见 op_api/aclnn_elu.cpp),真正的算子逻辑全部封装在执行器中。
六、aclnnInplaceEluGetWorkspaceSize 参数与返回码
6.1 参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| selfRef(aclTensor*) | 输入 | 表示ELU激活函数的输入,公式中的x | 支持空Tensor | FLOAT、FLOAT16、DOUBLE、BFLOAT16 | ND | 0-8 | √ |
| alpha(aclScalar*) | 输入 | 表示ELU激活函数的激活系数,公式中的α | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| scale(aclScalar*) | 输入 | 表示ELU激活函数的激活系数,公式中的scale | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| inputScale(aclScalar*) | 输入 | 表示ELU激活函数的输入的缩放系数 | 数据类型需要是可转换为FLOAT的数据类型 | - | - | - | - |
| workspaceSize(uint64_t*) | 输出 | 返回需要在Device侧申请的workspace大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回op执行器,包含了算子计算流程 | - | - | - | - | - |
说明:在 Atlas 训练系列产品(910 架构)上,
selfRef的数据类型仅支持 FLOAT、FLOAT16、DOUBLE。
从源码实现看,aclnnInplaceEluGetWorkspaceSize与aclnnEluGetWorkspaceSize共用同一套GetWorkspaceSizeCommon逻辑,仅将输出张量替换为输入张量自身:GetWorkspaceSizeCommon(selfRef, alpha, scale, inputScale, selfRef, ...)(见 op_api/aclnn_elu.cpp),即 inplace 版本通过"输出与输入同一地址"的方式实现原地计算。
6.2 返回值
返回aclnnStatus,参见 docs/zh/context/aclnn_return_code.md。第一段接口完成入参校验,错误场景如下:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 参数selfRef、alpha、scale、inputScale是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数selfRef的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数alpha、scale、inputScale的数据类型不可转换为FLOAT |
| ACLNN_ERR_PARAM_INVALID | 161002 | 参数selfRef的维度大于8 |
七、aclnnInplaceElu 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在Device侧申请的workspace内存地址 |
| workspaceSize | 输入 | 在Device侧申请的workspace大小,由第一段接口aclnnInplaceEluGetWorkspaceSize获取 |
| executor | 输入 | op执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的Stream |
返回值同样为aclnnStatus,参见 docs/zh/context/aclnn_return_code.md。
八、源码级纵深:计算流程与内核实现
8.1 第一段接口构造的计算图
aclnnEluGetWorkspaceSize/aclnnInplaceEluGetWorkspaceSize内部并非直接调用单一 ELU 内核,而是通过 l0op 接口编排出一条计算流水线(见 op_api/aclnn_elu.cpp):
- 空 Tensor 短路:若
self->IsEmpty()为真,直接返回workspaceSize = 0,不构造任何算子; - Contiguous:
l0op::Contiguous将非连续输入规整为连续张量; - Elu 主计算:
l0op::Elu(contiguousSelf, alpha, scale, inputScale, executor)执行元素级 ELU 计算; - Cast:
l0op::Cast将 ELU 结果转换到out的目标数据类型; - ViewCopy:
l0op::ViewCopy将中间结果写入最终输出张量(inplace 场景下即写回selfRef)。
该计算图在 op_api/aclnn_elu.h 的注释中以 mermaid 形式给出,清晰展示了Self → Contiguous → Elu → Cast → ViewCopy → out的数据流,以及alpha/scale/inputScale三个标量作为 ELU 节点属性的依赖关系。
8.2 Inf/NaN 特判分支
GetWorkspaceSizeCommon中存在一个特殊处理分支:当alpha或inputScale为 Inf/NaN 时(NeedInfNanHandling判断),不再走l0op::Elu,而是改用l0op::Threshold+l0op::Muls的组合规避数值异常:先将输入与 0 比较得到-alpha的阈值结果,再整体乘以scale(见 op_api/aclnn_elu.cpp)。这是源码中为确定性计算与数值安全额外引入的保护逻辑,调用方通常无需感知。
8.3 AiCore / AiCpu 双路径分流
在 l0op 层(op_api/elu.cpp),ELU 会根据平台架构与数据类型选择执行后端:
- AiCore 路径:
IsAiCoreSupport判定当前 NPU 架构支持且数据类型在DT_FLOAT/DT_FLOAT16(DAV_2201 架构或 regbase 模式下额外支持DT_BF16)时,通过ADD_TO_LAUNCHER_LIST_AICORE将算子加入 AiCore 任务队列; - AiCpu 路径:其余情况(如 DOUBLE 类型或架构不支持)回退到
ADD_TO_LAUNCHER_LIST_AICPU,属性名映射为{"alpha", "scale", "input_scale"},对应 op_kernel_aicpu/elu_aicpu.cpp 中的 AICPU 实现。
从算子定义侧看,op_host/elu_def.cpp 为 Elu 注册了ascend950与ascend350两个 AICore 配置,支持动态 shape、动态 rank,并开启PrecisionReduceFlag(精度降低标志),便于在精度与性能间取舍。
8.4 NPU 内核中的 expm1 优化
在 AiCore 侧,内核入口为 op_kernel/elu_apt.cpp,通过ElementwiseSch调度器执行 op_kernel/arch35/elu_dag.h 中定义的EluDag计算图。值得关注的是内核中对exp(x*inputScale) - 1的数值优化:
- 当
|z| < 0.1(z = x * inputScale)时,采用Horner 形式的多项式展开近似expm1(z):FLOAT 路径取到 5 阶(系数 1/120、1/24、1/6、1/2、1),非 FLOAT 路径取到 4 阶,避免小数值下直接做exp再减 1 引入的灾难性抵消; - 当
|z| ≥ 0.1时,使用硬件Exp指令计算后减 1; - 通过
Select指令按|z|阈值选择两路结果,随后乘alpha,再按x > 0用Select合并正负半轴结果,最后整体乘scale输出。
这一实现细节解释了公式中各系数在 NPU 上如何被高效、确定性地计算。
九、约束说明
- 确定性计算:
aclnnElu与aclnnInplaceElu默认即为确定性实现,即相同输入在多次运行下产生一致的输出结果,可参考 docs/zh/context/determinism_compute.md 了解 CANN 确定性计算的通用约定。
十、调用示例
以下示例代码仅供参考,具体编译与执行过程请参考 docs/zh/context/compile_and_run_sample.md。仓库中同时提供了可直接参考的完整样例 examples/test_aclnn_elu.cpp 与 examples/test_aclnn_inplace_elu.cpp。
10.1 aclnnElu 接口调用示例
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_elu.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; } 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** selfOrResult) { 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); // 计算连续selfOrResult的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 *selfOrResult = 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> selfShape = {2, 2}; std::vector<int64_t> outShape = {2, 2}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclScalar* alpha = nullptr; aclScalar* scale = nullptr; aclScalar* inputScale = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {-2, -1, 0, 1}; std::vector<float> outHostData = {0, 0, 0, 0}; float alphaValue = 1.0f; float scaleValue = 1.0f; float inputScaleValue = 1.0f; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建alpha aclScalar alpha = aclCreateScalar(&alphaValue, aclDataType::ACL_FLOAT); CHECK_RET(alpha != nullptr, return ret); // 创建scale aclScalar scale = aclCreateScalar(&scaleValue, aclDataType::ACL_FLOAT); CHECK_RET(scale != nullptr, return ret); // 创建inputScale aclScalar inputScale = aclCreateScalar(&inputScaleValue, aclDataType::ACL_FLOAT); CHECK_RET(inputScale != nullptr, 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; // 调用aclnnElu第一段接口 ret = aclnnEluGetWorkspaceSize(self, alpha, scale, inputScale, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnEluGetWorkspaceSize 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); } // 调用aclnnElu第二段接口 ret = aclnnElu(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnElu 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(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 ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyScalar(alpha); aclDestroyScalar(scale); aclDestroyScalar(inputScale); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }10.2 aclnnInplaceElu 接口调用示例
与aclnnElu示例相比,inplace 版本省略了out张量的创建与outHostData,结果直接写回selfRef:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_elu.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; } 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** selfOrResult) { 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); // 计算连续selfOrResult的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 *selfOrResult = 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> selfRefShape = {2, 2}; void* selfRefDeviceAddr = nullptr; aclTensor* selfRef = nullptr; aclScalar* alpha = nullptr; aclScalar* scale = nullptr; aclScalar* inputScale = nullptr; std::vector<float> selfRefHostData = {-2, -1, 0, 1}; float alphaValue = 1.0f; float scaleValue = 1.0f; float inputScaleValue = 1.0f; // 创建selfRef aclTensor ret = CreateAclTensor(selfRefHostData, selfRefShape, &selfRefDeviceAddr, aclDataType::ACL_FLOAT, &selfRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建alpha aclScalar alpha = aclCreateScalar(&alphaValue, aclDataType::ACL_FLOAT); CHECK_RET(alpha != nullptr, return ret); // 创建scale aclScalar scale = aclCreateScalar(&scaleValue, aclDataType::ACL_FLOAT); CHECK_RET(scale != nullptr, return ret); // 创建inputScale aclScalar inputScale = aclCreateScalar(&inputScaleValue, aclDataType::ACL_FLOAT); CHECK_RET(inputScale != nullptr, return ret); // 3. 调用CANN算子库API,需要修改为具体的API名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnInplaceElu第一段接口 ret = aclnnInplaceEluGetWorkspaceSize(selfRef, alpha, scale, inputScale, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceEluGetWorkspaceSize 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); } // 调用aclnnInplaceElu第二段接口 ret = aclnnInplaceElu(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceElu 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(selfRefShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy( resultData.data(), resultData.size() * sizeof(resultData[0]), selfRefDeviceAddr, 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 ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(selfRef); aclDestroyScalar(alpha); aclDestroyScalar(scale); aclDestroyScalar(inputScale); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfRefDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }对输入{-2, -1, 0, 1}、alpha = scale = inputScale = 1.0时,输出结果约为{-0.8647, -0.6321, 0, 1}:正半轴元素保持不变,负半轴元素按eˣ−1计算。读者可结合 tests/ut/op_api/test_aclnn_elu.cpp 与 tests/assets/golden.py 中的期望值生成脚本进一步验证。
十一、总结与扩展阅读
aclnnElu/aclnnInplaceElu是 CANN ops-nn 中结构清晰、易于上手的单算子 API:两段式接口负责"算 workspace + 执行",inplace 变体通过输入输出同址节省显存,三个aclScalar参数使 ELU 可灵活覆盖带缩放与输入缩放的广义激活场景。理解其源码实现后,可进一步阅读:
- docs/zh/context/two_phase_api.md:两段式接口通用范式与 workspace 概念;
- docs/zh/context/aclnn_return_code.md:aclnn 返回码定义;
- docs/zh/context/compile_and_run_sample.md:样例的编译与运行方法;
- docs/zh/context/determinism_compute.md:确定性计算约定;
- docs/zh/context/non_contiguous_tensor.md:非连续 Tensor 的支持机制;
- activation/elu/README.md:ELU 算子的模块级总览与调用方式索引。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考