ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CANN ops-math 算子剖析:aclnnCumprod 与 aclnnInplaceCumprod 累积乘积接口全解析

2026/9/20 14:12:57 拓冰建站 浏览量
CANN ops-math 算子剖析:aclnnCumprod 与 aclnnInplaceCumprod 累积乘积接口全解析 算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载本文基于 CANN 数学基础算子库 ops-math 中math/cumprod目录的技术文档与源码完整讲解 Cumprod 累积乘积算子的 aclnn 接口设计、两段式调用流程、参数约束、错误码语义与源码级实现原理。读完本文你将掌握如何在 NPU 上通过 aclnnCumprod / aclnnInplaceCumprod 接口完成沿指定维度的累积乘积计算并能独立阅读、移植和验证该算子对应的接口实现。一、算子与接口背景math/cumprod目录在 ops-math 仓库中的定位是仅承载 Cumprod 算子对应的 aclnnAscend CANN 对外 L2 级算子接口接口不包含该算子的 AscendC 内核kernel实现如需贡献 AscendC 实现请参考 贡献流程。这一点从目录结构可以得到印证op_api/aclnn_cumprod.h、op_api/aclnn_cumprod.cpp对外两段式接口声明与实现op_api/cumprod.h、op_api/cumprod.cppL0 层算子封装l0op::Cumprod负责把计算请求投递给底层 AICore/AICPU 任务framework/cumprod_tf_plugin.cppTensorFlow 框架侧的算子注册插件examples/test_aclnn_cumprod.cpp可直接运行的接口调用样例tests/ut/op_api/test_aclnn_cumprod.cpp接口层单元测试docs/aclnnCumprodaclnnInplaceCumprod.md本文所依据的官方接口说明文档。从框架侧看cumprod_tf_plugin.cpp 通过REGISTER_CUSTOM_OP(Cumprod)注册了 TensorFlow 原生算子Cumprod并采用AutoMappingByOpFn自动参数映射、ImplyType::TVM表示实现方式这说明该接口面向训练/推理场景的 TF 图下沉路径头文件中的 domain 标注为aclnn_ops_train也与之呼应。二、功能说明与计算公式cumprod函数用于计算输入张量在指定维度上的累积乘积cumulative product对输入中从起始位置到当前位置的元素做连乘输出与输入形状完全相同的张量。一维张量向量情况设输入向量x [x1, x2, x3, ..., xn]输出y [y1, y2, y3, ..., yn]满足y1 x1 y2 x1 × x2 y3 x1 × x2 × x3 ... yn x1 × x2 × x3 × ... × xn即数学表达式yi Π(xj, j1..i)其中i 1, 2, ..., n。高维张量情况以二维张量为例对于输入矩阵X [[x11, x12, ..., x1m], [x21, x22, ..., x2m], ... [xn1, xn2, ..., xnm]]当dim 0沿行方向即沿第一维结果Y中第j列的累积乘积为yij Π(xkj, k1..i)。直观理解是每个输出元素是同列、从第一行到当前行所有元素的连乘。当dim 1沿列方向即沿第二维结果满足yij Π(xik, k1..j)即每个输出元素是同行、从第一列到当前列所有元素的连乘。更高维张量可按照同样的规则沿指定dim逐维递推输出张量的 shape 始终与输入一致。这与 torch 的cumprod、TF 的tf.math.cumprod语义一致。三、产品支持情况依据 接口文档该接口在当前版本下的产品支持矩阵如下产品系列支持情况Ascend 950PR / Ascend 950DT不支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品310P不支持Atlas 训练系列产品910不支持提示以上为当前仓库文档声明的支持情况实际可用性请以你所部署的 CANN 版本配套文档为准。四、接口设计与两段式调用流程aclnnCumprod与aclnnInplaceCumprod实现相同的数学功能区别只在于结果落盘方式aclnnCumprod需要调用方新建一个输出张量对象out来存储计算结果输入不被修改aclnnInplaceCumprod无需新建输出张量直接在输入张量的内存中原地写入计算结果可省去一份输出内存。两个接口均遵循 CANN 统一的两段式接口范式必须先调用第一段aclnnCumprodGetWorkspaceSize或aclnnInplaceCumprodGetWorkspaceSize完成入参校验、构建算子执行图并计算出所需 workspace 大小再调用第二段aclnnCumprod或aclnnInplaceCumprod真正下发计算任务。第一段返回的 executor 是包含了算子计算流程的执行器第二段通过 workspace executor 在指定 stream 上异步执行。函数原型// 第一段计算 workspace 大小并生成执行器非原地版本 aclnnStatus aclnnCumprodGetWorkspaceSize( const aclTensor* input, const aclScalar* dim, const aclDataType dtype, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)// 第二段执行计算非原地版本 aclnnStatus aclnnCumprod( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)// 第一段原地版本 aclnnStatus aclnnInplaceCumprodGetWorkspaceSize( aclTensor* input, const aclScalar* dim, uint64_t* workspaceSize, aclOpExecutor** executor)// 第二段执行计算原地版本 aclnnStatus aclnnInplaceCumprod( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)四个接口的声明见 aclnn_cumprod.h注意头文件注释中标注接口会返回一个新的张量其形状与输入张量相同且原地版本会原地更新输入参数。五、第一段接口参数详解aclnnCumprodGetWorkspaceSize 参数表参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorinputaclTensor*输入需要计算累积乘积的数据支持空 TensorFLOAT、FLOAT16、BFLOAT16、DOUBLE、INT8、INT16、INT32、INT64、UINT8、UINT16、UINT32、UINT64ND不超过 8 维√dimaclScalar*输入指定计算累积乘积的维度二维张量 dim0 沿行方向、dim1 沿列方向取值范围 [-rank(input), rank(input))INT32---dtypeaclDataType输入指定计算过程 input 的数据类型若为 ACL_DT_UNDEFINED 使用 input 原始类型计算若指定具体类型需在支持范围内计算前将 input 转换为此类型----outaclTensor*输出累积乘积的结果dtypeACL_DT_UNDEFINED 时数据类型必须与 input 相同dtype 指定时数据类型必须与 dtype 相同out 的 shape 必须与 input 一致同 input 支持列表ND-√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----aclnnInplaceCumprodGetWorkspaceSize 参数表参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorinputaclTensor*输入/输出需要计算累积乘积的数据和结果不支持空 Tensor同上述支持列表ND不超过 8 维√dimaclScalar*输入指定计算累积乘积的维度取值范围 [-rank(input), rank(input)]INT32---workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含算子计算流程-----需要注意两处细微差异dim 取值范围非原地版本为[-rank(input), rank(input))不包含右端点原地版本文档标注为[-rank(input), rank(input)]。从实现看CheckShape 统一按inputAxis inputDim - 1 || inputAxis -inputDim判定越界即实际合法区间为[-rank, rank-1]同时兼容了 0 维标量张量此时仅允许 dim 为 0 或 -1。建议业务侧统一使用[-rank, rank-1]范围内的值。空 Tensor 支持非原地版本 input 支持空 Tensor此时第一段直接返回workspaceSize 0的空执行器见源码 aclnn_cumprod.cpp原地版本不支持空 Tensor。返回值与错误码两个第一段接口均返回aclnnStatus状态码完整语义参见 aclnn 返回码。第一段接口完成入参校验以下场景会报错返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 input、dim 是空指针ACLNN_ERR_PARAM_INVALID161002传入的 input、dim 的数据类型和格式不在支持范围内 / 传入的 dim 与 input 的 shape 约束不满足要求 / out 与 input 的 shape 不一致仅非原地版本错误码路径在源码中有完整对应CheckNotNull 对input、dim、workspaceSize做空指针检查并返回ACLNN_ERR_PARAM_NULLPTRCheckDtypeValid 与 CheckShape 负责数据类型、shape 约束校验并返回ACLNN_ERR_PARAM_INVALID。单元测试 test_aclnn_cumprod.cpp 中l2_cumprod_abnormal_self_nullptr用例正是用nullptr输入验证第一段返回ACLNN_ERR_PARAM_NULLPTR。六、第二段接口参数详解aclnnCumprod与aclnnInplaceCumprod的第二段参数完全一致参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream实现上两个第二段接口最终都调用公共执行入口CommonOpExecutorRun见 aclnn_cumprod.cpp同时通过L2_DFX_PHASE_1/2宏记录接口级 DFX诊断信息。七、约束说明确定性计算aclnnCumprod与aclnnInplaceCumprod默认即确定性实现即相同输入多次执行结果可复现。关于确定性计算的更完整约定可参考 确定性计算说明。八、源码级实现原理1. 支持的数据类型aclnn_cumprod.cpp 中定义了输入/输出校验清单DTYPE_SUPPORT_LISTstatic const std::initializer_listDataType DTYPE_SUPPORT_LIST { DataType::DT_FLOAT, DataType::DT_FLOAT16, DataType::DT_BF16, DataType::DT_DOUBLE, DataType::DT_INT8, DataType::DT_INT16, DataType::DT_INT32, DataType::DT_INT64, DataType::DT_UINT8, DataType::DT_UINT16, DataType::DT_UINT32, DataType::DT_UINT64};覆盖浮点FLOAT/FLOAT16/BF16/DOUBLE与有符号/无符号整型共 12 种类型dim的aclScalar仅支持 INT32DTYPE_SUPPORT_LIST_INT。需要注意aclnn_cumprod.h头文件注释中列出的类型还包含DT_COMPLEX128/DT_COMPLEX64但当前实现的实际校验清单并不包含复数类型使用时请以实现即文档参数表为准。2. dtype 参数的处理逻辑dtype 处理 的核心逻辑为auto tensorType op::ToOpDataType(dtype); if (tensorType DataType::DT_UNDEFINED) { tensorType out-GetDataType(); } OP_CHECK_DTYPE_NOT_MATCH(out, tensorType, return false);当调用方传入ACL_DT_UNDEFINED时以输出张量out的数据类型作为计算类型当显式指定具体类型时要求out的数据类型与之严格一致若input的实际类型与计算类型不同则会在图中插入一次l0op::Cast完成类型转换见下文。3. 执行图组装Contiguous → 0 维适配 → Cast → Cumprod → ViewCopy第一段接口在完成校验后会构建一条最小执行图aclnn_cumprod.cppauto inputContinuous l0op::Contiguous(input, uniqueExecutor.get()); ... auto inputReshape AdaptInputZeroDimTensor(inputContinuous, dimNum, uniqueExecutor.get()); ... if (input-GetDataType() ! tensorType) { inputReshape l0op::Cast(inputReshape, tensorType, uniqueExecutor.get()); } auto cumprodResult l0op::Cumprod(inputReshape, inputDim, false, false, uniqueExecutor.get()); auto result l0op::ViewCopy(cumprodResult, out, uniqueExecutor.get());各环节作用l0op::Contiguous非连续输入先转连续配合文档参数表中非连续 Tensor √的支持说明AdaptInputZeroDimTensor0 维标量张量会被 reshape 为 shape[1]再参与计算l0op::Cast仅当 input 类型与目标计算类型不一致时插入实现 dtype 参数描述的计算前转换l0op::Cumprod核心累积乘积算子exclusivefalse, reversefalse表示标准的包含当前位置、从左到右累积语义l0op::ViewCopy把计算结果拷贝回out从而兼容 out 为非连续 Tensor 的场景。最终通过uniqueExecutor-GetWorkspaceSize()汇总整条图所需的 workspace 大小并释放给调用方。4. L0 层 Cumprod 算子封装op_api/cumprod.cpp 中l0op::Cumprod以OP_TYPE_REGISTER(Cumprod)注册算子类型并通过internal::AicpuTaskSpace(Cumprod, ge::DEPEND_IN_SHAPE, true)声明一个依赖输入 shape的 AICPU 任务空间随后调用ADD_TO_LAUNCHER_LIST_AICPU将exclusive、reverse、Tidx三个属性与输入(x, axis)、输出out一起下发到 AICPU 执行。这也解释了为什么本目录不提供 AscendC kernel 实现——累积乘积的串行依赖特性由 AICPU 侧的内置实现承载L2 接口层只需完成参数校验、类型适配与任务下发。5. 原地版本的特殊处理aclnnInplaceCumprodGetWorkspaceSize的实现aclnn_cumprod.cpp非常简洁return doGetWorkspaceSize(input, dim, ToAclDataType(input-GetDataType()), input, workspaceSize, executor);它直接把input同时作为out传入内部函数dtype取 input 的原始类型因此整个执行图的计算结果会通过ViewCopy写回 input 自身内存实现原地更新输入的语义且天然保证输入输出数据类型一致、shape 一致。九、完整调用示例以下示例摘自 examples/test_aclnn_cumprod.cpp与接口文档附录示例一致展示了在 NPU 上完整跑通aclnnCumprod与aclnnInplaceCumprod的标准流程。编译与运行的整体流程可参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_cumprod.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; } void PrintOutResult(std::vectorint64_t shape, void** deviceAddr) { auto size GetShapeSize(shape); std::vectorint64_t resultData(size, 0); auto ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), *deviceAddr, 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); for (int64_t i 0; i size; i) { LOG_PRINT(result[%ld] is: %ld\n, i, resultData[i]); } } template typename T void PrintOutFloatResult(std::vectorT shape, void** deviceAddr, const char* name) { std::vectorfloat resultData(shape.size(), 0); auto ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), *deviceAddr, shape.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); for (int64_t i 0; i shape.size(); i) { LOG_PRINT(result var %s[%ld] is: %f\n, name, i, resultData[i]); } } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法acl初始化 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; } template typename T int CreateAclScalar(aclDataType dataType, T hostData, aclScalar** scalar) { *scalar aclCreateScalar(hostData, dataType); if (*scalar nullptr) { return -1; } return 0; } int main() { // 1.(固定写法)device/stream初始化, 参考acl对外接口列表, 根据自己的实际device填写deviceId int32_t deviceId 0; aclrtStream stream; auto ret Init(deviceId, stream); CHECK_RET(ret 0, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2.构造输入与输出需要根据API的接口自定义构造 void* xDeviceAddr nullptr; aclTensor* input nullptr; std::vectorint64_t xShape {3}; std::vectorint64_t xHostData {1, 2, 3}; // 创建原始输入x ret CreateAclTensor(xHostData, xShape, xDeviceAddr, aclDataType::ACL_INT64, input); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建axis aclScalar int32_t axis_value 0; aclScalar* axis nullptr; ret CreateAclScalar(aclDataType::ACL_INT32, axis_value, axis); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建result aclTensor std::vectorint64_t resultHostData(3, 0); std::vectorint64_t resultShape {3}; void* resultDeviceAddr nullptr; aclTensor* result nullptr; ret CreateAclTensor(resultHostData, resultShape, resultDeviceAddr, aclDataType::ACL_INT64, result); CHECK_RET(ret ACL_SUCCESS, return ret); uint64_t workspaceSize 0; aclOpExecutor* executor; aclDataType dtype ACL_INT64; void* workspaceAddr nullptr; // 3.调用CANN算子库API // 调用aclnnCumprod第一段接口 ret aclnnCumprodGetWorkspaceSize(input, axis, dtype, result, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCumprodGetWorkspaceSize 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(aclnnCumprod allocate workspace failed. ERROR: %d\n, ret); return ret); } // 调用aclnnCumprod第二段接口 ret aclnnCumprod(workspaceAddr, workspaceSize, executor, stream); // 4.(固定写法)同步等待任务执行结束 ret aclrtSynchronizeStream(stream); // 5.获取输出的值将device侧内存上的结果拷贝至host侧需要根据具体API的接口定义修改 PrintOutResult(resultShape, resultDeviceAddr); // 3.调用CANN算子库API // 调用aclnnInplaceCumprod第一段接口 ret aclnnInplaceCumprodGetWorkspaceSize(input, axis, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCumprodGetWorkspaceSize 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(aclnnInplaceCumprod allocate workspace failed. ERROR: %d\n, ret); return ret); } // 调用aclnnInplaceCumprod第二段接口 ret aclnnInplaceCumprod(workspaceAddr, workspaceSize, executor, stream); // 4.(固定写法)同步等待任务执行结束 ret aclrtSynchronizeStream(stream); // 5.获取输出的值将device侧内存上的结果拷贝至host侧需要根据具体API的接口定义修改 PrintOutResult(resultShape, xDeviceAddr); // 6.释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(input); aclDestroyScalar(axis); // 7.释放device资源需要根据具体API的接口定义修改 aclrtFree(xDeviceAddr); aclrtFree(resultDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例要点拆解初始化固定写法aclInit→aclrtSetDevice→aclrtCreateStream按实际环境修改deviceId构造输入输出CreateAclTensor内部完成 host→device 数据拷贝、连续 strides 计算和aclCreateTensorCreateAclScalar用aclCreateScalar构造dimINT32 标量。本示例输入为 shape{3}、数据{1, 2, 3}的 INT64 张量dim 0非原地流程先aclnnCumprodGetWorkspaceSize拿 workspace 大小并申请 device 内存再aclnnCumprod执行aclrtSynchronizeStream同步后从resultDeviceAddr拷回结果应得到[1, 2, 6]原地流程同样先aclnnInplaceCumprodGetWorkspaceSize再aclnnInplaceCumprod同步后直接从xDeviceAddrinput 自身内存读取结果此时input已被改写为[1, 2, 6]资源释放aclDestroyTensor/aclDestroyScalar销毁 tensor 与 scalaraclrtFree释放 device 内存含 workspace最后aclrtDestroyStream、aclrtResetDevice、aclFinalize收尾。十、小结Cumprod 累积乘积算子在 NPU 上通过math/cumprod目录提供的 aclnn 两段式接口对外暴露核心要点可归纳为语义沿dim维度做包含当前位置的从左到右累积连乘输出 shape 与输入一致支持浮点与整型共 12 种数据类型两种形态aclnnCumprod写新输出、aclnnInplaceCumprod原地覆写输入按内存预算选择调用范式严格遵循GetWorkspaceSize校验构图算 workspace→ 申请 workspace → 执行接口 → 同步 stream的两段式流程错误码 161001/161002 对应空指针与参数非法实现路径L2 接口层完成 Contiguous、0 维适配、Cast、Cumprod、ViewCopy 的图组装后由 AICPU 任务空间承载核心计算属确定性实现。需要深入阅读时可继续查阅接口文档、接口实现、L0 封装与单元测试关于两段式接口的通用规范与 aclnn 返回码约定可参见两段式接口与 aclnn 返回码。赞分享算子库人工智能CANN【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址https://gitcode.com/cann/ops-math点击查看免费下载相关推荐从ASCII网格到RL时间步ai-safety-gridworlds的pycolab引擎与环境API完全参考从ASCII网格到RL时间步ai safety gridworlds的pycolab引擎与环境API完全参考 ai safety gridworlds 是一套算子库人工智能CANNCANN ops-math 算子接口实战aclnnLinalgCross 向量叉乘Cross算子详解CANN ops math 算子接口实战aclnnLinalgCross 向量叉乘Cross算子详解 本篇文章以 CANN ops math 开源仓库中算子库人工智能CANNCANN ops-math 算子剖析aclnnCdistBackward 两段式接口与 CdistGrad 反向算子完整解读CANN ops math 算子剖析aclnnCdistBackward 两段式接口与 CdistGrad 反向算子完整解读 本文以 CANN 数学算子库 o算子库人工智能CANN上一篇【亲测免费】 OBS-RTSPServer 项目推荐下一篇掌握EdgeDB持续集成从自动化构建到部署的完整指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考