ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CANN ops-math 算子开发实战:aclnnTanh 与 aclnnInplaceTanh 两段式接口详解

CANN ops-math 算子开发实战:aclnnTanh 与 aclnnInplaceTanh 两段式接口详解 算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载本文以 CANN ops-math 仓库中 Tanh 算子的官方接口文档为核心系统讲解aclnnTanh与aclnnInplaceTanh两个 NPU 侧激活函数算子的功能定义、两段式调用流程、参数约束、错误码语义与完整示例代码并结合仓库源码aclnn_tanh.cpp、tanh.cpp、tanh_def.cpp剖析底层实现路径帮助开发者快速掌握在 CANN 环境中正确调用 Tanh 算子完成 elementwise 双曲正切计算并理解原地inplace与非原地接口的取舍。产品支持情况Tanh 算子在 CANN ops-math 中针对不同昇腾产品线的支持情况如下表所示开发者在跨产品部署时需要先确认目标硬件是否在支持列表内产品是否支持Ascend 950PR/Ascend 950DT支持Atlas A3 训练系列产品/Atlas A3 推理系列产品支持Atlas A2 训练系列产品/Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品支持其中Atlas 训练系列产品对应 tanh_def.cpp 中的ascend950、ascend350AI Core 配置以及 tanh_binary.json 中的算子二进制编排不支持 BFLOAT16 数据类型实际使用中应使用 FLOAT/FLOAT16 替代。功能说明Tanh双曲正切是深度学习网络中最常用的激活函数之一其输出范围收敛于 (-1, 1)具有零中心对称特性常用于隐藏层激活与归一化场景。算子功能激活函数。返回与输入 tensor shape 相同的 tensor对输入 tensor 进行 elementwise逐元素的计算。计算公式$$ tanh(self)\frac{e^{self}-e^{-self}}{e^{self} e^{-self}} $$即对输入张量中的每一个元素self_i独立计算上述公式输出张量out与输入self拥有完全一致的 shape。仓库在 golden.py 中给出了等价的标准实现以 numpy 为准可作为验证 NPU 计算结果的基准def tanh_golden(x, **kwargs): ori_dtype x.dtype if ori_dtype.name in (float16, bfloat16): x_cast x.astype(np.float32) res np.tanh(x_cast) return res.astype(ori_dtype, copyFalse) return np.tanh(x)注意其中对 float16/bfloat16 的处理先在 float32 精度下计算再回落到原精度这与算子底层低精度输入先提升精度计算的实现思路一致详见下文源码剖析。函数原型aclnnTanh 与 aclnnInplaceTanh 的差异aclnnTanh和aclnnInplaceTanh实现相同的计算功能二者的使用区别如下请根据自身实际场景选择合适的算子aclnnTanh需新建一个输出张量对象存储计算结果输入self在计算后保持不变aclnnInplaceTanh无需新建输出张量对象直接在输入张量的内存中存储计算结果可节省一份设备侧内存适合不需要保留原始输入的推理或激活后立即消费结果的场景。两个算子的完整原型定义位于 aclnn_tanh.h均为标准的两段式接口详见 两段式接口必须先调用aclnnTanhGetWorkspaceSize/aclnnInplaceTanhGetWorkspaceSize第一段接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器再调用aclnnTanh/aclnnInplaceTanh第二段接口执行计算。aclnnStatus aclnnTanhGetWorkspaceSize( const aclTensor* self, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnTanh( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)aclnnStatus aclnnInplaceTanhGetWorkspaceSize( aclTensor* selfRef, uint64_t* workspaceSize, aclOpExecutor** executor)aclnnStatus aclnnInplaceTanh( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)需要特别强调的是第二段接口aclnnTanh/aclnnInplaceTanh不能重复调用同一个executor仅对应一次计算流程重复调用会导致异常。如需多次计算必须重新走一遍第一段获取 workspace 与 executor → 第二段执行的完整流程。aclnnTanhGetWorkspaceSize第一段接口参数说明参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入公式中的输入self。shape需要与out一致和out的数据类型满足互推导关系。FLOAT、FLOAT16、BOOL、UINT8、INT8、INT16、INT32、INT64、BFLOAT16ND不大于8√outaclTensor*输出公式中的out。shape需要与self一致和self的数据类型满足互推导关系。FLOAT、FLOAT16、BFLOAT16ND-√workspaceSizeuint64_t*输出返回需要在Device侧申请的workspace大小。-----executoraclOpExecutor**输出返回op执行器包含了算子计算流程。-----Atlas 训练系列产品不支持 BFLOAT16。返回值aclnnStatus返回状态码具体参见 aclnn返回码。第一段接口完成入参校验出现以下场景时报错返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的self或out是空指针。ACLNN_ERR_PARAM_INVALID161002self和out的数据类型和数据格式不在支持的范围之内。self和out的数据类型不满足互推导关系。self和out的维度大于8。self和out的shape不一致。参数细节与底层校验逻辑从源码 aclnn_tanh.cpp 可以看到aclnnTanhGetWorkspaceSize内部按固定顺序完成四步参数校验CheckParams与上表错误码一一对应空指针检查CheckNotNullself或out为 null 时返回ACLNN_ERR_PARAM_NULLPTR161001数据类型检查CheckDtypeValidself与out的类型必须落在当前架构支持列表内且二者满足互推导关系。源码中针对不同架构维护了两套支持列表ASCEND910_DTYPE_SUPPORT_LIST对应 Atlas 训练系列产品FLOAT、FLOAT16、BOOL、UINT8、INT8、INT16、INT32、INT64输出仅 FLOAT、FLOAT16ASCEND910B_DTYPE_SUPPORT_LIST对应 Atlas A2/A3 系列等额外支持 BFLOAT16。值得关注的是DTYPE_CAST_LISTBOOL、UINT8、INT8、INT16、INT32、INT64当输入为整型/布尔类型时底层会先将其转换为 FLOAT32 再参与 tanh 计算这也是为什么self允许整型输入而out只支持浮点类型——整型输入经过 cast 后输出为浮点结果符合公式定义数据格式检查CheckFormat输入输出 ViewFormat 必须一致私有格式直接报错存储格式非 ND 时仅打印 warning 日志资料约定只支持 NDshape 检查CheckShapeself的维度不大于 8MAX_SUPPORT_DIMS_NUMS且self与outshape 一致。校验通过后还有一项重要分支若self或out为空 tensorIsEmpty则直接返回workspaceSize 0无需真正申请 workspace。aclnnTanh第二段接口参数说明参数说明参数名输入/输出描述workspace输入在Device侧申请的workspace内存地址。workspaceSize输入在Device侧申请的workspace大小由第一段接口aclnnTanhGetWorkspaceSize获取。executor输入op执行器包含了算子计算流程。stream输入指定执行任务的Stream。返回值aclnnStatus返回状态码具体参见 aclnn返回码。第二段接口本身不进行参数语义校验而是通过CommonOpExecutorRun将第一段接口构建好的计算流程按executor提交到指定stream上异步执行见 aclnn_tanh.cpp。底层计算流程剖析在 aclnn_tanh.cpp 的ExecTanhGetWorkspaceSize中可以清晰看到 tanh 算子在 L0 层的完整执行流水线与 aclnn_tanh.h 头文件中的 mermaid 注释图一致各步骤职责如下L0::Contiguous将非连续的输入self转换为连续 tensor。这正是上表非连续 Tensor √能力的实现基础——非连续输入如切片、转置视图无需用户手动contiguous()框架层自动处理L0::Cast条件触发若输入为DTYPE_CAST_LIST中的整型/布尔类型先转换为 FLOAT32 进行计算保证数值精度L0::Tanh真正的核心计算。对应 tanh.cpp 中的l0op::Tanh它通过AllocTensor按输入的存储 shape 与数据类型分配中间结果并通过ADD_TO_LAUNCHER_LIST_AICORE将 AICore 算子任务tanh_apt见 tanh_apt.cpp加入执行队列。算子定义侧tanh_def.cpp注册了ascend950与ascend350两个 AI Core 配置支持动态 rank、动态 shape 与精度规约PrecisionReduceFlag(true)L0::Cast输出将 tanh 计算结果转换为out声明时的数据类型保证输出类型与用户预期一致L0::ViewCopy将结果拷贝到outtensor 上即使out是非连续 tensor 也能正确落位。最后*workspaceSize uniqueExecutor-GetWorkspaceSize()汇总整条流水线所需的临时内存大小。此外tanh_def.cpp中通过ExtendCfgInfo(opFile.value, tanh_apt)与 tanh_binary.json 完成算子二进制与输入类型float16/float32/bfloat16shape 用-2表示动态的匹配映射shape 推导则复用 elewise 通用规则InferShape4Elewise见 tanh_infershape.cpp。aclnnInplaceTanhGetWorkspaceSize第一段接口参数说明参数说明参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfRefaclTensor*输入/输出公式中的self。-FLOAT、FLOAT16、BFLOAT16ND不大于8√workspaceSizeuint64_t*输出返回需要在Device侧申请的workspace大小。-----executoraclOpExecutor**输出返回op执行器包含了算子计算流程。-----Atlas 训练系列产品不支持 BFLOAT16。返回值aclnnStatus返回状态码具体参见 aclnn返回码。第一段接口完成入参校验出现以下场景时报错返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的selfRef是空指针。ACLNN_ERR_PARAM_INVALID161002selfRef的数据类型不在支持的范围之内。原地接口的实现要点从源码 aclnn_tanh.cpp 可以看到aclnnInplaceTanhGetWorkspaceSize先通过CheckInplaceParams做轻量校验仅检查selfRef非空及其数据类型是否在输出支持列表FLOAT、FLOAT16、BFLOAT16即GetOutDtypeSupportList随架构切换之内。随后直接执行auto out const_castaclTensor*(selfRef)将selfRef同时作为输入与输出传给ExecTanhGetWorkspaceSize——原地语义由此实现计算结果通过 ViewCopy 直接写回输入 tensor 的内存。这也解释了为何 inplace 接口的selfRef数据类型仅限浮点类型整型/布尔输入需要 cast 到 FLOAT32 计算无法在原有内存中原地完成因此不提供 inplace 支持。aclnnInplaceTanh第二段接口参数说明参数说明参数名输入/输出描述workspace输入在Device侧申请的workspace内存地址。workspaceSize输入在Device侧申请的workspace大小由第一段接口aclnnInplaceTanhGetWorkspaceSize获取。executor输入op执行器包含了算子计算流程。stream输入指定执行任务的Stream。返回值aclnnStatus返回状态码具体参见 aclnn返回码。约束说明确定性计算aclnnTanh与aclnnInplaceTanh默认即确定性实现deterministic即相同输入在同一硬件与软件栈下多次运行得到逐位一致的结果便于调试与结果复现相关概念可参考 确定性计算 与 基础概念。其他约束数据格式仅 ND、维度不大于 8、shape 一致、类型互推导等已在前文各接口的参数表格中列出调用前请逐项核对。调用示例以下示例代码演示如何在 CANN 环境中完整调用aclnnTanh与aclnnInplaceTanh涵盖环境初始化、张量构造、两段式调用、结果回拷与资源释放全流程。该示例与仓库中的 examples/test_aclnn_tanh.cpp 及 测试用例 结构一致仅供参考具体编译和执行过程请参考 编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_tanh.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 {2, 2}; std::vectorint64_t outShape {2, 2}; void* selfDeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {0, 1, 2, 3}; std::vectorfloat outHostData {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); // 3.调用CANN算子库API需要修改为具体的API名称 // aclnnTanh接口调用示例 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnTanh第一段接口 ret aclnnTanhGetWorkspaceSize(self, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnTanhGetWorkspaceSize 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); } // 调用aclnnTanh第二段接口 ret aclnnTanh(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnTanh failed. ERROR: %d\n, ret); return ret); // aclnnInplaceTanh接口调用示例 uint64_t inplaceWorkspaceSize 0; aclOpExecutor* inplaceExecutor; // 调用aclnnInplaceTanh第一段接口 ret aclnnInplaceTanhGetWorkspaceSize(self, inplaceWorkspaceSize, inplaceExecutor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceTanhGetWorkspaceSize 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); } // 调用aclnnInplaceTanh第二段接口 ret aclnnInplaceTanh(inplaceWorkspaceAddr, inplaceWorkspaceSize, inplaceExecutor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnInplaceTanh 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(aclnnTanh result[%ld] is: %f\n, i, resultData[i]); } auto inplaceSize GetShapeSize(selfShape); std::vectorfloat inplaceResultData(inplaceSize, 0); ret aclrtMemcpy(inplaceResultData.data(), inplaceResultData.size() * sizeof(inplaceResultData[0]), selfDeviceAddr, inplaceSize * sizeof(inplaceResultData[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 inplaceSize; i) { LOG_PRINT(aclnnInplaceTanh result[%ld] is: %f\n, i, inplaceResultData[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; }示例代码要点解读流程骨架固定InitaclInit → aclrtSetDevice → aclrtCreateStream与收尾aclrtSynchronizeStream → 资源释放 → aclFinalize是所有单算子 API 调用的固定写法可直接复用张量构造的关键点CreateAclTensor中手动计算连续 tensor 的 strides 后调用aclCreateTensorformat 固定为ACL_FORMAT_ND。示例展示的是连续张量若输入为切片/转置产生的非连续视图仍可直接传入——底层L0::Contiguous会自动处理workspace 按需申请workspaceSize 0时才调用aclrtMalloc申请临时内存避免空转申请释放时也以workspaceSize 0为判据与第一段接口返回值严格对应inplace 输出读取inplace 计算后结果直接写入selfDeviceAddr因此结果回拷读取的是selfDeviceAddr指向的内存示例第 5 步而非单独的 out 地址类型映射示例使用ACL_FLOATFLOAT32。如需验证 FLOAT16/BFLOAT16 或整型输入只需调整CreateAclTensor的dataType与 host 数据向量类型并注意整型输入输出应为浮点类型。小结与推荐阅读aclnnTanh与aclnnInplaceTanh是 CANN 数学算子库中典型的 elementwise 激活算子接口两者共享同一套底层 L0 计算流水线Contiguous → Cast → Tanh → Cast → ViewCopy区别仅在于输出内存的归属——前者写入独立的out张量后者原地覆写selfRef。实际工程中若后续仍需使用原始输入请选择aclnnTanh若内存敏感且原始数据不再需要aclnnInplaceTanh可减少一次设备侧内存分配。围绕本文涉及的概念可以进一步阅读仓库文档深入了解两段式接口workspace 与 executor 的完整语义编译与运行样例示例代码的编译、链接与运行方式aclnn返回码161001/161002 等错误码的完整含义互推导关系self与out数据类型匹配规则非连续Tensor非连续输入的底层处理机制源码入口aclnn_tanh.cpp、tanh.cpp、tanh_def.cpp、tanh_binary.json、golden.py。赞分享算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载相关推荐CANN ops-math 算子开发实战aclnnLog10 与 aclnnInplaceLog10 两段式接口详解CANN ops math 算子开发实战aclnnLog10 与 aclnnInplaceLog10 两段式接口详解 本文是 CANN 数学算子库 ops m算子库人工智能CANNCANN ops-math 算子开发实战aclnnBitwiseAndScalar 两段式接口全解析CANN ops math 算子开发实战aclnnBitwiseAndScalar 两段式接口全解析 本文以 CANN ops math 开源仓库中 acln算子库人工智能CANNCANN ops-math 算子开发指南aclnnRemainderTensorTensor 与 aclnnInplaceRemainderTensorTensor 两段式接口详解CANN ops math 算子开发指南aclnnRemainderTensorTensor 与 aclnnInplaceRemainderTensorTen算子库人工智能CANN创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表