ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

CANN ops-math 算子指南:aclnnReduceNansum 两段式 API 的接口说明与源码级调用实践

CANN ops-math 算子指南:aclnnReduceNansum 两段式 API 的接口说明与源码级调用实践 CANN ops-math 算子指南aclnnReduceNansum 两段式 API 的接口说明与源码级调用实践【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math在 CANN ops-math 数学算子库中ReduceNansum用于将输入 Tensor 中的 NaN 视为 0 后在指定维度上求和是数值计算中常见的“nan-safe 归约”算子。本文以 aclnnReduceNansum 接口文档 为主线完整讲解其产品支持情况、两段式接口原型、每个参数的数据类型与约束、错误返回码并结合仓库内 接口实现、算子定义 与 单元测试 剖析底层调用链最终给出可直接编译运行的完整 C 调用示例。读完本文你将掌握如何在 Ascend NPU 上通过 aclnn 接口正确、高效地完成 NaN 安全求和计算。功能说明与产品支持情况ReduceNansum的核心语义为将输入 Tensor 中的 NaN 处理为 0 后返回输入 Tensor 在给定维度上的和。该算子对含 NaN 的浮点数据如训练中出现的异常梯度进行归约求和时不会因为单个 NaN 污染整行结果适合作为容错性数值统计的基础算子。根据接口文档其产品支持情况如下产品型号支持情况Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品不支持注意产品支持情况与源码中的运行期校验相互印证。在 aclnn_reduce_nansum.cpp 中CheckDtypeValid会通过GetCurrentPlatformInfo().GetSocVersion()判断当前芯片仅当 SOC 版本为ASCEND910B、ASCEND910_93或ASCEND950时才允许执行否则直接以ACLNN_ERR_PARAM_INVALID报错从实现层面保证了“不支持的设备上不会误执行”。两段式接口与函数原型aclnnReduceNansum遵循 CANN 单算子 API 的两段式接口规范必须先调用第一段接口aclnnReduceNansumGetWorkspaceSize获取计算所需 workspace 大小以及包含算子计算流程的执行器再调用第二段接口aclnnReduceNansum执行计算。第一段接口计算 workspace 大小并构建执行器aclnnStatus aclnnReduceNansumGetWorkspaceSize( const aclTensor* self, const aclIntArray* dim, bool keepDim, aclDataType dtype, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)第二段接口执行计算aclnnStatus aclnnReduceNansum( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)两个接口的声明均位于仓库 aclnn_reduce_nansum.h属于aclnn_math领域。其中 workspace 是指除输入/输出外算子在 NPU 上完成计算所需的临时内存第二段接口不能重复调用同一个 executor 只能执行一次。aclnnReduceNansumGetWorkspaceSize 参数说明第一段接口的参数可归纳如下参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入输入 tensor。支持空 Tensor。数据类型需和 out 的 dtype 满足可转换关系参见互转换关系。FLOAT16、FLOAT32、INT8、INT16、INT32、INT64、UINT8、BOOL、BFLOAT16ND0-8√dimaclIntArray*输入参与计算的维度。取值范围为 [-self.dim(), self.dim())dim 数组长度为 0 时对所有轴做 ReduceNansum 计算。INT64---keepDimbool输入是否在输出张量中保留要缩减的维度。-----dtypeaclDataType输入返回张量的所需数据类型。-FLOAT16、FLOAT32、INT8、INT16、INT32、INT64、UINT8、BOOL、BFLOAT16---outaclTensor*输出输出 tensor。支持空 Tensor。数据类型需和 self 的 dtype 满足可转换关系参见互转换关系。shape 需要是 self 经过计算后的 shape。FLOAT16、FLOAT32、INT8、INT16、INT32、INT64、UINT8、BOOL、BFLOAT16ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小。-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程。-----需要特别说明的几点dtype 参数的语义它表示输出张量期望的数据类型接口内部会将输入先 cast 到该类型再执行归约。这意味着self、dtype、out三者之间存在“可转换”约束——源码中CheckPromoteTypeaclnn_reduce_nansum.cpp会校验 self 的数据类型能否无损转换为 out 的 dtype。dim 负数索引dim 取值范围为[-self.dim(), self.dim())支持负数索引。实现中GetPosDimaclnn_reduce_nansum.cpp会将负数转换为正索引dim 0 ? dim dimNum : dim并且用 64 位 bitset 掩码检查 dim 是否重复出现。空 dim 语义dim 数组长度为 0 时表示对所有轴做归约。第一段接口中会将其等价展开为全部轴见下文源码剖析。非连续 Tensorself 与 out 均支持非连续 Tensor接口内部通过l0op::Contiguous与l0op::ViewCopy完成连续化与结果回写。返回值与错误码第一段接口返回aclnnStatus状态码具体参见 aclnn 返回码。第一段接口完成入参校验出现以下场景时报错返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self、dim 或 out 是空指针。ACLNN_ERR_PARAM_INVALID161002self 或 out 的数据类型不在支持范围内。ACLNN_ERR_PARAM_INVALID161002dim 数组中的维度超出输入 tensor 的维度范围。ACLNN_ERR_PARAM_INVALID161002dim 指定的轴重复。ACLNN_ERR_PARAM_INVALID161002self 或 out 的 shape 超过 8 维。ACLNN_ERR_PARAM_INVALID161002dtype 和 out 的数据类型不一致时。ACLNN_ERR_PARAM_INVALID161002out shape 与实际不匹配。这些校验在源码中一一对应CheckNotNull空指针、CheckDtypeValid数据类型合法性及 dtype 与 out 一致性、CheckDimValid维度范围与重复轴、CheckShape最大 8 维对应MAX_SUPPORT_DIMS_NUMS最终由CheckParamsaclnn_reduce_nansum.cpp按固定顺序串联执行。aclnnReduceNansum 参数说明第二段接口参数较为简单参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址。workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnReduceNansumGetWorkspaceSize 获取。executor输入op 执行器包含了算子计算流程。stream输入指定执行任务的 Stream。返回值为aclnnStatus状态码具体参见 aclnn 返回码。在实现中第二段接口通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)aclnn_reduce_nansum.cpp调用框架能力完成异步计算因此调用后需要使用aclrtSynchronizeStream同步等待任务结束。源码级剖析接口内部的分类型执行路径aclnnReduceNansumGetWorkspaceSize的实现aclnn_reduce_nansum.cpp完整展示了该算子的内部流程可分为以下几个关键步骤参数校验依次执行空指针、数据类型、类型转换关系、dim 范围与重复性、最大维度检查任一失败即返回对应错误码。空 Tensor 短路处理若self-IsEmpty()为真接口不启动任何 kernel直接通过l0op::Filll0op::ViewCopy将 out 填充为 0并将workspaceSize置为 0 返回ACLNN_SUCCESS。空 dim 展开若dim-Size() 0则生成[0, 1, ..., dimNum-1]的完整轴数组等价于对所有轴做归约。连续化与类型转换通过l0op::Contiguous将非连续输入连续化再通过l0op::Cast将输入 cast 到 dtype 指定的类型。分类型执行路径这是实现的核心设计BOOL 类型走l0op::ReduceAny路径对 bool 做归约等价于 any 语义整型INT8/INT16/INT32/INT64/UINT8整型不存在 NaN走l0op::ReduceSumOpmath/reduce_sum/op_api/reduce_sum_op.h路径浮点类型FLOAT16/FLOAT32/BFLOAT16才真正走l0op::ReduceNansumkernel 路径将 NaN 视为 0 求和。结果回写与 workspace 计算通过CheckShapeAndScalarSame校验中间结果与 out 的 shape 一致再用l0op::ViewCopy将结果拷贝到可能非连续的 out 上最后通过uniqueExecutor-GetWorkspaceSize()汇总整条计算图所需的临时内存。从芯片类型看910B 与 950 平台上的支持策略略有差异在 aclnn_reduce_nansum.cpp 中950 上 nansum 仅支持浮点类型fp32/fp16/bf16整型与 bool 在 950 走 ReduceSum/ReduceAny 路径910B 系列支持完整类型列表。算子底层的 OpDef 注册与 InferShape 分别在 reduce_nansum_def.cpp 与 reduce_nansum_infershape.cpp 中完成axes支持 INT32/INT64 两种数据类型keep_dims与noop_with_empty_axes为可选属性Op 支持动态 Rank、动态 Shape 与动态编译。当axes为空 tensor 时InferShape 直接令输出 shape 等于输入 shape。对应的二进制配置 reduce_nansum_binary.json 中定义了 fp16/fp32/bf16 × int32/int64 共 6 种输入组合如ReduceNansum_float16_int32、ReduceNansum_bfloat16_int64且两个输入均标记为FormatAgnostic。约束说明确定性计算确定性计算aclnnReduceNansum为默认确定性实现即相同运行条件下多次执行会得到一致结果便于调试与回归测试。关于确定性计算的背景与注意事项可参见 确定性计算通常不推荐刻意关闭确定性计算因为确定性实现可能略慢但在需要定位问题、分析算法的场景中能显著提升效率部分非确定性算子可通过 Runtime 接口aclrtSetSysParamOpt设置ACL_OPT_DETERMINISTIC1开启确定性计算。完整调用示例以下示例代码来自仓库 examples/test_aclnn_reduce_nansum.cpp也与接口文档中的调用示例一致。它演示了输入 shape 为{4, 2}的 FLOAT32 数据对第 0 维做 ReduceNansumdim {0}、keepDim false、输出 shape{2}的完整流程。具体编译和执行过程请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_reduce_nansum.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 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::vectorint64_t selfShape {4, 2}; std::vectorint64_t outShape {2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclIntArray* dim nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {0, 1, 2, 3, 4, 5, 6, 7}; std::vectorfloat outHostData {0, 0}; std::vectorint64_t dimData {0}; bool keepDim false; auto dtype aclDataType::ACL_FLOAT; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); 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); // 创建dim aclIntArray dim aclCreateIntArray(dimData.data(), 1); CHECK_RET(dim ! nullptr, return ret); // 3.调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnReduceNansum第一段接口 ret aclnnReduceNansumGetWorkspaceSize(self, dim, keepDim, dtype, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnReduceNansumGetWorkspaceSize 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); } // 调用aclnnReduceNansum第二段接口 ret aclnnReduceNansum(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnReduceNansum 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 size; i) { LOG_PRINT(result[%ld] is: %f\n, i, resultData[i]); } // 6.释放aclTensor和aclIntArray需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyIntArray(dim); aclDestroyTensor(out); // 7.释放device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }对该示例稍作推算即可验证语义对{0,1,2,3,4,5,6,7}shape{4,2}沿第 0 维求和输出 shape 为{2}结果应为{0246, 1357} {12, 16}。若将selfHostData中的某个元素改为NAN如{0, NAN, 2, 3, 4, 5, 6, 7}则输出变为{12, 16}NaN 按 0 处理这正是 ReduceNansum 与普通 ReduceSum 的区别所在。注意本示例未含 NaN 数据读者可自行替换以验证 nan-safe 特性。测试用例与验证仓库在 tests/ut/op_api/test_aclnn_reduce_nansum.cpp 中提供了基于 gtest 的单元测试覆盖了关键边界场景ascend910B2_case_test_nullptr_selfself 传空指针第一段接口应返回非ACLNN_SUCCESSascend910B2_case_test_nullptr_dimdim 传空指针同样触发参数错误ascend910B2_case_test_nullptr_outout 传空指针触发参数错误ascend910B2_case_test_empty_selfself 为空 tensorshape 含 0 维度第一段接口应返回ACLNN_SUCCESS验证了“空 Tensor 短路填充 0”的路径。这些用例与接口文档中“self/dim/out 空指针报 ACLNN_ERR_PARAM_NULLPTR”“支持空 Tensor”的说明一一对应可直接作为开发者在集成测试时构造边界用例的参考。小结aclnnReduceNansum是 CANN ops-math 中处理含 NaN 数据的归约求和算子它通过两段式接口在 Host 侧完成参数校验、workspace 计算与计算图构建在 NPU 上完成实际计算。其实现按数据类型分派浮点走 nan-safe kernel、整型走 ReduceSum、bool 走 ReduceAny支持空 Tensor、空 dim全轴归约、非连续 Tensor、负数索引以及 0-8 维输入并默认提供确定性计算。结合本文的接口参数表、错误码表、源码剖析与完整示例开发者可以快速将该算子集成到自己的单算子调用流程中。【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进