pytorch api文档:连续张量、非连续张量

介绍

“连续张量”和“非连续张量”,两者的核心差异是 张量元素在内存中的存储顺序,是否和“按维度遍历”的顺序一致。

张量的内存是“一维的”

PyTorch 中无论张量是几维的(2维、3维、4维),其所有元素在计算机内存中都是 线性平铺存储 的(一维排列)。所谓的“维度”(比如 [2,3] 的行和列),只是 PyTorch 给这段一维内存加的“解读规则”。

例子

x = torch.tensor([[1, 2, 3],
                  [4, 5, 6]])

这个 [2,3] 的二维张量,在内存中是 线性存储 的:

1 → 2 → 3 → 4 → 5 → 6

而“二维维度”只是告诉我们:前3个元素是第一行,后3个是第二行。

连续张量(Contiguous Tensor)

定义

元素的内存存储顺序,和“按维度逐行遍历张量”的顺序完全一致,且满足“步长规则”。

关键特征

  • 遍历顺序 = 内存顺序:按 行→列(PyTorch 默认行优先)遍历张量的元素,和内存中读取的顺序完全一样;

  • 步长规则:对 N 维张量,第 i 维的步长 = 第 i+1 维的步长 × 第 i+1 维的大小(步长 stride 表示遍历该维度时,每次跳多少个元素)。

示例(还是上面的 x)

print(x.stride())  # 输出:(3, 1)
  • 维度1(也就是列)的步长是1:跳过1列,只需在内存中往后跳1个元素(从 1 到 2);

  • 维度0(也就是行)的步长是3:跳过1行,需要在内存中往后跳3个元素(从第一行第一个 1 到第二行第一个 4,跳了 1→2→3→4 共3步);

满足步长规则:

3 = 1 × 3 // 维度0步长 = 维度1步长 × 维度1大小

因此是连续张量。

直观理解

连续张量的内存就像“整齐排列的一排格子”,按维度遍历就是顺着格子读,不用回头、不用跳跃。


非连续张量(Non-contiguous Tensor)

定义

元素的 内存存储顺序,和“按维度逐行遍历张量”的顺序不一致,且不满足步长规则。

关键特征

  • 遍历顺序 ≠ 内存顺序:按维度遍历的元素顺序,和内存中实际存储的顺序脱节;

  • 步长规则不满足:维度的步长不符合“前一维步长 = 后一维步长 × 后一维大小”的规则;

  • 通常由 transpose()/permute()/切片(如 x[:,::2])等操作触发(这些操作只改“解读规则”,不改内存存储)。

示例(转置后的 x)

x_trans = x.transpose(0, 1)  # 交换行和列,变成 [3,2]
print(x_trans)  # 输出:[[1,4], [2,5], [3,6]]
print(x_trans.stride())  # 输出:(1, 3)

对 (1, 3) 的解释:

  • 维度1(也就是列)的步长是 3:跳过 1 列,需要在内存中跳 3 个元素,显然是 错误的

  • 维度0(行)的步长是 1 :跳过 1 行,只需在内存中跳1 个元素,显然是 错误的

代入 步长规则:第 i 维的步长 = 第 i+1 维的步长 × 第 i+1 维的大小,:`

不满足 步长规则:

1 ≠ 3 × 2 // 维度0步长 ≠ 维度1步长 × 维度1大小

因此是 非连续张量

正确步长

步长应该是:(2,1),

  • 维度1(也就是列)的步长是 1:跳过 1 列,需要在内存中跳 1 个元素

  • 维度0(行)的步长是 2 :跳过 1 行,只需在内存中跳2 个元素

代入 步长规则:第 i 维的步长 = 第 i+1 维的步长 × 第 i+1 维的大小,:`

满足 步长规则:

2 = 1 × 2 // 维度0步长 = 维度1步长 × 维度1大小

直观理解

非连续张量的内存还是原来的“整齐格子”,但“解读规则”被改了——按新的维度遍历,需要在内存中“跳着读”(比如读 x_trans 的第一行是 1→4,内存中要从 1 跳3步到 4),就像“看打乱的页码读书”。

打印数值,看不出是否连续

是否连续张量,打印数值是看不出来的,两者完全一样

打印矩阵时,PyTorch 会按照 “维度规则” 遍历张量并输出数值 —— 无论内存是否连续,遍历的 “逻辑结果” 都是一致的。

例子

  • 非连续的 :内存中还是 1→2→3→4→5→6,但 PyTorch 会按 stride=(1,3) 的规则遍历,取出 1(0×1+0×3)→4(0×1+1×3)→2(1×1+0×3)→5(1×1+1×3)→3(2×1+0×3)→6(2×1+1×3),最终打印出 [[1,4],[2,5],[3,6]];

  • 连续的 :内存已重新排列为 1→4→2→5→3→6,按 stride=(2,1) 的规则遍历,取出的数值顺序和前者完全一致,因此打印结果相同。

简单说:打印只关心 “最终显示的数值顺序”,不关心 “这些数值在内存中是怎么存的”—— 而 “连续 / 非连续” 恰恰是内存存储方式的差异,而非数值顺序的差异。

差异对比

特征 连续张量 非连续张量
内存顺序 vs 遍历顺序 完全一致 不一致
步长规则 满足(前维步长=后维步长×后维大小) 不满足
.is_contiguous() 返回 True 返回 False
.view() 兼容性 可直接调用 直接调用报错,需先 .contiguous()
内存访问效率 高(线性读取,缓存命中率高) 低(跳着读,缓存易失效)
常见触发操作 直接创建(torch.ones/randn)、.clone()、.contiguous() .transpose()/.permute()、切片(x[:,::2])、.t()

为什么要关心“连续性”?

  1. .view() 的硬性要求:.view() 是“重新解读内存”的操作,只能处理“整齐的内存”(连续张量),非连续张量直接调用会报错(因为无法按新形状解读跳着的内存);
  2. 计算效率:连续张量的内存访问是线性的,CPU/GPU 缓存能高效命中,计算速度远快于非连续张量;
  3. 算子兼容性:部分 CUDA 算子、底层优化仅支持连续张量,非连续张量可能导致报错或性能下降。

如何处理非连续张量?

唯一的方法是调用 .contiguous():

  • 若原张量已连续:直接返回自身(无开销);
  • 若原张量非连续:创建一个内存连续的新副本(数据和原张量一致,但内存顺序重新排列,不再共享内存)。

示例

# 非连续张量
x_trans = x.transpose(0,1)
# x_trans.view(-1)  # 报错!

# 转为连续张量
x_trans_contig = x_trans.contiguous()
print(x_trans_contig.is_contiguous())  # True
print(x_trans_contig.view(-1))  # 正常输出:[1,4,2,5,3,6]

总结

  1. 连续张量:内存顺序 = 遍历顺序,步长规则满足,可直接 .view(),计算效率高;
  2. 非连续张量:内存顺序 ≠ 遍历顺序,步长规则不满足,.view() 前需 .contiguous(),计算效率低;
  3. 核心本质:张量的“维度解读规则”和“内存存储顺序”是否匹配,匹配则连续,不匹配则非连续。

原文出处:/show_1GW2dN6bwpOg.html