多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

CANN ops-math 算子实战:aclnnPow2 两段式接口使用指南与 NPU 实现原理

CANN ops-math 算子实战:aclnnPow2 两段式接口使用指南与 NPU 实现原理 算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载导读aclnnPow2 是 CANN ops-math 仓库experimental/math/pow2提供的数学类基础算子用于在 NPU 上完成张量的元素级指数运算pow。本文以官方接口文档 test_aclnn_pow2.md 为主体完整讲解 aclnnPow2 的接口定义、参数约束、返回码语义与可运行的调用示例并结合仓库中的算子定义、Shape 推导、Tiling 切分与 Kernel 计算源码深入剖析其广播机制、workspace 管理策略以及底层的exp(x2 · ln|x1|)数值实现帮助你既能在工程中正确调用该算子也能理解其 NPU 端执行的全链路原理。产品支持情况aclnnPow2 当前支持的产品如下产品是否支持Atlas A2 训练系列产品 / Atlas 800I A2 推理产品 / A200I A2 Box 异构组件√在算子信息库层面该算子的 AICore 配置同样注册为 ascend910b见 pow2_def.cpp与文档声明的 A2 系列产品支持范围一致。功能说明与计算公式算子功能实现张量的指数运算对输入张量 x1底数和 x2指数进行元素级计算。计算公式$$ out_i \text{pow}(x1_i, x2_i) $$即输出张量 out 的每一个元素由底数张量 x1 与指数张量 x2 对应位置的元素按幂运算得到其中 x1、x2 均可为标量或多维张量支持通过广播机制扩展到输出张量。函数原型两段式接口与其他 aclnn 算子一致aclnnPow2 采用两段式接口调用方式必须先调用第一段接口aclnnPow2GetWorkspaceSize获取计算所需的 workspace 大小以及包含算子计算流程的执行器executor再调用第二段接口aclnnPow2执行计算。aclnnStatus aclnnPow2GetWorkspaceSize( const aclTensor *x1, const aclTensor *x2, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnPow2( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)说明workspace 指除输入/输出外算子在 NPU 上完成计算所需的临时内存第二段接口aclnnPow2不能重复调用同一 executor 必须遵循“GetWorkspaceSize → 申请内存 → 执行 → 释放”的单次调用流程。aclnnPow2GetWorkspaceSize 接口详解参数说明参数名输入/输出描述使用说明数据类型数据格式维度shape非连续 Tensorx1输入底数张量可为标量或多维张量支持广播到输出张量公式中的 x1无FLOAT16、FLOAT32、BFLOAT16、INT8、UINT8、INT16、INT32ND0-8√x2输入指数张量可为标量或多维张量支持广播到输出张量公式中的 x2无FLOAT16、FLOAT32、BFLOAT16、INT8、UINT8、INT16、INT32ND0-8√out输出元素级计算结果张量输出数据类型与输入类型一致或通过 Cast 转换公式中的 outshape 与输入广播结果一致FLOAT16、FLOAT32、BFLOAT16、INT8、UINT8、INT16、INT32ND0-8√workspaceSize输出返回需要在 Device 侧申请的 workspace 大小-----executor输出返回 op 执行器包含了算子计算流程-----从算子信息库定义pow2_def.cpp可以看到x1、x2 均声明为REQUIRED输入、out 对应输出 y三者都注册了FORMAT_ND数据格式并设置了AutoContiguous()内存自动连续化这与文档中“非连续 Tensor 支持 √”及 ND 格式的约束完全对应。返回值状态码与错误场景返回类型为 aclnnStatus具体返回码定义可参考 aclnn 返回码说明。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 tensor 是空指针。ACLNN_ERR_PARAM_INVALID161002x1、x2 和 out 的数据类型和数据格式不在支持的范围之内。ACLNN_ERR_PARAM_INVALID161002x1、x2 和 out 的数据维度超过了 8 维。ACLNN_ERR_PARAM_INVALID161002x1、x2 和 out 的数据形状不一致。需要说明的是这里的“形状不一致”指的是在广播规则下无法匹配的情况。事实上x1、x2 允许维度不同只要满足广播规则即可——这一点在算子源码中体现得十分明确Shape 推导阶段pow2_infershape.cpp从最后一个维度开始逐维对齐当两个维度d1 d2或其中一方为 1 时取二者较大者作为输出维度否则返回GRAPH_FAILEDTiling 阶段pow2_tiling.cpp对不满足s1 ! s2 s1 ! 1 s2 ! 1条件的输入直接报 Broadcast Fail, Please check your input shape。aclnnPow2 接口详解参数说明参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址。workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnPow2GetWorkspaceSize 获取。executor输入op 执行器包含了算子计算流程。stream输入指定执行任务的 Stream。返回值aclnnStatus返回状态码具体参见 aclnn 返回码说明。约束说明支持 FLOAT16、FLOAT32、BFLOAT16、INT8、UINT8、INT16、INT32 七种数据类型暂不支持 int64/uint64 类型参见 pow2 算子 README 的约束说明。输入张量维度可不同但需要通过广播规则匹配维度上限为 8 维数据格式为 ND。输出张量支持非连续non-contiguous输入算子通过 AutoContiguous 自动完成内存连续化。调用示例以下为完整的调用示例代码与仓库 examples/test_aclnn_pow2.cpp 中的 Fp32 用例一致完整编译和执行流程请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnn_pow2.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::vectorint64_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::vectorT hostData, const std::vectorint64_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::vectorint64_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 testFp32(){ LOG_PRINT(Test for Fp32\n); // 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::vectorint64_t selfShape {10240}; std::vectorint64_t exponentShape {10240}; std::vectorint64_t outShape {10240}; void* selfDeviceAddr nullptr; void* exponentDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* exponent nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {99, -1, -2, 0}; std::vectorfloat exponentHostData {0, 1, 2, 0}; std::vectorfloat outHostData {0, 0, 0, 0}; for(int i 4; i 10240; i){ selfHostData.push_back(i); exponentHostData.push_back(2); outHostData.push_back(0); } // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建 exponent aclTensor ret CreateAclTensor(exponentHostData, exponentShape, exponentDeviceAddr, aclDataType::ACL_FLOAT, exponent); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建out aclTensor ret CreateAclTensor(outHostData, outShape, outDeviceAddr, aclDataType::ACL_FLOAT, out); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用CANN算子库API需要修改为具体的API名称 // aclnnPow2接口调用示例 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnPow2第一段接口 ret aclnnPow2GetWorkspaceSize(self, exponent, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnPow2GetWorkspaceSize 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); } // 调用aclnnPow2第二段接口 ret aclnnPow2(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnPow2 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::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 5; i) { LOG_PRINT(aclnnPow2 result[%ld] is: %f\n, i, resultData[i]); } // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(exponent); aclDestroyTensor(out); // 7. 释放device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(exponentDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }调用流程要点资源初始化aclInit→aclrtSetDevice→aclrtCreateStream属于所有 aclnn 调用的固定写法。构造 Tensor通过aclrtMalloc申请 Device 侧内存aclrtMemcpy将 Host 数据搬入计算连续 Tensor 的 strides 后调用aclCreateTensor创建aclTensor格式为ACL_FORMAT_ND。两段式调用先aclnnPow2GetWorkspaceSize得到workspaceSize与executor若workspaceSize 0则按该大小aclrtMalloc申请 workspace再调用aclnnPow2(workspaceAddr, workspaceSize, executor, stream)提交执行。同步与取数aclrtSynchronizeStream等待任务结束再用aclrtMemcpyACL_MEMCPY_DEVICE_TO_HOST回拷结果。资源释放按“Tensor → Device 内存含 workspace→ Stream → Device → aclFinalize”的顺序释放避免内存泄漏。更多数据类型的调用样例仓库示例 examples/test_aclnn_pow2.cpp 中还提供了多种数据类型的完整用例main函数中默认开启testInt8UInt8()其余用例按需打开注释即可运行用例函数输入/输出类型测试要点testFp32x1/x2/out 均为 FLOAT32大 shape10240下的浮点指数运算testInt32x1/x2/out 均为 INT32整型指数运算testInt16x1/x2/out 均为 INT16shape {2, 2}多维整型输入testInt8x1/x2/out 均为 INT8shape 102400大 shape 整型输入testUint8x1/x2/out 均为 UINT8shape 102400无符号整型输入testInt8UInt8x1INT8、x2UINT8、outINT16混精输入输出升精度INT16testInt8Fp32x1INT8、x2/outFLOAT32混精输入输出为浮点testBroadcastx1{2,1,9}、x2{1,4,1}、out{2,4,9}典型广播场景各维至少一方为 1其中testBroadcast演示了{2,1,9}与{1,4,1}广播到{2,4,9}的过程与 pow2_infershape.cpp 中“从最后一维开始、维度相等或其一为 1 时取 max”的广播推导规则一一对应。源码级原理从 tiling 到 kernel构建接入方式pow2 算子通过仓库统一的模块接入宏注册CMakeLists.txtadd_all_modules_sources(OPTYPE pow2 ACLNNTYPE aclnn)即算子类型为 pow2对外提供 aclnn 前缀的 C 接口aclnn_pow2.h中的aclnnPow2GetWorkspaceSize/aclnnPow2。广播的 Host 端预计算在 Tiling 阶段pow2_tiling.cppHost 端会把 x1、x2 的 shape 按输出维度右对齐并预计算每维的有效 stride对广播维size 为 1将 stride 置 0从而把“逐元素寻址”化简为“按预计算 stride 定位”的 O(1) 计算。这些 stride 写入 tiling 数据pow2_tiling_data.h后由 Kernel 端GetBroadcastIndexEffpow2.h按输出线性坐标反解出各维坐标并累加有效偏移实现广播取数。数据切分与多核负载均衡Tiling 同时负责多核任务分配pow2_tiling.cpp以 256B 为基本块BLOCK_SIZE结合 UB 大小与BUFFER_NUM2的双缓冲计算每个核的 tile 数据量输入按 256B 对齐后的总块数均分到各核余数核coreIdx tailBlockNum多分一块实现负载均衡小核/大核数据量分别记录为smallCoreDataNum/bigCoreDataNumINT8/UINT8 输入时 UB 内数据块数从默认 10 提升到 24INT8_UB_NUM以适配窄数据类型的吞吐workspace 大小通过GetWorkspaceSizepow2_tiling.cpp返回由框架系统 workspace 大小GetLibApiWorkSpaceSize与用户自定义大小相加得到对应第一段接口返回的workspaceSize。Kernel 端标量优化与数值实现Kernel 端pow2.h在CopyIn阶段针对标量输入做了专门优化当某个输入为标量元素数 1时先GetValue(0)取回标量再用Duplicate在 UB 内广播填充int8/uint8 因Duplicate不支持而改用循环SetValue从而避免标量输入的重复 DMA 搬运无广播时则直接按输出偏移DataCopy广播时按有效 stride 逐元素取数。PowComputepow2.h实现的核心公式为$$ out \exp(x2 \cdot \ln|x1|) $$并在此基础上补齐了幂运算的特殊边界先计算exp(x2 · ln|x1|)得到主结果通过Compare Select处理x1 0的情况利用x2的奇偶性修正符号计算x2/2的余数并映射到 ±1 系数处理x2 0任何数的 0 次幂为 1Select将结果置 1处理x1 0直接沿用上述 x20 分支的结果Select覆盖为 1同时保证 x10、x2≠0 时输出为 0。所有参与对数/指数运算的数据在Compute阶段统一 Cast 到 float 精度计算输出再按目标类型 Cast整型输出使用CAST_RINT四舍五入int8/uint8 输出经 half 中转与文档中“输出数据类型与输入类型一致或通过 Cast 转换”的描述吻合。整个计算流程由Process()以“CopyIn → Compute → CopyOut”的双缓冲流水循环驱动pow2.h。总结aclnnPow2 是 CANN ops-math 仓库中一个结构完整、具备工程参考价值的元素级数学算子对外通过两段式 aclnn 接口提供 NPU 上的 pow 运算能力支持 7 种数据类型与广播语义对内则完整展示了算子从 OpDef 注册、InferShape 广播推导、Host Tiling 切分与 workspace 计算到 Kernel 端标量优化、广播寻址与数值边界处理的全部实现链路。无论是希望直接调用该接口加速网络中的幂运算还是想参照它学习如何在 CANN 开源仓库中落地一个自定义算子本文对应的接口文档 test_aclnn_pow2.md 与配套源码均可作为直接参考。赞分享算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载相关推荐CANN ops-math 算子实战aclnnCircularPad3dBackward 两段式接口原理与调用指南CANN ops math 算子实战aclnnCircularPad3dBackward 两段式接口原理与调用指南 本文围绕 CANN ops math 开源算子库人工智能CANNCANN ops-math ArgMin 算子使用指南aclnnArgMin 两段式接口详解与 NPU 源码实现CANN ops math ArgMin 算子使用指南aclnnArgMin 两段式接口详解与 NPU 源码实现 ArgMin 是 CANN ops math算子库人工智能CANNCANN ops-math Addcdiv 算子 aclnnAddcdiv / aclnnInplaceAddcdiv 接口使用指南两段式调用、参数校验与 NPU 实现原理CANN ops math Addcdiv 算子 aclnnAddcdiv / aclnnInplaceAddcdiv 接口使用指南两段式调用、参数校验与 N算子库人工智能CANN创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表