CANN ops-nn ScatterElementsV2 算子深度解析:基于 aclnnScatter/aclnnInplaceScatter 的 Tensor 散落更新实战指南
2026/9/23 2:33:01 网站建设 项目流程

CANN ops-nn ScatterElementsV2 算子深度解析:基于 aclnnScatter/aclnnInplaceScatter 的 Tensor 散落更新实战指南

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

ScatterElementsV2 是 CANN ops-nn 算子库中负责“按索引将源张量 src 的值替换、累加或累乘到目标张量 self”的散落(Scatter)类算子,是索引类算子家族(index/目录)中应用最广泛的基础组件之一。本文以 scatter_elements_v2/README.md 为骨架,结合仓库内算子定义、Host 侧 Tiling、Kernel 侧实现与单元测试源码,系统讲解 ScatterElementsV2 的功能语义、参数规范、两段式 aclnn 调用方式与底层实现原理,读完可直接上手编写、编译并运行基于 aclnnScatter / aclnnInplaceScatter 的 NPU 加速程序。

产品支持情况

ScatterElementsV2 算子在不同产品形态上的支持情况如下(以当前仓库 README 声明为准):

产品是否支持
Ascend 950PR/Ascend 950DT
Atlas A3 训练系列产品/Atlas A3 推理系列产品
Atlas A2 训练系列产品/Atlas A2 推理系列产品
Atlas 200I/500 A2 推理产品×
Atlas 推理系列产品×
Atlas 训练系列产品×

从算子注册源码 scatter_elements_v2_def.cpp 可以看到,该算子的 AICore 配置分三档注册:ascend910bascend910_93走同一套配置;ascend310p走单独的scatter_elements_v2_apt配置;ascend950ascend350共用注册配置,且ExtendCfgInfo("opFile.value", "scatter_elements_v2_apt")指向专用于高算力产品的 APT 实现,这从源码层面印证了不同产品的差异化适配路径。

功能说明

算子功能:将 tensorsrc中的值,按指定的轴(dim)与方向以及对应的位置关系(index),逐个替换(none)、累加(add)或累乘(mul)至 tensorself中。

对于一个 3D tensor,self会按照如下规则进行更新:

self[index[i][j][k]][j][k] += src[i][j][k] # 如果 dim == 0 && reduction == 1(累加) self[i][index[i][j][k]][k] *= src[i][j][k] # 如果 dim == 1 && reduction == 2(累乘) self[i][j][index[i][j][k]] = src[i][j][k] # 如果 dim == 2 && reduction == 0(替换)

也就是说,index中每个元素index[i][j][k]表示在dim轴上src[i][j][k]应写入self的目标位置,其余轴的坐标由(i, j, k)原样保留。

在计算时需要满足以下要求:

  • selfindexsrc的维度数量必须相同。
  • 对于每一个维度d,有index.size(d) <= src.size(d)的限制。
  • 对于每一个维度d,如果d != dim,有index.size(d) <= self.size(d)的限制。
  • dim的值必须在[-self的维度数量, self的维度数量-1]之间。
  • self的维度数应小于等于 8。
  • index中对应维度dim的值(索引值)大小必须在[0, self.size(dim)-1]之间。

上述约束在 aclnn 接口的 Host 侧校验中均有对应实现,见 aclnn_scatter.cpp 中的CheckShapeCheckTensorDimCheckDimRange等函数:CheckTensorDim校验三个张量维度数一致;CheckShape逐维比较index.shape <= src.shape、除dimindex.shape <= self.shape,并调用OP_CHECK_MAX_DIM限制不超过 8 维;CheckDimRangedim限制在[-N, N-1]

参数说明

下表为 ScatterElementsV2 算子的核心参数(源自 README 参数表,结合接口头文件 aclnn_scatter.h 补充说明):

参数名输入/输出/属性描述数据类型数据格式
self输入公式中的self,Device 侧 aclTensor,数据类型需与 src 一致UINT8、INT8、INT16、INT32、INT64、BOOL、FLOAT16、FLOAT32、DOUBLE、COMPLEX64、COMPLEX128、BFLOAT16ND
dim输入用来 scatter 的维度,数据类型为 INT64,范围[-N, N-1]int64_t-
index输入公式中的index,Device 侧 aclTensor,指定 src 写入 self 的位置INT32、INT64ND
src输入公式中的src,Device 侧 aclTensor,数据类型需与 self 一致UINT8、INT8、INT16、INT32、INT64、BOOL、FLOAT16、FLOAT32、DOUBLE、COMPLEX64、COMPLEX128、BFLOAT16ND
reduction输入Host 侧的字符串,选择应用的 reduction 操作string-
out输出公式中的输出,shape 与数据类型需与 self 一致FLOAT、FLOAT16、DOUBLE、BFLOAT16、INT8、INT16、INT32、INT64、UINT8、BOOLND

