CANN ops-cv 算子指南:aclnnUpsampleLinear1d 一维线性上采样算子两段式接口详解
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
本文以 CANN ops-cv 仓库中 aclnnUpsampleLinear1d 接口文档 为主体,结合 op_host/op_api 实现源码、kernel 实现 与 测试用例,系统讲解 UpsampleLinear1d 算子的算法原理、两段式调用流程、完整参数约束、内部调度实现与编译运行方法。读完本文,你将掌握如何在 NPU 上通过 aclnn 单算子 API 完成一维线性上采样计算,并能看懂该算子在 CANN 软件栈中的底层调度链路。
功能概述
aclnnUpsampleLinear1d对应 ops-cv 仓库中的UpsampleLinear1d算子,其功能是:对由多个输入通道组成的输入信号应用线性插值算法进行上采样。这是深度学习网络中常见的 feature map 尺寸调整操作(与 PyTorch 的torch.nn.functional.interpolate(..., mode='linear')语义一致,仓库测试即通过 PyTorch golden 生成脚本 调用torch._C._nn.upsample_linear1d对齐结果)。
算子面向 3 维张量(NCL 布局):输入 shape 为(N, C, L)时,输出 shape 为(N, C, outputSize),其中 N 为 batch、C 为通道数、L 为长度维,仅在 L 维上进行插值缩放,N、C 维度保持不变。算子注册信息见 upsample_linear1d_def.cpp,支持 FP16、BF16、FP32 三种数据类型。
算法原理与计算公式
核心算法逻辑
线性插值上采样的核心思路分为三步:
- 坐标映射:将目标图像的每一个点映射回原图,得到一个带小数的浮点坐标;
- 邻点选取:根据该浮点坐标,找出原图中前后相邻的两个点;
- 加权累加:分别计算相邻点到对应目标点的权重,按权重相乘累加得到目标点值。
缩放系数与坐标映射公式
缩放方式分为**角对齐(alignCorners=true)与边对齐(alignCorners=false)**两种:
- 角对齐:按原始图片左上角像素的中心点对齐,
alignCorners=true时输出张量的边界点直接保留输入张量的角点值; - 边对齐:按原始图片左上角顶点及两条边对齐,插值时外界边值使用边缘值填充,使操作在输入尺寸变化时独立于
scales。
两种模式下缩放系数scale的计算存在差异:
$$ scale =\begin{cases} (self.dim[2]-1) / (outputSize[0]-1) & alignCorners=true \ 1 / scales & alignCorners=false\ &\ scales>0\ self.dim[2] / outputSize[0] & alignCorners=false \end{cases} $$
将输出方向上的点p(x)映射回原始图像中的点q(x'):
$$ x' =\begin{cases} x * scale & alignCorners=true \ MAX(0,{(x+0.5)*scale-0.5}) & alignCorners=false \end{cases} $$
记相邻点坐标与权重:
$$ x_{0} =int(x'),\quad x_{1} =int(x')+1,\quad lambda_{0} = x_{1}-x',\quad lambda_{1} = 1-lambda_{0} $$
最终插值公式为:
$$ {V(p_{x})} = {V(p_{x0})} * {lambda_{0}} + {V(p_{x1})} * {lambda_{1}} $$
源码佐证:scale 计算与缩放范围限制
上述公式在算子实现中被严格遵循。从 aclnn_upsample_linear_1d.cpp 的CheckLinear1dScales函数可以看到:
alignCorners=true且输出长度大于 1 时,scales_w = (input_size - 1) / (output_size - 1),与公式第一分支完全一致;输出长度为 1 时scales_w = 0;alignCorners=false时,scales_w = (scale > 0) ? 1.0/scale : input_size/output_size,与公式第二、三分支对应;- 源码还限定了缩放系数合法区间:
scales_w必须满足MAX_SUPPORT_ZOOM_SCALE_REV(0.00125) <= scales_w <= MAX_SUPPORT_SHRINK_SCALE(50.0),即最大支持 50 倍缩小、约 800 倍放大,超出范围将拒绝走 AICore 专属内核路径。
产品支持情况
根据 aclnnUpsampleLinear1d.md 的说明,该接口的产品支持矩阵如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 支持 |
注意:在Atlas 训练系列产品上运行时,入参
self与出参out的数据类型不支持 BFLOAT16,仅支持 FP32 与 FP16。此外,算子AICore内核的 tiling 配置仅注册了ascend910b与ascend910_93两个平台(见 upsample_linear1d_def.cpp)。
两段式接口与函数原型
aclnnUpsampleLinear1d遵循 CANN 单算子 API 的两段式接口规范(详见 两段式接口说明):必须先调用第一段接口aclnnUpsampleLinear1dGetWorkspaceSize完成入参校验、算子图编排并计算所需 workspace 大小,再调用第二段接口aclnnUpsampleLinear1d在指定 Stream 上执行计算。
第一段接口原型:
aclnnStatus aclnnUpsampleLinear1dGetWorkspaceSize( const aclTensor *self, const aclIntArray *outputSize, const bool alignCorners, const double scales, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型:
aclnnStatus aclnnUpsampleLinear1d( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)两段接口的对外声明位于 aclnn_upsample_linear_1d.h,使用前需包含头文件#include "aclnnop/aclnn_upsample_linear_1d.h"。
aclnnUpsampleLinear1dGetWorkspaceSize 参数详解
参数说明
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| self(aclTensor*) | 输入 | 表示进行上采样的输入张量,对应公式中的self | 不支持空 Tensor;当数据格式为 ND 时,默认按照 NCL 格式处理 | FLOAT32、FLOAT16、BFLOAT16 | ND、NCL | 3 | √ |
| outputSize(aclIntArray*) | 输入 | 表示输出 out 在 L 维度上的空间大小,对应公式中的outputSize | size 为 1,且取值大于 0 | INT64 | - | - | - |
| alignCorners(bool) | 输入 | bool 类型参数,决定是否对齐角像素点,对应公式中的alignCorners | true:输入和输出张量按其角像素的中心点对齐,保留角像素处的值;false:输入和输出张量通过其角像素的角点对齐,插值使用边缘值填充外界边值,使操作在保持不变时独立于输入大小 scales | - | - | - | - |
| scales(double) | 输入 | 表示输出 out 的 L 维度乘数,对应公式中的scales | - | - | - | - | - |
| out(aclTensor*) | 输出 | 表示采样后的输出张量 | 不支持空 Tensor;输出维度必须是 3 维;数据类型、数据格式与入参self保持一致 | FLOAT32、FLOAT16、BFLOAT16 | ND、NCL | 3 | √ |
| workspaceSize(uint64_t*) | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor(aclOpExecutor**) | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
返回值与错误码
第一段接口返回aclnnStatus状态码,具体枚举含义参见 aclnn返回码。第一段接口完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入参数是必选输入、输出或必选属性,且是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 的数据类型不在支持的范围之内 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 out 的数据类型不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self 和 out 的维度不为 3 维 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outputSize 的 size 不等于 1 |
| ACLNN_ERR_PARAM_INVALID | 161002 | outputSize 的某个元素值小于 1 |
| ACLNN_ERR_PARAM_INVALID | 161002 | out 在 L 维度上的 size 与 outputSize[0] 不一致 |
| ACLNN_ERR_PARAM_INVALID | 161002 | self、outputSize、scales 不满足约束 |
源码级印证:上述校验逻辑在 aclnn_upsample_linear_1d.cpp 中由CheckNotNull(空指针检查)、CheckShape(维度、outputSize 数量、out 各维 shape 与{batch, channels, outL}一致性检查,并校验scale * input[2] == outputSize[0])与CheckDtypeValid(数据类型白名单{DT_FLOAT16, DT_FLOAT, DT_BF16}及 self/out 类型一致性检查)三部分组成。值得注意的细节是:当scale >= 0且非 RegBase 平台时,源码会强制校验scale与outputSize不冲突,否则报ACLNN_ERR_PARAM_INVALID。
aclnnUpsampleLinear1d 参数说明
第二段接口入参含义如下:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口aclnnUpsampleLinear1dGetWorkspaceSize获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
第二段接口同样返回aclnnStatus状态码(参见 aclnn返回码)。注意:第二段接口不能重复调用,同一 executor 重复执行会出现异常(详见 两段式接口说明)。
约束说明
数据格式约束:入参
self和出参out的数据格式不为 ND 或 NCL 时,输入其他数据格式会默认按照 NCL 处理。参数关联约束:
self、outputSize、scales需要满足如下关系:$$ outputSize = floor(self_L * scales) $$
确定性计算:
aclnnUpsampleLinear1d为默认确定性实现(确定性计算相关概念可参考 确定性计算说明)。非连续 Tensor:
self与out均支持非连续 Tensor 输入,第一段接口内部会通过l0op::Contiguous与l0op::ViewCopy自动完成连续性规整与结果回写(非连续张量的更多背景参见 非连续Tensor说明)。
源码深挖:接口内部调度实现
aclnnUpsampleLinear1dGetWorkspaceSize在完成参数校验后,会根据平台、scale 与数据量分派到不同的计算路径(见 aclnn_upsample_linear_1d.cpp):
- 空 Tensor 短路:
self->IsEmpty()为真时直接返回workspaceSize = 0,无需实际计算; - RegBase 平台路径:直接调用
l0op::ResizeLinear完成计算,再经ViewCopy写回 out; - 恒等变换路径:当
|scale - 1.0| < 1e-9或outL == self->GetViewShape().GetDim(2)时,即输出尺寸与输入一致,直接通过ViewCopy将输入拷贝到输出,跳过插值计算; - AICore 专属内核路径(
DAV_2201架构且缩放系数在合法区间内):调用GoUpsampleLinear1DAICORE,走l0op::UpsampleLinear1dNcdhw内核。该路径对大数据量(超过 500MB,由WORKSPACE_LIMIT与UB_LIMIT联合判断)的 FP16/BF16 输入先Cast到 FP32 计算再Cast回原类型,以兼顾精度与性能; - 通用 ResizeD 路径:其余情况将输入
View3dAs4d升维为 4D(Contiguous+Unsqueeze(2)),BF16 先转 FP32,再调用l0op::ResizeD(LINEAR_MODE)完成插值,最后ViewCopy写回。
在算子底层,kernel 入口 依据 tiling key 分发到两种内核实现:TILING_KEY_IS(1)时执行普通 ND 版本UpsampleLinear1dND<DTYPE_X>,TILING_KEY_IS(2)时执行基于 MatMul 的混合实现UpsampleLinear1dMixND<float>(利用 cube 单元加速大规模插值)。tiling 侧在 upsample_linear1d_tiling.cpp 中针对 16/32/64/128 等分块尺寸和多种 scale 区间(<1、<5、<8、<20、<50)设置了不同的性能调优参数,并通过getSlideSizeByScale动态计算滑窗大小。
调用示例
完整可运行的示例代码见 examples/test_aclnn_upsample_linear1d.cpp,与文档中 aclnnUpsampleLinear1d.md 的调用示例一致。核心流程如下(示例将 shape 为(1, 1, 2)的输入上采样到(1, 1, 3),即 L 从 2 变到 3):
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_upsample_linear_1d.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> selfShape = {1, 1, 2}; std::vector<int64_t> outShape = {1, 1, 3}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {1, 1}; std::vector<float> outHostData = {0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); 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); std::vector<int64_t> outArraySize = {3}; const aclIntArray* outputSize = aclCreateIntArray(outArraySize.data(), outArraySize.size()); CHECK_RET(outputSize != nullptr, return ACL_ERROR_INTERNAL_ERROR); // 3. 调用CANN算子库API,需要修改为具体的API名称 uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnUpsampleLinear1d第一段接口 ret = aclnnUpsampleLinear1dGetWorkspaceSize(self, outputSize, false, -1, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnUpsampleLinear1dGetWorkspaceSize 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); } // 调用aclnnUpsampleLinear1d第二段接口 ret = aclnnUpsampleLinear1d(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnUpsampleLinear1d 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(self); aclDestroyTensor(out); aclDestroyIntArray(outputSize); // 7. 释放device资源,需要根据具体API的接口定义修改 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }代码要点解读:
- 示例中
scales传入-1,表示不通过 scales 推导输出尺寸,而是完全由outputSize = {3}决定 L 维长度;此时约束关系outputSize = floor(self_L * scales)中的 scales 分支不生效; - 第一段接口的
executor由框架内部创建并持有算子计算图,第二段接口通过CommonOpExecutorRun在指定 stream 上执行; - workspace 为 0 时无需申请内存,示例代码对此做了
workspaceSize > 0的保护判断。
编译与运行
编译运行完整步骤参考 编译与运行样例,要点如下:
准备示例代码
test_aclnn_upsample_linear1d.cpp与 CMakeLists.txt(链接libacl_rt.so、libnnopbase.so、libopapi_math.so、libopapi_cv.so);配置环境变量:
source ${INSTALL_DIR}/set_env.sh其中
${INSTALL_DIR}为 CANN 软件安装路径;编译并运行:
mkdir -p build cd build cmake ../ -DCMAKE_CXX_COMPILER=g++ -DCMAKE_SKIP_RPATH=TRUE make cd bin ./opapi_test运行后将在终端打印
result[0] is: ...形式的插值结果。
测试与验证
仓库为该算子提供了完整的单元测试与场景测试支撑:
- UT 测试(host 侧 op_api):test_aclnn_upsample_Linear_1d.cpp 与 aclnnUpsampleLinear1d.py 配合使用,golden 结果直接由 PyTorch 的
torch._C._nn.upsample_linear1d生成,逐 case 对比 NPU 输出,验证算子与 PyTorch 语义的一致性; - UT 测试(tiling):test_upsample_linear1d_tiling.cpp 覆盖 tiling 数据计算正确性;
- UT 测试(kernel):test_upsample_linear1d.cpp 配合 gen_data.py 生成测试数据,验证 AICore 内核在不同 shape、scale、alignCorners 组合下的数值正确性;
- ST 场景测试:executor_aclnnUpsampleLinear1d.py 与 atk_aclnnUpsampleLinear1d.json 提供端到端的场景用例描述。
总结
aclnnUpsampleLinear1d是 CANN ops-cv 仓库中实现一维线性上采样的标准 aclnn 接口:算法上通过角对齐/边对齐两种缩放模式完成浮点坐标映射与双点加权插值;调用上遵循两段式接口规范,先由aclnnUpsampleLinear1dGetWorkspaceSize校验参数并计算 workspace,再由aclnnUpsampleLinear1d执行计算;实现上根据平台、scale 与数据量自动选择恒等拷贝、AICore 专属内核或通用 ResizeD 路径,并对大数据量的 FP16/BF16 输入做 FP32 中间计算以保证精度。结合 接口文档、源码实现 与 示例代码,开发者可以快速将该算子集成到自己的 NPU 推理或训练应用中。
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考