CANN ops-nn Mish 激活算子实战:公式推导、aclnn 两段式调用与 NPU 内核实现解析
CANN ops-nn Mish 激活算子实战公式推导、aclnn 两段式调用与 NPU 内核实现解析【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn本篇基于 CANN ops-nn 仓库中activation/mish模块的官方文档与源码完整讲解 Mish 激活算子在 NPU 上的落地方案从数学公式与数值特性到 aclnn 两段式 APIaclnnMish/aclnnInplaceMish的完整调用流程、参数约束与错误码再到图模式构图方式并结合仓库内核源码剖析其数值稳定的算子实现原理。读完后你可以直接复制可运行的 C 样例在 Ascend 设备上调用 Mish 算子并理解其底层 AIV 向量核的 DAG 计算流程。一、算子定位与产品支持Mish 是 ops-nn 算子库activation 目录中的一个逐元素element-wise激活算子官方定义其功能为一个自正则化的非单调神经网络激活函数Self-Regulating Non-Monotonic Neural Network Activation Function。各 Ascend 产品对 Mish 算子的支持情况如下引自 activation/mish/README.md产品是否支持Ascend 950PR/Ascend 950DT√Atlas A3 训练系列产品 / Atlas A3 推理系列产品√Atlas A2 训练系列产品 / Atlas A2 推理系列产品√Atlas 200I/500 A2 推理产品×Atlas 推理系列产品×Atlas 训练系列产品√从算子定义文件 mish_def.cpp 看该算子面向ascend950与ascend350两个平台注册了 AICore 配置并开启了动态 ShapeDynamicShapeSupportFlag(true)与动态 RankDynamicRankSupportFlag(true)支持这意味着推理/训练时在图模式下传入动态维度也是被允许的。二、数学公式与数值特性Mish 的计算公式为$$out_i self_i \times \tanh(\mathrm{Softplus}(self_i))$$其中 $\mathrm{Softplus}(self_i) \ln(1 \exp(self_i))$。它具有三个典型性质非单调在输入为较大负数时输出会轻微低于 0不像 ReLU 那样将负半轴截断为 0自正则化Self-Regularizing输出上界约为 $0.678 \cdot x$当 $x \to \infty$ 时 $out \approx x$当 $x \to -\infty$ 时 $out \to 0^-$激活幅度与输入幅度成正比无需像 PReLU 那样额外调节斜率参数处处平滑可导避免了 ReLU 在 0 点梯度不连续的问题。三、参数说明算子共有 1 个输入、1 个输出引自 activation/mish/README.md参数名输入/输出描述数据类型数据格式self输入待进行 Mish 激活计算的入参对应计算公式中的 selfFLOAT、FLOAT16、BFLOAT16NDout输出Mish 激活计算的出参对应计算公式中的 outFLOAT、FLOAT16、BFLOAT16ND数据类型限制Atlas 训练系列产品仅支持 FLOAT、FLOAT16不支持 BFLOAT16Ascend 950 系列、Atlas A2/A3 系列支持 FLOAT、FLOAT16、BFLOAT16 全量数据类型。约束说明输入 shape 限制为输入维度不能超过 8 维仅支持 0-8 维。从算子 IR 定义文件 mish_proto.h 可以确认图模式下的输入输出类型约束REG_OP(Mish) .INPUT(x, TensorType({DT_FLOAT, DT_FLOAT16, DT_BF16})) .OUTPUT(y, TensorType({DT_FLOAT, DT_FLOAT16, DT_BF16})) .OP_END_FACTORY_REG(Mish)输入x要求为 ND 格式张量支持 1D~8D且注释中说明该算子与 TensorFlow 的 Mish 算子语义兼容便于三方框架图到 CANN 图的转换。四、调用方式一aclnn 两段式接口Mish 提供aclnnMish与aclnnInplaceMish两组接口二者功能相同区别在于输出存储方式引自 aclnnMishaclnnInplaceMish.mdaclnnMish需新建一个输出张量对象存储计算结果aclnnInplaceMish无需新建输出张量对象直接在输入张量的内存中存储计算结果原地覆盖节省一份 Device 显存。两个算子均为两段式接口必须先调用GetWorkspaceSize段获取计算所需 workspace 大小以及包含算子计算流程的执行器executor再调用第二段接口执行计算。4.1 函数原型// aclnnMish 两段式接口 aclnnStatus aclnnMishGetWorkspaceSize( const aclTensor *self, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor) aclnnStatus aclnnMish( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream) // aclnnInplaceMish 两段式接口 aclnnStatus aclnnInplaceMishGetWorkspaceSize( aclTensor *selfRef, uint64_t *workspaceSize, aclOpExecutor **executor) aclnnStatus aclnnInplaceMish( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)接口头文件为aclnnop/aclnn_mish.h对应仓库中的公开声明见 aclnn_mish.h。4.2 第一段接口参数说明aclnnMishGetWorkspaceSize参数如下参数名输入/输出描述数据类型维度(shape)非连续TensorselfaclTensor*输入输入的张量公式中的输入 self。数据类型和 shape 与 out 相同支持空 TensorFLOAT16、FLOAT、BFLOAT160-8√outaclTensor*输出激活函数的输出公式中的输出 out。数据类型和 shape 与 self 相同数据格式需与 self 一致FLOAT16、FLOAT、BFLOAT160-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小---executoraclOpExecutor**输出返回 op 执行器包含算子计算流程---aclnnInplaceMishGetWorkspaceSize的参数selfRefaclTensor*为输入输出共用张量公式中的 self 与 out支持空 Tensor数据类型、维度、非连续 Tensor 支持与 self 相同。4.3 第一段接口错误码第一段接口会完成入参校验以下场景会报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self 或 outinplace 版本为 selfRef是空指针ACLNN_ERR_PARAM_INVALID161002self 和 out 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002self 与 out 的数据类型不同ACLNN_ERR_PARAM_INVALID161002self 与 out 的 shape 不同ACLNN_ERR_PARAM_INVALID161002dim 超出输入 self 的维度范围inplace 版本为 selfRef4.4 第二段接口参数说明aclnnMish与aclnnInplaceMish第二段接口的参数完全一致参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入Device 侧申请的 workspace 大小由第一段接口获取executor输入op 执行器包含算子计算流程stream输入指定执行任务的 Stream返回值为aclnnStatus状态码。另外Mish 的 aclnn 实现默认为确定性计算实现。4.5 完整调用示例仓库 examples 目录 提供了两个可直接编译运行的 C 样例test_aclnn_mish.cpp普通版与 test_aclnn_inplace_mish.cpp原地版二者与官方文档给出的调用示例一致。以aclnnMish为例核心流程如下#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_mish.h // ...CHECK_RET / LOG_PRINT 宏与 GetShapeSize 省略见完整样例 int main() { // 1. 固定写法device/stream 初始化 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. 构造输入与输出张量 std::vectorint64_t selfShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {1.0, 2.0, 3.0, 4.0, 5.0, 7.0, 8.0, 9.0}; std::vectorfloat outHostData {0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0}; // 通过 aclrtMalloc aclrtMemcpy 上卡再用 aclCreateTensor 创建 ND 格式 aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); ret CreateAclTensor(outHostData, outShape, outDeviceAddr, aclDataType::ACL_FLOAT, out); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用 CANN 算子库 API两段式调用 uint64_t workspaceSize 0; aclOpExecutor* executor; // 第一段获取 workspace 大小与执行器 ret aclnnMishGetWorkspaceSize(self, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnMishGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据 workspaceSize 申请 device 内存若为 0 则可不申请 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); } // 第二段执行计算 ret aclnnMish(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnMish 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 并打印 auto size GetShapeSize(outShape); std::vectorfloat 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 aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放 device 资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }其中CreateAclTensor是构造 ND 张量的通用写法先aclrtMalloc申请 Device 内存、aclrtMemcpy拷入 Host 数据再按行主序计算连续 strides最后调用aclCreateTensor数据格式为ACL_FORMAT_ND。inplace 版本的关键差异只需构造一个selfRef张量调用aclnnInplaceMishGetWorkspaceSize(selfRef, workspaceSize, executor)与aclnnInplaceMish(...)计算完成后直接从selfRefDeviceAddr拷回结果即可无需维护第二个输出张量。完整样例的编译与执行过程可参考仓库文档中的编译与运行样例说明。五、调用方式二图模式算子 IR 构图除 aclnn API 方式外Mish 还支持在图Graph模式下通过算子 IR 构图调用。算子的图侧注册见 mish_proto.h通过REG_OP(Mish)宏声明输入输出输入xND 张量支持 1D~8D类型限定为DT_FLOAT、DT_FLOAT16、DT_BF16输出y与x同类型、同 shape 的张量兼容性标注为 TensorFlow 的 Mish 算子便于模型转换框架做算子映射。图模式下的 shape 推导逻辑位于 mish_infershape.cpp直接复用逐元素算子的通用推导Ops::Base::InferShape4Elewise——即输出 shape 与输入完全一致这印证了 Mish 是纯逐元素运算。此外仓库还提供了 ONNX 插件framework/mish_onnx_plugin.cpp用于 ONNX 模型导入场景下将 ONNX 的 Mish 节点映射到本算子。六、源码级实现解析数值稳定的 AIV 内核6.1 Kernel 入口与多精度分发Device 侧 kernel 入口在 mish_apt.cpptemplate uint64_t schMode, uint64_t dType __global__ __aicore__ void mish(GM_ADDR x, GM_ADDR y, GM_ADDR workspace, GM_ADDR tiling) { REGISTER_TILING_DEFAULT(EleBaseTilingData16B); GET_TILING_DATA_PTR_WITH_STRUCT(EleBaseTilingData16B, tilingData, tiling); KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY); // 仅使用 AIV向量核 if constexpr (dType TPL_FP16) { ElementwiseSch16BschMode, MishDAGhalf::OpDag sch(tilingData); sch.Init(x, y); sch.Process(); } else if constexpr (dType TPL_BF16) { ElementwiseSch16BschMode, MishDAGbfloat16_t::OpDag sch(tilingData); // ... } else if constexpr (dType TPL_FP32) { ElementwiseSch16BschMode, MishDAGfloat::OpDag sch(tilingData); // ... } }从源码结构看Mish 被实现为一个纯 AIVKERNEL_TYPE_AIV_ONLY逐元素算子通过模板参数dType在 FP16/BF16/FP32 三种精度间静态分发每种精度实例化一个以MishDAGT描述计算图的ElementwiseSch16B调度器。6.2 计算 DAG为什么不用直接公式Mish 计算图定义op_kernel/arch35/mish_dag.h中的MishDAGT将计算拆分为 5 个 DAG 节点CopyIn(全局内存 - UB) - Cast 升精度到 float16bit 输入时 - MishCustom 核心计算 - Cast 降回原精度ROUND 模式 - CopyOut(UB - 全局内存)核心的MishCustom没有直接实现 $x \cdot \tanh(\ln(1e^x))$而是采用了数值稳定等价变换$$\tanh(\mathrm{Softplus}(x)) \frac{2e^{-x} 1}{2 2e^{-2x}} \quad (x 0)$$$$\tanh(\mathrm{Softplus}(x)) \frac{e^{2x} 2e^{x}}{e^{2x} 2e^{x} 2} \quad (x \ge 0)$$源码中可以看到这一分治结构先用Reg::Muls(vregInputNegNumerator, vregInput, FP32_NEG_ONE)计算 $-x$ 再取Exp得 $e^{-x}$走负半轴分支另一路计算 $e^{2x}$、$e^{x}$ 组合出正半轴分支再用Reg::Comparesfloat, CMPMODE::LT比较 $x$ 与 0 生成掩码用Reg::Select选取对应分支最后Reg::Mul(vregOutput, vregOutput, vregInput)乘回输入 $x$ 得到 Mish 输出。这种写法的关键收益是避免大正数时 $e^x$ 溢出正半轴用分子/分母同阶的比值形式和大负数时 $\ln(1e^x)$ 的精度损失负半轴改用 $e^{-x}$ 形式这对应了 README 中提到的自正则化激活在实现上必须处理的数值边界问题。6.3 精度策略对 FP16/BF16 输入数据先无损升到 FP32 在 UB/寄存器中完成全部指数、除法运算最后以CAST_MODE_RINTRound to Nearest, ties to even降回 16bit 写出FP32 输入则全程 FP32。结合 mish_def.cpp 中PrecisionReduceFlag(true)的配置可以推断该算子在精度策略上允许按此升-算-降的方式处理半精度输入以保证半精度下的数值精度。6.4 测试与 Golden 验证仓库测试体系完整覆盖了该算子Golden 参考实现golden.py 使用torch.nn.Mish作为参考且对 BF16/FP16 输入先升到 FP32 再计算最后转回原精度——与内核源码中升精度计算、降精度输出的策略一一对应算子 UT包含 host 侧 test_mish_infershape.cppshape 推导、test_aclnn_mish.cppAPI 参数校验与 test_mish_apt.cppkernel 数值精度ST 用例集ttk_kernel_mish_st.csv 提供 arch35 平台的稳定性测试用例数据。七、小结维度说明功能逐元素 Mish 激活$out x \cdot \tanh(\mathrm{Softplus}(x))$数据类型FLOAT / FLOAT16 / BFLOAT16Atlas 训练系列仅 FLOAT / FLOAT16数据格式ND维度 0-8支持非连续 Tensor 与空 Tensor调用方式aclnn 两段式aclnnMish/aclnnInplaceMish图模式 IR 构图ONNX 插件映射实现架构纯 AIV 向量核DAG 五节点计算图FP32 升精度数值稳定计算确定性默认确定性实现在模型开发中若需要节省显存或减少一次 HBM 分配优先选择aclnnInplaceMish若下游还需保留原始激活前的张量则使用aclnnMish。两组接口均遵循相同的两段式调用范式与错误码约定迁移成本很低。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考