CANN 信号处理加速库 swapLast2Axes 算子实战:C++ 示例运行与源码级解析
2026/9/18 8:22:38 网站建设 项目流程

CANN 信号处理加速库 swapLast2Axes 算子实战:C++ 示例运行与源码级解析

【免费下载链接】sip本项目是CANN提供的一款高效、可靠的高性能信号处理算子加速库,基于华为Ascend AI处理器,专门为信号处理领域而设计。项目地址: https://gitcode.com/cann/sip

本指南以信号处理加速库(SiP)中 BASE 域的swapLast2Axes算子为主题,完整讲解其数学原理、接口用法、编译环境配置、C++ Demo 构建与运行步骤,并结合仓库源码深入剖析算子从 Host 侧接口到 Tiling、Kernel 的完整调用链。读者学完后,将能够独立搭建 CANN 开发环境、编译 SiP 加速库、运行 example/A2/BASE/swap_last2_axes/example_swap_last2_axes.cpp 示例,并理解该算子的维度约束与数据格式要求。

一、算子概述与功能说明

swapLast2Axes是信号处理加速库 BASE 域提供的基础算子,核心功能是交换 Tensor 的最后两维,本质上是围绕最后两个轴执行一次转置(transpose)操作,与 PyTorch 中的x.transpose(-1, -2)语义等价(可参见 sip_pta/test/base/swap_last2_axes.py 中的对照实现)。

其计算公式为:

$$ \text{outTensor}{bij} = \text{inTensor}{bji} $$

其中,b为数据的批次号,i为输入数据的行号,j为输入数据的列号。也就是说,输出张量中第(b, i, j)个元素取自输入张量中第(b, j, i)个元素。

典型示例

示例一:输入为三维张量inTensor = [[[1.+0.j, 2.+0.j, 3.+0.j]]](shape 为[1, 1, 3]),调用swapLast2Axes后输出为:

[[[1.+0.j], [2.+0.j], [3.+0.j]]]

示例二:输入为inTensor(shape 为[2, 2, 3]):

[[[ 0.+0.j, 1.+0.j, 2.+0.j], [ 3.+0.j, 4.+0.j, 5.+0.j]], [[ 6.+0.j, 7.+0.j, 8.+0.j], [ 9.+0.j, 10.+0.j, 11.+0.j]]]

调用swapLast2Axes后输出(shape 变为[2, 3, 2]):

[[[ 0.+0.j, 3.+0.j], [ 1.+0.j, 4.+0.j], [ 2.+0.j, 5.+0.j]], [[ 6.+0.j, 9.+0.j], [ 7.+0.j, 10.+0.j], [ 8.+0.j, 11.+0.j]]]

从两个示例可以看出:3 维张量的第 0 维(批次维度)保持不变,最后两维被交换。2 维张量则等效于标准矩阵转置。

二、函数原型与参数说明

该算子在 include/base_api.h 中对外声明了两个接口。根据 docs/zh/API_Reference/base/swapLast2Axes.md 的说明,使用swapLast2Axes算子前,必须先调用swapLast2AxesGetWorkspaceSize获取 workspace 大小,再调用swapLast2Axes执行计算

swapLast2AxesGetWorkspaceSize

AsdSip::AspbStatus swapLast2AxesGetWorkspaceSize( size_t *size)
参数名输入/输出描述
size(size_t *)输入/输出swapLast2Axes算子执行所需的 workspace 大小

swapLast2Axes

AsdSip::AspbStatus swapLast2Axes( const aclTensor * inTensor, aclTensor * outTensor, void * stream, void * workspace = nullptr)
参数名输入/输出描述
inTensor(aclTensor *)输入输入张量,对应公式中的inTensor。输入的最大元素数为 3600000000(shape 在 [60000, 60000] 以内);数据类型仅支持 COMPLEX64,数据格式支持 ND;输入维度限制为 2 或 3
outTensor(aclTensor *)输出输出张量,对应公式中的outTensor;数据类型仅支持 COMPLEX64,且需与inTensor一致;若inTensor的 shape 为[k, x, y],则outTensor的 shape 为[k, y, x];数据格式支持 ND
workspace(void *)输入swapLast2Axes算子执行所需的 workspace 内存
stream(void *)输入NPU 执行流(aclrtStream)

