# Tensor 与 Storage · 同一块内存的不同看法

> 你天天用的 Tensor，其实不是数据本身，而是一张「怎么读一块扁平内存」的说明书——sizes、strides、offset。看懂这个，view/transpose/切片为什么零拷贝、contiguous 是什么、view 和 reshape 差在哪，全都通了。

- 作者：David（道雾轩）
- 专栏：PyTorch 源码漫游（https://daiw.org/manual/pytorch-internals.md）
- 最后更新：2026-07-13
- 原文：https://daiw.org/manual/pytorch-internals/tensor-and-storage
- 转载与引用：请注明出处并附原文链接（https://daiw.org/about/copyright）

# Tensor 与 Storage · 同一块内存的不同看法

漫游从最底下的地基开始。你天天 `torch.tensor(...)`，但**一个 Tensor 到底是什么？** 答案有点反直觉，而它是理解后面一切的钥匙：

> **Tensor 不是数据本身。Tensor 是一张「怎么读一块扁平内存」的说明书。真正的数据，躺在它下面的 `Storage` 里。**

## Storage：一条没有形状的数

先看底下那层。**`Storage`（存储）** 就是**一维、连续的一块内存**——一长条挨着排的数字，**没有任何形状信息**。它只知道「这里有 N 个 `float`，一个挨一个」，不知道自己是 `2×3` 还是 `3×2`，也不知道哪是行哪是列。

```
Storage(6 个 float):  [ 1.0  2.0  3.0  4.0  5.0  6.0 ]
```

就这样，一条扁平的数。**形状？顺序？怎么切？——这些都不在 Storage 里，而在上面的 Tensor 说明书里。**

## Tensor：读这条数的「说明书」

一个 **`Tensor`** = 一个 `Storage` + 一小撮**元数据(metadata)**:

- **`sizes`（形状）**：比如 `(2, 3)`——「把这条数当成 2 行 3 列来读」。
- **`strides`（步长）**：每个维度「走一步」要在 Storage 里跳几个元素。这是最关键、也最容易被忽略的一个。
- **`storage_offset`（偏移）**：从 Storage 的第几个元素开始读。
- **`dtype`、`device`**：数据类型、在 CPU 还是哪张 GPU 上。

有了这张说明书，读 `tensor[i, j]` 就是一道简单的算术：

$$
\texttt{tensor[i,j]} = \texttt{Storage}[\ \text{offset} + i \cdot \text{stride}[0] + j \cdot \text{stride}[1]\ ]
$$

拿上面那条 6 个数举例，配一张 `sizes=(2,3)`、`strides=(3,1)`、`offset=0` 的说明书，就读成：

```
              列 j→
          storage[ 0+i*3+j*1 ]
行 i↓   [ 1.0  2.0  3.0 ]      ← i=0: storage[0],[1],[2]
        [ 4.0  5.0  6.0 ]      ← i=1: storage[3],[4],[5]
```

`stride=(3,1)` 的意思：**行走一步跳 3 个**（跨过一整行）、**列走一步跳 1 个**（挨着的下一个）。这就是所谓的「行主序(row-major)/ C 连续」布局。

## 魔法揭穿：view / transpose / 切片，全是「改说明书」

一旦你接受「Tensor 只是说明书」，一堆看着神奇的操作立刻变得平平无奇——**它们根本不动数据，只是造一张新说明书，指向同一个 Storage**。所以它们**零拷贝、极快**：

- **转置 `x.t()` / `transpose`**：把两个维度的 stride **对调**一下就完事。同一条数，`strides=(3,1)` 改成 `(1,3)`，读出来就「转置」了——**一个字节都没搬**。
- **切片 `x[1:]` / `x[::2]`**：改 `offset` 和 `sizes`（隔行取还会改 stride），照样指着原 Storage。
- **`view` / `reshape`**：换个 `sizes`，重新解释同一条数。
- **广播 `expand`**：把某个维度的 stride 设成 **0**——「这一维走一步，内存不动」，于是一份数据被「假装」重复了很多份，还是零拷贝。

