CANN ops-nn 算子开发实战:aclnnMaxPoolingGrad 最大池化反向传播接口全解析
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
导读
本文以 CANN ops-nn 开源算子库中 experimental/pooling/max_pooling_grad 目录下的 aclnnMaxPoolingGrad 接口文档 为主线,系统讲解 MaxPoolingGrad 算子(最大池化的反向传播,计算输入梯度)的数学原理、两段式 aclnn 接口原型、参数约束、返回码含义与完整调用示例,并深入其 op_host / op_kernel 源码与单元测试,揭示 shape 推导、多核 Tiling 切分与 AscendC 向量指令实现细节。读完本文,你将掌握在 Atlas A2 系列产品上通过 aclnnMaxPoolingGradGetWorkspaceSize + aclnnMaxPoolingGrad 两阶段接口完成梯度计算、验证与资源管理的完整方法。
产品支持情况
根据接口文档与算子 README,aclnnMaxPoolingGrad 当前支持的产品范围如下:
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
从 op_host/max_pooling_grad_def.cpp 的算子定义源码可以看到,该算子在注册时仅配置了ascend910b(Ascend910B,arch22)这一 AI Core 架构:
OpAICoreConfig aicoreConfig910B; aicoreConfig910B.DynamicCompileStaticFlag(true) .DynamicFormatFlag(false) .DynamicRankSupportFlag(false) .DynamicShapeSupportFlag(true) // 支持动态 shape .NeedCheckSupportFlag(false) .PrecisionReduceFlag(true); this->AICore().AddConfig("ascend910b", aicoreConfig910B);这意味着该算子在当前仓库中面向 910B(对应 Atlas A2 系列)平台适配,且开启了动态 shape 支持。使用前请确认目标设备型号。
功能说明与数学原理
接口功能:最大池化的反向传播,计算输入梯度(gradient w.r.t. input)。
在卷积神经网络训练过程中,前向 max pooling 从每个窗口中选择最大值作为输出 $y$,反向传播时需要把上游梯度 $\frac{\partial L}{\partial y}$ 回传:只有"被选中为最大值"的那个输入元素能够接收到梯度,其余元素的梯度为 0。
对于非重叠窗口(stride = kernel_size,窗口元素一一对应)场景,计算公式为:
$$ \frac{\partial L}{\partial x_i} = \begin{cases} \frac{\partial L}{\partial y_i}, & \text{if } x_i = y_i \ 0, & \text{otherwise} \end{cases} $$
其中 $x_i$ 为前向输入元素,$y_i$ 为前向输出(该窗口的最大值),$\frac{\partial L}{\partial y_i}$ 为上游梯度(dy)。本算子不直接接收 kernel_size / stride 等池化参数,而是要求前向输出 $y$ 已按非重叠窗口展开到与 $x$ 完全相同的形状,因此整个反向计算退化为逐元素的"选择性传递":
dx[i] = (x[i] == y[i]) ? dy[i] : 0这一等价表达式在仓库源码中被多处明确记录,例如 op_kernel/max_pooling_grad_tiling_data.h 的文件头注释,以及 README.md 的约束说明。
函数原型:两段式接口
每个算子采用两段式接口:必须先调用aclnnMaxPoolingGradGetWorkspaceSize获取计算所需 workspace 大小以及包含算子计算流程的执行器(executor),再调用aclnnMaxPoolingGrad执行计算。
aclnnStatus aclnnMaxPoolingGradGetWorkspaceSize( const aclTensor* dy, const aclTensor* x, const aclTensor* y, const aclTensor* dx, uint64_t* workspaceSize, aclOpExecutor** executor) aclnnStatus aclnnMaxPoolingGrad( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)为什么需要两段式接口?从源码看,Tiling 阶段(op_host/max_pooling_grad_tiling.cpp)会查询平台信息、计算多核切分与 UB tile 大小,并为高级向量 API 申请系统 workspace:
// workspace: CompareScalar/Select 为高级向量 API,需要系统 workspace size_t systemWorkspaceSize = platform_ascendc::PlatformAscendC(context->GetPlatformInfo()).GetLibApiWorkSpaceSize(); currentWorkspace[0] = systemWorkspaceSize;workspace 大小只有在 Tiling 完成后才能确定,因此第一段接口负责完成入参校验、shape 推导、Tiling 计算并返回 workspaceSize 与 executor,第二段接口才真正把计算任务下发到指定 stream。
aclnnMaxPoolingGradGetWorkspaceSize 参数说明
参数表
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| dy (aclTensor*) | 输入 | 上游梯度 (upstream gradient),公式中的输入 $\frac{\partial L}{\partial y}$ | shape 需要与 x、y 保持一致 | FLOAT、FLOAT16 | ND | 1-8 | √ |
| x (aclTensor*) | 输入 | 前向输入张量,公式中的输入 $x$ | shape 需要与 dy、y 保持一致 | FLOAT、FLOAT16 | ND | 1-8 | √ |
| y (aclTensor*) | 输入 | 前向输出(最大值),公式中的输入 $y$ | shape 需要与 dy、x 保持一致 | FLOAT、FLOAT16 | ND | 1-8 | √ |
| dx (aclTensor*) | 输出 | 输入梯度,公式中的输出 $\frac{\partial L}{\partial x}$ | shape 与 dy、x、y 一致;数据类型与 dy 一致 | FLOAT、FLOAT16 | ND | 1-8 | √ |
| workspaceSize (uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor (aclOpExecutor**) | 输出 | 返回 op 执行器,包含算子计算流程 | - | - | - | - | - |
要点补充:
- 数据类型:仅支持 FLOAT(fp32)与 FLOAT16(fp16),不支持 BF16。这一点与 max_pooling_grad_def.cpp 中注册的
{ge::DT_FLOAT16, ge::DT_FLOAT}完全一致。 - 数据格式:仅支持 ND 格式(非连续张量详见 non_contiguous_tensor.md),不支持 NCHW/NHWC 等维度重排格式。
- 维度:支持 1-8 维,超过 8 维会在入参校验阶段报错。
- 非连续 Tensor:四个张量均支持传入非连续张量(strides 不紧致),这在拼接、切片后的子张量反向传播中很实用。
返回值(错误码)
第一段接口完成入参校验,返回aclnnStatus状态码(完整含义见 aclnn返回码)。出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的 dy、x、y 或 dx 是空指针时 |
| ACLNN_ERR_PARAM_INVALID | 161002 | dy 的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | x、y 的数据类型和 dy 不同 |
| ACLNN_ERR_PARAM_INVALID | 161002 | dy、x、y 和 dx 的 shape 不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | dy、x 或 y 的 shape 超过 8 维 |
其中"shape 不一致"的校验在 Host 侧 shape 推导阶段同样存在:查看 op_host/max_pooling_grad_infershape.cpp,InferShape 会严格检查三个输入 shape 必须相等,否则返回失败:
OP_CHECK_IF(*xShape != *dyShape || *yShape != *dyShape, OP_LOGE(context->GetNodeName(), "x/y shapes must equal dy shape"), return ge::GRAPH_FAILED); *dxShape = *dyShape; // 输出 dx 与输入同形(恒等映射)同时注册了InferOutDataTypeSameWithFirstInput(),保证输出 dx 的数据类型与第一个输入 dy 相同。
aclnnMaxPoolingGrad 参数说明
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口 aclnnMaxPoolingGradGetWorkspaceSize 获取 |
| executor | 输入 | op 执行器,包含算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值:aclnnStatus,具体参见 aclnn返回码。
约束说明
使用该接口时必须满足以下约束:
- 适用于非重叠窗口(stride = kernel_size)场景;
- dy / x / y / dx 四者形状相同;
y为前向 max pooling 的输出(每个窗口的最大值),已按非重叠窗口展开到与x相同的形状;算子在x与y同形前提下逐元素计算dx = (x == y) ? dy : 0;- 仅支持 ND 格式;
- 不支持 BF16 数据类型;
- 确定性计算:aclnnMaxPoolingGrad 默认为确定性实现(确定性计算的概念可参考 determinism_compute.md),同一输入在多次执行中结果可复现。
调用示例(完整可运行代码)
以下示例代码来自接口文档(与 examples/test_aclnn_max_pooling_grad.cpp 同构,后者使用 shape[64, 512]并演示了"前向最大值位置梯度透传、非最大值位置梯度置零"两种行为)。编译与运行的具体过程请参考编译与运行样例。
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_max_pooling_grad.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初始化 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. 构造输入与输出 std::vector<int64_t> shape = {2, 2}; void* dyDeviceAddr = nullptr; void* xDeviceAddr = nullptr; void* yDeviceAddr = nullptr; void* dxDeviceAddr = nullptr; aclTensor* dy = nullptr; aclTensor* x = nullptr; aclTensor* y = nullptr; aclTensor* dx = nullptr; // 测试数据: dy全1, x和y相同 (全为"最大值"), 期望dx全为1 std::vector<float> dyHostData = {1, 1, 1, 1}; std::vector<float> xHostData = {1, 2, 3, 4}; std::vector<float> yHostData = {1, 2, 3, 4}; std::vector<float> dxHostData = {0, 0, 0, 0}; // 创建dy aclTensor ret = CreateAclTensor(dyHostData, shape, &dyDeviceAddr, aclDataType::ACL_FLOAT, &dy); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建x aclTensor ret = CreateAclTensor(xHostData, shape, &xDeviceAddr, aclDataType::ACL_FLOAT, &x); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建y aclTensor ret = CreateAclTensor(yHostData, shape, &yDeviceAddr, aclDataType::ACL_FLOAT, &y); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建dx aclTensor ret = CreateAclTensor(dxHostData, shape, &dxDeviceAddr, aclDataType::ACL_FLOAT, &dx); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnMaxPoolingGrad第一段接口 ret = aclnnMaxPoolingGradGetWorkspaceSize(dy, x, y, dx, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMaxPoolingGradGetWorkspaceSize 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); } // 调用aclnnMaxPoolingGrad第二段接口 ret = aclnnMaxPoolingGrad(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMaxPoolingGrad 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. 获取输出的值 auto size = GetShapeSize(shape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), dxDeviceAddr, 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("dx[%ld] is: %f\n", i, resultData[i]); } // 6. 释放aclTensor aclDestroyTensor(dy); aclDestroyTensor(x); aclDestroyTensor(y); aclDestroyTensor(dx); // 7. 释放device资源 aclrtFree(dyDeviceAddr); aclrtFree(xDeviceAddr); aclrtFree(yDeviceAddr); aclrtFree(dxDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }代码要点逐段拆解
- 步骤 1(device/stream 初始化):
aclInit初始化 ACL 运行时,aclrtSetDevice指定设备(默认 device 0),aclrtCreateStream创建计算流。这是所有 aclnn 算子的固定前置流程。 - 步骤 2(构造张量):核心工具函数
CreateAclTensor完成三步:aclrtMalloc申请 Device 内存 →aclrtMemcpy(ACL_MEMCPY_HOST_TO_DEVICE)把 Host 数据拷入 → 计算连续张量 strides 后调用aclCreateTensor以ACL_FORMAT_ND格式创建aclTensor。四个张量必须使用相同 shape{2, 2}和相同数据类型ACL_FLOAT。 - 步骤 3(两段式调用):先调用
aclnnMaxPoolingGradGetWorkspaceSize获得workspaceSize与executor;注意只有当 workspaceSize > 0 时才需要aclrtMalloc申请 workspace,随后调用aclnnMaxPoolingGrad(workspaceAddr, workspaceSize, executor, stream)下发计算。 - 步骤 4-5(同步与取数):
aclrtSynchronizeStream阻塞等待任务完成,再用ACL_MEMCPY_DEVICE_TO_HOST把dx结果拷回 Host 并逐元素打印。 - 步骤 6-7(资源释放):依次销毁四个
aclTensor、释放四块 Device 内存与 workspace,最后销毁 stream、复位设备并aclFinalize。任何一步遗漏都可能导致显存泄漏。
对于本例数据,x == y(每个元素都是"最大值"),因此期望输出dx = {1, 1, 1, 1},即梯度完整透传。若想验证"非最大值位置梯度归零",可参考 examples/test_aclnn_max_pooling_grad.cpp 中前 5 个元素的构造:当x[4]=1而y[4]=2(x != y)时,即使dy[4]=9.9,输出dx[4]仍为 0。
源码纵深:从接口到 NPU 内核的实现链路
1. 算子定义(op_host)
max_pooling_grad_def.cpp 通过OpDef注册算子原型:三个必选输入dy/x/y与一个必选输出dx,数据类型限定{DT_FLOAT16, DT_FLOAT},格式限定FORMAT_ND,并显式声明动态 shape 支持(DynamicShapeSupportFlag(true))。
2. Shape 推导(op_host)
max_pooling_grad_infershape.cpp 实现恒等映射:校验三输入 shape 相等后将dy的 shape 直接赋给输出dx。对应的 Host 侧单元测试 tests/ut/op_host/test_max_pooling_grad_infershape.cpp 构造[2, 1, 4, 6]四维输入,断言 InferShape 返回成功且输出 shape 恒等于输入。
3. Tiling 与多核切分(op_host)
max_pooling_grad_tiling.cpp 是整个算子的性能关键,核心思路:
- 以
BLOCK_SIZE = 256(CompareScalar/Select 高级向量 API 的字节对齐要求)为基本单位; - 按
(ubSize / BLOCK_SIZE / BUFFER_NUM) / UB_PART_NUM计算每个 UB tile 能容纳的 256B block 数,进而得到tileDataNum; - 多核非均匀切分:将总数据按 256B 对齐后均匀分配到多个 AI Core,前
tailBlockNum个 core 作为"big core"多承担一个 block,其余为"small core",以处理无法整除的余数; - 边界钳位:由于 256B 对齐会"膨胀"数据量,Host 侧计算
lastCoreValidDataNum,保证最后一个 core 只处理真实有效数据,避免越界读写(kernel 侧对应逻辑见下); - 通过
context->SetBlockDim(coreNum)设置核数,并返回高级向量 API 所需的系统 workspace 大小。
TilingData 结构体定义在 op_kernel/max_pooling_grad_tiling_data.h,包含smallCoreDataNum、bigCoreDataNum、ubPartDataNum、尾部元素数与循环次数等字段。
4. 内核实现(op_kernel)
max_pooling_grad.cpp 是 kernel 入口,通过DTYPE_DY宏(默认half)模板实例化NsMaxPoolingGrad::KernelMaxPoolingGrad。核心计算在 op_kernel/max_pooling_grad.h 的Compute中完成:
// diff = x - y: 当 x 是最大值时 diff == 0 Sub(diff, xLocal, yLocal, processDataNum); // selector = (diff == 0): 标记最大值位置 CompareScalar(selector, diff, zeroVal, CMPMODE::EQ, computeDataNum); // dx = selector ? dy : 0 Select(dxLocal, selector, dyLocal, zeroTens, SELMODE::VSEL_TENSOR_TENSOR_MODE, computeDataNum);即"先减法求差、再比较定位最大值、最后按掩码选择"三步向量指令流水。值得一提的实现细节:
SelectorType特化:fp16 类型下CompareScalar输出uint8_t掩码,float 类型下输出T类型掩码,通过模板特化适配两种指令行为;Init中根据 coreIdx 判断 big/small core 并设置globalOffset,最后一个 core 若命中lastCoreValidDataNum则重新计算 loopNum 与 tailDataNum,防止 256B 对齐膨胀导致越界;Process主循环按 tile 分块执行 CopyIn → Compute → CopyOut,尾部数据量在最后一轮收敛。
5. 内核单元测试
tests/ut/op_kernel/test_max_pooling_grad.cpp 使用 gtest + ICPU 仿真跑 kernel:对 shape[2, 1, 4, 6]的 fp16 数据生成 dy/x/y 二进制数据(gen_data.py),手工构造 TilingData(smallCoreDataNum=128、ubPartDataNum=128、lastCoreValidDataNum=48等),ICPU_RUN_KF单核执行后写回结果并与 golden 比对(compare_data.py)。测试用例刻意覆盖了"对齐膨胀 + 边界钳位"场景:padding 区[48:128)不写入,测试通过 memset 预置 0 保证与 golden 一致。
常见问题与排查建议
- 返回 161002(ACLNN_ERR_PARAM_INVALID):优先检查 dy/x/y/dx 是否同 shape、同 dtype(仅 FLOAT16/FLOAT)、维度是否 ≤ 8、格式是否为 ND。
- 返回 161001(ACLNN_ERR_PARAM_NULLPTR):检查四个
aclTensor*是否创建成功(尤其aclCreateTensor失败场景),以及workspaceSize、executor指针是否有效。 - 输出 dx 全为 0 或不符合预期:确认
y确实是前向池化输出(最大值)且已展开为与x同形;若使用非重叠窗口前向结果直接回传,需先对y做形状展开。 - 使用 BF16 数据:接口不支持 BF16,需先转换为 FLOAT16 或 FLOAT 再调用。
- workspace 申请:仅当第一段接口返回的
workspaceSize > 0时才需要aclrtMalloc,调用结束后务必aclrtFree。
总结
aclnnMaxPoolingGrad 是 CANN ops-nn 中面向 Atlas A2 系列产品提供的一个轻量、确定性的最大池化反向传播接口:它将"非重叠窗口、元素一一对应"场景下的梯度回传简化为dx = (x == y) ? dy : 0的逐元素操作。通过本文你可以看到从 aclnn 两段式 API、Host 侧 shape 推导与 Tiling 多核切分,到 Device 侧 AscendC 向量指令(Sub / CompareScalar / Select)的完整实现链路,并可直接复用仓库中的 调用示例 与 单元测试 进行验证与二次开发。
【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考