4.1 Tensor 与自动微分
Tensor 和 autograd 是 PyTorch 的两块基石——前者决定了”数据怎么存、怎么算”,后者决定了”梯度怎么来”。本文从 Tensor 的创建、索引、变形、内存布局讲起,再深入 autograd 的计算图机制、梯度累积与常见踩坑,帮你建立扎实的底层认知,为后续的模型搭建、分布式训练和性能优化打好地基。
📑 目录
- 1. Tensor:一切计算的载体
- 2. 索引与切片
- 3. 形状变换
- 4. 广播机制
- 5. 设备管理与数据类型
- 6. 自动微分 autograd
- 7. 实战:从零实现梯度下降
- 总结
- 自我检验清单
- 参考资料
1. Tensor:一切计算的载体
打个比方:如果深度学习是盖房子,Tensor 就是砖头。模型的权重是砖头,输入数据是砖头,中间的计算结果也是砖头——PyTorch 里一切皆 Tensor。
正式定义:Tensor(张量)是一种多维数组数据结构,可以视为 NumPy ndarray 的超集——不仅支持 GPU 加速运算,还能接入自动微分引擎,是深度学习计算的基本单位。
1.1 创建 Tensor 的常用方式
PyTorch 提供了多种创建 Tensor 的方法,可以分为三大类:
从已有数据创建:
1 | import torch |
用固定值/分布创建:
1 | import torch |
按已有 Tensor 的形状创建:
1 | import torch |
1.2 Tensor 的核心属性
每个 Tensor 都有四个核心属性,任何时候拿到一个 Tensor,先看这几样:
| 属性 | 含义 | 示例 |
|---|---|---|
shape / size() |
各维度的大小 | torch.Size([2, 3]) |
dtype |
数据类型 | torch.float32 |
device |
所在设备 | device(type='cuda', index=0) |
requires_grad |
是否追踪梯度 | True / False |
1 | import torch |
💡 提示:在调试模型时,90% 的错误可以通过检查 shape、dtype、device 这三样来定位。形状不匹配、dtype 不一致、设备不同——这是 PyTorch 开发中最常见的三类报错。
1.3 基本运算
Tensor 支持完整的数学运算,和 NumPy 的用法高度一致:
1 | import torch |
AI Infra 视角:深度学习中绝大部分计算归结为矩阵乘法(GEMM)。理解 Tensor 运算的本质,能帮你在后续 CUDA 编程和算子优化中更精准地定位计算瓶颈。
2. 索引与切片
Tensor 的索引方式和 NumPy 几乎相同,但在 AI 场景下有几种高频用法值得单独掌握。
2.1 基础索引
1 | import torch |
2.2 布尔索引与花式索引
布尔索引在数据过滤和掩码操作中极为常用——Attention 的 mask 本质上就是布尔索引的一种应用。
1 | import torch |
2.3 索引与内存:视图 vs 副本
⚠️ 注意:基础索引(切片)返回的是视图(view),和原 Tensor 共享内存;高级索引(花式索引、布尔索引)返回的是副本(copy)。这个区别在处理大 Tensor 时直接影响显存占用。
1 | import torch |
3. 形状变换
形状变换是 Tensor 操作中最频繁的动作之一。在 Transformer 的实现里,几乎每一步都伴随着 view、permute、reshape——搞不清楚它们的区别,读模型代码就像看天书。
3.1 view 与 reshape:看似相同,内在有别
打个比方:一本 12 页的书,你可以说它是”3 章 × 4 页”,也可以说是”4 章 × 3 页”——内容(数据)没变,只是目录(形状)变了。view 和 reshape 做的就是这件事。
1 | import torch |
区别在哪里?关键词是内存连续(contiguous):
view要求底层内存连续,如果不连续会直接报错reshape在内存连续时等价于view,不连续时会自动拷贝一份连续内存再变形
1 | import torch |
💡 提示:日常开发中,如果你不确定内存是否连续,用 reshape 更安全。但在性能敏感的场景下(比如写 CUDA 内核),需要明确知道 view 不会触发内存拷贝。
3.2 permute 与 transpose:维度换位
permute 是 Transformer 代码里的常客——Multi-Head Attention 需要在 (batch, seq, heads, dim) 和 (batch, heads, seq, dim) 之间反复切换。
1 | import torch |
⚠️ 注意:permute 和 transpose 都不拷贝数据,只是改变了 stride(步长),所以返回的 Tensor 通常不是 contiguous 的。如果后续操作要求连续内存,需要显式调用 .contiguous()。
3.3 多头注意力中的典型变形
在 Transformer 实现中,最经典的变形操作就是将线性投影结果拆分成多个注意力头。这个 reshape → permute 的组合在 Transformer 代码中几乎无处不在:
1 | import torch |
3.4 squeeze / unsqueeze / expand
这三兄弟负责维度的”增删扩”:
1 | import torch |
3.5 contiguous 与 stride:理解内存布局
这部分稍微底层一些,但对于 AI Infra 工程师来说很有价值——理解 stride 能帮你判断哪些操作是”零拷贝”的,哪些会偷偷分配新内存。
打个比方:想象一本书的页码。正常翻页时,每翻一页,物理位置跳 1(stride=1)。如果你把书拆成左右两栏来读,”翻一栏”跳的物理位置就不再是 1 了——这就是 stride 的本质:从一个元素到下一个元素,需要在内存中跳多少步。
1 | import torch |
📌 关键点:view、permute、transpose、expand 等操作只改变 shape 和 stride,不会拷贝数据。只有当后续操作需要连续内存(如 view)或者你显式调用 .contiguous() 时,才会触发真正的内存拷贝。
4. 广播机制
广播(Broadcasting)是 NumPy 和 PyTorch 都遵循的规则——当两个形状不完全匹配的 Tensor 做运算时,自动将较小的 Tensor “扩展”到兼容的形状。
4.1 广播规则
规则很简单,从右往左逐维比较:
- 如果维度大小相同,直接对齐
- 如果其中一个大小为 1,将其扩展到另一个的大小
- 如果维度个数不同,在较少维度的 Tensor 前面补 1
1 | import torch |
4.2 实际应用场景
广播在深度学习中无处不在。Attention 分数加 mask、BatchNorm 减均值、偏置相加——这些都是广播操作:
1 | import torch |
⚠️ 注意:广播不分配新内存,但它可能会掩盖形状错误。如果你不小心传了一个形状略有偏差的 Tensor,广播不会报错,而是默默算出一个形状更大的结果——这种 bug 极其隐蔽。养成习惯:关键运算前 assert 一下形状。
5. 设备管理与数据类型
5.1 CPU ↔ GPU 搬运
1 | import torch |
AI Infra 视角:CPU→GPU 搬运走 PCIe 总线(Gen4 x16 单向 ≈ 32 GB/s),而 GPU 显存带宽远高于此(A100 HBM2e 约 2 TB/s,H100 HBM3 约 3.35 TB/s),两者差距可达 60~100 倍。频繁的
.cpu()和.cuda()调用是常见的性能杀手。正确做法:数据搬上 GPU 后尽量留在 GPU 上完成所有计算。
设备间数据传输带宽参考:
| 路径 | 典型带宽 |
|---|---|
| GPU 显存(HBM)内部 | 2-5 TB/s(A100: 2 TB/s, H100: 3.35 TB/s, H200: 4.8 TB/s) |
| PCIe Gen4 x16 | ~32 GB/s |
| PCIe Gen5 x16 | ~64 GB/s |
| NVLink(GPU 间) | 600-1800 GB/s(A100: 600, H100: 900, B200: 1800) |
5.2 设备一致性规则
所有参与运算的 Tensor 必须在同一个设备上。 这是 PyTorch 最常见的报错之一:
1 | import torch |
良好的习惯是在模型代码中统一设备管理:
1 | device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') |
5.3 多 GPU 场景下的设备指定
1 | import torch |
5.4 dtype:精度决定显存和速度
不同的数据类型直接影响显存占用和计算吞吐:
| dtype | 别名 | 位宽 | 典型用途 |
|---|---|---|---|
torch.float32 |
torch.float |
32 | 默认浮点类型,优化器状态 |
torch.float16 |
torch.half |
16 | 混合精度训练(需 GradScaler) |
torch.bfloat16 |
— | 16 | 大模型训练首选(指数范围与 fp32 相同) |
torch.float64 |
torch.double |
64 | 科学计算,深度学习极少用 |
torch.int64 |
torch.long |
64 | 默认整型,token ID、索引 |
torch.int32 |
torch.int |
32 | 较短的索引 |
torch.int8 |
— | 8 | 量化推理 |
torch.bool |
— | 8 | 掩码(mask) |
torch.float8_e4m3fn |
— | 8 | FP8 训练/推理(H100+ GPU) |
白话理解:fp32 像高清原图,精度好但占空间大;bf16 像智能压缩——体积减半,保留了 fp32 的指数范围(不容易溢出),但有效精度从 24 位降到了 8 位;fp16 有效精度 11 位(比 bf16 高),但指数范围只有 5 位,训练大模型时容易出现梯度下溢,需要配合 GradScaler 使用。
1 | import torch |
6. 自动微分 autograd
如果说 Tensor 是 PyTorch 的”骨骼”,autograd 就是”神经系统”——它自动帮你算梯度,让训练神经网络从”手动推导每一层的偏导数”变成”调一行 .backward()“。
6.1 计算图:autograd 的核心数据结构
比喻:想象你在烘焙蛋糕。面粉和鸡蛋搅拌成面糊,面糊进烤箱变成蛋糕——如果蛋糕味道不对(loss 太大),你需要反向追溯:是烤箱温度的问题,还是面粉和鸡蛋的比例不对?计算图就是 PyTorch 帮你记下来的”烘焙流程”:它记录每一步操作,这样就能从最终结果(蛋糕/loss)反向推导出每种原料(参数)对结果的影响程度(梯度)。
正式定义:计算图(Computational Graph)是一个有向无环图(DAG)。节点是 Tensor,边是运算操作(如加法、乘法、矩阵乘法)。PyTorch 在前向传播时动态构建这张图,在反向传播时沿着图的反方向计算梯度,然后立即释放整张图。
PyTorch 的特色是动态图(Define-by-Run):图是在每次前向传播时”现场”搭建的,因此天然支持 if/else、for 循环等 Python 控制流——这是它比静态图框架更灵活的根本原因。
6.2 requires_grad 与叶节点
并不是所有 Tensor 都需要计算梯度。requires_grad=True 就像给 Tensor 贴了一张标签:”请记录我参与的所有运算,以便之后算梯度。”
1 | import torch |
📌 关键点:只有叶节点的 .grad 属性才会在 backward() 后被填充。非叶节点的梯度在计算完成后会被释放(除非显式调用 y.retain_grad())。这个设计是出于显存效率考虑——模型可能有数十亿参数,保留所有中间梯度会占用巨大的显存。
上面的例子对应这样一张计算图——前向传播从叶子到输出(从上到下),反向传播从输出到叶子(从下到上),沿着 grad_fn 的链条追溯:
graph TD
W["w (叶子, requires_grad=True)"] --> MUL["× (MulBackward0)"]
X["x (叶子, requires_grad=False)"] --> MUL
MUL --> Y["y (非叶子)"]
Y --> SUM["sum (SumBackward0)"]
SUM --> Z["z (标量)"]
6.3 backward() 与链式法则
backward() 做的事情就是沿着计算图逆向执行链式法则,把梯度从输出一路传回到每个叶节点。
从数学角度看,假设有运算链:
$$
z = f(y), \quad y = g(w)
$$
链式法则告诉我们:
$$
\frac{\partial z}{\partial w} = \frac{\partial z}{\partial y} \cdot \frac{\partial y}{\partial w}
$$
backward() 就是自动完成这个逐层求导的过程。
1 | import torch |
6.4 用数学手动验证 autograd
来做一个稍复杂的验证,确保对 autograd 的理解是准确的:
$$
f(x) = x^3 + 2x^2 - 5x + 1
$$
$$
f’(x) = 3x^2 + 4x - 5
$$
当 $x = 2$ 时,$f’(2) = 3 \times 4 + 4 \times 2 - 5 = 15$。
1 | import torch |
这个例子验证了 autograd 能正确处理多项式的链式求导——无论函数多复杂,backward() 都能自动算出精确的梯度。
几条重要规则:
backward()只能对标量调用。如果 z 不是标量,需要传入一个与 z 同形状的gradient参数- 计算图用完即释放。第二次
backward()会报错——除非创建图时指定retain_graph=True - 梯度流经
requires_grad=True的路径。如果某条路径上所有 Tensor 都不需要梯度,这条路径不会被计算
1 | import torch |
6.5 梯度累积与清零
白话理解:PyTorch 的梯度就像一个只进不出的存钱罐——每次
backward()往里塞钱(累加梯度),但它不会自动清空。标准训练循环里,每个 step 开始前必须手动清零,否则上一轮的”存款”会混进来,搞乱优化方向。
这个设计不是 bug,而是有意为之:梯度累积是显存不够时模拟大 batch 的经典技巧——累积 N 个小 batch 的梯度再统一更新,效果等价于一次性跑一个 N 倍大的 batch。
1 | import torch |
实际训练中用 optimizer.zero_grad() 一次性清零所有参数的梯度:
1 | # 标准训练循环中的梯度处理 |
梯度累积实战示例:
1 | import torch |
6.6 torch.no_grad() 与 inference_mode
推理(inference)和评估(eval)时不需要计算梯度,关掉梯度追踪可以省显存、加速度。
1 | import torch |
两者的区别:
| 特性 | torch.no_grad() |
torch.inference_mode() |
|---|---|---|
| 禁用梯度计算 | ✅ | ✅ |
| 可以对结果做 in-place 操作 | ✅ | ✅ |
| 结果可以在外部参与梯度计算 | ✅ | ❌ |
| 性能 | 快 | 更快 |
💡 提示:如果你确定推理结果不会再参与任何梯度计算,用 inference_mode() 能获得更好的性能。在训练循环的验证阶段,用 no_grad() 更稳妥。
6.7 常见踩坑指南
踩坑 1:Device Mismatch——设备不一致
1 | import torch |
踩坑 2:in-place 操作破坏计算图
1 | import torch |
⚠️ 注意:PyTorch 中以 _ 结尾的方法(如 add_、mul_、zero_)都是 in-place 操作。对需要梯度追踪的 Tensor 使用 in-place 操作,可能导致计算图被破坏、梯度计算出错。规则很简单:如果 Tensor 参与了 autograd,就不要做 in-place 操作(zero_grad() 除外,它是特殊处理的)。
踩坑 3:忘记清零梯度导致训练不收敛
1 | # 错误写法:忘了 zero_grad |
踩坑 4:.item() 或 print(tensor) 导致隐式同步
在 CUDA 的异步执行模型中,Python 端提交 kernel 后不会等它执行完就继续往下跑。但 .item() 需要把 GPU 上的值搬到 CPU,这迫使 CPU 等待 GPU 完成所有排队的 kernel——相当于在 GPU 流水线上人为插了一道”路障”,打断了异步执行的节奏。
1 | import torch |
不过 .item() 也有正面用途——用它提取标量值可以避免 Tensor 累加导致的计算图泄漏:
1 | # ❌ 不推荐:用 Tensor 做 Python 累加,会保留整个计算图链条,导致显存泄漏 |
踩坑 5:detach() 的正确使用场景
1 | import torch |
💡 提示:detach() 在需要把中间结果”截断”梯度流时很有用,比如实现 stop-gradient 操作。另一个常见场景是将 GPU Tensor 转为 NumPy 数组——必须先 detach 再 cpu 再 numpy,这三步缺一不可:
1 | # Tensor → NumPy 的标准操作 |
7. 实战:从零实现梯度下降
把上面学到的 Tensor 操作和 autograd 串起来,手动实现一个最简单的线性回归训练,不用任何 nn.Module 和 optim。
目标:学习函数 $y = 3x + 1$
1 | import torch |
这段代码完整展示了 Tensor 的创建、广播运算、autograd 的前向/反向传播、梯度读取与清零,以及 torch.no_grad() 在参数更新中的使用——把本文的核心知识点串成了一条完整的链路。
📝 总结
本文覆盖了 PyTorch 的两大基石:
| 模块 | 核心内容 | 关键概念 |
|---|---|---|
| Tensor 创建 | 多种创建方式、属性三件套 | shape, dtype, device |
| 索引与切片 | 基础索引、布尔索引、花式索引 | 视图 vs 副本 |
| 形状变换 | view/reshape/permute/squeeze | contiguous, stride |
| 广播机制 | 自动形状扩展规则 | 从右往左逐维比较 |
| 设备与精度 | CPU↔GPU 搬运、dtype 选择 | fp32/fp16/bf16 |
| autograd | 计算图、backward、梯度累积 | 叶节点、requires_grad、链式法则 |
这些是 PyTorch 编程的”内功心法”——后续无论是搭建模型(4.2)、做性能调优(4.3),还是分布式训练,都建立在对这些基础概念的透彻理解之上。
🎯 自我检验清单
完成本文学习后,你应该能够:
- 能用至少 3 种方式创建指定形状和数据类型的 Tensor
- 能解释
view和reshape的区别,以及contiguous()何时需要调用 - 能说明
permute返回的 Tensor 为什么通常不是 contiguous 的(提示:stride 变了但数据没动) - 能手写多头注意力中
reshape → permute的维度变换过程 - 能手写广播规则的三条判断逻辑,并判断两个给定形状能否广播
- 能解释
from_numpy()和tensor()在内存共享上的区别 - 能画出一段简单 PyTorch 代码的计算图,标出叶节点和
grad_fn - 能手动验证 autograd 对多项式函数求导的正确性
- 能解释为什么训练循环中必须调用
optimizer.zero_grad(),以及梯度累积的工作原理 - 能写出不使用任何
nn.Module的手动梯度下降训练代码 - 能说明
torch.no_grad()和torch.inference_mode()的区别及适用场景 - 能用
detach().cpu().numpy()将 GPU Tensor 转为 NumPy 数组 - 能识别并修复 Device Mismatch、in-place 操作破坏计算图、
.item()导致隐式同步等常见问题