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.sh2. 编译信号处理加速库(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 侧tensorOutData。printTensor函数按"行 × 列"遍历并以(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 会依次完成以下工作:
- 校验环境变量:检查
ASDSIP_HOME_PATH是否已设置且目录存在(兼容以latest/或latest结尾的路径,自动归一化); - 配置动态库路径:将
$ASDSIP_HOME_PATH/lib追加到LD_LIBRARY_PATH; - 编译示例:使用
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);
- CANN 侧:
- 运行并清理:输出提示语
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 维,row、col分别取倒数第二维和最后一维;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:
- 从算子参数中取出
batch、row、col写入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 复数矩阵转置结果一致,进一步印证了算子语义。
七、常见问题与注意事项
- 数据格式与类型限制:
swapLast2Axes仅支持 COMPLEX64 类型与 ND 格式,传入其他数据类型(如 FLOAT16、INT32)会触发ACL_ERROR_UNSUPPORTED_DATA_TYPE错误; - 维度限制:仅支持 2 维或 3 维输入,1 维或超过 3 维的输入会被形状校验拒绝(返回
ACL_ERROR_OP_INPUT_NOT_MATCH); - 输出 shape 声明:创建输出张量时,shape 必须预先声明为交换后的形态(如输入
{3, 2}对应输出{2, 3}),且输出元素数不能少于输入元素数; - workspace 申请:必须在调用算子前调用
swapLast2AxesGetWorkspaceSize获取大小并申请内存,即使大小为 0 也应保持接口调用流程完整; - 编译前置条件:示例构建依赖
ASDSIP_HOME_PATH(由source output/set_env.sh提供)与ASCEND_HOME_PATH(由 CANNset_env.sh提供)两个环境变量,缺一不可; - 联网要求: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),仅供参考