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 配置分三档注册:ascend910b、ascend910_93走同一套配置;ascend310p走单独的scatter_elements_v2_apt配置;ascend950与ascend350共用注册配置,且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)原样保留。
在计算时需要满足以下要求:
self、index和src的维度数量必须相同。- 对于每一个维度
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 中的CheckShape、CheckTensorDim、CheckDimRange等函数:CheckTensorDim校验三个张量维度数一致;CheckShape逐维比较index.shape <= src.shape、除dim外index.shape <= self.shape,并调用OP_CHECK_MAX_DIM限制不超过 8 维;CheckDimRange将dim限制在[-N, N-1]。
参数说明
下表为 ScatterElementsV2 算子的核心参数(源自 README 参数表,结合接口头文件 aclnn_scatter.h 补充说明):
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| self | 输入 | 公式中的self,Device 侧 aclTensor,数据类型需与 src 一致 | UINT8、INT8、INT16、INT32、INT64、BOOL、FLOAT16、FLOAT32、DOUBLE、COMPLEX64、COMPLEX128、BFLOAT16 | ND |
| dim | 输入 | 用来 scatter 的维度,数据类型为 INT64,范围[-N, N-1] | int64_t | - |
| index | 输入 | 公式中的index,Device 侧 aclTensor,指定 src 写入 self 的位置 | INT32、INT64 | ND |
| src | 输入 | 公式中的src,Device 侧 aclTensor,数据类型需与 self 一致 | UINT8、INT8、INT16、INT32、INT64、BOOL、FLOAT16、FLOAT32、DOUBLE、COMPLEX64、COMPLEX128、BFLOAT16 | ND |
| reduction | 输入 | Host 侧的字符串,选择应用的 reduction 操作 | string | - |
| out | 输出 | 公式中的输出,shape 与数据类型需与 self 一致 | FLOAT、FLOAT16、DOUBLE、BFLOAT16、INT8、INT16、INT32、INT64、UINT8、BOOL | ND |
关于reduction与数值取值的对应关系,aclnn_scatter.cpp 中定义了常量:REDUCE_NONE = 0(替换)、REDUCE_ADD = 1(累加)、REDUCE_MUL = 2(累乘),并通过GetReduceStr将整数映射为字符串"none"/"add"/"mul"。此外接口层还扩展支持REDUCE_MAX = 3、REDUCE_MIN = 4、REDUCE_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 训练/推理系列产品:默认确定性。
- Ascend 950PR/Ascend 950DT:默认非确定性,支持通过
调用说明: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_NULLPTR | 161001 | self、index、src 或 out 存在空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、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 != dim时index.size(d) <= self.size(d)的限制 |
aclnnInplaceScatterGetWorkspaceSize的校验逻辑相同,只是将 self 换成 selfRef(同时省略了 self 与 out shape 一致性检查,因为二者共用同一内存)。
无转置快速路径
在进入通用执行路径前,接口会先调用 scatter_elements.h 中声明的l0op::SupportNoTranspose判断是否满足“无转置”快速路径条件;若满足则直接走ExecScatterNoTranspose,跳过dim轴搬移,从源码注释看该路径对self与out内存地址不同的场景会自动插入TensorMove拷贝,避免破坏输入数据。
调用示例(完整可编译代码)
以下示例源自 aclnnScatter&aclnnInplaceScatter.md 的调用示例,完整展示了 aclnnScatter 从初始化到结果回收的七个步骤。示例参数为:self形状{3, 4}、index形状{2, 3}、src形状{2, 3}、dim = 1、reduce = 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)注册:输入为var、indices、updates,输出为var,属性axis(默认 0)、reduction(默认"none")、include_self(默认true),并显式开启DynamicCompileStaticFlag、DynamicFormatFlag、DynamicRankSupportFlag、DynamicShapeSupportFlag,即该算子支持动态 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 是理解整个算子执行流程的最佳入口,其核心流程可概括为:
- 入参检查:
CheckNotNull(空指针)、CheckDtypeValid(dtype 支持列表与 self/src/out 一致性)、格式检查(仅支持 ND,NZ 格式打印警告)。 - 特殊场景短路:
index为空时直接执行ViewCopy(self → out)返回,即“无索引则原样输出 self”。 - 0 维张量处理:
InitializeTensor先通过l0op::Contiguous转为连续张量,0 维张量通过UnsqueezeNd扩为 1 维计算,输出侧再SqueezeNd还原。 - BF16 降级:非 910B/Regbase 平台将 BF16 强转为 FP32 计算(AICore 侧不支持 BF16 精度的平台)。
- special 路由:
expandFlag:当 index 的 strides 呈现[..., 1, 0]的广播/扩展形态且规模满足条件时,走DoScatterAddWithSorted(基于 scatter_add_with_sorted 与Sort的确定性累加路径,该路径还会依据ACL_OPT_DETERMINISTIC系统参数决定是否启用 sorted 分支);- Regbase 平台 2D 场景下可路由到 AICore 的
ScatterAdd; - 广播场景(
IsRouteToUpdate命中)且满足约束时路由到ScatterUpdate; - 否则进入通用
ScatterElements路径。
- 转置统一:当
dim不在最后一维时,构造 permutation 将dim轴交换到末维,调用l0op::Transpose对 self/index/src 统一转置,计算完成后对输出再转置回来,从而让 Kernel 侧只需处理“最后一维 scatter”这一种布局。 - 线性索引化: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/backward、transpose_batch_forward/backward、init_gather_offset、scatter_elements_cache等模块); - 否则走传统
KernelScatterElementsV2的ProcessSmall/ProcessScatter分支。
同时 arch35 目录提供了scatter_elements_v2_deterministic.h、scatter_elements_v2_multi.h、scatter_elements_with_sorted.h三个变体头文件,分别对应确定性模式、多核模式与 sorted 模式,与文档中“Ascend 950 默认非确定性、可通过系统参数开启确定性”的说明相互印证。
4. Tiling 与多架构适配
Tiling 计算在 arch22/scatter_elements_v2_tiling.cpp 与 arch35 目录下分架构实现(scatter_elements_v2_asc_tiling.cpp、scatter_elements_v2_base_tiling.cpp),不同产品通过独立的 binary 配置(op_host/config 下的ascend310p、ascend350、ascend910_93、ascend910b、ascend950五个子目录)挂载对应的算子二进制。
测试与验证
仓库为该算子提供了完整的单测与系统测试覆盖:
- 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 侧 UT:
tests/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 正确性。 - 系统测试 ST:
tests/st下按接口分组(aclnnScatter、aclnnScatterAdd、aclnnScatterReduce、aclnnScatterValue),每组的 JSON 描述用例参数、executor_*.py负责组装与执行,另有arch35目录下的自适应(adaptive)/Warp 累加专项用例(test_scatter_elements_v2_adaptive_add.py、test_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),仅供参考