
CANN ops-math aclnnGer 算子开发指南两段式接口、参数约束与源码级实现解析【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-mathaclnnGer 是 CANN ops-math 数学算子库中用于计算两个一维向量**外积Outer Product**的 Level 2 接口输出矩阵满足out self^T * vec2。本文以 math/ger/docs/aclnnGer.md 为核心完整讲解其产品支持情况、函数原型、参数语义与错误码并结合 aclnn_ger.cpp 等源码剖析两段式接口内部的数据类型推导、计算路径拆分与 shape 推导过程。读完本文你将能够在 Ascend NPU 上正确调用 aclnnGer 完成外积计算并理解其底层实现机制与适用限制。一、产品支持情况根据 aclnnGer.md 中的产品支持矩阵aclnnGer 在不同硬件产品上的支持情况如下产品支持情况Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I/500 A2 推理产品不支持Atlas 推理系列产品不支持Atlas 训练系列产品支持从源码看产品差异主要体现在数据类型支持范围上在 aclnn_ger.cpp 中Ascend 950IsRegBase()分支的数据类型支持列表ASCEND950_DTYPE_SUPPORT_LIST额外包含DT_BF16而其他版本走DTYPE_SUPPORT_LIST不含 BF16。也就是说BFLOAT16 仅在 Ascend 950 系列上支持这一结论与文档中Atlas A3/A2/Atlas 训练系列产品不支持 BFLOAT16的约束说明完全对应。二、功能说明与计算公式接口功能实现self和vec2两个向量的外积。计算公式为$$ out self^T \times vec2 $$其中self是形状为[N]的一维向量vec2是形状为[M]的一维向量输出out是形状为[N, M]的二维矩阵其每个元素满足$$ out[i][j] self[i] \times vec2[j] $$例如self {1, 2.1}、vec2 {1, 2, 3}时输出矩阵为[1*1, 1*2, 1*3 ] [2.1*1, 2.1*2, 2.1*3]这一计算形态在数学上等价于向量乘法self列向量与vec2行向量的矩阵乘积是 BLAS 中gergeneral rank-1 update操作的简化形式。三、两段式接口与函数原型aclnnGer 遵循 CANN 算子库的两段式接口规范必须先调用第一段接口aclnnGerGetWorkspaceSize获取计算所需的 workspace 大小以及包含算子计算流程的执行器executor再调用第二段接口aclnnGer执行计算。第一段接口原型aclnnStatus aclnnGerGetWorkspaceSize( const aclTensor* self, const aclTensor* vec2, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)第二段接口原型aclnnStatus aclnnGer( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)两个接口的声明位于 aclnn_ger.h均以extern C导出头文件aclnnop/aclnn_ger.h会随 CANN 安装包提供给用户侧编译使用。四、aclnnGerGetWorkspaceSize 参数说明4.1 参数语义参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续TensorselfaclTensor*输入公式中的输入 self外积计算的第一个向量self 与 vec2 的数据类型满足数据类型推导规则FLOAT、FLOAT16、DOUBLE、BFLOAT16、INT8、INT16、INT32、INT64、UINT8、BOOL、COMPLEX64、COMPLEX128ND1√vec2aclTensor*输入公式中的输入 vec2外积计算的第二个向量self 与 vec2 的数据类型满足数据类型推导规则FLOAT、FLOAT16、DOUBLE、BFLOAT16、INT8、INT16、INT32、INT64、UINT8、BOOL、COMPLEX64、COMPLEX128ND1√outaclTensor*输出公式中的输出 out外积计算的结果数据类型是 self 与 vec2 推导之后可转换的数据类型。shape 为{self.shape[0], vec2.shape[0]}FLOAT、FLOAT16、DOUBLE、BFLOAT16、INT8、INT16、INT32、INT64、UINT8、BOOL、COMPLEX64、COMPLEX128ND2√workspaceSizeuint64_t*输出返回需要在 Device 侧申请的 workspace 大小-----executoraclOpExecutor**输出返回 op 执行器包含了算子计算流程-----需要特别说明的是上表给出的宽泛数据类型列表是aclnn 接口层Level 2 API的支持范围接口内部会通过类型提升promotion、强制类型转换cast等手段将输入统一到内核支持的窄范围内具体见第六节的实现剖析。三个 Tensor 均支持非连续存储标注√接口内部会自动做contiguous处理调用方无需手动整理内存布局。输出out的 shape 必须为 2 维且out.shape[0] self.shape[0]、out.shape[1] vec2.shape[0]接口会对此做严格校验见 4.3 错误码表及 CheckShape 的实现。4.2 BFLOAT16 支持限制Ascend 950 系列950PR/950DT支持 BFLOAT16Atlas A3、Atlas A2910b、Atlas 训练系列910不支持 BFLOAT16。这与源码中ASCEND950_DTYPE_SUPPORT_LIST与DTYPE_SUPPORT_LIST的差异aclnn_ger.cpp保持一致属于硬件平台能力限制。4.3 返回值与入参校验错误码第一段接口返回aclnnStatus状态码具体参见aclnn 返回码。第一段接口会完成入参校验出现以下场景时报错返回码错误码描述ACLNN_ERR_PARAM_NULLPTR161001传入的 self、vec2 或 out 是空指针ACLNN_ERR_PARAM_INVALID161002self 和 vec2 的数据类型不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002self 和 vec2 的数据格式不在支持的范围之内ACLNN_ERR_PARAM_INVALID161002推导出的数据类型无法转换为指定输出 out 的类型ACLNN_ERR_PARAM_INVALID161002self 或 vec2 的 shape 不为 1 维ACLNN_ERR_PARAM_INVALID161002out 的 shape 不为 2 维ACLNN_ERR_PARAM_INVALID161002out 在 0 和 1 维度上的 size 大小与 self、vec2 的 size 大小不完全相同从 aclnn_ger.cpp 的CheckParams实现可以看到校验顺序① 空指针检查CheckNotNull→ ② 数据类型范围检查CheckDtypeValid→ ③ 类型推导与可转换性检查CheckPromoteType→ ④ shape 检查CheckShape。其中第③步先调用op::PromoteType计算 self 与 vec2 的隐式公共类型若无法推导返回DT_UNDEFINED或推导结果无法 cast 为 out 的类型都会返回ACLNN_ERR_PARAM_INVALID。五、aclnnGer 参数说明参数名输入/输出描述workspace输入在 Device 侧申请的 workspace 内存地址workspaceSize输入在 Device 侧申请的 workspace 大小由第一段接口 aclnnGerGetWorkspaceSize 获取executor输入op 执行器包含了算子计算流程stream输入指定执行任务的 Stream第二段接口本身不再做参数校验直接通过CommonOpExecutorRun(workspace, workspaceSize, executor, stream)将第一段接口构建好的计算图下发到指定 Stream 上执行见 aclnn_ger.cpp。返回值同样是aclnnStatus具体参见aclnn 返回码。六、约束说明确定性计算aclnnGer 默认采用确定性实现。也就是说在相同输入与相同运行环境下多次调用得到的结果逐元素完全一致不会因并行调度或浮点累加顺序不同而产生随机性差异。七、调用示例以下示例代码来自 aclnnGer.md与仓库中的 examples/test_aclnn_ger.cpp 及 tests/ut/op_api/test_aclnn_ger.cpp 结构一致编译和执行的具体过程请参考编译与运行样例。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_ger.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::vectorfloat 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: %f\n, i, resultData[i]); } } 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); aclFinalize(); return ret); ret aclrtCreateStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtCreateStream failed. ERROR: %d\n, ret); aclrtResetDevice(deviceId); aclFinalize(); 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}; std::vectorint64_t vec2Shape {3}; std::vectorint64_t outShape {2, 3}; void* selfDeviceAddr nullptr; void* vec2DeviceAddr nullptr; void* outDeviceAddr nullptr; aclTensor* self nullptr; aclTensor* vec2 nullptr; aclTensor* out nullptr; std::vectorfloat selfHostData {1, 2.1}; std::vectorfloat vec2HostData {1, 2, 3.0}; std::vectorfloat outHostData {0, 0.1, 0, 0, 0, 0}; // 创建self aclTensor ret CreateAclTensor(selfHostData, selfShape, selfDeviceAddr, aclDataType::ACL_FLOAT, self); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建vec2 aclTensor ret CreateAclTensor(vec2HostData, vec2Shape, vec2DeviceAddr, aclDataType::ACL_FLOAT, vec2); 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名称 uint64_t workspaceSize 0; aclOpExecutor* executor; // 调用aclnnGer第一段接口 ret aclnnGerGetWorkspaceSize(self, vec2, out, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnGerGetWorkspaceSize 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); } // 调用aclnnGer第二段接口 ret aclnnGer(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnGer 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的接口定义修改 PrintOutResult(outShape, outDeviceAddr); // 6. 释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(self); aclDestroyTensor(vec2); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(vec2DeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }7.1 示例要点说明两段式调用是硬性要求aclnnGerGetWorkspaceSize完成图构建与 workspace 大小计算aclnnGer真正下发计算二者缺一不可。workspace 内存按需申请只有当workspaceSize 0时才调用aclrtMalloc申请示例代码与仓库示例test_aclnn_ger.cpp均采用该防御式写法。输出 shape 必须预先指定outShape {selfShape[0], vec2Shape[0]} {2, 3}这是外积输出维度[N, M]的直接体现。非连续 Tensor 支持接口内部会自动处理用户无需在调用前对输入做contiguous化。八、源码级实现剖析两段式接口内部的完整计算流水8.1 第一段接口的图构建过程在 aclnn_ger.cpp 中aclnnGerGetWorkspaceSize完成参数校验后会依次执行以下步骤构建算子执行图空输入短路处理若self-IsEmpty()或vec2-IsEmpty()直接置*workspaceSize 0返回成功避免空张量进入计算流程Contiguous 化分别对self、vec2调用l0op::Contiguous将非连续存储的输入规整为连续内存这正是参数表中非连续 Tensor √的实现支撑类型提升 Cast调用op::PromoteType计算self与vec2的隐式公共类型如 INT8 与 FLOAT 提升为 FLOAT再分别l0op::Cast到该公共类型计算路径分派当提升后的类型属于内核原生支持类型时Ascend 950BF16/FP16/FP32其他平台FP16/FP32走l0op::Ger专用内核路径见 ger.cpp即调用ADD_TO_LAUNCHER_LIST_AICORE(Ger, ...)直接下发 Ger AiCore 算子其余类型如 INT、DOUBLE、BOOL、COMPLEX 等则分解为既有基础算子组合对self先UnsqueezeNd增维为[1, N]再与vec2做l0op::Mul布尔类型走l0op::LogicalAnd实现广播乘法从而在不新增内核的情况下覆盖全部声明支持的数据类型结果回写对计算结果Cast到out的声明类型再通过l0op::ViewCopy写入输出 Tensor汇总 workspace*workspaceSize uniqueExecutor-GetWorkspaceSize()并将执行器所有权ReleaseTo(executor)转交调用方。这一专用内核 基础算子组合兜底的双路径设计是理解第 4 节参数表宽泛数据类型列表的关键——接口层支持的类型范围远大于内核层非内核原生类型由 Mul/LogicalAnd 等基础算子组合实现。8.2 Shape 与数据类型推导shape 推导图模式下由 ger_infershape.cpp 的InferShape4Broadcast完成——校验两个输入均为 1 维后输出第 0 维取self.shape[0]、第 1 维取vec2.shape[0]与文档中out.shape {self.shape[0], vec2.shape[0]}完全一致数据类型推导图模式下由 ger_graph_infer.cpp 的InferDataType4Ger将输出类型直接置为输入类型算子原语定义ger_proto.h 通过REG_OP(Ger)声明两个输入x1、x2均支持DT_FLOAT16 / DT_FLOAT / DT_BF16与一个输出yger_def.cpp 进一步将其注册到 ascend950 的 AiCore 配置DynamicShapeSupportFlag(true)、PrecisionReduceFlag(true)等并指定内核文件为ger_apt。8.3 AiCore 内核实现Ascend 950 上的 Ger 内核位于 ger_apt.cpp基于 AscendC 编程模型实现支持bfloat16 / float16 / float32三种类型。内核通过BroadcastSch调度框架atvoss/broadcast/broadcast_sch.h配合 arch35/ger_dag.h 中定义的算子 DAG 完成向量外积的并行计算这也印证了文档中aclnnGer 默认确定性实现的约束说明——内核采用固定调度模式不引入随机并行分支。8.4 第二段接口的执行aclnnGer内部实现极为精简仅调用CommonOpExecutorRun(workspace, workspaceSize, executor, stream)aclnn_ger.cpp由框架统一完成 workspace 绑定与任务下发这也是两段式接口第一段建图、第二段执行设计意图的体现。九、测试验证仓库为 aclnnGer 提供了多层测试保障可用于验证上述实现行为UT 测试tests/ut/op_api/test_aclnn_ger.cpp 覆盖接口调用的正确性包括参数校验错误分支与正常计算分支ST 测试tests/st/arch35/ttk_kernel_ger_st.csv 提供 arch35Ascend 950 架构上的内核级系统测试用例。如需在本地验证外积结果可参照示例将self {1, 2.1}与vec2 {1, 2, 3}输入期望得到 6 个输出元素1.0, 2.0, 3.0, 2.1, 4.2, 6.3。十、总结aclnnGer 以两段式接口形式提供一维向量外积能力覆盖 Ascend 950、Atlas A2/A3 与 Atlas 训练系列等主流产品BF16 仅限 Ascend 950。接口层通过数据类型推导 Cast 专用 Ger 内核/基础算子组合的灵活实现在保持内核精简的同时支撑了多达 12 种数据类型的声明支持非连续 Tensor、确定性计算等特性则由接口内部框架层透明保障。开发者只需按照两段式接口规范依次调用aclnnGerGetWorkspaceSize与aclnnGer即可在 NPU 上高效完成外积计算。【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考