ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

CANN ops-nn 算子详解:aclnnForeachAddListInplace 张量列表原地相加

CANN ops-nn 算子详解:aclnnForeachAddListInplace 张量列表原地相加 CANN ops-nn 算子详解aclnnForeachAddListInplace 张量列表原地相加【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn本文系统讲解 CANN ops-nn 算子库中aclnnForeachAddListInplace接口的完整用法它以张量列表TensorList为输入将两个列表中对应位置的张量逐个执行x1_i x1_i alpha × x2_i的逐元素运算并将结果原地写回第一个张量列表。读者将掌握该算子的产品支持范围、两段式接口调用流程、全部入参约束与错误码语义并得到一份可直接编译运行、可验证输出结果的完整 C 示例。功能说明与计算公式aclnnForeachAddListInplace是一个针对张量列表的逐元素原地加法算子其语义可以理解为 PyTorch 中TensorList.foreach_add_带 alpha 系数、原地版本在 NPU 上的实现。它的核心特点是输入是两个张量列表x1、x2列表中位于相同位置的张量一一对应每个对应位置执行带系数的逐元素加法x1_i x1_i alpha × x2_i计算结果不额外开辟输出张量而是直接写回第一个输入列表x1因此它是原地inplace算子能够有效节省 Device 侧内存并减少数据搬运alpha是一个单元素张量用于缩放第二个输入列表的贡献。计算公式如下$$ x1 [{x1_0}, {x1_1}, ... {x1_{n-1}}], x2 [{x2_0}, {x2_1}, ... {x2_{n-1}}] $$$$ x1_i x1_i alpha \times x2_i \quad (i0,1,...n-1) $$从算子定义源码可以印证这一原地语义。在 op_host/foreach_add_list_inplace_def.cpp 中ForeachAddListInplace的 OpDef 同时声明了x1输入与x1输出没有独立的输出名注释明确写道 Inplace: x1 x1 alpha * x2, x1 serves as both input and output (no Output declared)即输入与输出共享同一份 Device 内存。GE 侧的算子原型定义在 op_graph/foreach_add_list_inplace_proto.h同样以DYNAMIC_INPUT声明x1、x2并以DYNAMIC_OUTPUT(x1, ...)复用输入名作为输出。产品支持情况根据官方接口文档该算子在不同硬件产品上的支持情况如下Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品不支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品不支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品不支持即该算子目前仅在 Ascend 950 系列arch35/Ascend950上提供实现。这一点可以从仓库结构得到佐证算子的 kernel 实现位于 op_kernel/arch35/foreach_add_list_inplace.cpphost 侧算子二进制配置位于 op_host/config/ascend950/foreach_add_list_inplace_binary.json算子定义中也仅调用了this-AICore().AddConfig(ascend950, regbaseCfg)注册该平台的 AICore 配置。两段式接口与函数原型与 CANN 算子库中其他 aclnn 接口一致aclnnForeachAddListInplace采用两段式接口设计必须先调用GetWorkspaceSize接口获取计算所需 workspace 大小以及包含算子计算流程的执行器再调用执行接口真正下发计算。第一段接口原型aclnnStatus aclnnForeachAddListInplaceGetWorkspaceSize( aclTensorList *x1Ref, const aclTensorList *x2, const aclTensor *alpha, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型aclnnStatus aclnnForeachAddListInplace( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)第一段接口负责入参校验与执行器构建在 Host 侧完成不涉及实际计算第二段接口使用第一段返回的executor与workspace在指定stream上异步执行算子。完整的两段式调用流程、执行器与 workspace 的概念说明可参考 docs/zh/context/two_phase_api.md。aclnnForeachAddListInplaceGetWorkspaceSize 参数详解第一段接口共 5 个参数其中前 3 个为算子业务入参后 2 个为输出参数参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensorx1RefaclTensorList*输入/输出加法运算的第一个输入张量列表同时也是原地更新的输出张量列表对应公式中的x1支持空 Tensor列表中所有 Tensor 的数据类型保持一致FLOAT32、FLOAT16、BFLOAT16、INT32ND0-8√x2aclTensorList*输入加法运算的第二个输入张量列表对应公式中的x2支持空 Tensor列表中所有 Tensor 的数据类型保持一致数据类型、数据格式和 shape 与入参x1Ref一致FLOAT32、FLOAT16、BFLOAT16、INT32ND0-8√alphaaclTensor*输入加法运算中第二个输入的系数对应公式中的alpha不支持空 Tensor元素个数为 1数据类型与x1Ref有一定对应关系见下文FLOAT32、FLOAT16、INT32ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----关于alpha数据类型与x1Ref的对应关系文档明确如下规则当x1Ref的数据类型为 FLOAT32、FLOAT16、INT32 时alpha的数据类型与x1Ref保持一致当x1Ref的数据类型为 BFLOAT16 时alpha的数据类型支持 FLOAT32。这一规则在实现中得到了一一印证在 op_host/config/ascend950/foreach_add_list_inplace_binary.json 中可以看到 4 个二进制配置条目分别对应 float16、float32、int32、bfloat16 四种x1/x2输入组合其中bfloat16 条目下alpha的 dtype 为 float32其余条目下alpha与输入 dtype 一致。算子定义源码 foreach_add_list_inplace_def.cpp 中alpha的数据类型声明为{DT_FLOAT16, DT_FLOAT, DT_INT32, DT_FLOAT}同样体现了这种BF16 配 FP32 标量的放宽策略。返回值与错误码两段接口统一返回aclnnStatus状态码具体取值可参考 aclnn 返回码。第一段接口完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 x1Ref、x2、alpha 是空指针ACLNN_ERR_PARAM_INVALID161002x1Ref、x2、alpha 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002x1Ref、x2 的数据类型不一致ACLNN_ERR_PARAM_INVALID161002x1Ref、x2 中存在空指针 TensorACLNN_ERR_INNER_TILING_ERROR561002x1Ref、x2 的 shape 不满足约束ACLNN_ERR_INNER_TILING_ERROR561002x1Ref、x2 中的 Tensor 的数据类型不一致ACLNN_ERR_INNER_TILING_ERROR561002x1Ref、x2 中的 Tensor 维度超过 8 维ACLNN_ERR_INNER_TILING_ERROR561002alpha 元素个数不为 1ACLNN_ERR_INNER_TILING_ERROR561002x1Ref、x2 的 Tensor 数量不一致上述校验逻辑不仅存在于 aclnn 接口层在 host 侧 tiling 阶段同样被强制执行。以仓库中的 tiling 单元测试 tests/ut/op_host/arch35/test_foreach_add_list_inplace_tiling.cpp 为例test_op_registered_and_positive_arch35验证 x1/x2 shape 完全一致时 tiling 成功返回GRAPH_SUCCESStest_neg_shape_mismatch_arch35验证x1shape 为{32,4}、x2shape 为{32,8}时 tiling 返回GRAPH_FAILED对应shape 不满足约束test_neg_x2_dtype_mismatch_arch35验证x2数据类型与x1不一致时同样失败对应数据类型不一致。测试注释还说明Tensor 数量不一致的校验由共享模板GetShapeAttrsInfo强制执行。aclnnForeachAddListInplace 参数详解第二段接口共 4 个参数均为执行所需的运行时资源参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口aclnnForeachAddListInplaceGetWorkspaceSize获取executor输入op 执行器包含算子计算流程stream输入指定执行任务的 Stream使用要点workspace内存必须通过aclrtMalloc在 Device 侧申请申请大小严格使用第一段接口返回的workspaceSize当该值为 0 时无需申请executor由第一段接口创建调用方无需也不应手动构造stream决定了算子在哪个任务流上异步执行调用后通常需要aclrtSynchronizeStream同步等待任务完成再读取结果。约束说明确定性计算aclnnForeachAddListInplace默认采用确定性实现即相同输入在任何运行下均得到确定一致的输出结果。列表规模约束入参x1与x2中 Tensor 的数量必须相同且单个 Tensor 列表包含的 Tensor 数量不超过 256 个该约束见 foreach_add_list_inplace/README.md。维度约束列表中每个 Tensor 的维度数不超过 8 维超出时报ACLNN_ERR_INNER_TILING_ERROR561002。数据类型约束x1与x2列表中所有 Tensor 数据类型保持一致且两个列表间的 dtype、shape 一一对应一致。底层 kernel 实现原理从源码看该算子在 NPU 上的计算路径清晰且高效kernel 入口 op_kernel/arch35/foreach_add_list_inplace.cpp 根据 tiling 阶段确定的数据类型键FOREACH_TILING_KEY_HALF/FLOAT/INT/BF16分别实例化模板half与bfloat16_t使用float作为标量类型参与计算即半精度/脑浮点以浮点精度累乘float、int则直接按自身类型计算实际运算逻辑定义在 op_kernel/arch35/foreach_add_list_inplace_regbase.h它继承共享模板ForeachBinaryAlphaCastInplaceRegbase通过向量指令Add(dst, a, b, dataCount)完成逐元素相加alpha 缩放与数据搬运均由共享基类完成共享基类模板位于 foreach/foreach_utils/op_kernel/arch35/foreach_binary_alpha_cast_inplace.h注释说明 bf16 标量强转在 dav-3510 上不支持因此 BF16 场景下统一以 float 完成系数乘法这是上文BF16 配 FP32 alpha约束的硬件原因。完整调用示例以下示例取自官方文档与仓库中 examples/arch35/test_aclnn_foreach_add_list_inplace.cpp 保持一致展示了从初始化、构造输入、两段式调用、同步等待到资源释放的完整流程。该示例构造两个形状不同的张量列表x1 [{2,3}张量, {1,3}张量]x2 [{2,3}张量, {1,3}张量]alpha 1.2最终结果原地写回x1。具体编译与执行过程请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_foreach_add_list_inplace.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 alphaShape {1}; void* input1DeviceAddr nullptr; void* input2DeviceAddr nullptr; void* other1DeviceAddr nullptr; void* other2DeviceAddr nullptr; void* alphaDeviceAddr nullptr; aclTensor* input1 nullptr; aclTensor* input2 nullptr; aclTensor* other1 nullptr; aclTensor* other2 nullptr; aclTensor* alpha nullptr; std::vectorfloat input1HostData {1, 2, 3, 4, 5, 6}; std::vectorfloat input2HostData {7, 8, 9}; std::vectorfloat other1HostData {1, 2, 3, 4, 5, 6}; std::vectorfloat other2HostData {7, 8, 9}; std::vectorfloat alphaValueHostData {1.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); // 创建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); // 创建alpha aclTensor ret CreateAclTensor(alphaValueHostData, alphaShape, alphaDeviceAddr, aclDataType::ACL_FLOAT, alpha); 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()); // 3. 调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnForeachAddListInplace第一段接口 ret aclnnForeachAddListInplaceGetWorkspaceSize(tensorListInput1, tensorListInput2, alpha, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachAddListInplaceGetWorkspaceSize 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); } // 调用aclnnForeachAddListInplace第二段接口 ret aclnnForeachAddListInplace(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnForeachAddListInplace 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. 获取输出的值结果原地写回x1将device侧内存上的结果复制至host侧需要根据具体API的接口定义修改 auto size GetShapeSize(selfShape1); std::vectorfloat out1Data(size, 0); ret aclrtMemcpy(out1Data.data(), out1Data.size() * sizeof(out1Data[0]), input1DeviceAddr, 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(selfShape2); std::vectorfloat out2Data(size, 0); ret aclrtMemcpy(out2Data.data(), out2Data.size() * sizeof(out2Data[0]), input2DeviceAddr, 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); aclDestroyTensor(alpha); // 7.释放device资源需要根据具体API的接口定义修改 aclrtFree(input1DeviceAddr); aclrtFree(input2DeviceAddr); aclrtFree(other1DeviceAddr); aclrtFree(other2DeviceAddr); aclrtFree(alphaDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例结果分析以示例中的输入为例x1列表包含 shape 为{2,3}的张量元素 1~6与 shape 为{1,3}的张量元素 7~9x2列表对应位置元素相同alpha 1.2。执行后input1shape{2,3}每个元素变为原值 1.2 × 原值即{2.2, 4.4, 6.6, 8.8, 11.0, 13.2}input2shape{1,3}每个元素变为原值 1.2 × 原值即{15.4, 17.6, 19.8}。需要说明的是示例中两个列表对应位置的张量 shape 恰好相等{2,3}对应{2,3}、{1,3}对应{1,3}符合接口数据类型、shape 一致的约束。若alpha取 1则退化为普通的列表逐元素相加x1 x2。实践要点总结两段式调用是硬性要求必须先调GetWorkspaceSize拿到workspaceSize与executor再按返回大小申请 Device 内存并调用执行接口二者缺一不可alpha 的类型跟随规则x1Ref为 FLOAT32/FLOAT16/INT32 时alpha同类型x1Ref为 BFLOAT16 时alpha使用 FLOAT32列表级校验严格两个列表 Tensor 数量必须相同不超过 256 个、对应位置 shape 一致、数据类型一致否则第一段接口或 tiling 阶段会直接报错原地语义结果写回x1Ref指向的 Device 内存读取结果时直接从原输入地址取数即可无需额外申请输出内存硬件前提当前实现仅支持 Ascend 950 系列Ascend 950PR/Ascend 950DT在其他 Atlas 产品上调用会失败使用前请确认运行环境确定性算子为确定性实现便于结果复现与精度对齐。如需进一步了解算子相关的通用概念可继续阅读仓库中的 两段式接口说明、aclnn 返回码说明 与 编译与运行样例 等文档。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

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