关于reduction与数值取值的对应关系,aclnn_scatter.cpp 中定义了常量:REDUCE_NONE = 0(替换)、REDUCE_ADD = 1(累加)、REDUCE_MUL = 2(累乘),并通过GetReduceStr将整数映射为字符串"none"/"add"/"mul"。此外接口层还扩展支持REDUCE_MAX = 3REDUCE_MIN = 4REDUCE_MEAN = 5(映射到"max"/"min"/"mean"),但对应分支会打印"Maximum/Minimum/Mean mode is experimental!"警告日志,属于实验性能力,正式业务请优先使用 0/1/2 三档。

需要说明的是,README 参数表中的self数据类型集合与算子注册文件 scatter_elements_v2_def.cpp 中按产品分档的数据类型集合并不完全一致:例如ascend910b/ascend910_93档位注册了DT_FLOAT/DT_FLOAT16/DT_INT32/DT_UINT8/DT_INT8/DT_BF16/DT_BOOL的组合,而ascend950/ascend350档位支持DT_INT64/DT_INT32/DT_INT16/DT_UINT8/DT_INT8/DT_FLOAT/DT_FLOAT16/DT_BF16。实际可用类型以具体产品形态为准,调用前建议核对目标产品的算子配置。

约束说明

  • 约束说明:无额外约束(README 原文)。
  • 确定性说明(源自同目录文档 aclnnScatter&aclnnInplaceScatter.md):
    • Ascend 950PR/Ascend 950DT:默认非确定性,支持通过aclrtCtxSetSysParamOpt开启确定性;但当 reduction 为累乘场景时,开启确定性也不生效,依旧为非确定性。
    • Atlas A3 训练/推理系列产品、Atlas A2 训练/推理系列产品:默认确定性。

调用说明:aclnn 两段式接口

ScatterElementsV2 算子通过 aclnn 接口对外暴露,调用入口统一为aclnnScatter及其 inplace 变体aclnnInplaceScatter(两者功能相同,区别在于:aclnnScatter需要新建输出张量对象存储计算结果;aclnnInplaceScatter无需新建输出张量,直接在输入张量selfRef的内存中存储计算结果)。

调用方式调用样例说明
aclnn 调用test_aclnn_scatter.cpp通过 aclnnScatter&aclnnInplaceScatter 接口方式调用 ScatterElementsV2 算子

每个算子采用“两段式接口”设计,必须依次调用两段接口才能完成计算:

第一段接口:获取 workspace 大小与执行器。入参校验(空指针、数据类型、数据格式、shape、dim 范围等)均在此阶段完成:

aclnnStatus aclnnScatterGetWorkspaceSize( const aclTensor* self, int64_t dim, const aclTensor* index, const aclTensor* src, int64_t reduce, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)

第二段接口:执行计算。使用第一段返回的 workspace 与 executor 在指定 stream 上异步执行:

aclnnStatus aclnnScatter( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)

inplace 变体两段接口原型如下:

