news 2026/9/20 17:11:32

CANN ops-math 中 aclnnComplex 算子详解:用实部与虚部张量构造复数

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CANN ops-math 中 aclnnComplex 算子详解:用实部与虚部张量构造复数
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载

本篇技术指南围绕 CANN 数学算子库 ops-math 中Complex算子的 aclnn 两层接口展开,完整讲解aclnnComplex/aclnnComplexGetWorkspaceSize的函数原型、参数约束、返回码、平台支持情况与调用示例,并结合仓库源码(aclnn_complex.cpp)剖析其入参校验、广播推导与 kernel 分发机制。读完本文,你将掌握如何在 NPU 上通过两个实数 Tensor 构造复数 Tensor,并能独立编写、编译和运行基于两段式接口的 aclnn 调用程序。

一、功能说明:从实部、虚部到复数

aclnnComplex是 CANN ops-math 提供的数学类基础算子接口,用于将"实部 Tensor"与"虚部 Tensor"组合成一个复数 Tensor。接口功能要求:输入两个 Shape 满足 broadcast 关系、Dtype 一致的 Tensor,逐元素生成复数输出。

其计算公式为:

$$ \text{out}[i] = \text{real}[i] + \text{imag}[i]\times \mathrm{j} $$

其中 $\mathrm{j}$ 为虚数单位,real代表复数的实部,imag代表复数的虚部,out为复数类型输出。

该算子的官方功能说明与公式定义见 math/complex/docs/aclnnComplex.md,其 aclnn 接口的对外声明位于 aclnn_complex.h,接口实现位于 aclnn_complex.cpp。

数据类型映射关系

输入(实部/虚部)与输出(复数)的数据类型存在严格的一一对应关系:

输入 Dtype(real / imag)输出 Dtype(out)
FLOATCOMPLEX64
FLOAT16COMPLEX32
DOUBLECOMPLEX128

这一映射关系在源码中有明确体现。aclnn_complex.cpp 中定义了DTYPE_PAIR

static const std::initializer_list<std::pair<op::DataType, op::DataType>> DTYPE_PAIR = { {DataType::DT_FLOAT16, DataType::DT_COMPLEX32}, {DataType::DT_FLOAT, DataType::DT_COMPLEX64}, {DataType::DT_DOUBLE, DataType::DT_COMPLEX128} };

同时,第一段接口在入参校验阶段会强制校验该配对关系(见CheckDtypeValid),若 real 为 FLOAT 而 out 为 COMPLEX128 等不配对组合,将直接返回参数非法错误。

二、产品支持情况

依据官方文档,aclnnComplex在不同硬件平台上的支持情况如下:

产品型号支持情况
Ascend 950PR / Ascend 950DT支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持
Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持
Atlas 200I/500 A2 推理产品不支持
Atlas 推理系列产品支持
Atlas 训练系列产品支持

此外,针对 Atlas A3 训练/推理系列产品与 Ascend 950PR/950DT,官方文档明确 out 的数据类型支持 COMPLEX64(输入只能是 FLOAT)、COMPLEX32(输入只能是 FLOAT16)、COMPLEX128(输入只能是 DOUBLE)。

从源码结构可以进一步印证平台差异:算子的 AICore 配置在 complex_def.cpp 中通过OpAICoreConfig仅注册了ascend950平台,并将DynamicShapeSupportFlagDynamicRankSupportFlag置为 true,表明 950 平台走动态 shape/rank 的 AICore 执行路径;而 complex.cpp 中针对不同 SoC 版本定义了不同的 AICore dtype 支持列表(如 910B 支持 FLOAT/FLOAT16,950 支持 FLOAT/FLOAT16),不满足条件时则回退到 AICPU(tf_kernel)路径。

三、两段式接口与函数原型

与 CANN aclnn 体系一致,aclnnComplex采用两段式接口设计:必须先调用aclnnComplexGetWorkspaceSize获取计算所需的 workspace 大小以及包含算子计算流程的执行器(executor),再调用aclnnComplex执行计算。

第一段接口原型

aclnnStatus aclnnComplexGetWorkspaceSize( const aclTensor* real, const aclTensor* imag, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)

第二段接口原型

