尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

CANN ops-nn 中 aclnnPreluBackward 算子接口深度解析:PReLU 反向传播的两段式调用实现

CANN ops-nn 中 aclnnPreluBackward 算子接口深度解析:PReLU 反向传播的两段式调用实现 CANN ops-nn 中 aclnnPreluBackward 算子接口深度解析PReLU 反向传播的两段式调用实现【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn导读aclnnPreluBackward是 CANN 神经网络算子库 ops-nn 中 PReluGradUpdate 模块对外提供的反向传播接口用于完成 PReLU 激活函数aclnnPrelu的反向梯度计算一次性产出输入self的梯度gradInput与权重weight的梯度gradWeight。本文以 aclnnPreluBackward.md 为骨架结合仓库内的接口实现、算子定义与 Kernel 源码完整讲解两段式接口的函数原型、全部参数约束、入参校验错误码、产品支持矩阵并给出可直接编译运行的调用示例与底层调用链分析帮助开发者正确、高效地在昇腾 NPU 上调用该反向算子。功能说明PReLU 反向传播的数学定义PReLUParametric ReLU的正向计算为prelu(x) max(0, x) weight * min(0, x)反向时需要在一次调用中同时算出两条梯度路径。aclnnPreluBackward接口完成 aclnnPrelu 的反向计算输入为反向传播梯度gradOutput、正向输入self与权重weight输出为gradInputself 的梯度和gradWeightweight 的梯度。gradInput的计算公式如下$$ gradInput_{i,j,...} \begin{cases} gradOutput_{i,j,...}, if\ self_{i,j,...} 0 \ gradOutput_{i,j,...} * weight_{i}, if\ self_{i,j,...} 0 \end{cases} $$gradWeight的计算公式如下对通道方向做归约求和$$ gradWeight_{j}\sum_{i,...} \begin{cases} 0, if\ self_{i,j,...} 0 \ gradOutput_{i,j,...} * self_{i,j,...}, if\ self_{i,j,...} 0 \end{cases} $$从仓库实现看这两条梯度路径在底层被拆分为两个算子协同完成PReluGradUpdate负责逐元素计算dx并输出中间结果updatePReluGradReduce负责把update沿非通道维归约得到da即 gradWeight。这一拆分逻辑在融合 pass PReluGradFusionPass.md 中有详细说明本文后续章节会结合源码展开。产品支持情况aclnnPreluBackward接口的 NPU 型号支持矩阵如下以当前仓库文档为准产品型号是否支持Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品支持需要特别说明的是底层算子PReluGradUpdate本身仅在 Ascend 950PR/Ascend 950DT 上注册了 AICore 配置见 p_relu_grad_update_def.cpp 中AddConfig(ascend950, ...)与AddConfig(ascend350, ...)而aclnnPreluBackward高层接口通过平台判断在 Atlas A2/A3/训练系列等型号上做适配上层接口与底层 Kernel 的型号覆盖范围不完全一致实际使用时请以目标产品的算力支持为准。两段式接口函数原型与调用模式与 CANN 其他 aclnn 算子一致aclnnPreluBackward采用两段式接口设计必须先调用 GetWorkspaceSize 接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器executor再调用执行接口真正下发计算。第一段接口原型aclnnStatus aclnnPreluBackwardGetWorkspaceSize( const aclTensor* gradOutput, const aclTensor* self, const aclTensor* weight, aclTensor* gradInput, aclTensor* gradWeight, uint64_t* workspaceSize, aclOpExecutor** executor)第二段接口原型aclnnStatus aclnnPreluBackward( void* workspace, uint64_t workspace_size, aclOpExecutor* executor, aclrtStream stream)从源码看第一段接口在 aclnn_prelu_backward.cpp 中完成了创建执行器 → 参数检查 → 空 Tensor 短路返回 → 输入连续化/升维/扁平化 → 构建 L0 Kernel 计算图 → 输出 ViewCopy → 计算 workspace 大小的全流程第二段接口则直接调用CommonOpExecutorRun完成图执行aclnn_prelu_backward.cpp。aclnnPreluBackwardGetWorkspaceSize 参数说明参数总览参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorgradOutputaclTensor*输入反向传播的梯度值公式中的 gradOutput支持空 Tensordtype 需要与 self 相同shape 需要与 self 满足 broadcast 关系且 Broadcast 后 shape 与 self 的 shape 相等FLOAT16、FLOAT32、BFLOAT16ND0-8√selfaclTensor*输入prelu 的正向输入值公式中的 self支持空 TensorFLOAT16、FLOAT32、BFLOAT16ND0-8√weightaclTensor*输入prelu 的权重公式中的 weight支持空 Tensordtype 需要与 self 相同当 self 的 shape 维度大于 1 维时weight 的 shape 维度可以与 self 的 shape 维度相同且第 2 维度的值保持一致同时 weight 的 shape 其他维度的值为 1或者 weight 是 1 维 Tensor元素个数为 self 的 shape 的第 2 维度否则weight 元素个数为 1FLOAT16、FLOAT32、BFLOAT16ND0-8√gradInputaclTensor*输出为 self 的梯度值dtype 需要与 self 相同shape 需要与 gradOutput 满足 broadcast 关系gradInput 的 shape 和数据类型与 self 的相同FLOAT16、FLOAT32、BFLOAT16ND0-8√gradWeightaclTensor*输出为 weight 的梯度值dtype 需要与 self 相同需要与 weight 的数据类型相同gradWeight 的 shape 与 weight 的 shape 保持一致FLOAT16、FLOAT32、BFLOAT16ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程-----平台相关的数据类型差异Atlas 训练系列产品上数据类型仅支持FLOAT16、FLOAT32不支持 BFLOAT16。这一约束与源码中的GetDtypeSupportList()实现一致在 aclnn_prelu_backward.cpp 中ASCEND910_DTYPE_SUPPORT_LIST仅包含DT_FLOAT与DT_FLOAT16而ASCEND910B_DTYPE_SUPPORT_LIST额外包含DT_BF16接口按当前 NPU 架构DAV_2201 或 regbase 模式选择支持列表。参数校验细节源码视角在 CheckShape 中接口对 shape 的校验逻辑包括所有 Tensor 维度不得超过MAX_SUPPORT_DIMS_NUMS8 维gradInput的 shape 必须与self完全一致当weight元素个数不为 1 时不支持 0 维 Tensorself为 1 维时视为单通道channelSize 1否则通道数取self的第 2 维weight为 1 维时元素个数必须等于通道数weight为多维时维数必须与self相同、第 2 维等于通道数、其余维必须为 1gradWeight的 shape 必须与weight完全一致gradOutput与self做 broadcast 推导推导出的 shape 必须与self的 shape 相等。返回值与错误码返回aclnnStatus状态码具体参见 aclnn 返回码。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 gradOutput、self、weight、gradInput、gradWeight 是空指针ACLNN_ERR_PARAM_INVALID161002gradOutput、self、weight、gradInput、gradWeight 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002gradOutput、self、weight、gradInput、gradWeight 的数据类型不同ACLNN_ERR_PARAM_INVALID161002gradOutput、self、weight、gradInput、gradWeight 大于 8 维ACLNN_ERR_PARAM_INVALID161002weight 的元素个数不等于 self 的通道数或者 1ACLNN_ERR_PARAM_INVALID161002weight 的元素个数为 1 时gradWeight 的 shape 与 weight 不相同ACLNN_ERR_PARAM_INVALID161002gradOutput 和 self 的 shape 不满足 broadcast shape 条件aclnnPreluBackward 参数说明第二段接口入参含义如下参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnPreluBackwardGetWorkspaceSize 获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream返回值为aclnnStatus状态码具体参见 aclnn 返回码。调用时应注意只有当第一段接口返回的workspaceSize大于 0 时才需要调用aclrtMalloc申请 Device 侧 workspace 内存并在计算结束后通过aclrtFree释放。约束说明确定性计算aclnnPreluBackward默认采用确定性实现即相同输入在多次运行下保证可复现的确定性结果无需额外配置。源码级调用链解析从 aclnn 接口到 Kernel 的完整链路结合仓库源码aclnnPreluBackward的执行链路可以归纳为以下四层aclnn 高层接口层aclnn_prelu_backward.cpp完成参数校验、空 Tensor 短路、l0op::Contiguous连续化、1 维 self 升维为(1, N)、多维 weight 扁平化为 1 维等预处理再调用 L0 Kernel 并做输出ViewCopyL0 算子层prelu_grad_update.cpp通过OP_TYPE_REGISTER(PReluGradUpdate)注册算子用executor-AllocTensor分配输出gradInput与update通过ADD_TO_LAUNCHER_LIST_AICORE将 AICore 任务加入执行队列随后再调用PReluGradReduce算子归约得到 gradWeight算子定义与 shape 推导层p_relu_grad_update_def.cpp 声明了 3 个必选输入grads、features、weights与 2 个必选输出dx、update均支持DT_BF16/DT_FLOAT16/DT_FLOAT与 ND 格式并开启动态 shape/动态 rank 支持prelu_grad_infershape.cpp 将dx的 shape 推导为features的 shape、da的 shape 推导为weights的 shapeAICore Kernel 层p_relu_grad_update_apt.cpp 与 prelu_grad_update_dag.h核函数入口prelu_grad_update基于BroadcastSch调度 DAG 完成计算。矢量计算的 DAG 实现在 prelu_grad_update_dag.h 中两条梯度路径分别用两个自定义算子实现PReluGradDxCustom先Mul(weights, gradOutput)得到候选值再通过Compare(features, 0, GT)与Select完成分段选择——features 0时取gradOutput否则取weights * gradOutput即 gradInput 的计算PReluGradDaCustom先Mul(features, gradOutput)再对features 0的位置Select为 0即 updategradWeight 归约前的中间结果的计算。DAG 中通过CopyInBrc带广播的搬入、CopyOut与MemOptCfgMemLevel::LEVEL_2完成输入输出与二级缓存优化count按VECTOR_REG_WIDTH / sizeof(T)计算向量长度并循环处理保证任意元素个数下的正确性。图融合视角PReluGrad 拆分在 PReluGradFusionPass.md 中说明图编译阶段存在一个融合 pass将原PReluGrad算子拆分为PReluGradUpdate与PReluGradReduce两个算子前者基于 dy、x、weight 计算 dx 并输出中间结果 update后者基于 dy、x、weight 与 update 计算 da。结构约束要求图中存在输入为 dy、x、weight、输出为 dx、da 的 PReluGrad 算子数据类型要求三者一致且支持 FLOAT16、FLOAT32、BFLOAT16格式约束为 NDx 的 shape 为 2D~8Dweight 为 scalar 或 1D 张量元素个数等于通道数或为 1也支持比 x 少一维的广播形式如 x 为 4D 时 weight 可为 [C,1,1]。该融合 pass 目前支持 Ascend 950PR/Ascend 950DT。调用示例完整可编译以下示例代码来自 test_aclnn_prelu_backward.cpp以 shape 为{4, 2}的 self、{2}的 weight 为例完整演示了两段式接口的调用流程。具体编译和执行过程请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_prelu_backward.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 shape_size 1; for (auto i : shape) { shape_size * i; } return shape_size; } 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初始化, 参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId 0; aclrtStream stream; auto ret Init(deviceId, stream); // check根据自己的需要处理 CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2. 构造输入与输出需要根据API的接口自定义构造 std::vectorint64_t selfShape {4, 2}; std::vectorint64_t weightShape {2}; std::vectorint64_t gradOutputShape {4, 2}; std::vectorint64_t gradInputShape {4, 2}; std::vectorint64_t gradWeightShape {2}; void* selfDeviceAddr nullptr; void* gradOutputDeviceAddr nullptr; void* weightDeviceAddr nullptr; void* gradInputDeviceAddr nullptr; void* gradWeightDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* weight nullptr; aclTensor* gradOutput nullptr; aclTensor* gradInput nullptr; aclTensor* gradWeight nullptr; std::vectorfloat selfHostData {0, 1, 2, 3, 4, 5, 6, 7}; std::vectorfloat weightHostData {0.5, 0.5}; std::vectorfloat gradOutputHostData {1, 1, 1, 1, 1, 1, 1, 1}; std::vectorfloat gradInputHostData {0, 0, 0, 0, 0, 0, 0, 0}; std::vectorfloat gradWeightHostData {0, 0}; // 创建weight aclTensor ret CreateAclTensor(weightHostData, weightShape, weightDeviceAddr, aclDataType::ACL_FLOAT, weight); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建gradOutput aclTensor ret CreateAclTensor(gradOutputHostData, gradOutputShape, gradOutputDeviceAddr, aclDataType::ACL_FLOAT, gradOutput); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建gradInput aclTensor ret CreateAclTensor(gradInputHostData, gradInputShape, gradInputDeviceAddr, aclDataType::ACL_FLOAT, gradInput); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建gradWeight aclTensor ret CreateAclTensor(gradWeightHostData, gradWeightShape, gradWeightDeviceAddr, aclDataType::ACL_FLOAT, gradWeight); CHECK_RET(ret ACL_SUCCESS, return ret); // 3. 调用CANN算子库API需要修改为具体的API uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnPreluBackward第一段接口 ret aclnnPreluBackwardGetWorkspaceSize(gradOutput, self, weight, gradInput, gradWeight, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnPreluBackwardGetWorkspaceSize 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); } // 调用aclnnPreluBackward第二段接口 ret aclnnPreluBackward(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnPreluBackward 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 gradInputSize GetShapeSize(gradInputShape); std::vectorfloat gradInputResultData(gradInputSize, 0); ret aclrtMemcpy(gradInputResultData.data(), gradInputResultData.size() * sizeof(gradInputResultData[0]), gradInputDeviceAddr, gradInputSize * sizeof(float), 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 gradInputSize; i) { LOG_PRINT(gradInput[%ld] is: %f\n, i, gradInputResultData[i]); } auto gradWeightSize GetShapeSize(gradWeightShape); std::vectorfloat gradWeightResultData(gradWeightSize, 0); ret aclrtMemcpy(gradWeightResultData.data(), gradWeightResultData.size() * sizeof(gradWeightResultData[0]), gradWeightDeviceAddr, gradWeightSize * sizeof(float), 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 gradWeightSize; i) { LOG_PRINT(gradWeight[%ld] is: %f\n, i, gradWeightResultData[i]); } // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(gradOutput); aclDestroyTensor(self); aclDestroyTensor(weight); aclDestroyTensor(gradInput); aclDestroyTensor(gradWeight); // 7. 释放device资源 需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(gradOutputDeviceAddr); aclrtFree(weightDeviceAddr); aclrtFree(gradInputDeviceAddr); aclrtFree(gradWeightDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例结果推导针对示例数据self {0,1,2,3,4,5,6,7}weight {0.5, 0.5}gradOutput 全 1shape 为 {4,2}共 2 个通道按照公式可预期gradInputself 0的位置即第一个元素 0取gradOutput * weight 1 * 0.5 0.5其余位置取gradOutput 1即结果为{0.5, 1, 1, 1, 1, 1, 1, 1}gradWeight对每个通道分别求sum(self * gradOutput)且self 0的位置计 0。通道 0 的元素为 0满足 0条件贡献0 * 1 0通道 1 的元素全大于 0贡献 0即结果为{0, 0}。测试与验证途径仓库为aclnnPreluBackward提供了多层测试覆盖可供参考与二次验证单测层面test_aclnn_prelu_backward.cpp 覆盖 aclnn 接口的入参与调用路径test_p_relu_grad_update_apt.cpp 配合 gen_data.py 验证 Kernel 层计算结果test_prelu_grad_fusion_pass.cpp 验证图融合 passtest_prelu_grad_update_tiling.cpp 验证 arch35 平台的 tiling 逻辑系统测试层面atk_aclnnPreluBackward.json 与 executor_aclnnPreluBackward.py 提供了 ATK 框架下的端到端执行用例。总结aclnnPreluBackward是 CANN ops-nn 中 PReLU 反向传播的标准入口它遵循两段式接口范式一次调用同时产出gradInput与gradWeight在底层由PReluGradUpdate与PReluGradReduce两个算子协作完成逐元素梯度计算与通道归约。开发者使用时需重点核对五点所有 Tensor 数据类型一致、gradInput与self同 shape、gradWeight与weight同 shape、gradOutput与self满足 broadcast 关系、weight元素个数等于 self 通道数或为 1。结合本文给出的源码调用链与完整示例即可在支持的 NPU 型号上正确集成该反向算子。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表