CANN ops-nn 算子详解aclnnForeachAddScalarList 张量列表逐元素标量加法接口【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn导读aclnnForeachAddScalarList 是 CANN ops-nn 神经网络算子库中 foreach 系列的高阶组合算子它将张量列表 标量列表整体绑定为一次算子调用对列表中的每个张量逐元素加上对应的标量。本文基于 关联文档 与仓库源码系统讲解其功能语义、产品支持矩阵、两段式 API 原型、参数与错误码约束并给出可直接编译运行的完整调用示例同时深入到 算子定义、内核实现 与底层通用模板 foreach_one_scalar_list_binary.h帮助开发者理解该算子在 NPU 上的执行原理并快速完成二次开发。功能说明与计算公式该接口的功能是输入张量列表和输入标量列表执行逐元素相加运算。也就是说它并不是对单个张量加单个标量而是同时处理一组列表张量和一组列表标量二者按下标一一配对。设输入张量列表为x标量列表为scalars输出张量列表为y三者元素个数均为n$$ x [{x_0}, {x_1}, ... {x_{n-1}}]\ scalars [{scalars_0}, {scalars_1}, ... {scalars_{n-1}}]\ y [{y_0}, {y_1}, ... {y_{n-1}}] $$输出列表中的第i个张量等于输入列表第i个张量与第i个标量逐元素相加的结果$$ y_ix_iscalars_i (i0,1,...n-1) $$从语义上讲该算子等价于 PyTorch 中的torch._foreach_addtensors 与 scalars 均为列表的形态其价值在于把循环调用多个单张量 add 算子合并为一次算子下发减少多次 kernel 启动带来的调度开销尤其适合优化器更新、逐参数加偏置等多个同形状张量执行同一运算的批量场景。仓库中 README 对其定位的描述与之完全一致。产品支持情况该算子在以下产品上受支持与 README 中的支持矩阵一致产品是否支持Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品不支持Kirin X90 处理器系列产品支持Kirin 9030 处理器系列产品支持说明文档aclnnForeachAddScalarList.md的产品支持情况仅列出 6 类产品而仓库 README 额外补充了 Kirin X90 / Kirin 9030 处理器系列两者可相互印证。从 算子定义源码 看该算子通过AICore().AddConfig(ascend950)、AddConfig(ascend910_93)、AddConfig(ascend910b)、AddConfig(kirinx90)、AddConfig(kirin9030)注册了多种核架构配置与上述支持矩阵一一对应。其中 Kirin 系列kirinx90/kirin9030通过GetKirinCoreConfig()单独配置其支持的数据类型收窄为 FLOAT16、FLOAT32、INT32不含 BFLOAT16这一点与 README 中Kirin X90/Kirin 9030 处理器系列产品不支持 BFLOAT16的说明吻合。函数原型两段式接口该算子遵循 CANN 的两段式接口设计必须先调用aclnnForeachAddScalarListGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器executor再调用aclnnForeachAddScalarList接口执行计算。第一段接口获取 workspace 大小与执行器aclnnStatus aclnnForeachAddScalarListGetWorkspaceSize( const aclTensorList *x, const aclScalarList *scalars, const aclTensorList *out, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口执行计算aclnnStatus aclnnForeachAddScalarList( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)两段式接口的意义在于第一段在 Host 侧完成入参校验、shape 推导与 workspace 大小计算属于轻量同步调用第二段才真正将算子任务下发到 Stream 上异步执行。开发者需要根据第一段返回的workspaceSize在 Device 侧申请内存并在计算结束后释放。aclnnForeachAddScalarListGetWorkspaceSize 参数说明第一段接口的完整参数如下参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorxaclTensorList*输入表示进行加法运算的输入张量列表对应公式中的x支持空 Tensor该参数中所有 Tensor 的数据类型保持一致FLOAT32、FLOAT16、BFLOAT16、INT32ND0-8√scalarsaclScalarList*输入表示加法运算的输入标量列表对应公式中的scalars元素个数与x中 Tensor 的个数相等数据类型与入参x存在对应关系见下方说明FLOAT32、INT64---outaclTensorList*输出表示进行加法运算的输出张量列表对应公式中的y支持空 Tensor所有 Tensor 数据类型保持一致数据类型和数据格式与入参x一致shape size 大于等于入参x的 shape sizeFLOAT32、FLOAT16、BFLOAT16、INT32ND0-8×workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程-----其中scalars与x的数据类型对应关系非常关键直接决定输入组合是否合法当入参x的数据类型为FLOAT32、FLOAT16、BFLOAT16时scalars的数据类型仅支持 FLOAT32当入参x的数据类型为INT32时scalars的数据类型仅支持 INT64。这一规则在源码中有直接依据在 算子定义 中张量侧支持ge::DT_FLOAT16 / ge::DT_FLOAT / ge::DT_INT32 / ge::DT_BF16标量侧则通过DtypeTensor2Scalar工具函数从张量类型映射而来而图模式 IR 定义中scalars的TensorType明确为{DT_FLOAT, DT_INT64}与文档约束完全一致。此外x支持非连续 Tensor表格中 √而out不支持非连续 Tensor×输出张量必须按连续内存组织shape 维度支持 0-8 维0 维即标量张量。从 内核入口 的GET_TILING_DATA与注释foreach(vector) not need workspace可以推断该算子执行时不需要额外的 workspace 计算缓冲第一段接口返回的workspaceSize在多数场景下为 0。返回值与错误码两个接口均返回aclnnStatus状态码具体错误码含义可参考 aclnn返回码。第一段接口完成入参校验在以下场景会报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 x、scalars、out 是空指针ACLNN_ERR_PARAM_INVALID161002x、scalars、out 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002x 和 out 的数据类型不一致ACLNN_ERR_INNER_TILING_ERROR561002x 与 out 的 shape 不满足约束ACLNN_ERR_INNER_TILING_ERROR561002x 或 out 中的 Tensor 数据类型不一致ACLNN_ERR_INNER_TILING_ERROR561002x 或 out 中的 Tensor 维度超过 8 维开发建议调用第一段接口后应优先检查返回码是否为ACL_SUCCESS再根据错误码定位是参数为空161001、类型/形状非法161002还是tiling 计算内部错误561002避免直接带着非法参数进入第二段接口。aclnnForeachAddScalarList 执行接口参数说明第二段接口为纯执行接口参数全部为输入参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnForeachAddScalarListGetWorkspaceSize 获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream调用完成后需要通过aclrtSynchronizeStream(stream)同步等待任务执行结束再将结果从 Device 侧拷贝回 Host 侧具体方法见下文调用示例。约束说明确定性计算aclnnForeachAddScalarList为默认确定性实现即在相同输入与运行环境下多次执行结果一致不会引入不确定的并行归约顺序。这一点对依赖可复现结果的训练/调试场景很重要。输出张量不支持非连续 Tensor见上文参数表。Kirin 系列产品不支持 BFLOAT16输入README 与源码双重印证。标量列表元素个数必须与张量列表元素个数相等且遵循上文的数据类型对应关系。调用示例完整可运行的 C 样例文档提供了完整的示例代码仓库中 examples/test_aclnn_foreach_add_scalar_list.cpp 为同源可编译版本整体编译和执行流程可参考 编译与运行样例。下面按文档原样保留完整代码并结合仓库实现做逐步解读。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_foreach_add_scalar_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 outShape1 {2, 3}; std::vectorint64_t outShape2 {1, 3}; void* input1DeviceAddr nullptr; void* input2DeviceAddr nullptr; void* out1DeviceAddr nullptr; void* out2DeviceAddr nullptr; aclTensor* input1 nullptr; aclTensor* input2 nullptr; aclScalar* alpha1 nullptr; aclScalar* alpha2 nullptr; aclTensor* out1 nullptr; aclTensor* out2 nullptr; std::vectorfloat input1HostData {1, 2, 3, 4, 5, 6}; std::vectorfloat input2HostData {7, 8, 9}; std::vectorfloat out1HostData(6, 0); std::vectorfloat out2HostData(3, 0); float alpha1Value 1.2f; float alpha2Value 2.2f; // 创建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); // 创建alpha1 aclScalar alpha1 aclCreateScalar(alpha1Value, aclDataType::ACL_FLOAT); CHECK_RET(alpha1 ! nullptr, return ret); // 创建alpha2 aclScalar alpha2 aclCreateScalar(alpha2Value, aclDataType::ACL_FLOAT); CHECK_RET(alpha2 ! nullptr, 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* tempInput{input1, input2}; aclTensorList* tensorListInput aclCreateTensorList(tempInput.data(), tempInput.size()); std::vectoraclTensor* tempOutput{out1, out2}; aclTensorList* tensorListOutput aclCreateTensorList(tempOutput.data(), tempOutput.size()); std::vectoraclScalar* tempscalar{alpha1, alpha2}; aclScalarList* scalarlist aclCreateScalarList(tempscalar.data(), tempscalar.size()); // 3. 调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnForeachAddScalarList第一段接口 ret aclnnForeachAddScalarListGetWorkspaceSize(tensorListInput, scalarlist, tensorListOutput, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachAddScalarListGetWorkspaceSize 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); } // 调用aclnnForeachAddScalarList第二段接口 ret aclnnForeachAddScalarList(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachAddScalarList 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和aclScalar需要根据具体API的接口定义修改 aclDestroyTensorList(tensorListInput); aclDestroyTensorList(tensorListOutput); aclDestroyScalarList(scalarlist); // 7.释放device资源需要根据具体API的接口定义修改 aclrtFree(input1DeviceAddr); aclrtFree(input2DeviceAddr); aclrtFree(out1DeviceAddr); aclrtFree(out2DeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例代码逐段解读资源初始化固定写法aclInit→aclrtSetDevice→aclrtCreateStream完成 ACL 运行时初始化。deviceId需按实际硬件环境填写。构造输入与输出示例构造了两个 FLOAT32 张量shape 分别为{2,3}与{1,3}和两个 FLOAT32 标量1.2f、2.2f。CreateAclTensor模板函数完成 host 数据 → device 内存拷贝并依据 shape 反推连续 strides 后调用aclCreateTensor创建aclTensor标量通过aclCreateScalar创建aclScalar。最后分别用aclCreateTensorList与aclCreateScalarList将多个张量/标量聚合成列表参数。注意由于x为 FLOAT32scalars也必须使用 FLOAT32示例中aclCreateScalar传入ACL_FLOAT正是遵循了前文的数据类型对应规则。调用两段式接口先调aclnnForeachAddScalarListGetWorkspaceSize拿到workspaceSize与executor若workspaceSize 0则用aclrtMalloc申请 device 侧 workspace随后调用aclnnForeachAddScalarList(workspaceAddr, workspaceSize, executor, stream)真正执行。同步等待aclrtSynchronizeStream(stream)保证算子异步执行完成后才读取结果。回拷结果将输出张量从 device 内存aclrtMemcpy回 host 并打印。预期out1为{11.2, 21.2, ..., 61.2}out2为{72.2, 82.2, 92.2}。释放列表资源aclDestroyTensorList、aclDestroyScalarList释放列表句柄。释放 device 资源aclrtFree释放各输入/输出/workspace 内存aclrtDestroyStream、aclrtResetDevice、aclFinalize收尾。结合源码理解底层实现Host 侧算子定义OpDefforeach_add_scalar_list_def.cpp 通过OpDef描述算子的输入输出契约输入xParamType(DYNAMIC)即动态个数的 Tensor 列表支持 FLOAT16 / FLOAT / INT32 / BF16格式 ND并标记AutoContiguous()对应支持非连续 Tensor约束输入scalarsScalarList()ParamType(REQUIRED)即标量列表数据类型由DtypeTensor2Scalar从张量类型映射而来输出yParamType(DYNAMIC)数据类型与x一致AICore 配置开启DynamicCompileStaticFlag(true)、DynamicRankSupportFlag(true)、DynamicShapeSupportFlag(true)即支持动态 shape、动态 rank0-8 维的编译执行这与参数表中shape 0-8 维的约束一一对应。内核侧实现Kernelforeach_add_scalar_list.cpp 是算子内核入口foreach_add_scalar_list通过TILING_KEY_IS(...)按数据类型分发到模板实例TILING_KEY1ForeachOneScalarListBinaryhalf, half, AddsAdapterhalf, 1, 1FLOAT16TILING_KEY2ForeachOneScalarListBinaryfloat, float, AddsAdapterfloat, 1, 1FLOAT32TILING_KEY3__CCE_AICORE__ 220ForeachOneScalarListBinaryint, int, AddsAdapterint, 1, 1INT32TILING_KEY4部分新架构排除 NPU 3003/3113ForeachOneScalarListBinarybfloat16_t, float, AddsAdapterfloat, 1, 1BFLOAT16标量侧用 float 参与计算。其中AddsAdapter是对 AscendC 向量指令Adds(dst, src, scalar, count)的封装——注意标量以单值广播方式参与逐元素运算。从#if __CCE_AICORE__ 220与__NPU_ARCH__的编译期分支可以看出不同架构对 BFLOAT16 / INT32 的支持存在差异这也解释了为何 Kirin 系列不含 BF16与部分旧架构在支持矩阵上的区别。底层通用模板该算子复用了 foreach 系列的通用模板 foreach_one_scalar_list_binary.h。模板类ForeachOneScalarListBinary继承自KernelForeachUnary核心流程为Init中将inScalarGM绑定到标量列表的全局内存SetGlobalBuffer按列表下标取值ProcessPlusInLoop在每次循环迭代开始时通过inScalarGM.GetValue(index)取出与当前张量配对的标量值BFLOAT16 场景先将标量提升为 float 再计算Compute从输入队列取数据、调用InnerComputer执行Adds逐元素加法、结果入输出队列。因此整个内核本质上是对张量列表 标量列表做扁平化的流水处理外层遍历列表元素内层以向量指令完成批量逐元素加法一次 kernel 调用即可处理整组张量避免了逐张量发 kernel 的开销。图模式调用除了 aclnn 接口该算子也支持图模式构图调用。图模式下的算子 IR 定义在 foreach_add_scalar_list_proto.hREG_OP(ForeachAddScalarList)声明了DYNAMIC_INPUT(x, ...)、INPUT(scalars, ...)、DYNAMIC_OUTPUT(y, ...)的图 IR 契约。两种调用方式的差异可参考 README 的调用说明调用方式样例代码说明aclnn 接口test_aclnn_foreach_add_scalar_list通过 aclnnForeachAddScalarList 接口方式调用 ForeachAddScalarList 算子图模式-通过 算子IR 构图方式调用 ForeachAddScalarList 算子测试验证仓库为该算子提供了完整的 UT/ST 测试链可作为二次开发时的行为参照内核 UTtests/ut/op_kernel/test_foreach_add_scalar_list.cpp 通过gen_data.py生成多组 shape如{{128, 64}, {16, 128}, {32, 128}}与 float32/float16 等类型数据利用ICPU_SET_TILING_KEY指定数据类型分支、ICPU_RUN_KF在模拟环境下跑内核再用compare_data.py与 golden 数据比对覆盖多张量列表 多标量列表的批量加法正确性Host 侧 UTtests/ut/op_host/下的test_foreach_add_scalar_list_infershape.cpp与test_foreach_add_scalar_list_tiling.cpp分别验证 shape 推导与 tiling 计算逻辑ST 测试tests/st/aclnnForeachAddScalarList/提供 ATK 用例配置atk_aclnnForeachAddScalarList.json与执行器脚本tests/assets/golden.py生成标准答案配置档op_host/config/下按ascend910_93、ascend910b、ascend950、kirin9030、kirinx90分别提供foreach_add_scalar_list_binary.json与算子定义的 AICore 注册一一对应。总结aclnnForeachAddScalarList 是 CANN ops-nn 中面向批量张量 批量标量逐元素加法场景的 foreach 组合算子支持 FLOAT32 / FLOAT16 / BFLOAT16 / INT32 输入标量分别为 FLOAT32 / INT64覆盖 Ascend 950、A2、A3 及 Kirin 系列产品。开发者只需按两段式接口规范调用即可在 NPU 上高效完成整组张量的标量加法如需深入定制可沿 算子定义 → 内核实现 → 通用模板 的链路理解其数据契约、tiling 分发与向量化计算过程并借助仓库内 UT/ST 测试快速验证改动。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考