aclnnStatus aclnnComplex( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

两段接口的详细声明同样可以在头文件 aclnn_complex.h 中查阅,其中标注了参数类型、所属域(aclnn_math)与基本约束。

四、aclnnComplexGetWorkspaceSize 参数说明

第一段接口负责入参校验、构建算子计算流程并返回 workspace 大小,其参数说明如下:

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
real (aclTensor*)输入公式中的输入 real,代表复数的实部shape 需要与 imag 满足 broadcast 关系FLOAT、FLOAT16、DOUBLEND-
imag (aclTensor*)输入公式中的输入 imag,代表复数的虚部shape 需要与 real 满足 broadcast 关系FLOAT、FLOAT16、DOUBLEND-
out (aclTensor*)输出公式中的 out,复数类型的 Tensorshape 需要是 real 与 imag broadcast 之后的 shapeCOMPLEX64、COMPLEX32、COMPLEX128ND-
workspaceSize (uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor (aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程-----

注意:参数表中"非连续 Tensor"一栏为 √,表示 real、imag、out 均支持非连续 Tensor 输入;数据格式为 ND。

从实现层面看,aclnn_complex.cpp 中第一段接口的执行流程为:

  1. 创建OpExecutorCREATE_EXECUTOR());
  2. 通过CheckNotNull校验 real、imag、out 三个指针非空;
  3. 处理空 Tensor 场景:若 real 或 imag 为空,直接返回workspaceSize = 0并成功退出;
  4. 通过CheckParams完成 dtype、shape/广播、format 校验;
  5. 分别用l0op::Contiguous将 real、imag 转为连续 Tensor;
  6. 调用l0op::Complex构建计算节点;
  7. l0op::ViewCopy将计算结果写入 out(兼容非连续 out);
  8. 通过GetWorkspaceSize汇总返回 workspace 大小。

返回值与入参校验错误码

aclnnStatus返回状态码的具体含义参见 aclnn 返回码。第一段接口完成入参校验,出现以下场景时报错:

返回码错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 real、imag 或 out 是空指针
ACLNN_ERR_PARAM_INVALID161002real 和 imag 的数据类型和数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002real 和 imag 的 shape 无法做 broadcast
ACLNN_ERR_PARAM_INVALID161002real 和 imag 的维度大于 8
ACLNN_ERR_PARAM_INVALID161002real 和 imag 的数据类型不一样

上述校验逻辑在源码中均有对应实现:

  • 空指针检查:CheckNotNull(aclnn_complex.cpp);
  • dtype 支持范围、real/imag dtype 一致性、输入输出 dtype 配对:CheckDtypeValid(aclnn_complex.cpp);
  • 最大维度(8 维)与广播推断、out shape 一致性:CheckOutShape(aclnn_complex.cpp),其中MAX_DIM = 8,通过OP_CHECK_BROADCAST_AND_INFER_SHAPE完成广播 shape 推导;
  • format 校验:CheckFormat(aclnn_complex.cpp),要求 real、imag、out 三者 format 一致且为非私有格式(ND 系列),该检查仅在 Ascend 950 平台上生效(见CheckParamsGetCurrentPlatformInfo().GetSocVersion() == SocVersion::ASCEND950分支)。

五、aclnnComplex 参数说明

第二段接口执行实际计算,参数说明如下:

参数名输入/输出描述
workspace输入在 Device 侧申请的 workspace 内存地址
workspaceSize输入在 Device 侧申请的 workspace 大小,由第一段接口 aclnnComplexGetWorkspaceSize 获取
executor输入op 执行器,包含了算子计算流程
stream输入指定执行任务的 Stream

返回值为aclnnStatus状态码,具体参见 aclnn 返回码。实现上,第二段接口直接调用框架统一的执行入口CommonOpExecutorRun(workspace, workspaceSize, executor, stream)(见 aclnn_complex.cpp),由框架完成计算调度。

六、约束说明

  • 确定性计算aclnnComplex默认采用确定性实现,即相同输入在相同环境下多次执行结果可复现。关于确定性计算的更多背景可参考 确定性计算说明。

七、源码级实现原理

7.1 算子定义与注册

算子通过 complex_def.cpp 完成 OpDef 注册:输入realimag(FLOAT/FLOAT16,ND 格式,必选),输出out(COMPLEX64/COMPLEX32,ND 格式),属性Tout(可选,Int,默认 0)。同时该文件为 AICore 配置了ascend950平台,并开启动态 shape、动态 rank 支持。

7.2 底层 L0 接口与执行路径选择

l0op::Complex(见 complex.cpp)内部首先通过BroadcastInferShape推导广播后的输出 shape,再按输入 dtype 推导输出 dtype(FLOAT16 → COMPLEX32,否则默认 COMPLEX64),最后根据平台与 dtype 选择执行路径:

  • IsAiCoreSupport返回 true 时走ComplexAiCore(AICore kernel,通过ADD_TO_LAUNCHER_LIST_AICORE加入任务队列);
  • 否则走ComplexAiCpu(AICPU tf_kernel,通过ADD_TO_LAUNCHER_LIST_AICPU加入任务队列),但 COMPLEX32 输出在 AICPU 路径不被支持,会直接报错返回。

其中 AICore kernel 入口由 complex_apt.cpp 提供,分派至arch35架构实现(kernel_operator.h+arch35/complex.h)。

7.3 算子二进制配置

在 complex_binary.json 中,算子按输入 dtype 拆分为两个二进制配置项:Complex_float32(float32 → complex64)与Complex_float16(float16 → complex32),shape 均标记为-2(动态 shape),format 为 ND;complex_simplified_key.ini 则给出简化 key 的默认值。

7.4 框架插件

在 TensorFlow 框架侧,complex_tf_plugin.cpp 通过REGISTER_CUSTOM_OP("Complex")将 TF 原算子Complex映射到本实现,并使用AutoMappingByOpFn自动完成参数映射,ImplyType::TVM表明其映射方式。

八、调用示例

以下示例代码来自官方文档,完整可运行版本也可参考仓库中的 test_aclnn_complex.cpp(该版本使用 RAII 智能指针管理资源)。具体编译和执行过程请参考 编译与运行样例。

#include <iostream> #include <vector> #include <complex> #include "acl/acl.h" #include "aclnnop/aclnn_complex.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::vector<int64_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::vector<T>& hostData, const std::vector<int64_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::vector<int64_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::vector<int64_t> realShape = {4, 2}; std::vector<int64_t> imagShape = {4, 2}; std::vector<int64_t> outShape = {4, 2}; void* realDeviceAddr = nullptr; void* imagDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* real = nullptr; aclTensor* imag = nullptr; aclTensor* out = nullptr; std::vector<float> realHostData = {0, 1, 2, 3, 4, 5, 6, 7}; std::vector<float> imagHostData = {1, 1, 1, 2, 2, 2, 3, 3}; std::vector<std::complex<float>> outHostData(8, 0); // 创建real aclTensor ret = CreateAclTensor(realHostData, realShape, &realDeviceAddr, aclDataType::ACL_FLOAT, &real); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建imag aclTensor ret = CreateAclTensor(imagHostData, imagShape, &imagDeviceAddr, aclDataType::ACL_FLOAT, &imag); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_COMPLEX64, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API,需要修改为具体的API名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnComplex第一段接口 ret = aclnnComplexGetWorkspaceSize(real, imag, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnComplexGetWorkspaceSize 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); } // 调用aclnnComplex第二段接口 ret = aclnnComplex(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnComplex 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::vector<float> 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]); } // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改 aclDestroyTensor(real); aclDestroyTensor(imag); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(realDeviceAddr); aclrtFree(imagDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

示例关键步骤解读

  1. 资源初始化aclInitaclrtSetDeviceaclrtCreateStream,固定写法;
  2. 构造输入输出:示例中 real 与 imag 均为{4, 2}的 FLOAT Tensor(数据分别为{0..7}{1,1,1,2,2,2,3,3}),out 为{4, 2}的 COMPLEX64 Tensor。由于 real/imag shape 完全相同,广播后的输出 shape 仍为{4, 2}
  3. 两段式调用:先调用aclnnComplexGetWorkspaceSize获取workspaceSizeexecutor,按需用aclrtMalloc申请 workspace 内存(注意workspaceSize > 0时才需要申请),再调用aclnnComplex执行;
  4. 同步与取数aclrtSynchronizeStream等待任务结束,随后将结果从 Device 拷贝回 Host 并打印;
  5. 资源释放:依次销毁aclTensor、释放 Device 内存、销毁 Stream、重置 Device 并aclFinalize

按公式计算,示例输出的复数应为0+1j, 1+1j, 2+1j, 3+2j, 4+2j, 5+2j, 6+3j, 7+3j

九、单元测试验证

仓库在 test_aclnn_complex.cpp 中提供了基于 gtest 的接口级单元测试,覆盖以下场景:

  • complex64 正常路径:FLOAT 输入 + COMPLEX64 输出,期望ACLNN_SUCCESS
  • complex32 正常路径:FLOAT16 输入 + COMPLEX32 输出,期望ACLNN_SUCCESS
  • dtype 配对校验:COMPLEX32 输入 + COMPLEX128 输出,期望ACLNN_ERR_PARAM_INVALID(输入 dtype 不支持);
  • 输出 dtype 校验:DOUBLE 输入 + COMPLEX64 输出,期望ACLNN_ERR_PARAM_INVALID(dtype 不配对);
  • 空 Tensor:shape 含 0 维的空输入,期望ACLNN_SUCCESS(对应第一段接口中的空 Tensor 短路处理)。

这些用例与文档中的错误码表格一一对应,可作为复现"入参校验报错"行为的快捷验证手段。

十、相关文档导航

  • 两段式接口说明:了解 aclnn 接口为什么分两段调用;
  • broadcast 关系说明:理解 real/imag 之间广播规则;
  • aclnn 返回码:查询ACLNN_ERR_PARAM_NULLPTRACLNN_ERR_PARAM_INVALID等状态码含义;
  • 编译与运行样例:示例代码的编译与运行指引;
  • 确定性计算说明:了解默认确定性实现的背景。
  • 算子库
  • 人工智能
  • CANN

【免费下载链接】ops-math

本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。

项目地址:https://gitcode.com/cann/ops-math
点击查看免费下载

相关推荐

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 17:11:13

LLVM编译器基础设施核心原理与实战:从IR到Pass机制全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:10:39

MATLAB ode45隔震-锁榫系统地震响应分段仿真与参数扫参

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:09:58

10 分钟用 TaoToken 跑通 MCP 文件服务器

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 17:08:52

API升级不再怕:3步手写实现本地模型兜底方案

做后端和AI应用的最怕听到一句话&#xff0c;不是“需求变了”&#xff0c;而是“我们升级一下依赖”。这个项目就是这么来的&#xff1a;我一直在维护一个手写数字识别的小工具&#xff0c;原本是前端传图片&#xff0c;后端调云端的视觉理解API来做识别。靠着现成的大模型接口…

作者头像 李华
网站建设 2026/9/20 17:08:48

Windows更新暂停100年:注册表延长暂停日期完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华