两个接口的返回值均为AsdSip::AspbStatus状态码,具体含义可参见 SiP返回码。

约束说明

  • 算子实际计算时,不支持维度大于 3 的 ND 高维度运算
  • 输入数据类型仅支持 COMPLEX64(复数双精度),数据格式仅支持 ND。

三、环境配置:CANN 与 SiP 编译

1. 配置 CANN 环境变量

示例运行依赖 CANN(Ascend CANN Toolkit)环境,需要先加载其环境变量:

source [CANN安装路径]/set_env.sh

默认安装路径下命令为:

source /usr/local/Ascend/ascend-toolkit/set_env.sh

2. 编译信号处理加速库(SiP)

进入 SiP 仓库根目录,执行如下指令完成加速库编译,并加载加速库环境变量:

cd ${SiP_root_path} bash build.sh source output/set_env.sh

需要特别注意:

  • 上述编译方式仅支持通过 git 下载的加速库,以 zip 压缩包方式下载的加速库不支持该编译方式;
  • 编译过程需要联网下载依赖库,因此编译环境必须联网
  • 编译过程包含两步:获取并编译 ascend-boost-comm(昇腾分布式通信加速库)组件,以及编译信号处理加速库本身。更多命令介绍可查看 build.sh 文件。

更多编译命令说明请参考编译与构建。source output/set_env.sh执行后会设置加速库相关的环境变量(如ASDSIP_HOME_PATH),这是后续编译示例所必需的。

3. 产品支持情况

该示例适用于以下产品型号(与 docs/zh/API_Reference/base/swapLast2Axes.md 中的产品支持矩阵一致):

  • 支持:Atlas A2 训练系列产品、Atlas A2 推理系列产品、Atlas A3 训练系列产品、Atlas A3 推理系列产品(README 中描述的 Atlas A2/A3 训练系列、Atlas 800I A2 推理产品、Atlas A3 推理系列);
  • 不支持:Ascend 950PR/Ascend 950DT、Atlas 200I/500 A2 推理产品、Atlas 推理系列产品、Atlas 训练系列产品(A910 等)。

四、示例代码逐段解读

示例主程序位于 example/A2/BASE/swap_last2_axes/example_swap_last2_axes.cpp,核心流程为:ACL 初始化 → 创建 Host/Device 张量 → 查询 workspace → 调用算子 → 回拷结果 → 释放资源。下面分段解读关键逻辑。

1. 头文件与状态检查宏

#include <iostream> #include <vector> #include "asdsip.h" #include "acl/acl.h" #include "acl_meta.h" using namespace AsdSip;

示例使用ASD_STATUS_CHECK宏统一检查算子返回状态,使用CHECK_RET宏检查 ACL 接口返回码,使用LOG_PRINT宏打印日志,避免大量重复的错误处理代码。

2. ACL 环境初始化(Init 函数)

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; }

Init是 CANN 开发的固定模板:aclInit初始化 ACL 运行环境,aclrtSetDevice指定使用的 NPU 设备(示例中使用deviceId = 0),aclrtCreateStream创建执行流,后续算子调用与数据搬运均在该流上排队执行。

3. 创建 aclTensor(CreateAclTensor 模板函数)

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) * 2; // 调用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; }