<Callout type="info">
  这解释了一个新手常踩的坑：**`b = a.t()` 之后改 `b`，`a` 也变了。** 因为 `b` 和 `a` 共享同一个 Storage，`b` 只是 `a` 的另一张说明书。想要独立的一份，得 `.clone()` 真正拷贝底层数据。**「谁和谁共享内存」，看的就是它们是不是指向同一个 Storage。**
</Callout>

## contiguous：说明书和内存「对不对得齐」

既然 transpose 只是换 stride 不搬数据，就带来一个问题：**转置后，元素在内存里的物理顺序，和你「按行读」的逻辑顺序对不上了。**

- 一个 tensor 叫**连续(contiguous)**，当它的 strides 正好是「紧凑打包」的标准布局（逻辑顺序 = 物理顺序）。
- `transpose` 之后，strides 变成 `(1,3)`，不再紧凑——它**非连续**了。

很多底层内核（尤其涉及 `view` 的）要求输入是连续的，否则那道 `offset + i*stride` 的寻址假设会崩。这时就得 **`.contiguous()`**：它**真正拷贝一份数据**，按紧凑布局重排，让说明书和内存重新对齐。

<Callout type="warn">
  还记得 [nanoGPT 注意力那章](https://daiw.org/manual/nanogpt/attention)最后那行 `y.transpose(1, 2).contiguous().view(B, T, C)` 吗？**现在你彻底懂它了**：`transpose` 换了 stride（非连续），而紧接着的 `view` 要求连续，所以中间必须 `.contiguous()` 先把内存排整齐，`view` 才不会报错。当时说「view 前得先把内存排整齐」，底下就是这套 stride 机制。
</Callout>

这也顺带讲清了 **`view` 和 `reshape` 的区别**：`view` 只肯「改说明书」，所以要求输入连续，否则报错；`reshape` 更随和——能改说明书就改（零拷贝），不能就替你 `.contiguous()` 拷一份。想要「零拷贝或报错」的确定性用 `view`，想要「反正给我搞定」用 `reshape`。

## 源码在哪

想去源码里印证：

- 这张「说明书」在 C++ 里叫 **`TensorImpl`**(`c10/core/TensorImpl.h`)——里面就存着 sizes、strides、storage_offset、dtype、device，以及一个指向 `StorageImpl` 的指针。
- 底层那条扁平数是 **`StorageImpl`**(`c10/core/StorageImpl.h`)，它裹着一个 `DataPtr` 指向真正的内存。
- 你在 Python 里拿到的 `torch.Tensor`，就是对一个 C++ `TensorImpl` 的包装。

`c10` 是 PyTorch 的核心库（**C**ore + Ten**sor**...大致这个意思），Tensor 的地基就浇在这儿。

## 地基打好了，该看「调用」怎么走

「Tensor 是说明书、Storage 是数据」——记住这一条，后面全都顺。现在数据结构清楚了，下一站我们跟着一次真实的调用往下走：你写下 `a + b`，这个 `+` 是怎么从 Python 一路找到「该在 CPU 还是 CUDA 上、该不该记梯度」的正确内核的？答案是 PyTorch 的中枢——**dispatcher**。👉 [一次 a + b 的旅程：dispatcher 分发器](https://daiw.org/manual/pytorch-internals/dispatch-journey)

## 参考文献

- [1] Edward Z. Yang. “PyTorch internals.” 2019. http://blog.ezyang.com/2019/05/pytorch-internals/ —— Tensor / Storage / strides 的权威讲解，本章主要依据。
- [2] Paszke et al. “PyTorch: An Imperative Style, High-Performance Deep Learning Library.” NeurIPS 2019. arXiv:1912.01703. —— PyTorch 整体设计。
- [3] PyTorch 文档：“Tensor Views.” https://pytorch.org/docs/stable/tensor_view.html —— 哪些操作是零拷贝视图。
- 源码：`c10/core/TensorImpl.h`、`c10/core/StorageImpl.h`(pytorch/pytorch)。
