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

资讯详情

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

CANN ops-nn 中 aclnnHardswish 与 aclnnInplaceHardswish 激活算子的接口设计与源码级解析

CANN ops-nn 中 aclnnHardswish 与 aclnnInplaceHardswish 激活算子的接口设计与源码级解析 CANN ops-nn 中 aclnnHardswish 与 aclnnInplaceHardswish 激活算子的接口设计与源码级解析【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn本文围绕 CANN ops-nn 仓库中 aclnnHardswishaclnnInplaceHardswish 算子文档 展开系统讲解 HardSwish 激活函数的数学定义、两段式 ACLNN 接口调用流程、各参数约束与报错场景并结合仓库中 API 实现、Kernel 实现 与 单元测试 源码帮助你在自研推理链路中正确调用该算子、理解其底层计算路径并定位参数校验失败的问题。一、算子功能与数学定义aclnnHardswish与aclnnInplaceHardswish实现的是 HardSwish 激活函数返回与输入 tensor shape 相同的输出 tensor。其计算公式为$$ Hardswish(x)\begin{cases} x, x\ge3 \ 0, x\le -3 \ \frac{x \cdot (x 3)}{6}, otherwise \end{cases} $$即输入值 x 小于等于 -3 时输出 0大于等于 3 时输出 x 本身中间区间输出 x·(x3)/6。该函数是 Sigmoid 类平滑激活的“折线化”近似仅依赖加减乘除适合在 NPU 上以向量指令高效实现。产品支持情况按官方文档标注Ascend 950PR/Ascend 950DT支持Atlas A3 训练系列产品/Atlas A3 推理系列产品支持Atlas A2 训练系列产品/Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品支持但数据类型仅支持 FLOAT16、FLOAT32不支持 BFLOAT16二、两个接口的关系非原地 vs 原地两个接口实现相同的功能区别仅在于输出张量的存储位置可根据实际场景选择aclnnHardswish需新建一个输出张量对象存储计算结果适用于输出还需要保留输入原值、或输入输出类型/布局不同的场景aclnnInplaceHardswish无需新建输出张量对象直接在输入张量的内存中存储计算结果节省一次输出内存分配适用于激活层就地更新、输入值后续不再使用的场景。从源码看这种“原地 输入即输出”的约定在实现中直接体现aclnn_hardswish.cpp 中aclnnInplaceHardswishGetWorkspaceSize的实现就是将self强制转换为out后转调非原地版本aclnnStatus aclnnInplaceHardswishGetWorkspaceSize(const aclTensor* self, uint64_t* workspaceSize, aclOpExecutor** executor) { auto out const_castaclTensor*(self); return aclnnHardswishGetWorkspaceSize(self, out, workspaceSize, executor); }这说明两个接口共享同一套参数校验与计算流程构造逻辑原地版本仅在接口签名上少传一个out参数。三、两段式接口与函数原型每个算子分为两段式接口必须先调用aclnnHardswishGetWorkspaceSize或aclnnInplaceHardswishGetWorkspaceSize获取计算所需 workspace 大小以及包含算子计算流程的执行器aclOpExecutor*再调用aclnnHardswish或aclnnInplaceHardswish执行计算。四个函数原型如下aclnnStatus aclnnHardswishGetWorkspaceSize( const aclTensor* self, const aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnHardswish( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)aclnnStatus aclnnInplaceHardswishGetWorkspaceSize( const aclTensor* self, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnInplaceHardswish( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)接口声明及完整的 Doxygen 注释可参见头文件 aclnn_hardswish.h。第一段接口aclnnHardswishGetWorkspaceSize 参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入表示激活函数的输入张量公式中的输入 xself 的数据类型需要可转换为 out 的数据类型参见互转换关系BFLOAT16、FLOAT16、FLOAT32ND0-8√outaclTensor*输出表示激活函数的输出张量公式中的输出 Hardswishout 的数据类型需要可转换为 self 的数据类型参见互转换关系BFLOAT16、FLOAT16、FLOAT32ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----返回值aclnnStatus具体参见 aclnn返回码。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self 或 out 是空指针ACLNN_ERR_PARAM_INVALID161002self、out 的数据类型和数据格式不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002self 的 shape 与输出 out 的 shape 不一致ACLNN_ERR_PARAM_INVALID161002self 与 out 的数据类型不满足互转换规则这些校验并非文档孤证可在源码 CheckParams 中逐条对应CheckNotNull报ACLNN_ERR_PARAM_NULLPTRCheckDtypeValid与CheckShape报ACLNN_ERR_PARAM_INVALID。其中数据类型校验会根据 SOC 版本动态选择支持列表910B~910E 及以上平台支持 FP32/FP16/BF16更早的 910 平台仅支持 FP32/FP16与文档中“Atlas 训练系列产品仅支持 FLOAT16、FLOAT32”的标注一致。第二段接口aclnnHardswish 参数说明参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口aclnnHardswishGetWorkspaceSize获取executor输入op 执行器包含算子计算流程stream输入指定执行任务的 Stream返回值aclnnStatus具体参见 aclnn返回码。第二段接口内部通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)驱动第一段构造好的计算流程执行见 aclnn_hardswish.cpp。第一段接口aclnnInplaceHardswishGetWorkspaceSize 参数说明参数名输入/输出描述数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入表示激活函数的输入/输出张量公式中的 x 和输出结果BFLOAT16、FLOAT16、FLOAT32ND0-8√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程----返回值aclnnStatus。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self 是空指针ACLNN_ERR_PARAM_INVALID161002self 的数据类型和数据格式不在支持的范围之内由于原地版本内部复用非原地版本的校验逻辑前文已述其校验行为与非原地版本一致只是不再有 out 相关的 shape 与互转换检查。第二段接口aclnnInplaceHardswish 参数说明参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口aclnnInplaceHardswishGetWorkspaceSize获取executor输入op 执行器包含算子计算流程stream输入指定执行任务的 Stream返回值aclnnStatus具体参见 aclnn返回码。四、源码视角计算流程的构造与执行4.1 基本计算路径Contiguous → HardSwish → ViewCopy头文件注释中标注了 api 计算的基本路径self → L0::Contiguous → L0::HardSwish → L0::ViewCopy → out。在 aclnnHardswishGetWorkspaceSize 实现 中可看到这一路径的完整落地参数校验CheckParams完成空指针、dtype、shape 三重检查空 tensor 支持self-IsEmpty()时直接返回workspaceSize 0并成功无需执行任何计算。这一点也有对应的单元测试l2_hardswish_test_004shape 为 {2, 0} 时返回ACLNN_SUCCESS见 test_hardswish.cppl0op::Contiguous将可能非连续的输入self转换为连续 tensor这正是参数表中“非连续Tensor √”的能力来源条件 Cast在 910 平台非 910B~910E 区间若输入为 BF16会先Cast到 FP16 再计算DTYPE_CAST_LIST与CheckSocVersionGe910B的判断逻辑l0op::HardSwish调用算子本体执行逐元素激活计算l0op::Castl0op::ViewCopy将计算结果转换到out的数据类型并拷贝回outout可能是非连续 tensor由 ViewCopy 处理 strided 写回。整个流程在OpExecutor中编排workspace 大小由uniqueExecutor-GetWorkspaceSize()汇总得出最终通过ReleaseTo(executor)移交调用方。4.2 形状推导输出 shape 输入 shape图侧形状推理由 hard_swish_infershape.cpp 实现逐元素单目运算*outputShape *inputShape即输出 shape 严格等于输入 shape。这也解释了第一段接口中OP_CHECK_SHAPE_NOT_EQUAL(self, out)强制两者 shape 一致的原因。4.3 Kernel 层双缓冲流水线与 -inf 边界修复以 arch35Ascend950实现为例kernel 入口 按 tiling key 分派三条模板路径FP32 走HardSwishfloatFP16 走HardSwishFp16提升至 FP32 计算后转回BF16 走HardSwishBf16同样提升计算。核心计算类定义在 arch35/hard_swish.h几个值得注意的工程细节双缓冲流水线BUFFER_NUM 2的TQue队列VECIN 输入队列、中间 tmp 队列、VECOUT 输出队列支撑CopyInGM→UB→ ComputeUB 向量计算→ CopyOutUB→GM三级流水Process中按ubFactor分块循环尾部不足一块时按实际余数处理公式的 clamp 形式kernel 内将分段函数等价改写为y x * clamp(x 3, 0, 6) / 6由Adds → Mins(6) → Maxs(0) → Mul → Muls(1/6)六步向量指令完成天然覆盖 x≥3 输出 x、x≤-3 输出 0 的边界行为-inf 边界修复ISSUE-001源码注释明确指出若直接计算x * clamp(x3, 0, 6)当 x -inf 时得到-inf * 0 NaN。修复方式是在乘法前对 x 额外执行Maxs(xLocal, xLocal, -3.0f)将 x clamp 到 ≥ -3由于 x ≤ -3 时 clamp 项为 0max(x, -3) * 0 0仍为正确结果同时消除了 NaN 风险低精度类型提升FP16/BF16 路径先在 UB 内Cast到 FP32 计算最后以CAST_ROUND舍入转回原类型保证中间运算精度。切分参数由 HardSwishTilingData 描述totalNum为张量展平后总元素数blockFactor为多核切分每 AI Core 处理量kernel 内通过GetBlockIdx()定位本核负责的数据段ubFactor为单核内每轮 UB 循环处理的元素数。4.4 参数校验行为的测试佐证单元测试 test_hardswish.cpp 对文档所列报错场景逐条做了验证l2_hardswish_test_001/002self 为 INT64/INT32 时aclnnHardswishGetWorkspaceSize返回ACLNN_ERR_PARAM_INVALID对应“数据类型不在支持范围”l2_hardswish_test_003self shape {2,3,5} 与 out shape {2,3} 不一致时返回ACLNN_ERR_PARAM_INVALIDl2_hardswish_test_004空 tensor{2, 0}返回ACLNN_SUCCESS另有 非连续 tensor 测试 验证 strided 输入路径。五、约束说明确定性计算aclnnHardswish与aclnnInplaceHardswish默认为确定性实现。逐元素运算本身无随机性kernel 采用固定分块循环同输入必得同输出可用于可复现的推理流水线。六、完整调用示例以下示例代码完整继承自算子文档覆盖aclnnHardswish与aclnnInplaceHardswish的完整调用流程。具体编译和执行过程请参考 编译与运行样例仓库中也提供了对应的可运行样例 test_aclnn_hard_swish.cpp。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_hardswish.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 0, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2. 构造输入与输出需要根据API的接口自定义构造 std::vectorint64_t selfShape {4, 2}; std::vectorint64_t outShape {4, 2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {-4, -3, -2, 0, 1, 2, 4, 5}; std::vectorfloat outHostData {0, 0, 0, 0, 0, 0, 0, 0}; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); 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); // aclnnHardswish接口调用示例 // 3. 调用CANN算子库API // 调用aclnnHardswish第一段接口 uint64_t workspaceSize 0; aclOpExecutor* executor; ret aclnnHardswishGetWorkspaceSize(self, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnHardswishGetWorkspaceSize 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;); } // 调用aclnnHardswish第二段接口 ret aclnnHardswish(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnHardswish 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(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 size; i) { LOG_PRINT(result[%ld] is: %f\n, i, resultData[i]); } // aclnnInplaceHardswish接口调用示例 // 3. 调用CANN算子库API LOG_PRINT(\ntest aclnnInplaceHardswish\n); // 调用aclnnInplaceHardswish第一段接口 uint64_t inplaceWorkspaceSize 0; aclOpExecutor* inplaceExecutor; ret aclnnInplaceHardswishGetWorkspaceSize(self, inplaceWorkspaceSize, inplaceExecutor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceHardswishGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* inplaceWorkspaceAddr nullptr; if (inplaceWorkspaceSize 0) { ret aclrtMalloc(inplaceWorkspaceAddr, inplaceWorkspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(allocate workspace failed. ERROR: %d\n, ret); return ret;); } // 调用aclnnInplaceHardswish第二段接口 ret aclnnInplaceHardswish(inplaceWorkspaceAddr, inplaceWorkspaceSize, inplaceExecutor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceHardswish 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的接口定义修改 size GetShapeSize(outShape); ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), selfDeviceAddr, size * 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 size; i) { LOG_PRINT(result[%ld] is: %f\n, i, resultData[i]); } // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device资源需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } if (inplaceWorkspaceSize 0) { aclrtFree(inplaceWorkspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }几点示例解读输入数据{4, 2}的 8 个值{-4, -3, -2, 0, 1, 2, 4, 5}恰好覆盖三个分段-4/-3 落入x ≤ -3段输出 04/5 落入x ≥ 3段输出自身中间值按x(x3)/6计算第二段接口调用前必须根据第一段返回的workspaceSize申请 workspace且仅在workspaceSize 0时才分配inplace 版本执行后结果直接写回selfDeviceAddr因此第 5 步回拷的源地址是selfDeviceAddr而非outDeviceAddr资源释放需覆盖tensor 对象、两块数据内存、两块 workspace 内存、stream最后aclFinalize收尾。七、延伸阅读接口细节与本文对应文档aclnnHardswishaclnnInplaceHardswish.md两段式接口的通用说明two_phase_api.md数据类型互转换规则conversion_relationship.md返回码定义aclnn_return_code.md样例编译与运行compile_and_run_sample.md算子仓库目录说明本目录仅含 HardSwish 的 aclnn 接口AscendC 实现贡献请参考仓库贡献流程README.md【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表