关键点说明:

  • GetShapeSize计算 shape 各维度乘积得到元素总数;由于示例使用 COMPLEX64 复数类型(每个复数由 2 个 float 组成),内存大小计算为元素数 * sizeof(float) * 2
  • aclrtMalloc在 Device 侧申请内存,aclrtMemcpy将 Host 侧数据拷贝到 Device;
  • 代码中手工计算连续张量的 strides(最后一维 stride 为 1,向前逐维累乘),并以ACL_FORMAT_ND格式调用aclCreateTensor创建aclTensor描述对象。

4. 主流程:数据准备与算子调用

int64_t row = 3; int64_t col = 2; const int64_t tensorSize = row * col * 2; std::vector<float> tensorInData; tensorInData.resize(tensorSize); for (int64_t i = 0; i < tensorSize; i++) { tensorInData[i] = 0.0 + i; } std::vector<float> tensorOutData; tensorOutData.resize(tensorSize); std::vector<int64_t> inShape = {row, col}; std::vector<int64_t> outShape = {col, row};

示例使用 2 维张量:输入 shape 为{3, 2},输出 shape 为{2, 3},即对 3×2 的复数矩阵做转置。数据按 0 到 11 线性填充(0.0 + i),便于肉眼核对转置结果。README 中也特别说明:示例中生成的数据不代表实际场景,可根据具体使用场景进行数据修改

算子调用部分:

void* workspace = nullptr; size_t lwork = 0; swapLast2AxesGetWorkspaceSize(lwork); std::cout << "lwork = " << lwork << std::endl; if (lwork > 0) { ret = aclrtMalloc(&workspace, static_cast<int64_t>(lwork), ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ::ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } ASD_STATUS_CHECK(swapLast2Axes(input, output, stream, workspace));

该段遵循"先查 workspace、再执行算子"的规范流程:调用swapLast2AxesGetWorkspaceSize获得所需 workspace 大小,若大于 0 则用aclrtMalloc申请,随后将输入、输出、执行流与 workspace 一并传入swapLast2Axes完成计算。

5. 结果回拷与打印

ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ::ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); ret = aclrtMemcpy(tensorOutData.data(), tensorSize * sizeof(float), outputDeviceAddr, tensorSize * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ::ACL_SUCCESS, LOG_PRINT("copy output tensor from device to host failed. ERROR: %d\n", ret); return ret); std::cout << "row = " << row << ", col = " << col << std::endl; std::cout << "------- Input ------- " << std::endl; printTensor(tensorInData.data(), row, col); std::cout << "------- Output -------" << std::endl; printTensor(tensorOutData.data(), col, row);

先通过aclrtSynchronizeStream同步等待流上任务完成,再用aclrtMemcpy将 Device 侧结果拷回 Host 侧tensorOutDataprintTensor函数按"行 × 列"遍历并以(real, imag)形式打印每个复数元素。

6. 资源释放

aclrtFree(inputDeviceAddr); aclrtFree(outputDeviceAddr); aclDestroyTensor(input); aclDestroyTensor(output); if (lwork > 0) { aclrtFree(workspace); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize();

按照与申请相反的顺序依次释放 Device 内存、销毁aclTensor描述、销毁流、复位设备并调用aclFinalize完成 ACL 环境收尾,避免资源泄漏。

五、运行 Demo:编译与执行

进入示例所在目录并执行构建脚本:

cd ${示例所在目录} bash build.sh

示例目录下的 build.sh 会依次完成以下工作:

  1. 校验环境变量:检查ASDSIP_HOME_PATH是否已设置且目录存在(兼容以latest/latest结尾的路径,自动归一化);
  2. 配置动态库路径:将$ASDSIP_HOME_PATH/lib追加到LD_LIBRARY_PATH
  3. 编译示例:使用g++编译example_swap_last2_axes.cpp,链接参数包括:
    • CANN 侧:-I${ASCEND_HOME_PATH}/include/aclnn-I${ASCEND_HOME_PATH}/include,链接-lascendcl -lopapi -lnnopbase(来自${ASCEND_HOME_PATH}/lib64/);
    • SiP 侧:-I${ASDSIP_HOME_PATH}/include,链接-lmki -lasdsip -lasdsip_core -lasdsip_host(来自${ASDSIP_HOME_PATH}/lib);
  4. 运行并清理:输出提示语Execute the example of SwapLast2AxesOperation, swap the last two dimensions of tensors:后执行./example,运行结束后删除临时可执行文件。

成功运行后,程序会依次打印 workspace 大小(lwork)、输入矩阵、输出矩阵,并输出Execute successfully.表示算子调用成功。

六、源码级原理解析:从 Host 接口到 Kernel

1. Host 侧接口实现(参数校验与算子下发)

swapLast2Axes的 Host 侧实现位于 core/base/swaplast2axes.cpp,主要完成三步工作:

数据类型校验SwapLast2AxesDtypeCheck):通过SIP_OP_CHECK_DTYPE_NOT_MATCH检查输入输出张量数据类型均为ACL_COMPLEX64,否则返回ACL_ERROR_UNSUPPORTED_DATA_TYPE

