CANN ops-nn 算子 aclnnForeachMinimumList 详解:张量列表逐元素最小值的两段式接口调用实战 CANN ops-nn 算子 aclnnForeachMinimumList 详解张量列表逐元素最小值的两段式接口调用实战【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn导读aclnnForeachMinimumList 是 CANN ops-nn 神经网络算子库中 ForeachMinimumList 算子的 aclnn 接口用于对两个张量列表TensorList逐元素比较并计算最小值在逐张量批处理场景如优化器遍历参数列表、逐层裁剪梯度等中可显著减少多次单算子调用的开销。本文以 aclnnForeachMinimumList 官方文档 为骨架完整讲解其产品支持情况、两段式接口原型、参数约束、错误返回码与可编译运行的完整调用示例并结合仓库内算子定义、tiling 与 kernel 源码剖析其底层实现原理帮助读者从 API 使用直达实现细节。产品支持情况ForeachMinimumList 算子在当前仓库的 README 与 aclnn 文档中均给出了明确的产品支持矩阵汇总如下产品是否支持Ascend 950PR/Ascend 950DT√Atlas A3 训练系列产品/Atlas A3 推理系列产品√Atlas A2 训练系列产品/Atlas A2 推理系列产品√Atlas 200I/500 A2 推理产品×Atlas 推理系列产品×Atlas 训练系列产品×Kirin X90 处理器系列产品√Kirin 9030 处理器系列产品√从算子定义源码 foreach_minimum_list_def.cpp 可以印证这一矩阵算子通过AICore().AddConfig()依次注册了ascend950、ascend910_93对应 Atlas A3、ascend910b对应 Atlas A2以及kirinx90、kirin9030的 AICore 配置而 op_host 目录下的 config 也对应提供了 ascend950、ascend910_93、ascend910b、kirin9030、kirinx90 多套二进制算子配置文件。需要注意一个产品差异Kirin X90/Kirin 9030 处理器系列不支持 BFLOAT16 数据类型详见下文数据类型章节与 README。功能说明与计算公式接口功能对张量列表x1和张量列表x2执行逐元素比较计算每个元素对应的最小值。两个列表中的 Tensor 数量一一对应即x1中第i个张量与x2中第i个张量逐元素求min结果写入输出列表y的第i个张量。计算公式$$ x1 [{x1_0}, {x1_1}, ... {x1_{n-1}}], x2 [{x2_0}, {x2_1}, ... {x2_{n-1}}]\ y [{y_0}, {y_1}, ... {y_{n-1}}] $$$$ {\rm y}_i \min(x1_i, x2_i) \quad (i0,1,\dots,n-1) $$其中n为张量列表中的 Tensor 数量。该算子属于 foreach 系列中的二元算子、无标量形态与仓库中foreach_utils目录下的通用二元无标量模板 foreach_no_scalar_binary.h 相对应ForeachNoScalarBinaryT, T, MinMin即逐元素求最小值的核心计算单元。两段式接口调用模型与 CANN 其他 aclnn 算子一致aclnnForeachMinimumList 采用两段式接口设计必须先调用aclnnForeachMinimumListGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器再调用aclnnForeachMinimumList接口执行计算aclnnStatus aclnnForeachMinimumListGetWorkspaceSize( const aclTensorList *x1, const aclTensorList *x2, const aclTensorList *out, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnForeachMinimumList( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)从实现来看该算子为 foreach向量化形态tiling 阶段 workspace 大小为 0见 foreach_minimum_list_tiling_arch35.cpp 中WS_SYS_SIZE 0U与 kernel 中GM_ADDR userWS nullptr的写法因此在示例代码中workspaceSize通常为 0、无需额外申请 workspace 内存但接口调用流程仍须保持两段式结构。aclnnForeachMinimumListGetWorkspaceSize 参数说明第一段接口完成入参校验与 workspace 规划参数含义如下参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensorx1aclTensorList*输入表示取最小值运算的输入张量列表对应公式中的x1。支持空 Tensor该参数中所有 Tensor 的数据类型保持一致shape 与入参x2的 shape 一致。FLOAT32、FLOAT16、INT32、BFLOAT16ND0-8√x2aclTensorList*输入表示取最小值运算的输入张量列表对应公式中的x2。支持空 Tensor该参数中所有 Tensor 的数据类型保持一致数据类型、数据格式和 shape 与入参x1一致。FLOAT32、FLOAT16、INT32、BFLOAT16ND0-8√outaclTensorList*输出表示取最小值运算的输出张量列表对应公式中的y。支持空 Tensor该参数中所有 Tensor 的数据类型保持一致数据类型和数据格式与入参x1一致shape size 大于等于入参x1的 shape size。FLOAT32、FLOAT16、INT32、BFLOAT16ND0-8×workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小。-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程。-----要点解读数据类型支持 FLOAT32、FLOAT16、INT32、BFLOAT16 四种且 x1、x2、out 三者类型必须一致。这一约束与算子定义 foreach_minimum_list_def.cpp 中的tensor_dtype_list {ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_INT32, ge::DT_BF16}完全对应而 Kirin 平台的算子配置中数据类型列表仅包含{ge::DT_FLOAT16, ge::DT_FLOAT, ge::DT_INT32}见同文件 GetKirinCoreConfig这正是 Kirin 系列不支持 BFLOAT16 的源码级依据。shape 规则x1 与 x2 的 shape 一致out 的 shape size 大于等于 x1 的 shape size维度支持 0-8 维支持标量/空维度。非连续 Tensorx1、x2 支持非连续 Tensor表中√而 out 不支持非连续 Tensor表中×这一约束在 README 的约束说明中也有明确说明输出不支持非连续Tensor。如需了解非连续 Tensor 的详细概念可参考 non_contiguous_tensor.md。返回值与错误场景aclnnStatus返回状态码具体参见 aclnn 返回码。第一段接口完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 x1、x2、out 是空指针。ACLNN_ERR_PARAM_INVALID161002x1、x2、out 的数据类型不在支持的范围之内。ACLNN_ERR_PARAM_INVALID161002x1、x2、out 的数据类型不一致。ACLNN_ERR_INNER_TILING_ERROR561002x1、x2、out 的 shape 不满足约束。ACLNN_ERR_INNER_TILING_ERROR561002x1、x2 或 out 中的 Tensor 的数据类型不一致。ACLNN_ERR_INNER_TILING_ERROR561002x1、x2 或 out 中的 Tensor 维度超过 8 维。其中 561002tiling 错误场景与 tiling 阶段的校验逻辑相互呼应例如 foreach_minimum_list_tiling_arch35.cpp 中会校验列表中 Tensor 数量不超过MAX_TENSOR_NUM 256并逐一读取各 Tensor 的 storage shape 累加元素总数foreach_minimum_list_def.cpp 中DynamicRankSupportFlag(true)表示支持动态 rank但上限仍受 8 维约束。aclnnForeachMinimumList 参数说明第二段接口真正下发计算任务参数含义如下参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址。workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnForeachMinimumListGetWorkspaceSize 获取。executor输入op 执行器包含了算子计算流程。stream输入指定执行任务的 Stream。返回值同样为aclnnStatus具体参见 aclnn 返回码。约束说明确定性计算aclnnForeachMinimumList 默认确定性实现即相同输入在多次执行下结果可复现。确定性计算的背景说明可参考 determinism_compute.md。输出非连续 Tensor 限制输出列表 out 不支持非连续 Tensor与上文参数表中的约束一致。Kirin 平台数据类型限制Kirin X90/Kirin 9030 处理器系列不支持 BFLOAT16。完整调用示例以下示例代码来自文档原文仓库中对应的可运行版本见 test_aclnn_foreach_minimum_list.cpp编译与运行样例的通用流程可参考 compile_and_run_sample.md。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_foreach_minimum_list.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 selfShape1 {2, 3}; std::vectorint64_t selfShape2 {1, 3}; std::vectorint64_t otherShape1 {2, 3}; std::vectorint64_t otherShape2 {1, 3}; std::vectorint64_t outShape1 {2, 3}; std::vectorint64_t outShape2 {1, 3}; void* input1DeviceAddr nullptr; void* input2DeviceAddr nullptr; void* other1DeviceAddr nullptr; void* other2DeviceAddr nullptr; void* out1DeviceAddr nullptr; void* out2DeviceAddr nullptr; aclTensor* input1 nullptr; aclTensor* input2 nullptr; aclTensor* other1 nullptr; aclTensor* other2 nullptr; aclTensor* out1 nullptr; aclTensor* out2 nullptr; std::vectorfloat input1HostData {1, 2, 3, 4, 5, 6}; std::vectorfloat input2HostData {7, 8, 9}; std::vectorfloat other1HostData {6, 5, 4, 3, 2, 1}; std::vectorfloat other2HostData {9, 8, 7}; std::vectorfloat out1HostData(6, 0); std::vectorfloat out2HostData(3, 0); // 创建input1 aclTensor ret CreateAclTensor(input1HostData, selfShape1, input1DeviceAddr, aclDataType::ACL_FLOAT, input1); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建input2 aclTensor ret CreateAclTensor(input2HostData, selfShape2, input2DeviceAddr, aclDataType::ACL_FLOAT, input2); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建other1 aclTensor ret CreateAclTensor(other1HostData, otherShape1, other1DeviceAddr, aclDataType::ACL_FLOAT, other1); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建other2 aclTensor ret CreateAclTensor(other2HostData, otherShape2, other2DeviceAddr, aclDataType::ACL_FLOAT, other2); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建out1 aclTensor ret CreateAclTensor(out1HostData, outShape1, out1DeviceAddr, aclDataType::ACL_FLOAT, out1); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建out2 aclTensor ret CreateAclTensor(out2HostData, outShape2, out2DeviceAddr, aclDataType::ACL_FLOAT, out2); CHECK_RET(ret ACL_SUCCESS, return ret); std::vectoraclTensor* tempInput1{input1, input2}; aclTensorList* tensorListInput1 aclCreateTensorList(tempInput1.data(), tempInput1.size()); std::vectoraclTensor* tempInput2{other1, other2}; aclTensorList* tensorListInput2 aclCreateTensorList(tempInput2.data(), tempInput2.size()); std::vectoraclTensor* tempOutput{out1, out2}; aclTensorList* tensorListOutput aclCreateTensorList(tempOutput.data(), tempOutput.size()); // 3. 调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnForeachMinimumList第一段接口 ret aclnnForeachMinimumListGetWorkspaceSize(tensorListInput1, tensorListInput2, tensorListOutput, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachMinimumListGetWorkspaceSize 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); } // 调用aclnnForeachMinimumList第二段接口 ret aclnnForeachMinimumList(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachMinimumList 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(outShape1); std::vectorfloat out1Data(size, 0); ret aclrtMemcpy(out1Data.data(), out1Data.size() * sizeof(out1Data[0]), out1DeviceAddr, size * sizeof(out1Data[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(out1 result[%ld] is: %f\n, i, out1Data[i]); } size GetShapeSize(outShape2); std::vectorfloat out2Data(size, 0); ret aclrtMemcpy(out2Data.data(), out2Data.size() * sizeof(out2Data[0]), out2DeviceAddr, size * sizeof(out2Data[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(out2 result[%ld] is: %f\n, i, out2Data[i]); } // 6. 释放aclTensor需要根据具体API的接口定义修改 aclDestroyTensorList(tensorListInput1); aclDestroyTensorList(tensorListInput2); aclDestroyTensorList(tensorListOutput); // 7.释放device资源需要根据具体API的接口定义修改 aclrtFree(input1DeviceAddr); aclrtFree(input2DeviceAddr); aclrtFree(other1DeviceAddr); aclrtFree(other2DeviceAddr); aclrtFree(out1DeviceAddr); aclrtFree(out2DeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例要点拆解资源初始化aclInit → aclrtSetDevice → aclrtCreateStream为固定写法结束后按逆序释放资源aclrtDestroyStream → aclrtResetDevice → aclFinalize。张量构造CreateAclTensor模板函数完成 host 数据aclrtMalloc申请 Device 内存、aclrtMemcpy拷入、计算连续 strides并通过aclCreateTensor创建aclTensor数据格式为ACL_FORMAT_ND。示例中两个输入张量 shape 分别为{2,3}与{1,3}验证了列表内各 Tensor 允许 shape 不同、但 x1 与 x2 对应位置 shape 需一致的规则。TensorList 组装通过aclCreateTensorList将多个aclTensor*打包为aclTensorList*分别作为 x1、x2、out 传给接口。两段式调用第一段GetWorkspaceSize拿到workspaceSize与executor当workspaceSize 0时用aclrtMalloc申请 workspace第二段aclnnForeachMinimumList下发执行。结果回读aclrtSynchronizeStream同步后用aclrtMemcpyDEVICE_TO_HOST取回结果并打印。本例中min(1..6, 6..1) {1,2,3,3,2,1}、min(7..9, 9..7) {7,8,7}可作为自测预期值。深入源码从接口到算子的实现链路算子 IR 定义图模式入口除 aclnn 接口外该算子还支持图模式调用其 IR 定义位于 foreach_minimum_list_proto.h通过REG_OP(ForeachMinimumList)注册动态输入x1、x2与动态输出y数据类型均为{DT_FLOAT, DT_FLOAT16, DT_INT32, DT_BF16}与 aclnn 接口支持的类型一致。Host 侧算子定义与平台适配foreach_minimum_list_def.cpp 中ForeachMinimumList类继承OpDef核心配置包括输入输出均声明为DYNAMIC动态列表格式统一为FORMAT_ND并开启AutoContiguous()AICore 配置开启DynamicCompileStaticFlag、DynamicRankSupportFlag、DynamicShapeSupportFlag与PrecisionReduceFlag表明算子支持动态 shape 与动态 rank 的编译运行Kirin 平台复用GetKirinCoreConfig()且数据类型缩减为 FLOAT16、FLOAT、INT32 三种。Tiling 策略多核切分与 tiling key 分发arch22Atlas A2/A3 等路径见 foreach_minimum_list_tiling_arch22.cpp复用foreach_utils中的ForeachCommonTiling以BINARY_LIST_OP_CODE初始化后执行RunBigKernelTiling()即按列表二元算子通用模板完成多核切分。arch35Ascend 950路径见 foreach_minimum_list_tiling_arch35.cpptiling 函数会读取 AIV core 数量与 UB 内存大小遍历输入列表中每个 Tensor 的 storage shape累加得到totalElements并写入tensorSizes[]、tensorNum将总元素数按核数均分并对齐到 32 字节ALIGN_SIZE计算出perCoreElements、needCoreNum、lastCoreElements并通过SetBlockDim设置实际使用的核数最后根据首元素数据类型FLOAT/FLOAT16/BF16/INT32设置对应的 tiling key见同目录 foreach_minimum_list_tiling_key.h。空列表totalElements 0时仅使用 1 核避免空任务调度。Kernel 实现按类型分发与 SIMT 处理通用 kernel 入口foreach_minimum_list.cppforeach_minimum_listkernel 函数根据TILING_KEY_IS(n)依次实例化ForeachNoScalarBinaryhalf, half, MinFLOAT16、ForeachNoScalarBinaryfloat, float, MinFLOAT32、ForeachNoScalarBinaryint, int, MinINT32与 BF16 分支其中 BF16 分支通过__CCE_AICORE__ 220且排除特定 arch 的编译条件隔离与Kirin 不支持 BFLOAT16的产品约束保持一致。foreach 向量化形态无需额外 workspacekernel 内userWS nullptr。arch35 kernel 入口foreach_minimum_list.cpp根据编译期schModeFLOAT/FLOAT16/BF16/INT32调用NsForeachMinimumList::ProcessT, mode底层基于 SIMT 实现见 foreach_minimum_list_simt.htiling 数据结构定义在 foreach_minimum_list_tiling_data.h。测试与验证体系仓库为 ForeachMinimumList 提供了完整的 st系统测试与 ut单元测试支撑st 测试测试目录 tests/st/aclnnForeachMinimumList 下的执行器脚本 executor_aclnnForeachMinimumList.py 直接以torch._foreach_minimum(x1, x2)作为参考实现来校验算子输出说明该算子与 PyTorch 原生torch._foreach_minimum语义对齐配套的 atk_aclnnForeachMinimumList.json 定义了测试用例与数据生成规则。ut 测试tests/ut 下包含 host 侧 tiling 单测arch22/arch35 两套见 test_foreach_minimum_list_tiling.cpp、infershape 单测 test_foreach_minimum_list_infershape.cpp 以及 kernel 侧单测 test_foreach_minimum_list.cpp后者通过 foreach_minimum_list_tensorlist.h 构造 TensorList 输入数据由 minimum_list_data 下的gen_data.py/compare_data.py生成与比对。小结aclnnForeachMinimumList 通过张量列表 两段式接口的设计把多个min元素级运算合并为一次算子调用是 CANN foreach 系列接口在逐张量批处理场景下的典型代表。使用要点可归纳为x1/x2 类型、shape 一一对应且支持非连续 Tensor输出不支持非连续 Tensor维度不超过 8 维Kirin 平台不支持 BFLOAT16接口必须按GetWorkspaceSize → 申请 workspace → 执行 → 同步的顺序调用。若需在训练脚本中快速验证可参考 st 测试中torch._foreach_minimum的对照用法并结合 examples/test_aclnn_foreach_minimum_list.cpp 的完整可运行样例上手。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考