aclnnStatus aclnnInplaceScatterGetWorkspaceSize( aclTensor* selfRef, int64_t dim, const aclTensor* index, const aclTensor* src, int64_t reduce, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnInplaceScatter( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

第一段接口返回码说明

aclnnScatterGetWorkspaceSize完成入参校验,出现以下场景时报错:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001self、index、src 或 out 存在空指针
ACLNN_ERR_PARAM_INVALID161002self、index、src 或 out 的数据类型不在支持范围内;self、src、out 的数据类型不一致;self、index、src 的维度数不一致;self 与 out 的 shape 不一致;dim 不在[-N, N-1]之间;self 的维度数超过 8;shape 不满足index.size(d) <= src.size(d)d != dimindex.size(d) <= self.size(d)的限制

aclnnInplaceScatterGetWorkspaceSize的校验逻辑相同,只是将 self 换成 selfRef(同时省略了 self 与 out shape 一致性检查,因为二者共用同一内存)。

无转置快速路径

在进入通用执行路径前,接口会先调用 scatter_elements.h 中声明的l0op::SupportNoTranspose判断是否满足“无转置”快速路径条件;若满足则直接走ExecScatterNoTranspose,跳过dim轴搬移,从源码注释看该路径对selfout内存地址不同的场景会自动插入TensorMove拷贝,避免破坏输入数据。

调用示例(完整可编译代码)

以下示例源自 aclnnScatter&aclnnInplaceScatter.md 的调用示例,完整展示了 aclnnScatter 从初始化到结果回收的七个步骤。示例参数为:self形状{3, 4}index形状{2, 3}src形状{2, 3}dim = 1reduce = 1(累加):

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_scatter.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::vector<int64_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::vector<T>& hostData, const std::vector<int64_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::vector<int64_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手册 int32_t deviceId = 0; // 根据自己的实际device填写deviceId 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的接口自定义构造 int64_t dim = 1; int64_t reduce = 1; std::vector<int64_t> selfShape = {3, 4}; std::vector<int64_t> indexShape = {2, 3}; std::vector<int64_t> srcShape = {2, 3}; std::vector<int64_t> outShape = {3, 4}; void* selfDeviceAddr = nullptr; void* indexDeviceAddr = nullptr; void* srcDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* index = nullptr; aclTensor* src = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}; std::vector<int64_t> indexHostData = {0, 0, 2, 1, 0, 2}; std::vector<float> srcHostData = {-1, -2, -3, -4, -5, -6}; std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建index aclTensor ret = CreateAclTensor(indexHostData, indexShape, &indexDeviceAddr, aclDataType::ACL_INT64, &index); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建src aclTensor ret = CreateAclTensor(srcHostData, srcShape, &srcDeviceAddr, aclDataType::ACL_FLOAT, &src); 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名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnScatter第一段接口 ret = aclnnScatterGetWorkspaceSize(self, dim, index, src, reduce, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnScatterGetWorkspaceSize 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); } // 调用aclnnScatter第二段接口 ret = aclnnScatter(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnScatter 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::vector<float> 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和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(index); aclDestroyTensor(src); aclDestroyTensor(out); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(indexDeviceAddr); aclrtFree(srcDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

inplace 版本示例整体结构与上述代码一致,差异仅在以下几点:

  • 不需要创建out张量,selfRef直接作为输入/输出使用;
  • 第一段接口换为aclnnInplaceScatterGetWorkspaceSize(selfRef, dim, index, src, reduce, &workspaceSize, &executor)
  • 第二段接口换为aclnnInplaceScatter(workspaceAddr, workspaceSize, executor, stream)
  • 结果直接从selfRefDeviceAddr拷回 Host 侧查看。

算子执行流程与底层实现解析

1. 算子定义与 shape 推导(Host 侧)

算子通过 scatter_elements_v2_def.cpp 中的OP_ADD(ScatterElementsV2)注册:输入为varindicesupdates,输出为var,属性axis(默认 0)、reduction(默认"none")、include_self(默认true),并显式开启DynamicCompileStaticFlagDynamicFormatFlagDynamicRankSupportFlagDynamicShapeSupportFlag,即该算子支持动态 shape、动态 rank 与动态格式编译。

shape 推导在 scatter_elements_infershape.cpp 中实现,逻辑非常简单直接:InferShape4ScatterElementsV2将输出 shape 直接赋值为输入var的 shape(*var_out_shape = *var_in_shape),InferDataType4ScatterElementsV2将输出数据类型继承自var,即输出与目标张量同 shape 同类型。

2. aclnn 入口:参数校验与执行路由

接口层 aclnn_scatter.cpp 是理解整个算子执行流程的最佳入口,其核心流程可概括为:

  1. 入参检查CheckNotNull(空指针)、CheckDtypeValid(dtype 支持列表与 self/src/out 一致性)、格式检查(仅支持 ND,NZ 格式打印警告)。
  2. 特殊场景短路index为空时直接执行ViewCopy(self → out)返回,即“无索引则原样输出 self”。
  3. 0 维张量处理InitializeTensor先通过l0op::Contiguous转为连续张量,0 维张量通过UnsqueezeNd扩为 1 维计算,输出侧再SqueezeNd还原。
  4. BF16 降级:非 910B/Regbase 平台将 BF16 强转为 FP32 计算(AICore 侧不支持 BF16 精度的平台)。
  5. special 路由
    • expandFlag:当 index 的 strides 呈现[..., 1, 0]的广播/扩展形态且规模满足条件时,走DoScatterAddWithSorted(基于 scatter_add_with_sorted 与Sort的确定性累加路径,该路径还会依据ACL_OPT_DETERMINISTIC系统参数决定是否启用 sorted 分支);
    • Regbase 平台 2D 场景下可路由到 AICore 的ScatterAdd
    • 广播场景(IsRouteToUpdate命中)且满足约束时路由到ScatterUpdate
    • 否则进入通用ScatterElements路径。
  6. 转置统一:当dim不在最后一维时,构造 permutation 将dim轴交换到末维,调用l0op::Transpose对 self/index/src 统一转置,计算完成后对输出再转置回来,从而让 Kernel 侧只需处理“最后一维 scatter”这一种布局。
  7. 线性索引化:910B 系列将 index 通过l0op::LinearIndex转换为全局线性索引(同时处理负数索引),再交由底层 scatter 实现。

3. Kernel 侧实现

Kernel 入口为 scatter_elements_v2.cpp 中的scatter_elements_v2,通过TILING_KEY宏根据(valueType, indexType)组合(如 110 对应 float+int、220 对应 half+long 等)实例化模板。内部根据IsCacheOpTiling判定结果分流:

  • 命中 Cache 算子 Tiling(coreNums/xDim0/xDim1/indicesDim0/...任一非 0)时,走 scatter_elements_v2_low_memory 目录下的ExecTransposeAndScatterElements低内存实现(含transpose_tile_forward/backwardtranspose_batch_forward/backwardinit_gather_offsetscatter_elements_cache等模块);
  • 否则走传统KernelScatterElementsV2ProcessSmall/ProcessScatter分支。

同时 arch35 目录提供了scatter_elements_v2_deterministic.hscatter_elements_v2_multi.hscatter_elements_with_sorted.h三个变体头文件,分别对应确定性模式、多核模式与 sorted 模式,与文档中“Ascend 950 默认非确定性、可通过系统参数开启确定性”的说明相互印证。

4. Tiling 与多架构适配

Tiling 计算在 arch22/scatter_elements_v2_tiling.cpp 与 arch35 目录下分架构实现(scatter_elements_v2_asc_tiling.cppscatter_elements_v2_base_tiling.cpp),不同产品通过独立的 binary 配置(op_host/config 下的ascend310pascend350ascend910_93ascend910bascend950五个子目录)挂载对应的算子二进制。

测试与验证

仓库为该算子提供了完整的单测与系统测试覆盖:

  • aclnn 接口 UT:tests/ut/op_api/test_aclnn_scatter.cpp 使用OP_API_UT框架分别对reduction = 0/1/2/3/4五种模式调用TestGetWorkspaceSize校验成功路径,并用test_run_invalid覆盖非法场景(预期返回ACLNN_ERR_PARAM_INVALID)。值得关注的是空 tensor 用例:self+index+src全空、仅 index 空、仅 src 空被断言为合法;而self空或src空(此时 index 非空)被断言为非法,与接口实现中“index 为空直接拷贝输出”的短路逻辑一致。
  • Host 侧 UTtests/ut/op_host覆盖 infershape 单测(test_scatter_elements_v2_infershape.cpp)以及 arch22/arch35 两套 Tiling 单测。
  • Kernel 侧 UT:tests/ut/op_kernel 通过gen_data.py/gen_tiling.py生成输入与 Tiling 数据,运行 test_scatter_elements_v2.cpp 验证 Kernel 正确性。
  • 系统测试 STtests/st下按接口分组(aclnnScatter、aclnnScatterAdd、aclnnScatterReduce、aclnnScatterValue),每组的 JSON 描述用例参数、executor_*.py负责组装与执行,另有arch35目录下的自适应(adaptive)/Warp 累加专项用例(test_scatter_elements_v2_adaptive_add.pytest_scatter_elements_v2_warp_add.py),覆盖确定性模式下的多核累加正确性。

总结

ScatterElementsV2 是 CANN ops-nn 索引类算子中语义最基础、覆盖产品最广的 scatter 实现:通过 aclnnScatter 系列两段式接口,用户只需依次完成“GetWorkspaceSize 获取 workspace 与 executor”和“执行计算”两步,即可在 NPU 上完成替换/累加/累乘三种模式的按索引散落更新。从源码看,其工程实现充分体现了 CANN 算子库的典型分层设计——aclnn 入口层负责校验与路由、Host 侧负责 shape 推导与 Tiling、Kernel 侧通过模板特化与低内存/确定性/多核等变体适配不同算力平台。理解本算子后,可以同类推广到index/目录下的 scatter_add、scatter_nd、scatter_update 等系列算子,快速掌握 CANN 索引类算子的通用开发与调优方法。

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询