形状校验SwapLast2AxesShapeCheck):调用aclGetStorageShape获取输入输出张量的存储形状,校验以下条件:

  • 输出元素数不少于输入元素数(outNum >= inNum),否则返回ACL_ERROR_INVALID_PARAM
  • 输入输出维度数相等,且满足1 < dim < 4(即仅支持 2 维或 3 维),否则返回ACL_ERROR_OP_INPUT_NOT_MATCH

这与 API 文档中"输入 dim 限制为 2 或 3"的约束一一对应。

参数组装与算子下发:将维度信息换算为算子参数:

int64_t dim = static_cast<int64_t>(inStorageDimsNum); param.batch = 1; int64_t endDimIdx = 2; int64_t frontDimIdx = 1; for (int64_t i = 0; i < dim - endDimIdx; i++) { param.batch *= static_cast<uint32_t>(storageDims[i]); } param.row = static_cast<uint32_t>(storageDims[dim - endDimIdx]); param.col = static_cast<uint32_t>(storageDims[dim - frontDimIdx]);

即 3 维输入时batch取第 0 维,rowcol分别取倒数第二维和最后一维;2 维输入时batch为 1。随后通过RunAsdOpsV2将算子下发到 NPU 执行。

swapLast2AxesGetWorkspaceSize的实现非常轻量:

AsdSip::AspbStatus swapLast2AxesGetWorkspaceSize(size_t& size) { size = ASYNC_WORKSPACE_SIZE; return ErrorType::ACL_SUCCESS; }

该接口将 workspace 大小固定为异步执行所需的ASYNC_WORKSPACE_SIZE常量。

2. 算子注册与 Kernel 选择

算子类SwapLast2AxesOperation定义在 ops/base/swaplast2axes/swap_last_2axes_operation.cpp,通过REG_OPERATION(SwapLast2AxesOperation)完成注册。其中两个关键方法:

  • GetBestKernel:根据输入张量的数据类型选择核函数——FLOAT 类型选择SwapLast2AxesF32Kernel,COMPLEX64 类型选择SwapLast2AxesC64Kernel
  • InferShapeImpl:完成输出 shape 推导,即把dims[dim-2]dims[dim-1]互换,同时保持 dtype 与 format 与输入一致。

Kernel 侧定义在 ops/base/swaplast2axes/swap_last_2axes_kernel.cpp:SwapLast2AxesKernel继承KernelBase,其CanSupport校验参数类型、输入/输出张量数量;GetTilingSize返回sizeof(SwapLast2AxesTilingData)InitImpl调用SwapLast2AxesTiling生成 Tiling 信息。COMPLEX64 对应的SwapLast2AxesC64Kernel进一步校验输入输出 dtype 均为TENSOR_DTYPE_COMPLEX64,并通过REG_KERNEL_BASE注册。

3. Tiling 策略

