CANN ops-nn 算子开发实战:aclnnMaxPoolingGrad 最大池化反向传播接口全解析

CANN ops-nn 算子开发实战:aclnnMaxPoolingGrad 最大池化反向传播接口全解析 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 两阶段接口完成梯度计算、验证与资源管理的完整方法。产品支持情况根据接口文档与算子 READMEaclnnMaxPoolingGrad 当前支持的产品范围如下产品是否支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品√从 op_host/max_pooling_grad_def.cpp 的算子定义源码可以看到该算子在注册时仅配置了ascend910bAscend910Barch22这一 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)非连续Tensordy (aclTensor*)输入上游梯度 (upstream gradient)公式中的输入 $\frac{\partial L}{\partial y}$shape 需要与 x、y 保持一致FLOAT、FLOAT16ND1-8√x (aclTensor*)输入前向输入张量公式中的输入 $x$shape 需要与 dy、y 保持一致FLOAT、FLOAT16ND1-8√y (aclTensor*)输入前向输出最大值公式中的输入 $y$shape 需要与 dy、x 保持一致FLOAT、FLOAT16ND1-8√dx (aclTensor*)输出输入梯度公式中的输出 $\frac{\partial L}{\partial x}$shape 与 dy、x、y 一致数据类型与 dy 一致FLOAT、FLOAT16ND1-8√workspaceSize (uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----executor (aclOpExecutor**)输出返回 op 执行器包含算子计算流程-----要点补充数据类型仅支持 FLOATfp32与 FLOAT16fp16不支持 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_NULLPTR161001传入的 dy、x、y 或 dx 是空指针时ACLNN_ERR_PARAM_INVALID161002dy 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002x、y 的数据类型和 dy 不同ACLNN_ERR_PARAM_INVALID161002dy、x、y 和 dx 的 shape 不一致ACLNN_ERR_PARAM_INVALID161002dy、x 或 y 的 shape 超过 8 维其中shape 不一致的校验在 Host 侧 shape 推导阶段同样存在查看 op_host/max_pooling_grad_infershape.cppInferShape 会严格检查三个输入 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::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初始化 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::vectorint64_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::vectorfloat dyHostData {1, 1, 1, 1}; std::vectorfloat xHostData {1, 2, 3, 4}; std::vectorfloat yHostData {1, 2, 3, 4}; std::vectorfloat 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::vectorfloat 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; }代码要点逐段拆解步骤 1device/stream 初始化aclInit初始化 ACL 运行时aclrtSetDevice指定设备默认 device 0aclrtCreateStream创建计算流。这是所有 aclnn 算子的固定前置流程。步骤 2构造张量核心工具函数CreateAclTensor完成三步aclrtMalloc申请 Device 内存 →aclrtMemcpyACL_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]2x ! y时即使dy[4]9.9输出dx[4]仍为 0。源码纵深从接口到 NPU 内核的实现链路1. 算子定义op_hostmax_pooling_grad_def.cpp 通过OpDef注册算子原型三个必选输入dy/x/y与一个必选输出dx数据类型限定{DT_FLOAT16, DT_FLOAT}格式限定FORMAT_ND并显式声明动态 shape 支持DynamicShapeSupportFlag(true)。2. Shape 推导op_hostmax_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_hostmax_pooling_grad_tiling.cpp 是整个算子的性能关键核心思路以BLOCK_SIZE 256CompareScalar/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_kernelmax_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手工构造 TilingDatasmallCoreDataNum128、ubPartDataNum128、lastCoreValidDataNum48等ICPU_RUN_KF单核执行后写回结果并与 golden 比对compare_data.py。测试用例刻意覆盖了对齐膨胀 边界钳位场景padding 区[48:128)不写入测试通过 memset 预置 0 保证与 golden 一致。常见问题与排查建议返回 161002ACLNN_ERR_PARAM_INVALID优先检查 dy/x/y/dx 是否同 shape、同 dtype仅 FLOAT16/FLOAT、维度是否 ≤ 8、格式是否为 ND。返回 161001ACLNN_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),仅供参考