CANN ops-nn 非连续 Tensor 详解:shape / strides / offset 三要素表示法与算子支持机制
2026/9/19 21:16:06 网站建设 项目流程

CANN ops-nn 非连续 Tensor 详解:shape / strides / offset 三要素表示法与算子支持机制

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

本篇文章围绕 CANN ops-nn 算子库中"非连续 Tensor(Non-contiguous Tensor)"这一核心概念展开,说明一个 aclTensor 如何通过(shape, strides, offset)三元组在内存中描述不连续的视图(view),并给出两个完整的内存排布实例。读完本文,你将掌握 strides 与 offset 的精确定义、非连续地址的推算方法、通过aclCreateTensor构造非连续 aclTensor 的实战写法,以及 ops-nn 算子内部为支持非连续输入所采用的 Contiguous 转换与 ViewCopy 机制。

一、背景:为什么需要"非连续 Tensor"

在神经网络计算中,张量(Tensor)通常以连续内存(行优先 / Row-Major)的方式存储,即逻辑下标(i0, i1, ..., in-1)对应的元素在内存中是依次相邻的。但实际应用中大量操作会自然产生"视图不连续"的数据,典型场景包括:

  • 切片(Slice):从一个大张量中截取一个子块,子块内部各行之间并不相邻;
  • 转置(Transpose):交换维度后,原始连续排布在新视图下变成跳跃式访问;
  • 步幅采样:如按步长 2 抽帧、跨行读取等。

若每次产生这类视图都物理拷贝一份连续数据,会带来明显的内存与带宽开销。为此,CANN 算子库引入"非连续 Tensor"概念:一个 Tensor 可以通过(shape, strides, offset)表示,用 strides 描述各维度相邻元素的间隔、用 offset 描述首元素相对基地址的偏移,从而在不拷贝数据的前提下描述一个逻辑张量视图。目前大部分算子 API 的输入 aclTensor 都支持这种表示方式。

二、核心三要素:shape、strides、offset

非连续 Tensor 的表示由三个要素共同决定:

要素含义取值特点
shape描述 Tensor 的逻辑形状,即每个维度上的元素个数与连续 Tensor 的 shape 含义完全一致
strides描述 Tensor 各维度上相邻两个元素在内存中的间隔(以元素个数计)某维度 stride 为 1 时该维度连续;stride 大于 1 时该维度上元素之间存在间隔,即非连续
offset表示该 Tensor 的首元素相对基地址addr的偏移(以元素个数计)取值为非负整数,指向视图的第一个有效元素

其中 strides 是判断连续性的关键:如果在维度 1 上的 stride 为 1,该维度是连续的;如果在维度 0 上的 stride 为 10,那么相邻的元素间隔 10 个元素,即非连续。offset 则解决了"视图不从基地址第 0 个元素开始"的问题,例如从一个大矩阵中间截取子块时,首元素往往并不位于内存起点。

在 strided 视图表示下,逻辑下标(i0, i1, ..., in-1)对应的元素在内存中的位置可表达为:

base_addr + (offset + i0 * strides[0] + i1 * strides[1] + ... + in-1 * strides[n-1]) * 元素大小

当所有维度的 strides 恰好满足"行优先连续"的递推关系(最后一维 stride 为 1,前一维 stride 等于后一维 shape 与 stride 的乘积)时,该 Tensor 退化为连续 Tensor,连续 Tensor 可视作 offset 为 0、strides 由 shape 递推得出的特例。

三、示例 1:shape=(6, 5)、strides=(10, 1)、offset=22 的行连续切片

假设底层一块连续内存被组织为一个 10×10 的逻辑矩阵(基地址为addr),现有 Tensor 的shape=(6, 5)strides=(10, 1)offset=22,其内存排布如下(深色为有效元素):

行\列0123456789
0a0,0a0,1a0,2a0,3a0,4a0,5a0,6a0,7a0,8a0,9
1a1,0a1,1a1,2a1,3a1,4a1,5a1,6a1,7a1,8a1,9
2a2,0a2,1a2,2a2,3a2,4a2,5a2,6a2,7a2,8a2,9
3a3,0a3,1a3,2a3,3a3,4a3,5a3,6a3,7a3,8a3,9
4a4,0a4,1a4,2a4,3a4,4a4,5a4,6a4,7a4,8a4,9
5a5,0a5,1a5,2a5,3a5,4a5,5a5,6a5,7a5,8a5,9
6a6,0a6,1a6,2a6,3a6,4a6,5a6,6a6,7a6,8a6,9
7a7,0a7,1a7,2a7,3a7,4a7,5a7,6a7,7a7,8a7,9
8a8,0a8,1a8,2a8,3a8,4a8,5a8,6a8,7a8,8a8,9
9a9,0a9,1a9,2a9,3a9,4a9,5a9,6a9,7a9,8a9,9