Tiling 实现位于 ops/base/swaplast2axes/tiling/swap_last_2axes_tiling.cpp:

  • 从算子参数中取出batchrowcol写入SwapLast2AxesTilingData
  • 查询当前平台的 Vector Core 数量PlatformInfo::Instance().GetCoreNum(CoreType::CORE_TYPE_VECTOR)作为算子切分的块维度(kernelInfo.SetBlockDim(maxCore)),最大程度利用多核并行能力;
  • 该算子无需额外 scratch 内存(GetScratchSizes().push_back(0)),这解释了为什么 workspace 只需满足异步执行的基础开销。

从源码结构看,NPU 侧核函数将基于 Tiling 数据中的 batch/row/col 对复数数据完成"最后两维交换",本质上等价于带批次维的矩阵转置,通过多 Vector Core 并行分摊转置搬运的工作量。

4. PyTorch 侧对照与测试验证

仓库 sip_pta 提供了基于 PyTorch + torch_npu 的 Python 封装,测试用例 sip_pta/test/base/swap_last2_axes.py 验证了算子的正确性:

  • 2D 复数张量(3, 2):调用torch_sip.swap_last2_axes(x),与x.transpose(-1, -2)的参考结果做torch.allclose数值比对(rtol/atol 均为 1e-4);
  • 3D 复数张量(2, 4, 5):验证带批次维时的storageDims兼容性;
  • 边界用例:1D 张量输入应抛出异常,验证了"输入 dim 限制为 2 或 3"的约束。

PyTorch 参考实现x.transpose(-1, -2)与 C++ 示例的 3×2 复数矩阵转置结果一致,进一步印证了算子语义。

七、常见问题与注意事项

  1. 数据格式与类型限制swapLast2Axes仅支持 COMPLEX64 类型与 ND 格式,传入其他数据类型(如 FLOAT16、INT32)会触发ACL_ERROR_UNSUPPORTED_DATA_TYPE错误;
  2. 维度限制:仅支持 2 维或 3 维输入,1 维或超过 3 维的输入会被形状校验拒绝(返回ACL_ERROR_OP_INPUT_NOT_MATCH);
  3. 输出 shape 声明:创建输出张量时,shape 必须预先声明为交换后的形态(如输入{3, 2}对应输出{2, 3}),且输出元素数不能少于输入元素数;
  4. workspace 申请:必须在调用算子前调用swapLast2AxesGetWorkspaceSize获取大小并申请内存,即使大小为 0 也应保持接口调用流程完整;
  5. 编译前置条件:示例构建依赖ASDSIP_HOME_PATH(由source output/set_env.sh提供)与ASCEND_HOME_PATH(由 CANNset_env.sh提供)两个环境变量,缺一不可;
  6. 联网要求:SiP 加速库编译需要联网下载依赖库(ascend-boost-comm 等),离线环境无法完成整库编译。

八、相关参考

  • 示例代码:example/A2/BASE/swap_last2_axes/example_swap_last2_axes.cpp
  • 示例构建脚本:example/A2/BASE/swap_last2_axes/build.sh
  • API 接口声明:include/base_api.h
  • 完整 API 文档:swapLast2Axes
  • Host 侧实现:core/base/swaplast2axes.cpp
  • 算子注册与 Kernel:ops/base/swaplast2axes/swap_last_2axes_operation.cpp、ops/base/swaplast2axes/swap_last_2axes_kernel.cpp
  • Tiling 实现:ops/base/swaplast2axes/tiling/swap_last_2axes_tiling.cpp
  • Python 侧测试:sip_pta/test/base/swap_last2_axes.py
  • 编译指南:编译与构建
  • 返回码说明:SiP返回码

【免费下载链接】sip本项目是CANN提供的一款高效、可靠的高性能信号处理算子加速库,基于华为Ascend AI处理器,专门为信号处理领域而设计。项目地址: https://gitcode.com/cann/sip

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询