pytorch api文档:连续张量、非连续张量 作者:马育民 • 2026-01-21 19:21 • 阅读:10000 # 介绍 “连续张量”和“非连续张量”,两者的核心差异是 **张量元素在内存中的存储顺序,是否和“按维度遍历”的顺序一致**。 # 张量的内存是“一维的” PyTorch 中无论张量是几维的(2维、3维、4维),其所有元素在计算机内存中都是 **线性平铺存储** 的(一维排列)。所谓的“维度”(比如 `[2,3]` 的行和列),只是 PyTorch 给这段一维内存加的“解读规则”。 ### 例子 ```python 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`) ```python 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`) ```python 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()`: - 若原张量已连续:直接返回自身(无开销); - 若原张量非连续:创建一个**内存连续的新副本**(数据和原张量一致,但内存顺序重新排列,不再共享内存)。 ### 示例 ```python # 非连续张量 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. 核心本质:张量的“维度解读规则”和“内存存储顺序”是否匹配,匹配则连续,不匹配则非连续。 原文出处:http://malaoshi.top/show_1GW2dN6bwpOg.html