该 Tensor 的有效数据即上图的深色位置,具体解读如下:

  • offset=22:在 10×10 的连续排布中,第 0 行占 10 个元素(索引 0~9)、第 1 行占 10 个元素(索引 10~19),索引 20、21 对应 a2,0、a2,1,因此首元素 a2,2 的偏移为 22;
  • 维度 1 的 stride 为 1:同一行内相邻元素紧挨着存放,所以每行取连续 5 个元素(a2,2 ~ a2,6);
  • 维度 0 的 stride 为 10:下一行的首元素与当前行的首元素间隔 10 个元素,即从 a2,2 出发,行号每 +1 需要跳过 10 个元素的位置,依次得到 a3,2、a4,2……直到 a7,2,共 6 行。

这个完整的 Tensor 在内存排布上是不连续的:维度 1 上是连续的(stride=1),但维度 0 上的元素间隔 10 个元素,属于非连续。该视图等价于"从 10×10 矩阵中截取第 2~7 行、第 2~6 列"的切片,且全程无需任何数据拷贝。

四、示例 2:shape=(4, 3)、strides=(20, 2)、offset=22 的跳跃采样视图

再看一个维度 0 和维度 1 都不连续的示例。同样基于 10×10 的逻辑矩阵,现有 Tensor 的shape=(4, 3)strides=(20, 2)offset=22,其内存排布如下:

行\列0123456789
0a0,0a0,1a0,2a0,3a0,4a0,5a0,6a0,7a0,8a0,9
1a1,0a1,1a1,2a1,3a1,4a1,5a1,6a1,7a1,8a1,9
2a2,0a2,1a2,2a2,3a2,4a2,5a2,6a2,7a2,8a2,9
3a3,0a3,1a3,2a3,3a3,4a3,5a3,6a3,7a3,8a3,9
4a4,0a4,1a4,2a4,3a4,4a4,5a4,6a4,7a4,8a4,9
5a5,0a5,1a5,2a5,3a5,4a5,5a5,6a5,7a5,8a5,9
6a6,0a6,1a6,2a6,3a6,4a6,5a6,6a6,7a6,8a6,9
7a7,0a7,1a7,2a7,3a7,4a7,5a7,6a7,7a7,8a7,9
8a8,0a8,1a8,2a8,3a8,4a8,5a8,6a8,7a8,8a8,9
9a9,0a9,1a9,2a9,3a9,4a9,5a9,6a9,7a9,8a9,9

该 Tensor 的有效数据即上图的深色位置,具体解读如下:

  • offset=22:与示例 1 相同,首元素为 a2,2;
  • 维度 1 的 stride 为 2:同一行内相邻两个有效元素之间间隔 1 个元素,因此每行取 3 个元素,依次为 a2,2、a2,4、a2,6(跳过了列 3、列 5);
  • 维度 0 的 stride 为 20:下一行首元素与当前行首元素间隔 20 个元素,即行号每 +1 需要跳过 20 个元素的位置,因此依次落在第 2、4、6、8 行,共 4 行。

该 Tensor 同样在内存排布上是不连续的:维度 1 上的 stride 为 2,该维度上间隔 1 个元素;维度 0 上的 stride 为 20,相邻元素间隔 20 个元素。它等价于对 10×10 矩阵做"隔行隔列"采样得到的 4×3 视图,两个维度均为非连续,比示例 1 的切片场景更具代表性。

通过示例 1 与示例 2 的对比可以看出:stride 越大,意味着该维度方向上的元素在内存中散布得越开;只要任一维度的 stride 不满足连续递推关系,整个 Tensor 即为非连续,而 offset 仅负责定位视图的起点,不参与连续性的判定。

五、如何创建非连续 aclTensor:aclCreateTensor 实战

创建 aclTensor 统一通过算子库公共接口aclCreateTensor完成(参见 docs/zh/context/data_structure.md 中关于 aclTensor 数据结构的说明,以及《算子库接口》"公共接口 > aclCreateTensor"章节)。从仓库中的算子示例代码 activation/celu_v2/examples/test_aclnn_celu.cpp 可以确认其典型调用形式:

