- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
aclnnMaxUnpool3d 是 CANN ops-nn 神经网络算子库中用于 3D 最大反池化(Max Unpooling)的上采样算子,功能上是 aclnnMaxPool 在 3D 场景下的逆运算:依据 indices 索引把输入 self 的元素放回由 outputSize 决定尺寸的输出张量 outRef 的对应位置,其余位置置 0。本文基于 index/scatter_elements/docs/aclnnMaxUnpool3d.md 并结合仓库源码,完整介绍其产品支持情况、计算公式、两段式接口原型、全部参数与错误码,并深入剖析其基于 ScatterElements 的底层实现与测试验证,帮助开发者快速完成调用、规避入参陷阱。
产品支持情况
aclnnMaxUnpool3d 在不同产品形态上的支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 不支持 |
可以看到,该算子仅面向 Ascend 950 与 A2/A3 系列产品开放,使用前需先确认目标设备的算力平台归属。
功能说明
算子功能
aclnnMaxUnpool3d 是 aclnnMaxPool 在 3D 场景下的逆运算:
- 由
outputSize决定outRef的 D、H、W 轴大小; - 根据
indices索引,在outRef中填入self的元素值; - 其余位置全部设置为 0。
也就是说,MaxPool3d 在做下采样时记录每个最大值的位置(indices),而 MaxUnpool3d 则利用这些索引把数值"放回"原位,得到一个更大尺寸但大部分位置为 0 的稀疏上采样结果。
计算公式
输入为 4 维,各维度依次为 N、D、H、W(N(Batch)为批量大小、D(Depth)为特征图深度、H(Height)为特征图高度、W(Width)为特征图宽度):
$$ outRef[N][indices[N][i]] = self[N][i] $$
输入为 5 维,各维度依次为 N、C、D、H、W(C(Channels)为特征图通道数):
$$ outRefN][C][indices[N][C][i]] = self[N][C][i] $$
其中outRef、indices和self是最后两轴(4 维场景)或最后三轴(5 维场景)合为一轴后 reshape 得到的,i ∈ [0, D×H×W)。
从源码看,这一步 reshape 正是算子在设备侧实际执行的第一步:aclnn_max_unpool3d.cpp 将输入与输出分别压缩为(N, C, D*H*W)与(N, C, outD*outH*outW)的形状,随后在最后一个维度上按索引做散射写入。
两段式接口与函数原型
aclnnMaxUnpool3d 遵循 CANN 算子库通用的两段式接口设计:先调用aclnnMaxUnpool3dGetWorkspaceSize获取计算所需 workspace 大小以及包含了算子计算流程的执行器(executor),再调用aclnnMaxUnpool3d执行计算。
第一段接口原型:
aclnnStatus aclnnMaxUnpool3dGetWorkspaceSize( const aclTensor* self, const aclTensor* indices, const aclIntArray* outputSize, const aclIntArray* stride, const aclIntArray* padding, aclTensor* outRef, uint64_t* workspaceSize, aclOpExecutor** executor)第二段接口原型:
aclnnStatus aclnnMaxUnpool3d( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)对应的头文件声明位于 index/scatter_elements/op_api/aclnn_max_unpool3d.h,两段接口均以ACLNN_API导出,供上层以 C 语言 ABI 方式调用。
aclnnMaxUnpool3dGetWorkspaceSize 参数说明
第一段接口完成入参校验并计算 workspace 大小,其参数说明如下:
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续 Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 公式中的 self,表示待转换的目标张量 | 数据类型与 outRef 一致;shape 与 indices 保持一致;4 维时各维度依次为 N、D、H、W,5 维时依次为 N、C、D、H、W | FLOAT、FLOAT16、INT16、INT32、INT64、INT8、UINT8、DOUBLE | ND | 4-5 | √ |
| indices(aclTensor*) | 输入 | 公式中的 indices,表示输入 self 的元素在输出结果中的索引位置 | shape 与 self 保持一致;4 维时各维度依次为 N、D、H、W,5 维时依次为 N、C、D、H、W | INT64、INT32 | ND | 4-5 | √ |
| outputSize(aclIntArray*) | 输入 | 输出结果在 D、H、W 维度上的空间大小 | size 大小为 3,三个元素的乘积需大于等于 self 在 D、H、W 维度上的 size 乘积 | - | - | - | - |
| stride(aclIntArray*) | 输入 | 最大池化窗口在 D、H、W 维度上的步长 | 预留参数,当前版本不参与计算,需传入 size 为 3、值大于 0 的 Host 侧 aclIntArray | - | - | - | - |
| padding(aclIntArray*) | 输入 | 最大池化窗口在 D、H、W 维度上的填充值 | 预留参数,当前版本不参与计算,需传入 size 为 3 的 Host 侧 aclIntArray | - | - | - | - |
| outRef(aclTensor*) | 输出 | 公式中的 outRef,即上采样结果 | 数据类型与 self 一致;4 维时各维度依次为 N、D、H、W,5 维时依次为 N、C、D、H、W | FLOAT、FLOAT16、INT16、INT32、INT64、INT8、UINT8、DOUBLE | ND | 4-5 | √ |
| workspaceSize(uint64_t*) | 输出 | 需要在 Device 侧申请的 workspace 大小 | 由第一段接口计算返回,随后按该值调用 aclrtMalloc 申请 | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | op 执行器,包含算子计算流程 | 第二段接口直接使用 | - | - | - | - |
关键约束解读
- dtype 白名单:self/outRef 支持 FLOAT、FLOAT16、INT16、INT32、INT64、INT8、UINT8、DOUBLE;indices 仅支持 INT64、INT32。这与源码中定义的 DTYPE_SUPPORT_LIST 和 INDEX_DTYPE_SUPPORT_LIST 完全一致。
- 预留参数:stride 与 padding 虽然出现在函数签名中,但当前版本不参与计算,属于为与 MaxPool 参数语义对齐而保留的占位参数,仍需传入符合 size 约束的合法值。
- outRef 必须是连续张量:源码中的 CheckOutContiguous 明确校验
outRef必须为连续(contiguous)张量,否则直接返回ACLNN_ERR_PARAM_INVALID。
返回值与错误码
两段接口均返回aclnnStatus状态码,具体定义参见 aclnn返回码。第一段接口完成入参校验,以下场景会报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | self、indices、outputSize、stride、padding 或 outRef 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 indices 的数据类型不在支持范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 outRef 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的维度不为 4 维或 5 维 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 indices 的 shape 不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outputSize 的 size 大小不等于 3 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outputSize 的三个元素乘积小于 self 在 D、H、W 维度上的 size 乘积 |
| ACLNN_ERR_PARAM_INVALID | 161002 | stride 的 size 大小不等于 3 |
| ACLNN_ERR_PARAM_INVALID | 161002 | padding 的 size 大小不等于 3 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 在 C、D、H、W 维度上的 size 不大于 0 |
| ACLNN_ERR_PARAM_INVALID | 161002 | stride 的元素值不大于 0 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outRef 在 N、C 维度上的 size 与 self 不完全相同 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outRef 在 D、H、W 维度上的 size 与 outputSize 中的三个元素值不相等 |
这些校验逻辑与源码中 CheckParams 的执行顺序一一对应:空指针检查 → outRef 连续性检查 → dtype 检查 → self/indices shape 检查 → self 元素值合法性检查 → outputSize/stride/padding 的 size 与取值检查 → outRef shape 与 outputSize 的一致性检查。
aclnnMaxUnpool3d 参数说明
第二段接口在获取到 workspace 与 executor 后执行实际计算:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnMaxUnpool3dGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
从源码实现看,第二段接口 aclnnMaxUnpool3d 只是将参数透传给通用的CommonOpExecutorRun完成异步下发,真正的计算逻辑全部封装在第一段接口构造的执行器之中。
源码级实现原理:ScatterElements 路由与 reshape 流程
该算子虽然在index/scatter_elements目录下提供 API,但它的计算本质是"按索引散射写入",仓库 README 明确指出:aclnnMaxUnpool3d 通过调用 ScatterElements 算子的 L0 接口实现,在 Ascend 950 上实际路由到 ScatterElementsV2 算子。
结合 aclnn_max_unpool3d.cpp,第一段接口内部构造的计算图为:
- Contiguous 归一化:对 self、indices、outRef 分别调用
l0op::Contiguous,将非连续张量转换为连续布局(这也是参数表中"非连续 Tensor"标记为 √ 的原因); - Reshape 压缩:self 与 indices 统一 reshape 为
(N, C, D*H*W),outRef reshape 为(N, C, outD*outH*outW),把空间维度合并为单一轴; - ZerosLike 初始化:以 reshape 后的 outRef 为模板生成全零张量,保证非索引位置为 0;
- ScatterElements 散射:在 axis=2(即合并后的空间轴)上以
reduction="none"模式执行ScatterElements(zeroOut, indicesReshape, selfReshape),将 self 的每个元素写入 indices 指向的位置; - Reshape 还原 + ViewCopy:把散射结果恢复为 outRef 的原始 shape,再通过 ViewCopy 拷贝到调用方提供的 outRef 中。
这一设计意味着 aclnnMaxUnpool3d 并不需要单独的 kernel 实现,而是复用了成熟的 ScatterElements 底层算子,因此天然具备确定性(见下文"约束说明"),且能够覆盖 4 维与 5 维两种输入形态。
另外,IsEmpty 快速路径 表明:当 self 为空张量时,第一段接口直接返回workspaceSize = 0并成功结束,不会执行后续构图。
约束说明
- 确定性计算:aclnnMaxUnpool3d 默认为确定性实现,即在相同输入与运行环境下,多次执行结果完全一致,便于结果比对与问题复现。
调用示例
示例代码如下,完整编译与运行流程请参考编译与运行样例。示例以 4 维输入演示:self 形状为(1, 1, 2, 2),输出形状为(1, 1, 4, 4),即把 2×2 的输入按索引上采样到 4×4 的输出。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_max_unpool3d.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手册 // 根据自己的实际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::vector<int64_t> selfShape = {1, 1, 2, 2}; std::vector<int64_t> outShape = {1, 1, 4, 4}; void* selfDeviceAddr = nullptr; void* indicesDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; aclTensor* indices = nullptr; std::vector<float> selfHostData = {1, 2, 3, 4}; std::vector<float> outHostData = {0, 0, 0, 0.0, 0, 0, 0, 0, 0, 0, 0, 0.0, 0, 0, 0, 0}; std::vector<int64_t> indicesHostData = {3, 8, 11, 13}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建indices aclTensor ret = CreateAclTensor(indicesHostData, selfShape, &indicesDeviceAddr, aclDataType::ACL_INT64, &indices); 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); std::vector<int64_t> arraySize1 = {1, 4, 4}; const aclIntArray *outputSize = aclCreateIntArray(arraySize1.data(), arraySize1.size()); CHECK_RET(outputSize != nullptr, return ACL_ERROR_INTERNAL_ERROR); std::vector<int64_t> arraySize2 = {1, 2, 3}; const aclIntArray *stride = aclCreateIntArray(arraySize2.data(), arraySize2.size()); CHECK_RET(stride != nullptr, return ACL_ERROR_INTERNAL_ERROR); const aclIntArray *padding = aclCreateIntArray(arraySize2.data(), arraySize2.size()); CHECK_RET(padding != nullptr, return ACL_ERROR_INTERNAL_ERROR); // 3. 调用CANN算子库API,需要修改为具体的API名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnMaxUnpool3d第一段接口 ret = aclnnMaxUnpool3dGetWorkspaceSize(self, indices, outputSize, stride, padding, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMaxUnpool3dGetWorkspaceSize 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); } // 调用aclnnMaxUnpool3d第二段接口 ret = aclnnMaxUnpool3d(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMaxUnpool3d 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> outData(size, 0); ret = aclrtMemcpy(outData.data(), outData.size() * sizeof(outData[0]), outDeviceAddr, size * sizeof(outData[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("out[%ld] is: %f\n", i, outData[i]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); aclDestroyTensor(indices); aclDestroyIntArray(outputSize); aclDestroyIntArray(stride); aclDestroyIntArray(padding); // 7. 释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(indicesDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }该示例的运行逻辑:self 的 4 个元素{1, 2, 3, 4}分别被写入 4×4 输出扁平化后的位置 3、8、11、13,其余 12 个位置保持为 0。可以直观验证outRef[N][indices[N][i]] = self[N][i]的计算公式。
示例要点提示
- indices 的值域:示例中 indices 取
{3, 8, 11, 13},均落在[0, 4*4)区间内,即合并后的输出空间轴索引,对应上采样后每个原始元素应放置的扁平位置。 - stride/padding 传参:示例中二者均传
{1, 2, 3}(size 为 3、值大于 0),满足预留参数的最低校验要求。 - 输出初始值:outHostData 全部初始化为 0,即便不初始化,算子内部也会通过 ZerosLike 清零非索引位置,这里显式给出便于结果比对。
测试与验证
仓库为 aclnnMaxUnpool3d 提供了完整的两级测试:
单元测试:index/scatter_elements/tests/ut/op_host/test_aclnn_max_unpool3d.cpp 覆盖了大量入参校验场景,与上文错误码表一一对应:
- 空指针场景:
case_self_nullptr、case_indices_nullptr、case_out_nullptr均断言返回ACLNN_ERR_PARAM_NULLPTR; - 非法 dtype:
case_bfloat16、case_bool、case_complex64、case_complex128均断言返回ACLNN_ERR_PARAM_INVALID,证明 BF16、BOOL、复数类型不在支持白名单内; - 非法 shape:
case_self_shape2(self 为 2 维)、case_indices_shape3(indices 与 self 维度不一致)、case_neg1_tensor(含 -1 动态维度)均返回ACLNN_ERR_PARAM_INVALID; - 非法 IntArray:
case_output_size3(outputSize 仅 2 个元素)、case_stride_size4、case_padding_size4(size 不等于 3)、case_neg1_output_size(outputSize 含负值)均返回ACLNN_ERR_PARAM_INVALID; - 空张量:
case_0_tensor断言返回ACL_SUCCESS,验证了源码中的空张量快速路径。
ST 测试:index/scatter_elements/tests/st/aclnnMaxUnpool3d/executor_aclnnMaxUnpool3d.py 用 PyTorch 的Tensor.scatter_在 CPU/NPU 上构造参考实现(先将张量 reshape 为(N, C, -1)扁平形态,再在最后一维做 scatter),用于与算子输出做数值比对。该参考实现与算子内部"reshape + ScatterElements"的计算流程完全同构,进一步印证了前文对实现原理的分析。
总结
aclnnMaxUnpool3d 是 CANN ops-nn 中实现 3D 最大反池化的标准接口:功能上作为 aclnnMaxPool 的逆运算,通过outputSize决定输出空间尺寸、以indices还原最大值位置、其余位置补零;接口上采用两段式设计,先经aclnnMaxUnpool3dGetWorkspaceSize完成校验并申请 workspace,再经aclnnMaxUnpool3d在指定 Stream 上异步执行。理解其"Contiguous → Reshape → ZerosLike → ScatterElements → ViewCopy"的底层计算图,有助于开发者正确构造 self/indices/outputSize 参数、预判常见错误码,并快速定位基于该接口实现的各类上采样场景问题。
- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
相关推荐
CANN ops-nn 算子开发指南:aclnnHardShrink 两段式接口详解与 NPU 实现原理
CANN ops nn 算子开发指南:aclnnHardShrink 两段式接口详解与 NPU 实现原理 HardShrink(硬收缩)是一种逐元素稀疏化激活函
人工智能算子库深度学习CANNAscendCANN ops-nn 算子详解:aclnnSquaredRelu 两段式接口使用指南与实现原理
CANN ops nn 算子详解:aclnnSquaredRelu 两段式接口使用指南与实现原理 本文以 CANN ops nn 仓库中 aclnnSquare
人工智能算子库深度学习CANNAscendCANN ops-nn ForeachLog2 算子详解:aclnnForeachLog2 两段式接口使用与实现原理
CANN ops nn ForeachLog2 算子详解:aclnnForeachLog2 两段式接口使用与实现原理 本文以 CANN ops nn 算子库中的
人工智能算子库深度学习CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考