- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
本文以 CANN 神经网络算子库(ops-nn)中 Swish 激活算子的官方接口文档 aclnnSwish.md 为主线,系统讲解 aclnnSwish 两段式接口的完整用法、参数语义、返回码约束,并结合仓库内 aclnn_swish.cpp、swish_def.cpp、swish_tiling.cpp 与 swish.h 等源码,深入剖析算子从 Host 侧校验、构图到 Device 侧 Kernel 执行的完整链路。读完本文,你将掌握 aclnnSwish 的调用姿势、参数约束、常见报错排查方法,以及 Swish 算子在 NPU 上的实现细节。
功能说明与数学定义
aclnnSwish 用于对输入 Tensor 逐元素进行 Swish 激活函数运算并输出结果 Tensor。Swish(又称 SiLU)是一种平滑、非单调的激活函数,被广泛用于深度神经网络中,以替代 ReLU 等传统激活函数,有助于缓解梯度消失问题。
其计算公式为:
$$ s(x) = x \cdot \sigma(\beta x) $$
其中 $\sigma(x)$ 为 sigmoid 函数:
$$ \sigma(x) = \frac{1}{1 + e^{-x}} $$
式中的 $\beta$ 是用于控制 Swish 函数形状与斜率(曲线陡峭程度)的可调标量参数。当 $\beta=1$ 时,Swish 退化为标准形式 $x \cdot \sigma(x)$,即与 SiLU 等价的常见形态;当 $\beta$ 取不同值时,函数曲线会相应拉伸或压缩。
从仓库的算子定义来看,swish_def.cpp 中Swish的 OpDef 将scale(即公式中的 $\beta$)声明为OPTIONAL类型的 Float 属性,默认值为1.0,与文档"betaOptional 为空指针时以 1.0 计算"的约定一一对应。
产品支持情况
根据接口文档,aclnnSwish 的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
算子定义源码 swish_def.cpp 中通过this->AICore().AddConfig("ascend910b", aicoreConfig)为该算子注册了 ascend910b 的 AI Core 配置,与文档所列 Atlas A2 系列产品(Ascend 910B 平台)保持一致。此外,swish_tiling.cpp 在 tiling 阶段会对 SocVersion 做校验:仅ASCEND910B、ASCEND310B两个版本支持 BF16 输入,其他平台传入 BF16 数据会直接返回失败,这也是接口文档中"Atlas 推理系列产品、Atlas 训练系列产品:数据类型支持 FLOAT16、FLOAT"这一限制的底层来源。
两段式接口与函数原型
aclnnSwish 遵循 CANN 算子库的两段式接口设计规范:必须先调用第一段接口aclnnSwishGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器(executor),再调用第二段接口aclnnSwish执行计算。
第一段接口原型:
aclnnStatus aclnnSwishGetWorkspaceSize( const aclTensor* self, const aclScalar* betaOptional, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)第二段接口原型:
aclnnStatus aclnnSwish( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)两个接口的声明位于 aclnn_swish.h,均以extern "C"导出,头文件通过#include "aclnnop/aclnn_swish.h"引入。接口实现位于 aclnn_swish.cpp。
aclnnSwishGetWorkspaceSize 参数详解
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续 Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 表示用于计算激活函数的张量,公式中的 x | 支持空 Tensor;self 的 shape 和数据类型与 out 一致 | BFLOAT16、FLOAT16、FLOAT | ND | 0-8 | √ |
| betaOptional(aclScalar*) | 输入 | 表示可调节参数,用于控制 Swish 函数形状和斜率的标量,公式中的 β | 数据类型需为可转换为 FLOAT 的数据类型(参见互转换关系);当 betaOptional 为空指针时,接口以 1.0 进行计算 | - | - | - | - |
| out(aclTensor*) | 输出 | 表示 Swish 函数的输出,公式中的 s(x) | 支持空 Tensor;out 的 shape 和数据类型与 self 一致 | BFLOAT16、FLOAT16、FLOAT | ND | 0-8 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
补充说明:
- 维度上限 0-8:文档规定 self 与 out 的维度范围是 0-8。从源码看,aclnn_swish.cpp 中
CheckDim通过OP_CHECK_MAX_DIM(self, MAX_SUPPORT_DIMS_NUMS, ...)对输入输出做最大维度校验,超出MAX_SUPPORT_DIMS_NUMS会返回ACLNN_ERR_PARAM_INVALID。单元测试 test_aclnn_swish.cpp 用 9 维 shape 验证了这一行为。值得注意的是,第二段接口执行时对超过最大支持维度的长 Tensor 会通过reshapeLongTensor借助l0op::Reshape处理,因此接口能力是"参数校验限制 8 维以内,但底层可兼容长 shape 场景"。 - betaOptional 的可转换性:源码中 CheckDtypeValidBetaToFloat 调用
CanCast(betaOptional->GetDataType(), DataType::DT_FLOAT)校验 beta 数据类型能否无损转换为 FLOAT,若不能则记录OP_LOGE并返回ACLNN_ERR_PARAM_INVALID。从单元测试可见,bool、double、uint8_t、int8_t、FLOAT16 等类型的 beta 均被用于测试,说明凡能转换为 FLOAT 的标量类型均可用。 - 支持空 Tensor:当 self 或 out 为空 Tensor 时,aclnn_swish.cpp 会直接置
workspaceSize = 0并返回ACLNN_SUCCESS,无需真正分配 workspace。对应测试用例为 test_swish_empty_input。 - 支持非连续 Tensor:第一段接口内部会先通过
l0op::Contiguous将非连续输入规整为连续内存再参与构图,所以调用方无需手动搬运数据,传入非连续 Tensor 即可正确计算(测试用例 test_swish_uncontiguous 覆盖了带 strides 的非连续输入)。
返回值与异常校验
aclnnStatus返回状态码,具体参见 aclnn 返回码。第一段接口会完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 self 或 out 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、betaOptional 或 out 的数据类型不在支持的范围内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 out 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 out 的 shape 不一致 |
上述校验在源码中的落点非常清晰:CheckParams 依次执行:
CheckNotNull2Tensor(self, out)—— 任一为空指针则返回ACLNN_ERR_PARAM_NULLPTR(对应 161001),测试用例见 test_swish_nullptr_input 与 test_swish_nullptr_out;GetDtypeSupportListV1(ASCEND910B_DTYPE_SUPPORT_LIST, ASCEND910_DTYPE_SUPPORT_LIST)按平台获取支持的数据类型列表,其中 910 平台支持DT_FLOAT、DT_FLOAT16,910B 平台额外支持DT_BF16(见 aclnn_swish.cpp),随后CheckDtypeValidActivation校验 self/out 类型;CheckDtypeValidBetaToFloat校验 beta 可转换为 FLOAT;CheckDim校验维度不超过上限;CheckSameShapeNotlimit1In1Out校验 self 与 out 的 shape 完全一致。
单元测试 test_swish_inconsistent_shape 与 test_swish_inconsistent_dtype 分别用 shape 不一致(如 {2,16,32,16} 对 {2,16,32,18})与 dtype 不一致(FLOAT 对 FLOAT16)验证了ACLNN_ERR_PARAM_INVALID的返回。
aclnnSwish 参数详解
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnSwishGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值同样为aclnnStatus,状态码含义参见 aclnn 返回码。第二段接口的实现非常轻量:aclnn_swish.cpp 中aclnnSwish仅做L2_DFX_PHASE_2打点,然后调用CommonOpExecutorRun将第一阶段构造好的执行器提交到指定 stream 上运行,所有构图与 workspace 计算工作均在第一段接口内完成。
约束说明
- 确定性计算:aclnnSwish 默认采用确定性实现,即相同的输入与配置在多次运行中产生完全一致的结果。确定性计算的通用机制可参考 确定性计算。
调用示例
以下示例代码来自 aclnnSwish.md,仓库中的可运行版本见 test_aclnn_swish.cpp,两者逻辑一致。具体编译和执行过程请参考编译与运行样例。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_swish.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> outShape = {4, 2}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclScalar* betaOptional = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0}; float betaValue = 1.1f; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建betaOptional aclScalar betaOptional = aclCreateScalar(&betaValue, aclDataType::ACL_FLOAT); CHECK_RET(betaOptional != 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; // 调用aclnnSwish第一段接口 ret = aclnnSwishGetWorkspaceSize(self, betaOptional, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSwishGetWorkspaceSize 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;); } // 调用aclnnSwish第二段接口 ret = aclnnSwish(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSwish 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(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(self); aclDestroyScalar(betaOptional); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例的执行流程可分为七个步骤:初始化 device/stream → 构造 self、betaOptional、out 三个对象 → 依次调用两段式接口(含 workspace 申请)→ 同步等待 → 结果回拷并打印 → 释放 Tensor/Scalar → 释放 Device 资源。其中 workspace 的申请遵循"仅当workspaceSize > 0时才aclrtMalloc"的惯例,避免无谓的内存分配。
源码级实现原理
Host 侧构图流程(aclnn_swish.cpp)
第一段接口aclnnSwishGetWorkspaceSize在完成参数校验后,会构造一条由底层 Level 0 算子组成的计算图(aclnn_swish.cpp):
- 空 Tensor 短路处理:self 或 out 为空时直接返回
ACLNN_SUCCESS,workspace 置 0; l0op::Contiguous(self, ...):将非连续输入规整为连续 Tensor;- 读取 beta:
scale = betaOptional != nullptr ? betaOptional->ToFloat() : 1.0f,与文档"空指针时按 1.0 计算"的约定一致; l0op::Swish(reshapeSelf, scale, ...):携带 scale 标量调用底层 Swish 算子;- 长 shape 场景通过
reshapeLongTensor(借助l0op::Reshape)恢复原始维度; l0op::ViewCopy(reshapeSwishOut, out, ...):将计算结果拷贝到用户指定的 out Tensor;*workspaceSize = uniqueExecutor->GetWorkspaceSize():汇总全图所需 workspace 并返回执行器。
算子原型与 Infershape(swish_def.cpp / swish_infershape.cpp)
- swish_def.cpp 声明了算子输入
x(tensor,支持 DT_FLOAT16/DT_FLOAT/DT_BF16,ND 格式)、输出y(tensor,约束同 x)与可选 Float 属性scale(默认 1.0),并在 AICore 配置中开启动态 Shape、动态 Rank 支持与 PrecisionReduce 标志; - swish_infershape.cpp 的
InferShapeSwish将输出 shape 直接赋值为输入 shape(*y_shape = *x1_shape),印证了"out 与 self 的 shape 一致"的接口约束。
Tiling 策略(swish_tiling.cpp)
swish_tiling.cpp 负责在编译期决定核数、分块与 workspace:
- 依据 UB 容量、Block 大小与双缓冲(
BUFFER_NUM = 2)计算每个 tile 可承载的数据量,FLOAT 类型每个 UB 单元可容纳 2 个元素,其他类型为 4 个; - 根据输入长度决定使用 1 个核还是多核并行,多核时按"大核 + 小核"模式均衡负载(
bigCoreDataNum/smallCoreDataNum),并将每个核的工作量进一步切分为多个 tile 与尾部数据; - 依据 scale 取值选择不同的 tiling key:scale 为 -1 时走
TPL_SCALE_NEG_ONE,为 0 时走TPL_SCALE_ZERO,其余情况走TPL_SCALE_OTHER(swish_tiling.cpp),使 Kernel 侧可以对特殊 scale 做简化计算; - workspace 大小来自
ascendcPlatform.GetLibApiWorkSpaceSize()。
Device 侧 Kernel(swish.cpp / swish.h)
swish.cpp 是 AscendC 核函数入口,通过GET_TILING_DATA_WITH_STRUCT反序列化 tiling 数据,然后驱动 swish.h 中的KernelSwish完成计算。核心计算逻辑在Compute中:
- FLOAT 路径:
Muls(乘以 -scale,见 swish.h 中this->scale = -1.0f * scale)→Exp(求 e^(-βx))→Adds(加 1,构成分母 1+e^(-βx))→Div(x 除以分母),即 $x / (1 + e^{-\beta x})$,等价于文档中的 $x \cdot \sigma(\beta x)$; - 非 FLOAT 路径(FP16/BF16):先
Cast到 FP32 中间缓冲计算(calcBuf1/calcBuf2),计算完成后再Cast回原类型(RoundMode::CAST_ROUND),以提升中间运算精度。
Process采用"循环 tile + 尾部处理"模式:对前tileNum - 1个 tile 执行常规的 CopyIn/Compute/CopyOut 流水(配合双缓冲隐藏访存延迟),最后一个 tile 以tailDataNum处理余量数据。
构建与运行验证
Swish 算子目录的 README.md 提供了完整的编译与运行方式:
编译算子包:
bash build.sh --pkg --soc=ascend910b --experimental --ops=swish运行 aclnn 调用示例:
bash build.sh --run_example swish eager cust --vendor_name=custom --experimental其中--experimental表示从实验特性目录构建,--ops=swish只编译 Swish 算子以加速迭代。运行示例对应的样例代码为 test_aclnn_swish.cpp,通过 aclnn 两段式接口完成算子调用与结果校验。
单元测试覆盖
仓库为 aclnnSwish 提供了较完整的 UT 覆盖,位于 tests/ut/op_api/test_aclnn_swish.cpp,可归纳为以下几类:
| 测试用例 | 验证点 |
|---|---|
| test_swish_dataType_error | 非法数据类型(INT8/INT32/DOUBLE/BF16 等)返回校验错误 |
| test_swish_format | 多 format 下的兼容性(ND 之外的 format 不报错,构图层做格式适配) |
| test_swish_inconsistent_shape / _dtype | self 与 out 的 shape、dtype 不一致时返回ACLNN_ERR_PARAM_INVALID |
| test_swish_empty_input | 空 Tensor 正常返回ACLNN_SUCCESS |
| test_swish_nullptr_input / _out / _beta | 空指针校验:self/out 为空返回ACLNN_ERR_PARAM_NULLPTR,beta 为空(走默认 scale=1.0)正常执行 |
| test_swish_FP32 / FP16 | 主流精度的数值正确性(相对/绝对误差 1e-4) |
| test_swish_uncontiguous | 非连续 Tensor 的正确性 |
| test_swish_shape_larger_8 / *_invalid_dim | 超过 8 维或 shape 非法时返回ACLNN_ERR_PARAM_INVALID |
此外还有 InferShape 与 Tiling 的 Host 侧单测(test_swish_infershape.cpp、test_swish_tiling.cpp),共同保证算子各阶段行为符合接口文档约定。
总结
aclnnSwish 是 CANN ops-nn 中实现 Swish 激活函数的 Level 2 接口算子:数学上以s(x) = x·σ(βx)逐元素计算,接口上遵循"先 GetWorkspaceSize 后执行"的两段式规范,数据上支持 FLOAT16/FLOAT/BFLOAT16 与 ND 格式、0-8 维、空 Tensor 与非连续 Tensor。通过阅读 aclnnSwish.md 并结合 aclnn_swish.cpp 等源码,可以清楚看到参数校验、构图、tiling 与 Kernel 计算各环节如何协同实现接口文档中的每一条约束——这对在 Atlas A2 系列产品上正确、高效地使用 Swish 激活函数,以及排查接口调用问题都具有直接的实践价值。
- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
相关推荐
CANN ops-nn 算子 aclnnAddRelu / aclnnInplaceAddRelu 接口详解:两段式调用、参数约束与源码实现
CANN ops nn 算子 aclnnAddRelu / aclnnInplaceAddRelu 接口详解:两段式调用、参数约束与源码实现 aclnnAddR
人工智能算子库深度学习CANNAscendCANN ops-nn 中 aclnnLogSoftmax 算子详解:两段式接口、参数约束与调用实战
CANN ops nn 中 aclnnLogSoftmax 算子详解:两段式接口、参数约束与调用实战 本文基于 CANN 神经网络算子库(ops nn)中 Lo
人工智能算子库深度学习CANNAscendCANN ops-nn 算子 aclnnGeGluV3Backward 反向接口详解:两段式调用、参数约束与源码实现
CANN ops nn 算子 aclnnGeGluV3Backward 反向接口详解:两段式调用、参数约束与源码实现 aclnnGeGluV3Backward
人工智能算子库深度学习CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考