介绍
“连续张量”和“非连续张量”,两者的核心差异是 张量元素在内存中的存储顺序,是否和“按维度遍历”的顺序一致。
张量的内存是“一维的”
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() |
为什么要关心“连续性”?
.view()的硬性要求:.view()是“重新解读内存”的操作,只能处理“整齐的内存”(连续张量),非连续张量直接调用会报错(因为无法按新形状解读跳着的内存);- 计算效率:连续张量的内存访问是线性的,CPU/GPU 缓存能高效命中,计算速度远快于非连续张量;
- 算子兼容性:部分 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]
总结
- 连续张量:内存顺序 = 遍历顺序,步长规则满足,可直接
.view(),计算效率高; - 非连续张量:内存顺序 ≠ 遍历顺序,步长规则不满足,
.view()前需.contiguous(),计算效率低; - 核心本质:张量的“维度解读规则”和“内存存储顺序”是否匹配,匹配则连续,不匹配则非连续。