*tensor = aclCreateTensor(shape.data(), // shape:各维度大小 shape.size(), // ndim:维度个数 dataType, // 数据类型,如 ACL_FLOAT strides.data(), // strides:各维度步长 0, // offset:首元素相对基地址的偏移 aclFormat::ACL_FORMAT_ND, // 数据格式 shape.data(), // originShape:原始 shape shape.size(), // originNdim:原始维度个数 deviceAddr); // device 侧基地址

对于连续 Tensor,示例代码中给出了 strides 的标准递推算法——从最后一维开始,最后一维 stride 为 1,前一维 stride 等于后一维 shape 与 stride 的乘积(这正是行优先连续的充要条件):

// 计算连续 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]; }

要创建非连续的 aclTensor,只需把上述"由 shape 推导出的连续 strides"替换为视图实际对应的 strides,并填写非零的 offset。以示例 2 为例(示意写法,基地址沿用 10×10 数据块的 device 地址):

// 非连续视图:shape=(4,3),strides=(20,2),offset=22 std::vector<int64_t> shape = {4, 3}; std::vector<int64_t> strides = {20, 2}; int64_t offset = 22; aclTensor* view = aclCreateTensor(shape.data(), shape.size(), aclDataType::ACL_FLOAT, strides.data(), offset, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), deviceAddr);

注意aclCreateTensor的 offset 参数与示例代码中连续场景传0的区别:连续 Tensor 的首元素即基地址首元素,offset 为 0;非连续场景下必须显式传入视图首元素相对基地址的偏移量。

六、底层机制:算子内部如何消化非连续输入

非连续 Tensor 作为 API 层统一支持的输入形态,其底层处理在 ops-nn 仓库中可以找到对应的实现证据。

以 CELU 算子为例,其二阶段接口的实现 activation/celu_v2/op_api/aclnn_celu.cpp 中采用"先连续化、计算、再拷回"的固定套路:

// 将输入 self 转换成连续的 Tensor auto contiguousSelf = l0op::Contiguous(self, uniqueExecutor.get()); // 在连续 Tensor 上调用算子 kernel auto celuV2Out = l0op::CeluV2(contiguousSelf, alpha->ToFloat(), uniqueExecutor.get()); // 将计算结果拷贝到输出 out 上,out 可能是非连续的 tensor auto viewCopyResult = l0op::ViewCopy(castOut, out, uniqueExecutor.get());

即算子内部先通过Contiguous把非连续输入规整为连续张量参与 kernel 计算,计算完成后通过ViewCopy将结果按输出 aclTensor 的 strides/offset 视图写回,从而对调用方屏蔽非连续排布的细节。该算子头文件 activation/celu_v2/op_api/aclnn_celu.h 与 README(activation/celu_v2/README.md)中均明确标注"支持非连续 Tensor"。

另一方面,公共 fallback 层 common/inc/op_graph/fallback_common_2stages_nn.h 展示了非连续视图的另一种典型用法:在对 MatMul 类算子做转置处理时,直接通过交换对应维度的 strides 构造一个非连续视图,实现零拷贝的"虚拟转置":

if (transpose) { auto swap = strides[dimN]; strides[dimN] = strides[dimM]; strides[dimM] = swap; // 交换 strides 即可得到转置视图,无需搬移数据 viewShape[dimN] = shape[dimM]; viewShape[dimM] = shape[dimN]; }

从源码结构看,非连续 Tensor 机制正是通过"视图描述(strides/offset)+ 显式连续化(Contiguous)+ 结果写回(ViewCopy)"的组合,让上层 API 既能享受零拷贝描述视图的收益,又能让下层 kernel 始终基于规整数据计算,兼顾了灵活性与性能。

七、使用注意事项与限制

  1. 支持范围:目前大部分算子 API 的输入 aclTensor 支持非连续 Tensor,但并非全部。个别 API 是否支持、支持哪些维度的非连续,需以该 API 实际文档/README 的"非连续 Tensor"标注为准(例如 activation/celu_v2/docs/aclnnCelu&aclnnInplaceCelu.md 的参数表中逐项标注了输入输出的非连续支持情况)。
  2. 数据格式约束:非连续描述基于 ND(行优先)排布语义展开;非 ND 的私有格式(如 FRACTAL_NZ 等)当前绝大多数 aclnn API 尚不支持,具体以 API 描述为准,详见 docs/zh/context/data_format.md。
  3. 与空 Tensor 等特性的叠加:部分算子在支持非连续 Tensor 的同时还支持空 Tensor 与 0~8 维输入,使用时应一并核对 API 的完整约束。
  4. 更多基本概念:非连续 Tensor 属于算子库"基本概念"体系的一部分,与之配套的还有两段式接口、数据结构、数据类型、broadcast 关系、互推导关系等,可参阅 docs/zh/context/basic_concept.md 总览全部概念。

综上,(shape, strides, offset)三要素构成了 CANN ops-nn 描述非连续 Tensor 的完整模型:shape 决定逻辑形状,strides 决定各维度的内存间隔与连续性,offset 定位视图起点。理解并善用这一机制,可以在切片、转置、采样等场景下以零拷贝方式构造输入视图,同时借助算子内部 Contiguous + ViewCopy 的支撑,让非连续输入与普通连续输入一样直接参与 NPU 计算。

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

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

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

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

立即咨询