
CANN ops-nn 算子 aclnnAddRelu / aclnnInplaceAddRelu 接口详解两段式调用、参数约束与源码实现【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nnaclnnAddRelu 与 aclnnInplaceAddRelu 是 CANN ops-nn 仓库activation/relu 目录中实现加法 ReLU 激活融合计算的单算子 API二者计算语义完全相同区别仅在于输出是否复用输入内存。本文以 aclnnAddReluaclnnInplaceAddRelu 文档 为核心骨架结合 op_api 源码 与单元测试完整讲解两段式接口原型、全部参数约束、返回码、约束说明及可运行示例帮助读者在 NPU 上正确、高效地完成self alpha × other后的 ReLU 激活计算。一、产品支持情况根据算子文档与 activation/relu/README.md 中的产品支持矩阵aclnnAddRelu / aclnnInplaceAddRelu 在各类硬件上的支持情况如下产品是否支持Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品310P 系列不支持Atlas 训练系列产品910 系列支持需要注意的是Atlas 训练系列产品910 系列上存在 BFLOAT16 支持差异self、other、alpha、out的数据类型不支持 BFLOAT16见文档中的平台差异标注。这一差异与 aclnn_add_relu.cpp 中按 NPU 架构选择数据类型支持列表的逻辑一致——支持 BF16 的列表仅对DAV_2201A2 架构与DAV_3510950 架构生效。二、功能说明与计算公式接口功能完成加法计算后对结果进行 ReLU 激活。即先计算self alpha × other再将结果中大于 0 的值原样保留、小于等于 0 的值置为 0。计算公式$$ out_i self_i \alpha \times other_i $$$$ relu(self) \begin{cases} self, self\gt 0 \ 0, self\le 0 \end{cases} $$从 op_api 源码 的注释可以还原出该融合算子在 NPU 上的完整计算流水线self other | | \ / Contiguous(workspace_0) Contiguous(workspace_2) \ / Cast(workspace_1) Cast(workspace_3) \ / Add(workspace_4) | Cast(workspace_5) | Relu(workspace_6) | ViewCopy | result其中Contiguous用于将非连续张量转换为连续张量Cast负责数据类型隐式转换Add完成加法Relu完成激活最后的ViewCopy把结果写回输出张量输出可能是非连续 Tensor。该流水线中的每个中间步骤都会占用 workspace 临时内存这也正是第一段接口必须返回workspaceSize的原因。三、两段式接口与函数原型aclnnAddRelu 与 aclnnInplaceAddRelu 实现相同的功能使用区别如下请根据自身实际场景选择合适的算子aclnnAddRelu需新建一个输出张量对象存储计算结果输入self与输出out是两个独立的 aclTensor。aclnnInplaceAddRelu无需新建输出张量对象直接在输入张量selfRef的内存中存储计算结果即selfRef同时充当输入与输出原地计算省内存、省拷贝。两个算子均遵循 CANN 单算子 API 的两段式接口调用范式必须先调用xxxGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器executor再调用第二段接口执行计算。workspace 是指除输入/输出外算子在 NPU 上完成计算所需要的临时内存。注意第二段接口不能重复调用一个 executor 只能执行一次。四个函数原型如下aclnnStatus aclnnAddReluGetWorkspaceSize( const aclTensor *self, const aclTensor *other, aclScalar *alpha, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnAddRelu( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)aclnnStatus aclnnInplaceAddReluGetWorkspaceSize( aclTensor *selfRef, const aclTensor *other, aclScalar *alpha, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnInplaceAddRelu( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)从源码看aclnnInplaceAddReluGetWorkspaceSize 在完成selfRef与other的 broadcast 校验后直接以selfRef同时作为输入与输出复用了aclnnAddReluGetWorkspaceSize的完整实现这从实现层面印证了二者计算语义等价、仅输出方式不同。四、aclnnAddReluGetWorkspaceSize 参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入公式中的输入 self表示待转换的目标张量。shape 需要与 other 满足 broadcast 关系与 other 的数据类型需满足数据类型推导规则参见互推导关系。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64ND0-8√otheraclTensor*输入公式中的输入 other。shape 需要与 self 满足 broadcast 关系与 self 的数据类型需满足数据类型推导规则参见互推导关系。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64ND0-8√alphaaclScalar*输入公式中的 alpha。数据类型需要可转换成 self 与 other 推导后的数据类型。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64---outaclTensor*输出公式中的 out。数据类型需要是 self 与 other 推导之后可转换的数据类型shape 需要是 self 与 other broadcast 之后的 shape。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小。-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程。-----平台差异Atlas 训练系列产品910上参数self、other、alpha、out的数据类型不支持 BFLOAT16。关于 shape 与数据类型的约束可以从源码校验逻辑进一步理解其含义源码中通过OP_CHECK_MAX_DIM(self, MAX_DIM_LEN, ...)限制最大维度为 8MAX_DIM_LEN 8对应表格中维度 0-8的限制OP_CHECK_BROADCAST_AND_INFER_SHAPE(self, other, broadcastShape, ...)要求两个输入可 broadcast且输出 shape 必须严格等于 broadcast 推导出的 shapeCheckShapeop::PromoteType(self, other)完成数据类型互推导随后依次校验 alpha 能否 cast 到推导类型、推导类型能否 cast 到输出类型CheckPromoteType两个输入与输出若为FORMAT_FRACTAL_NZ格式会输出精度风险告警日志CheckFormat。五、aclnnAddRelu 参数说明第二段执行接口参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址。workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnAddReluGetWorkspaceSize 获取。executor输入op 执行器包含了算子计算流程。stream输入指定执行任务的 Stream。该接口内部通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)见 aclnn_add_relu.cpp调用框架能力将第一段接口编排好的计算流程在指定 stream 上异步执行。六、aclnnInplaceAddReluGetWorkspaceSize 参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfRefaclTensor*输入|输出公式中的 self 与 out表示待转换的目标张量。与 other 的数据类型需满足数据类型推导规则参见互推导关系且需要是推导之后可转换的数据类型。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64ND0-8√otheraclTensor*输入公式中的输入 other。shape 需要与 selfRef 满足 broadcast 关系与 selfRef 的数据类型需满足数据类型推导规则参见互推导关系。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64ND0-8√alphaaclScalar*输入公式中的 alpha。数据类型需要可转换成 selfRef 与 other 推导后的数据类型。BFLOAT16、FLOAT16、FLOAT32、INT8、UINT8、INT16、INT32、INT64---workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小。-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程。-----平台差异Atlas 训练系列产品910上参数selfRef、other、alpha的数据类型不支持 BFLOAT16。原地接口额外要求selfRef自身的 shape 必须与selfRef和otherbroadcast 推导出的 shape 一致见 CheckInplace否则返回ACLNN_ERR_PARAM_INVALID——因为结果要写回selfRef所在内存其形状无法随 broadcast 结果动态变化。七、aclnnInplaceAddRelu 参数说明第二段执行接口参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址。workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnInplaceAddReluGetWorkspaceSize 获取。executor输入op 执行器包含了算子计算流程。stream输入指定执行任务的 Stream。八、返回值与错误码两个算子均返回aclnnStatus状态码具体参见 aclnn 返回码。第一段接口完成入参校验出现以下场景时报错aclnnAddReluGetWorkspaceSize返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self、other、alpha 或 out 是空指针。ACLNN_ERR_PARAM_INVALID161002self 和 other 的数据类型不在支持的范围之内。ACLNN_ERR_PARAM_INVALID161002self 和 other 无法做数据类型推导。ACLNN_ERR_PARAM_INVALID161002推导出的数据类型无法转换为指定输出 out 的类型。ACLNN_ERR_PARAM_INVALID161002self 和 other 的 shape 无法做 broadcast。ACLNN_ERR_PARAM_INVALID161002alpha 无法转换为 self 和 other 推导后的数据类型。aclnnInplaceAddReluGetWorkspaceSize返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 selfRef、other 或 alpha 是空指针。ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 的数据类型不在支持的范围之内。ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 无法做数据类型推导。ACLNN_ERR_PARAM_INVALID161002推导出的数据类型无法转换为 selfRef 的类型。ACLNN_ERR_PARAM_INVALID161002selfRef 和 other 的 shape 无法做 broadcast。ACLNN_ERR_PARAM_INVALID161002alpha 无法转换为 selfRef 和 other 推导后的数据类型。这些错误场景与 单元测试 一一对应例如case_nullptr/case_020验证空指针返回ACLNN_ERR_PARAM_NULLPTRcase_021用不支持的ACL_UINT32类型验证返回ACLNN_ERR_PARAM_INVALIDcase_022用无法 broadcast 的 shape 验证返回ACLNN_ERR_PARAM_INVALIDcase_026验证 alpha 无法转换时同样返回ACLNN_ERR_PARAM_INVALID。九、约束说明确定性计算aclnnAddRelu 与 aclnnInplaceAddRelu 默认确定性实现不会引入随机性导致多次运行结果不一致。混合精度边界针对selfRef数据类型为 INT8、other数据类型为 INT32 的场景由于 cast 算子将 INT32 转换成 INT8 类型时存在精度问题参见 ops-math 仓库的 aclnnCast 文档该场景下输出结果精度无法保证使用时需谨慎评估精度影响。此外从源码还可以观察到如下实现细节可作为使用约束的补充理解当输入为 FLOAT16/BF16 与 FLOAT 混合类型且alpha 1时会直接走 L0 层带混合数据类型的 Add kernel 而跳过 Cast浮点推导类型统一升到 FLOAT 计算DoReluAndCopy中若结果为 INT16 会先 cast 到 INT32 再执行 ReLU避免 INT16 溢出若为 UINT8 则跳过 ReLU 计算UINT8 无负值ReLU 恒等。空 Tensorshape 中存在 0 维被 kernel 支持第一段接口直接返回workspaceSize 0成功退出。十、调用示例以下示例代码同时演示了 aclnnAddRelu 与 aclnnInplaceAddRelu 的完整调用流程仅供参考具体编译和执行过程请参考编译与运行样例。仓库中对应可独立编译的版本位于 test_aclnn_add_relu.cpp 与 test_aclnn_inplace_add_relu.cpp。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_add_relu.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初始化参考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::vectorint64_t selfShape {4, 2}; std::vectorint64_t otherShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* otherDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* other nullptr; aclScalar* alpha nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {0, 1, 2, 3, 4, 5, 6, 7}; std::vectorfloat otherHostData {1, 1, 1, 2, 2, 2, 3, 3}; std::vectorfloat outHostData(8, 0); float alphaValue 1.2f; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建other aclTensor ret CreateAclTensor(otherHostData, otherShape, otherDeviceAddr, aclDataType::ACL_FLOAT, other); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建alpha aclScalar alpha aclCreateScalar(alphaValue, aclDataType::ACL_FLOAT); CHECK_RET(alpha ! nullptr, return ret); // 创建out aclTensor ret CreateAclTensor(outHostData, outShape, outDeviceAddr, aclDataType::ACL_FLOAT, out); CHECK_RET(ret ACL_SUCCESS, return ret); uint64_t workspaceSize 0; aclOpExecutor* executor; // aclnnAddRelu接口调用示例 // 3. 调用CANN算子库API // 调用aclnnAddRelu第一段接口 ret aclnnAddReluGetWorkspaceSize(self, other, alpha, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnAddReluGetWorkspaceSize 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); } // 调用aclnnAddRelu第二段接口 ret aclnnAddRelu(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnAddRelu 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::vectorfloat 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]); } // 释放aclnnAddRelu申请的workspace避免后续原地接口申请时覆盖指针造成内存泄漏 if (workspaceSize 0) { aclrtFree(workspaceAddr); workspaceAddr nullptr; } // aclnnInplaceAddRelu接口调用示例 // 3. 调用CANN算子库API LOG_PRINT(\ntest aclnnInplaceAddRelu\n); // 调用aclnnInplaceAddRelu第一段接口self同时作为输入与输出 ret aclnnInplaceAddReluGetWorkspaceSize(self, other, alpha, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceAddReluGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } // 调用aclnnInplaceAddRelu第二段接口 ret aclnnInplaceAddRelu(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceAddRelu 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. 获取输出的值原地接口的结果直接写回selfDeviceAddr指向的内存 ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), selfDeviceAddr, 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(other); aclDestroyScalar(alpha); aclDestroyTensor(out); // 7. 释放Device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(otherDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例输入数据self {0,1,2,3,4,5,6,7}、other {1,1,1,2,2,2,3,3}、alpha 1.2时先计算self 1.2 × other再对结果做 ReLU本例无负值ReLU 后数值不变。十一、源码实现与测试验证11.1 计算分支策略DoAdd 函数 体现了算子内部的性能优化分支选择alpha 1 且输入为受支持的混合浮点类型FLOAT16FLOAT、BF16FLOAT 双向组合时直接调用 L0 层带混合数据类型的Addkernel避免多余的 Cast 开销alpha 1 且非混合类型时将两个输入 Cast 到推导类型后调用Addalpha ! 1时若推导类型在 AXPY 支持列表FLOAT、INT32、FLOAT16内则调用Axpy一步完成self alpha × other否则将 alpha 转为 Tensor 后先Mul再Add。11.2 Relu 内核注册ReLU 阶段最终会调用底层的 Relu kernel其各数据类型bfloat16/float16/float32/int32/int8/int64ND 格式的二进制注册信息可参见 op_host/config/ascend950/relu_binary.json950 架构对应 ascend950/relu_binary.json这与参数表中数据格式仅支持 ND的约束一致。11.3 测试验证仓库为 aclnnAddRelu / aclnnInplaceAddRelu 提供了完整的测试覆盖单元测试UTtest_aclnn_add_relu.cpp 与 test_aclnn_inplace_add_relu.cpp 覆盖了 FLOAT/FLOAT16/INT32/INT64/INT16/INT8/UINT8 等全部支持类型、不同 formatNCHW/NHWC/ND/NDHWC 等、混合 dtypefp16fp32、bf16fp32、广播场景如{2,3,4,5}与{4,5}、非连续 Tensor、空 Tensor 以及各类非法参数空指针、不支持类型、不可 broadcast、不可推导的错误码断言ST golden 实现executor_aclnnAddRelu.py 使用 PyTorch 的torch.mul、torch.add、torch.relu按torch.result_type推导规则复现期望结果作为 NPU 算子输出的精度对照基准测试场景参数见 atk_aclnnAddRelu.json。十二、总结aclnnAddRelu / aclnnInplaceAddRelu 是 CANN ops-nn 中加法 ReLU融合激活的标准单算子 API二者功能等价普通版本输出独立张量原地版本结果直接写回selfRef内存必须按两段式接口先GetWorkspaceSize再执行支持 8 种数据类型910 平台不支持 BFLOAT16、ND 格式、0-8 维、非连续 Tensor输入间需满足广播关系与数据类型互推导规则全部参数校验与计算编排逻辑可在 op_api/aclnn_add_relu.cpp 中查阅行为已由仓库内 UT/ST 测试充分验证。在需要省内存、避免结果拷贝的推理或训练融合场景中优先考虑原地版本需要保留原始输入时使用普通版本即可。【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考