PyTorch02-PyTorch基础知识

二、PyTorch基础知识

本章的的内容包含如下:张量概念及特点、张量的运算、自动微分、并行计算简介以及并行计算加速设备。

2.1 张量

2.1.1 张量简介

张量就是“多维数组”,其中的元素的数据必须一致。它是 PyTorch 传输、存储和计算数据的统一标准载体

无论在深度学习中处理的是什么数据,最终都会被转化成张量:

  • 0维张量:标量(Scalar),就是一个独立的数字(如 loss = 0.05)。
  • 1维张量:向量(Vector),如一维数组 [1.0, 2.0, 3.0]
  • 2维张量:矩阵(Matrix),如 Excel 表格或灰度图(高 × 宽)。
  • 3维张量:时间序列数据、股价、单张彩色图像 (宽, 高, 通道) 或一段文本序列(Batch × 序列长度)。
  • N维张量:更高维度的数据集合(比如 4维张量常用来表示一批彩色图片 (Batch_size, Channel, Height, Width))。

张量的核心是一个数据容器,一般情况下由数字组成,但也能由字符串组成。

张量 torch.Tensor 是PyTorch中存储和变换数据的主要工具。它跟Numpy多维数组非常类似,可以联想起来对比学习。但Tensor 天生地提供了GPU计算和自动求梯度等更多功能,使用其更加适用于深度学习。

2.1.2 创建张量

创建张量的方法有多种,可以根据需求使用不同方法。常用的如下。

(1)从无到有创建张量

从无到有创建指定形状的或随机张量或特定元素的张量

函数 功能 注意点 代码示例
Tensor(sizes) 基础构造函数,根据指定形状(shape)在内存中申请空间,创建未初始化的张量。现代 PyTorch 更推荐写 torch.empty(sizes) 里面的元素是内存残留的“垃圾值”;实际上是 torch.FloatTensor,数据类型只能是 torch.float32 x = torch.Tensor(2, 3) # 创建 2 行 3 列未初始化的张量
empty(sizes) 官方推荐的未初始化张量创建函数(替代Tensor(sizes))。根据指定的形状(sizes)直接在内存中申请空间,不填充任何默认值。 比 torch.Tensor(sizes) 更灵活、更规范,可以通过 dtype 指定数据类型(如 torch.int32)、通过 device 指定硬件设备(如 'cuda')。

常用于作为预分配内存的容器,后续会被其他计算结果(如 torch.matmul(..., out=x) 或切片赋值)直接覆盖。
import torch
x = torch.empty(2, 3)# 创建一个 2x3 的未初始化 float32 张量

y = torch.empty((2, 3), dtype=torch.int64, device='cuda')# 也可以直接指定类型和设备(如直接在 GPU 上预分配一块 int64 内存)
tensor(data) 工厂函数,类似于 np.array,从已有的数据(如 Python 列表、元组、NumPy 数组)深拷贝创建张量。 会自动推断数据类型(整数推断为 int64,浮点数推断为 float32);传入的是具体数据内容而非形状。 x = torch.tensor([[1, 2], [3, 4]]) # 根据列表生成 2x2 张量
ones(sizes) 创建指定形状且所有元素均为 1 的张量。 常用于全 1 矩阵初始化或掩码(Mask)生成;可通过 dtype 参数指定类型。 x = torch.ones(2, 3) # 生成 2x3 全 1 张量
zeros(sizes) 创建指定形状且所有元素均为 0 的张量。 最常用的预分配内存或清零方法,常用于梯度累加器、Padding 填充或偏置(Bias)初始化。 x = torch.zeros(2, 3) # 生成 2x3 全 0 张量
eye(sizes) 创建主对角线上元素为 1,其余元素为 0 的二维单位矩阵。 只能创建二维张量;若传 1 个参数 n 则生成 \(n \times n\) 方阵;若传 2 个参数 n, m 则生成 \(n \times m\) 对角阵。 x = torch.eye(3) # 生成 3x3 的单位矩阵
arange(s,e,step) 在区间 \([s, e)\) 内,按等差步长 step 生成一维张量(类似于 Python 的 range)。 左闭右开,即包含起点 \(s\),不包含终点 \(e\) x = torch.arange(0, 10, 2) # 输出: tensor([0, 2, 4, 6, 8])
linspace(s,e,steps) 在区间 \([s, e]\) 内,均匀切割成 steps 个点,生成一维张量。 双闭区间(包含终点 \(e\));第三个参数是点的个数,而不是步长(容易与 arange 混淆)。 x = torch.linspace(0, 10, 5) # 输出: tensor([ 0.0, 2.5, 5.0, 7.5, 10.0])
rand/randn(sizes) rand是生成[0,1)上均匀分布的随机张量;randn是生成服从N(0,1)的正态分布的随机张量 randn 的结果包含负数,常用于模型权重的初始化(高斯初始化)。 u = torch.rand(2, 3) # 2x3 均匀分布随机数。 n = torch.randn(2, 3) # 2x3 标准正态分布随机数
normal(mean,std) 生成符合自定义正态分布 \(\mathcal{N}(\text{mean}, \text{std}^2)\) 的随机张量。 meanstd 既可以是具体的标量,也可以是张量;若传入张量,可实现为不同位置指定各自不同的均值和标准差。 x = torch.normal(mean=0.0, std=1.0, size=(2, 3)) # 指定均值为 0,标准差为 1
randperm(m) 生成一个包含 \(0\)\(m - 1\)随机排列整数序列(Random Permutation)。 返回一维张量;最常用于给数据集做随机打乱(Shuffle)或选取随机采样索引。 x = torch.randperm(5) # 输出类似于: tensor([3, 1, 0, 4, 2])

(2)仿照已有张量的形状

不只能仿照形状,还可以同时继承数据类型(dtype)和设备(device),甚至可以根据已有的数据内容来创建。

  • 继承已有张量的属性(形状、类型、设备)

常见,通常以 _like 结尾。它们会自动继承原张量 xshapedtypedevice(无需手动重新指定 CPU/GPU 或数据类型):

函数 功能与特点 代码示例
torch.empty_like(x) 创建一个与 x 同形状、同类型、同设备的未初始化张量(最推荐用于预分配) y = torch.empty_like(x)
torch.zeros_like(x) 创建与 x 同属性的全 0 张量 y = torch.zeros_like(x)
torch.ones_like(x) 创建与 x 同属性的全 1 张量 y = torch.ones_like(x)
torch.rand_like(x) 创建与 x 同属性的 [0, 1) 均匀分布随机张量 y = torch.rand_like(x)
torch.randn_like(x) 创建与 x 同属性的标准正态分布随机张量 y = torch.randn_like(x)
torch.full_like(x, fill_value) 创建与 x 同属性且指定填充值的张量 y = torch.full_like(x, 3.14)

即使使用了 _like 函数,仍可以通过显式传入参数来“覆盖”某些属性。例如:

1
2
# 继承 x 的 shape 和 device,但强制将类型改为 float16
y = torch.zeros_like(x, dtype=torch.float16)
  • 浅拷贝已有的张量(共享数据内存)

不是想仿照形状,而是想直接引用或变换已有张量的“内容”,同时避免额外的内存开销:

  • x.clone()深拷贝。在内存中开辟一块全新空间,完整复制 x 的数据(修改新张量不会影响 x)。

  • x.detach()断开计算图。创建一个与 x 共享内存空间的新张量,但将其从 PyTorch 的自动求导(Autograd)计算图中剥离(常用于推理阶段或固定某些层参数)。

  • x.view(...) / x.reshape(...)改变形状(Reshape)。基于 x 的数据生成一个新的形状,通常与 x 共享底层存储内存,不涉及数据复制,速度极快。

操作方式 精简解释 是否共享内存? 是否断开计算图? 关键注意点 示例代码
y = x (引用赋值) 别名绑定 仅创建了一个指向原张量的 Python 引用,未创建新对象。 (完全同一块内存) (保留原计算图) • 修改 y直接改变 x。 • 并非真正意义上的拷贝,仅为同一对象的别名。 y = x y[0] = 999 (x[0] 也会变成 999)
y = x.clone() (深拷贝) 深拷贝 开辟全新物理内存空间,完整复制数据内容。 (独立全新内存) (保留原计算图) • 修改 y 完全不影响 x。 • clone() 动作本身会被记录在计算图中,梯度会通过 y 回传给 x y = x.clone()
y = x.detach() (断开计算图) 截断梯度 创建一个与 x 共享数据内存的新张量,但剥离 Autograd 记录。 (共享底层数据) (截断梯度) • 修改 y 的数值仍会影响 x! • 常用于固定参数、推理阶段或将张量转为 NumPy。 y = x.detach()
y = x.view(...) (维度变换) 视图变换 在不改变底层内存排列的前提下,重构张量形状。 (共享底层数据) (保留原计算图) • 要求内存必须连续(is_contiguous())。 • 修改 y 的数值会同步改变 x y = x.view(2, 3)
y = x.reshape(...) (安全维度变换) 灵活变换 优先返回共享内存的视图;若内存不连续则自动触发内存拷贝。 视情况而定 (通常共享,必要时拷贝) (保留原计算图) • 比 view() 更安全,无需手动调 .contiguous()。 • 绝大多数情况下共享内存,但不能保证 100% 共享 y = x.reshape(2, 3)
  • 仅读取已有张量的形状属性 (x.shapex.size())

只需要 x 的形状,但想改变设备或类型,你可以直接把 x.shape(或 x.size())作为普通尺寸参数传给任何创建函数:

1
2
3
# 假设 x 是一个位于 GPU 上的 float32 张量
# 只需它的形状,在 CPU 上创建一个全新的 int32 张量:
y = torch.zeros(x.shape, dtype=torch.int32, device='cpu')

2.1.3 张量的操作

  • 加法操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 张量的加法操作
import torch
x = torch.ones(4, 3)

# 方式1
y = torch.rand(4, 3)
print(x + y)

# 方式2
print(torch.add(x, y))
print(x)
print(y)

# 方式3 In-place原内存修改
y.add_(x)
print(y)
# 输出如下:
tensor([[1.8563, 1.3937, 1.2896],
[1.6563, 1.4142, 1.2966],
[1.6438, 1.2423, 1.7658],
[1.2770, 1.2897, 1.4249]])
tensor([[1.8563, 1.3937, 1.2896],
[1.6563, 1.4142, 1.2966],
[1.6438, 1.2423, 1.7658],
[1.2770, 1.2897, 1.4249]])
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
tensor([[0.8563, 0.3937, 0.2896],
[0.6563, 0.4142, 0.2966],
[0.6438, 0.2423, 0.7658],
[0.2770, 0.2897, 0.4249]])
tensor([[1.8563, 1.3937, 1.2896],
[1.6563, 1.4142, 1.2966],
[1.6438, 1.2423, 1.7658],
[1.2770, 1.2897, 1.4249]])
  • 索引操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# 索引操作
import torch
x = torch.rand(4,3)
# 取第二列。
# 切片 y 是 x 的视图(View),两者共享底层数据内存。
y = x[:, 1]
print(x)
print(y)

# 就地操作 (In-place)
# += 没有开辟新内存,而是直接把当前内存里的数据全部在原位置增加了 1。由于 y 依然指向这块内存,y 的数值被同步改变!
x += 1 # 或 x.add_(1)
print(x)
print(y)

# 再次就地操作
# 继续对 y 指向的内存区域(即 x 的第 2 列)在原位置增加 2
# 切片赋值,修改原内存区域的值
y += 2 # y[:] = y + 1
print(x)
print(y)
# 输出如下:
tensor([[0.8241, 0.3185, 0.7592],
[0.0244, 0.9210, 0.6008],
[0.6160, 0.0310, 0.5361],
[0.5361, 0.8280, 0.9263]])
tensor([0.3185, 0.9210, 0.0310, 0.8280])
tensor([[1.8241, 1.3185, 1.7592],
[1.0244, 1.9210, 1.6008],
[1.6160, 1.0310, 1.5361],
[1.5361, 1.8280, 1.9263]])
tensor([1.3185, 1.9210, 1.0310, 1.8280])
tensor([[1.8241, 2.3185, 1.7592],
[1.0244, 2.9210, 1.6008],
[1.6160, 2.0310, 1.5361],
[1.5361, 2.8280, 1.9263]])
tensor([2.3185, 2.9210, 2.0310, 2.8280])

注意与下面内容进行区分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 索引操作
import torch
x = torch.rand(4,3)
# 取第二列。
# 切片操作 y = x[:, 1] 创建了一个共享内存的视图(View)。此时,y 和 x 的第二列指向物理内存中的同一块地址。
y = x[:, 1]
print(x)
print(y)

# 计算 x + 1,并在内存中开辟一块全新的空间存放计算结果。然后将变量名 x 重新指向这块全新的内存。
x = x + 1
print(x)
# 此时 y 依然指向最初的那块旧内存。
print(y)

# 计算 y + 2,开辟另一块全新的内存存放结果。然后将变量名 y 重新指向这块新内存。
y = y + 2
print(x)
print(y)
# 输出如下:
tensor([[0.2157, 0.0046, 0.2315],
[0.5701, 0.9606, 0.8074],
[0.0919, 0.3664, 0.7715],
[0.1560, 0.3951, 0.6495]])
tensor([0.0046, 0.9606, 0.3664, 0.3951])
tensor([[1.2157, 1.0046, 1.2315],
[1.5701, 1.9606, 1.8074],
[1.0919, 1.3664, 1.7715],
[1.1560, 1.3951, 1.6495]])
tensor([0.0046, 0.9606, 0.3664, 0.3951])
tensor([[1.2157, 1.0046, 1.2315],
[1.5701, 1.9606, 1.8074],
[1.0919, 1.3664, 1.7715],
[1.1560, 1.3951, 1.6495]])
tensor([2.0046, 2.9606, 2.3664, 2.3951])
表达式 内存行为 是否影响共享内存的其他变量?
x = x + 1 开辟新内存,重新绑定变量名 x ❌ 不会(断开原内存关联)
x += 1 / x.add_(1) 就地修改(In-place),改动当前内存 ✅ 会(共享内存变量同步改变)
x[:] = x + 1 就地覆盖(In-place),改动当前内存 ✅ 会(共享内存变量同步改变)
  • 取值操作

x.item() 的设计用途是把只包含“唯一一个元素”的张量(1-element tensor)转换为 Python 的原生数值(如 floatint

1
2
3
4
5
6
7
8
9
10
11
import torch
x = torch.rand(1)
print(x)
print(type(x))
print(x.item())
print(type(x.item()))
# 输出如下:
tensor([0.5535])
<class 'torch.Tensor'>
0.5535328984260559
<class 'float'>
1
2
3
4
5
6
7
8
9
# 把整个张量转换为 Python 的列表(List)
import torch

x = torch.rand(2)
print(x)
print(x.tolist()) # 输出类似于: [0.1234, 0.5678]
# 输出如下:
tensor([0.8016, 0.4396])
[0.8016369938850403, 0.43957531452178955]
1
2
3
4
5
6
7
8
9
10
# 想提取张量中的某一个元素转为 Python float:
# 对具体索引调用 .item() 即可:
import torch

x = torch.rand(2)
print(x)
print(x[0].item()) # 提取第 0 个元素转为 Python float
# 输出如下:
tensor([0.0081, 0.3879])
0.008058488368988037

PyTorch中对Tensor 操作很多,包括转置、索引、切片、数学运算、线性代数、随机数等等,具体使用方法可参考官方文档

2.1.4 广播机制

概念与触发规则

  • 概念:广播机制是 PyTorch(以及 NumPy)中一种极其强大的自动处理机制:当对两个形状(Shape)不同的张量进行逐元素算术运算(如加、减、乘、除)时,PyTorch 会在不复制物理内存的前提下,自动把两个张量扩展为相同的形状,以便按位置进行计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 广播机制
x = torch.arange(1, 3).view(1, 2)
print(x)
y = torch.arange(1, 4).view(3, 1)
print(y)
print(x + y)
# 输出如下:
tensor([[1, 2]])
tensor([[1],
[2],
[3]])
tensor([[2, 3],
[3, 4],
[4, 5]])

  • 触发规则:

    两个张量能否广播,需要从右向左(从末尾维度开始)依次对齐比较各个维度:

    • 维度大小相等
    • 其中一个张量的该维度大小为 1
    • 其中一个张量缺失该维度(例如二维张量与一维张量运算)。

    只有当所有对应维度都满足上述条件之一时,广播才能成功。在计算时,维度大小为 \(1\) 的张量会被自动沿该维度“复制/拉伸”,达到与另一个张量相同的形状。


示例分析1

按照上述规则顺序对示例进行分析。如下。

  • 第一步:列出两个张量的形状(Shape)

    • 张量 x 的形状:(1, 2)

    • 张量 y 的形状:(3, 1)

  • 第二步:从右向左(从末尾维度开始)逐维对齐比较

    我们将两个形状从右端对齐放在一起比较:

维度位置 x 的维度 y 的维度 对齐比较结果 是否满足广播条件? 最终广播目标维度
倒数第 1 维 (最右) 2 1 x 的大小为 2,y 的大小为 1 满足 (符合规则 2:y 该维度大小为 1) 2 (y 沿此维度拉伸)
倒数第 2 维 1 3 x 的大小为 1,y 的大小为 3 满足 (符合规则 2:x 该维度大小为 1) 3 (x 沿此维度拉伸)

由于所有对应维度均符合广播规则,因此本次广播成功!

  • 第三步:按规则推导最终的运算过程与形状

​ 经过上述逐维分析后:

  1. 确定最终形状:结合每个维度的对齐结果,合并后的目标形状确定为 (3, 2)

  2. 张量 x 的广播拉伸

    • 原形状:(1, 2)(数值为 [[1, 2]]
    • 在倒数第 2 维上从 1 延伸到 3(沿着行方向复制 3 次):

    \[ \begin{bmatrix} 1 & 2 \end{bmatrix} \xrightarrow{\text{拉伸为 (3, 2)}} \begin{bmatrix} 1 & 2 \\ 1 & 2 \\ 1 & 2 \end{bmatrix} \]

  3. 张量 y 的广播拉伸

    • 原形状:(3, 1)(数值为 [[1], [2], [3]]
    • 在倒数第 1 维上从 1 延伸到 2(沿着列方向复制 2 次):

    \[ \begin{bmatrix} 1 \\ 2 \\ 3 \end{bmatrix} \xrightarrow{\text{拉伸为 (3, 2)}} \begin{bmatrix} 1 & 1 \\ 2 & 2 \\ 3 & 3 \end{bmatrix} \]

  4. 逐元素相加

两者拉伸至相同的形状 (3, 2) 后,进行对应位置点对点相加,即得输出: \[ \begin{bmatrix} 1+1 & 2+1 \\ 1+2 & 2+2 \\ 1+3 & 2+3 \end{bmatrix} = \begin{bmatrix} 2 & 3 \\ 3 & 4 \\ 4 & 5 \end{bmatrix} \]


示例分析2

再来一个举例。针对上述广播规则中的第3条规则。

当其中一个张量缺失某个维度时,广播机制的规则是:

在逻辑上,从左侧(高维方向)自动为缺失维度的张量补齐大小为 \(1\) 的维度,然后再按照规则进行拉伸。

一、 规则对齐分析

假设我们有两个张量:

  • 张量 A 的形状:(3, 4)(二维张量,例如 3 行 4 列的矩阵)
  • 张量 B 的形状:(4,)(一维张量,包含 4 个元素)

当它们进行 A + B 运算时,PyTorch 会按如下步骤进行对齐:

  1. 自动在左侧补齐维度

一维张量 B 形状为 (4,),在最左边补齐一个维度后,逻辑形状变成了 (1, 4)

  1. 从右向左逐维比较
维度位置 A 的维度 B 的逻辑维度 比较情况 广播行为 最终维度
倒数第 1 维 (最右) 4 4 大小相等 无需广播(直接对应位置计算) 4
倒数第 2 维 3 1 (补齐的) 缺失维度 (补为 1) 进行广播B 沿此维度复制 3 次) 3

最终相加结果的形状为 (3, 4)

二、 代码与图解示例

我们来看一个实际的代码场景:给矩阵的每一行都加同一个向量

Python

1
2
3
4
5
6
7
8
9
10
11
import torch

# A: 3行4列的矩阵 (二维)
A = torch.ones(3, 4)

# B: 包含4个元素的向量 (一维)
B = torch.tensor([1, 2, 3, 4])

# 相加
C = A + B
print(C)

计算过程分解:

  1. A 的数值(3 行 4 列)

    \[\begin{bmatrix} 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 \end{bmatrix}\]

  2. B 的自动扩展过程

    • 原形状:[1, 2, 3, 4] 形状为 (4,)

    • 补齐左侧维度为 (1, 4)

      \[\begin{bmatrix} 1 & 2 & 3 & 4 \end{bmatrix}\]

    • 沿第 0 维(行方向)复制 3 份,拉伸为 (3, 4)

      \[\begin{bmatrix} 1 & 2 & 3 & 4 \\ 1 & 2 & 3 & 4 \\ 1 & 2 & 3 & 4 \end{bmatrix}\]

  3. 最终输出结果 C

    \[\begin{bmatrix} 1+1 & 1+2 & 1+3 & 1+4 \\ 1+1 & 1+2 & 1+3 & 1+4 \\ 1+1 & 1+2 & 1+3 & 1+4 \end{bmatrix} = \begin{bmatrix} 2 & 3 & 4 & 5 \\ 2 & 3 & 4 & 5 \\ 2 & 3 & 4 & 5 \end{bmatrix}\]

神经网络中的经典应用

这个规则在深度学习中最常见的使用场景就是加上偏置项(Bias Addition)

假设网络隐藏层输出特征矩阵 X 形状为 (batch_size, feature_dim)(比如 (64, 128),即 64 个样本,每个样本 128 个特征),而偏置项 b 只是一个形状为 (128,) 的一维向量。

直接执行 X + b 时,PyTorch 就会利用“缺失维度自动补齐”的规则,把 b 自动扩展到每一个样本上,无缝完成偏置加法!

2.2 自动微分

PyTorch 中,所有神经网络的核心是 autograd包。autograd包为张量上的所有操作提供了自动求导机制。它是一个在运行时定义 ( define-by-run )的框架,这意味着反向传播是根据代码如何运行来决定的,并且每次迭代可以是不同的。用一句总结就是:PyTorch 中的 autograd 是实现神经网络反向传播与自动求导的核心引擎。

2.2.1 核心要点概括

  1. 动态计算图(Define-by-Run)
  • 运行时构建:计算图是在代码执行前向传播(Forward Pass)时实时动态生成的,无需预先定义静态图。
  • 灵活性高:反向传播完全取决于当前代码的实际执行路径,支持在每次迭代中使用不同的网络结构(如条件判断、循环语句等)。
  1. 三大核心机制与属性
  • requires_grad=True

    将 Tensor 的此属性设为 True 时,PyTorch 开始追踪在该 Tensor 上的所有数学运算,以便后续计算梯度。

  • grad_fn

    记录创建该 Tensor 的计算函数(如 AddBackward0),用于在反向传播时根据链式法则回溯节点。

  • .backward().grad

    调用标量变量的 .backward() 方法会自动触发反向传播,求出的导数结果将累加并存储在对应叶子节点 Tensor 的 .grad 属性中。

  1. 常见控制开关
  • torch.no_grad()

    上下文管理器,用于在推理/测试阶段关闭梯度追踪与计算,能显著减少显存占用并提升计算速度。

2.2.2 autograd简介

torch.Tensor是这个包的核心类。如果设置它的属性.requires_gradTrue,那么它将会追踪对于该张量的所有操作。当完成计算后可以通过调用.backward(),来自动计算所有的梯度。这个张量的所有梯度将会自动累加到.grad属性。

注意:在 y.backward() 时,如果 y 是标量,则不需要为 backward() 传入任何参数;否则,需要传入一个与 y 同形的Tensor。

要阻止一个张量被跟踪历史,可以调用.detach()方法将其与计算历史分离,并阻止它未来的计算记录被跟踪。为了防止跟踪历史记录(和使用内存),可以将代码块包装在 with torch.no_grad():中。在评估模型时特别有用,因为模型可能具有 requires_grad = True 的可训练的参数,但是我们不需要在此过程中对他们进行梯度计算。

还有一个类对于autograd的实现非常重要:FunctionTensorFunction 互相连接生成了一个无环图 (acyclic graph),它编码了完整的计算历史。每个张量都有一个.grad_fn属性,该属性引用了创建该 Tensor 的 Function (除非这个张量是用户手动创建的,即这个张量的grad_fnNone )。

2.2.2.1 requires_grad标识

创建一个张量可以手动指定其requires_grad属性的值,如果未指定、默认情况下此属性的值是False。且张量的此参数值在张量创建出来以后还可以修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# requires_grad 默认值与修改
a = torch.randn(2, 2) # 默认 requires_grad = False
a = ((a * 3) / (a - 1))
print(a.requires_grad)

a.requires_grad_(True)
print(a.requires_grad)

b = (a * a).sum()
print(b.grad_fn)


# 输出如下:
False
True
<SumBackward0 object at 0x7fb67c47e860>

2.2.2.2 示例1-手动创建张量grad_fn属性为None

当张量由用户手动创建时,其grad_fn属性返回结果是None。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
"""
每个张量都有一个.grad_fn属性,该属性引用了创建该 Tensor 的数学函数(Function)
但当这个张量是用户手动创建的,则这个张量的grad_fn是 None
"""
import torch

# 1. 用户手动创建叶子张量 x(requires_grad=True),并设置requires_grad=True用来追踪其计算历史
x = torch.randn(3, 3, requires_grad=True)
print(x)
print("x.grad_fn:", x.grad_fn) # 输出: None(因为是手动直接创建的)

# 2. 通过数学运算(平方和加法)创建新张量 y
y = x ** 2 + 2
print(y) # y是计算的结果,所以它的grad_fn属性不为None
print("y.grad_fn:", y.grad_fn) # 输出: <AddBackward0 ...>

# 3. 再对 y 进行一次矩阵求和,创建新张量 z
z = y.sum()
print(z) # x也是计算的结果,所以它的grad_fn属性也不为None
print("z.grad_fn:", z.grad_fn) # 输出: <SumBackward0 ...>

# 4. 反向传播
z.backward()
print(x.grad)

# 输出如下:
tensor([[-1.1100, 0.1339, -0.6549],
[-0.1653, 0.6160, -0.2651],
[ 0.0685, 0.9087, -1.0536]], requires_grad=True)
x.grad_fn: None
tensor([[3.2322, 2.0179, 2.4289],
[2.0273, 2.3794, 2.0703],
[2.0047, 2.8258, 3.1100]], grad_fn=<AddBackward0>)
y.grad_fn: <AddBackward0 object at 0x7fb67e1cf010>
tensor(22.0965, grad_fn=<SumBackward0>)
z.grad_fn: <SumBackward0 object at 0x7fb67e1cf010>

原理解析

  1. y.grad_fny 是通过 x ** 2 + 2 算出来的,它的最后一步运算是加法 +,因此它的 .grad_fn 属性指向了一个 <AddBackward0> 对象。这个对象记录了“如何对加法求导”的算子逻辑。
  2. z.grad_fnz 是通过对 y 调用 .sum() 得到的,因此它的 .grad_fn 属性指向了一个 <SumBackward0> 对象。

当后续调用 z.backward() 进行反向传播时,PyTorch 的自动求导引擎(Autograd)就是沿着这一条由 .grad_fn 构成的链条逆向追溯,层层推导计算出 x 的梯度的。


2.2.2.3 示例2-展示反向传播过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import torch

# 1. 创建叶子张量 x(定义一个简单的 1 维张量方便计算),并设置requires_grad=True用来追踪其计算历史
x = torch.tensor([2.0, 3.0], requires_grad=True)

# 2. 前向传播计算:
# y = x ** 2
# z = y.sum()
y = x ** 2
z = y.sum()

print("--- 0. 查看每个张量的值 ---")
print("x:", x)
print("y:", y)
print("z:", z)

print("\n--- 1. 查看每个张量的 grad_fn ---")
print("z.grad_fn:", z.grad_fn)
print("y.grad_fn:", y.grad_fn)
print("x.grad_fn:", x.grad_fn)

print("\n--- 2. 通过 grad_fn 链条逆向追溯 ---")
# z.grad_fn 的 next_functions 属性记录了它的上一级节点
prev_from_z = z.grad_fn.next_functions[0][0]
print("z 的上游节点是:", prev_from_z) # 这就是 y.grad_fn!

prev_from_y = y.grad_fn.next_functions[0][0]
print("y 的上游节点是:", prev_from_y) # 这指向了叶子节点 x 的 AccumulateGrad

print("\n--- 3. 执行反向传播 ---")
print("反向传播前 x.grad:", x.grad)
z.backward() # 触发 Autograd 沿着上面的链条反向计算。z 此时其实是一个0维向量即标量,所以不需要传参
print("反向传播后 x.grad:", x.grad)
# 输出如下:
--- 0. 查看每个张量的值 ---
x: tensor([2., 3.], requires_grad=True)
y: tensor([4., 9.], grad_fn=<PowBackward0>)
z: tensor(13., grad_fn=<SumBackward0>)

--- 1. 查看每个张量的 grad_fn ---
z.grad_fn: <SumBackward0 object at 0x7fb67dd993c0>
y.grad_fn: <PowBackward0 object at 0x7fb67dd993c0>
x.grad_fn: None

--- 2. 通过 grad_fn 链条逆向追溯 ---
z 的上游节点是: <PowBackward0 object at 0x7fb67e195d80>
y 的上游节点是: <AccumulateGrad object at 0x7fb67e070ac0>

--- 3. 执行反向传播 ---
反向传播前 x.grad: None
反向传播后 x.grad: tensor([4., 6.])

为了彻底看清这套机制,现直接打印出上述代码反向传播自动求导完整的链条结构,并手动演算一次梯度,以直观看到 .grad_fn 是如何把计算图“串”起来的。

详细过程拆解(把逻辑图可视化)

在内存中,代码前向传播时,PyTorch 默默建立了一张动态计算图

1
2
3
4
5
6
7
[ x ] (叶子节点, 无 grad_fn)

▼ (执行 x ** 2)
[ y ] ─── 指向 ───> <PowBackward0> (知道怎么对 x^2 求导)
│ ▲
▼ (执行 y.sum()) │ 指向 (通过 next_functions)
[ z ] ─── 指向 ───> <SumBackward0> (知道怎么对 sum 求导)

当调用 z.backward() 时,后台发生了什么?

Autograd 引擎就像一个顺藤摸瓜的机器人:

  1. 起点:找到 z.grad_fn(即 <SumBackward0>),计算出 \(\frac{\partial z}{\partial y} = [1, 1]\)
  2. 第一次追溯:顺着 <SumBackward0>.next_functions 找到上一级节点 <PowBackward0>(即 y.grad_fn)。
  3. 链式法则计算<PowBackward0> 接收到上一级传来的梯度,结合链式法则计算 \(\frac{\partial y}{\partial x} = 2x\),算出 \(\frac{\partial z}{\partial x} = 1 \times 2x = 2x\)
  4. 第二次追溯:继续顺着 <PowBackward0>.next_functions 向上找,发现到了叶子节点 x 的累加节点(<AccumulateGrad>)。
  5. 终点与赋值:把最终算出的梯度 2x(即 \(2 \times [2.0, 3.0] = [4.0, 6.0]\))存入 x.grad 属性中,追溯结束。
核心总结
  • .grad_fn 就像是面包屑导航:每个由运算产生的张量都用 .grad_fn 记录着“我是被哪个数学函数算出来的”。
  • next_functions 就像是链条的锁扣:把前后的算子串在一起,形成一条可以倒着走回源头 x 的通路。
  • backward() 就是倒着走这个链条:按照微积分的链式法则(Chain Rule),从终点 z 一路乘回起点 x,最终把算好的梯度写入 x.grad

2.2.2.4 示例3-反向传播的数学推导和代码逻辑

1
2
3
4
5
6
7
8
9
10
import torch
# 创建一个张量并设置requires_grad=True用来追踪其计算历史
x = torch.tensor([2.0, 3.0], requires_grad=True)
y = x ** 2 # y 是 [y1, y2] = [4.0, 9.0]

# 传入同形的权重向量 v = [1.0, 1.0]
# 相当于对 S = 1.0 * y1 + 1.0 * y2 求导
y.backward(torch.tensor([1.0, 1.0]))

print(x.grad) # 输出: tensor([4., 6.])

结合刚才的示例,把数学推导和代码逻辑一步步拆解出来。

2.2.2.4.1 初始条件与前向传播

假设有:

  • 输入向量:\(\vec{x} = \begin{bmatrix} x_1 \\ x_2 \end{bmatrix} = \begin{bmatrix} 2.0 \\ 3.0 \end{bmatrix}\)
  • 前向运算:\(\vec{y} = \vec{x}^2 = \begin{bmatrix} x_1^2 \\ x_2^2 \end{bmatrix} = \begin{bmatrix} 2.0^2 \\ 3.0^2 \end{bmatrix} = \begin{bmatrix} 4.0 \\ 9.0 \end{bmatrix}\)
  • 传入的梯度权重向量:\(\vec{v} = \begin{bmatrix} v_1 \\ v_2 \end{bmatrix} = \begin{bmatrix} 1.0 \\ 1.0 \end{bmatrix}\)

代码对应的调用是:y.backward(torch.tensor([1.0, 1.0]))

2.2.2.4.2 数学推导过程
步骤 1:构造等价标量 \(S\)

根据公式 \(S = \vec{y} \cdot \vec{v} = y_1 v_1 + y_2 v_2\),将 \(\vec{y}\) 代入:

\[S = (x_1^2 \cdot v_1) + (x_2^2 \cdot v_2)\]

把已知数值 \(v_1 = 1.0, v_2 = 1.0\) 带入:

\[S = x_1^2 \cdot 1.0 + x_2^2 \cdot 1.0 = x_1^2 + x_2^2\]

步骤 2:对各分量分别求偏导数

为了得到梯度 \(\nabla_{\vec{x}} S = \begin{bmatrix} \frac{\partial S}{\partial x_1} \\ \frac{\partial S}{\partial x_2} \end{bmatrix}\),需要利用微积分求导法则:

  • \(x_1\) 求偏导(把 \(x_2\) 视为常数):

    \[\frac{\partial S}{\partial x_1} = \frac{\partial}{\partial x_1}(x_1^2 \cdot v_1) + 0 = 2 \cdot x_1 \cdot v_1\]

  • \(x_2\) 求偏导(把 \(x_1\) 视为常数):

    \[\frac{\partial S}{\partial x_2} = 0 + \frac{\partial}{\partial x_2}(x_2^2 \cdot v_2) = 2 \cdot x_2 \cdot v_2\]

即梯度的符号表达式为:

\[\text{grad} = \begin{bmatrix} 2 \cdot x_1 \cdot v_1 \\ 2 \cdot x_2 \cdot v_2 \end{bmatrix}\]

步骤 3:带入具体的数值

将前向传播时的 \(x_1 = 2.0, x_2 = 3.0\) 以及 \(v_1 = 1.0, v_2 = 1.0\) 带入上面的偏导公式:

  • 第 1 个分量的梯度:

    \[\frac{\partial S}{\partial x_1} = 2 \times 2.0 \times 1.0 = 4.0\]

  • 第 2 个分量的梯度:

    \[\frac{\partial S}{\partial x_2} = 2 \times 3.0 \times 1.0 = 6.0\]

2.2.2.4.3 最终结果

经过上述计算,Autograd 将算出的梯度保存在 x.grad 中:

\[\text{x.grad} = \begin{bmatrix} 4.0 \\ 6.0 \end{bmatrix}\]

也就是 PyTorch 最终打印出来的结果:tensor([4., 6.])

2.2.2.4.4 延伸思考:如果改变传入的 v 会发生什么?

如果传入一个不同的权重,比如 v = torch.tensor([1.0, 0.5])

  1. 等价标量\(S = 1.0 \cdot x_1^2 + 0.5 \cdot x_2^2\)(其实就是公式:\(S = v1 \cdot x_1^2 + v2 \cdot x_2^2\)
  2. 偏导公式
    • \(\frac{\partial S}{\partial x_1} = 2 \cdot x_1 \cdot v1 = 2 \times 2.0 \times 1.0 = 4.0\)
    • \(\frac{\partial S}{\partial x_2} = 2 \cdot x_2 \cdot v2 = 2 \times 3.0 \times 0.5 = 3.0\)
  3. 最终 x.gradtensor([4., 3.])

这就是为什么说传入的向量 \(\vec{v}\) 实际上就是给非标量输出的各个分量赋予了不同的求导权重

2.2.3 梯度

2.2.3.1 向量函数

标量(Scalar) = 只有一个孤零零的数字(如:温度、体重、Loss值)。

向量(Vector) = 有顺序排列的一排数字(如:空间坐标、颜色RGB值、特征向量)。

向量函数 = 只要输出结果是一排数字(向量)的函数,就叫向量函数!

2.2.3.2 梯度计算分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 梯度
x = torch.ones(2, 2, requires_grad=True)
# 对这个张量做一次幂运算
y = x**2 # dy/dx = 2*x
# 对 y 进行更多操作
z = y * y * 3 # dz/dy = 6*y
out = z.mean() # dout/dz = 1/4

out.backward()
# 输出导数 d(out)/dx = d(out)/dz * dz/dy * dy/dx = (1/4)*(6*y)*(2*x) = (1/4)*(6*x*x)*(2*x) = 3*(x**3)
print(x.grad)
# 输出如下:
tensor([[3., 3.],
[3., 3.]])

因为out 是一个标量,因此out.backward()out.backward(torch.tensor(1.)) 等价。假如要求导的out 是一个严格的张量(非0维张量),那么调用out.backward()函数就需要传入一个与out 形状一样的张量参数(比如全1),否则报错。

数学上,对于向量函数 \(\vec{y} = f(\vec{x})\) ,其中\(\vec{y} 与 \vec{x}\) 都是一个向量(简单理解成一排数字),那么 \(\vec{y}\) 关于 \(\vec{x}\) 的梯度就是一个雅可比矩阵:

\[ J = \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{pmatrix} \]

其中因变量y的个数与自变量的个数可以不一样,一般情况下也不一样。比如天气查询系统,用户输入经度、纬度、时间,得到的某地某时的天气可以表述为一个由多个数组成的一维张量(也叫向量),其中每个数可能温度、湿度、降水概率、空气PM2.5指数、紫外线强度等等。

可以使用 torch.autograd 这个包来计算一些雅可比矩阵的乘积。例如,如果 \(v\) 是一个标量函数 \(l = g(\vec{y})\) 的梯度:\(v = \begin{pmatrix} \frac{\partial l}{\partial y_1} & \dots & \frac{\partial l}{\partial y_m} \end{pmatrix}\) ,其中向量函数 \(\vec{y} = f(\vec{x})\) 求梯度的方法已在上面描述。根据链式法则,我们可以得到标量函数 \(l\)\(x\) 的最终梯度(即导数)$v J $ 如下: \[ v J = \begin{pmatrix} \frac{\partial l}{\partial y_1} & \dots & \frac{\partial l}{\partial y_m} \end{pmatrix} \begin{pmatrix} \frac{\partial y_1}{\partial x_1} & \dots & \frac{\partial y_1}{\partial x_n} \\ \vdots & \ddots & \vdots \\ \frac{\partial y_m}{\partial x_1} & \dots & \frac{\partial y_m}{\partial x_n} \end{pmatrix} = \begin{pmatrix} \frac{\partial l}{\partial x_1} & \dots & \frac{\partial l}{\partial x_n} \end{pmatrix} \] 根据这套链式法则计算方法,不论经过了多少轮运算,我们总能计算出多轮计算后得到的最终变量out对最初的自变量x的梯度。

梯度在反向传播过程中默认是累积的,就是说如果做多次反向传播,后一次计算梯度值时会直接将前一次梯度值累加给当次成为新的梯度值,为了保证梯度是非累加后的,可以在反向传播之前把梯度清零(即先执行x.grad.data.zero_())。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 反向传播的结果,结果是累加的
# 创建一个张量并设置requires_grad=True用来追踪其计算历史
import torch
x = torch.ones(2, 2, requires_grad=True)
# 对这个张量做一次幂运算
y = x**2
l = y.sum()

l.backward(retain_graph=True) # 第一次进行反向传播,参数“retain_graph=True”的意思是启用“计算图复用”
print(x.grad)

l.backward(retain_graph=True) # 第二次进行反向传播
print(x.grad)
# 输出如下:
tensor([[2., 2.],
[2., 2.]])
tensor([[4., 4.],
[4., 4.]])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
x = torch.ones(2, 2, requires_grad=True)

y1 = x*x
y1.backward(torch.ones(2, 2)) # y1不是0维张量,做反向传播时要传入一个与y1相同形状的张量
print(x.grad)

y2 = x*x*x
#x.grad.data.zero_() # 再次反向传播前不清零梯度,看到累加效果
y2.backward(torch.ones(2, 2))
print(x.grad)
# 输出如下:
tensor([[2., 2.],
[2., 2.]])
tensor([[5., 5.],
[5., 5.]])
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import torch
x = torch.ones(2, 2, requires_grad=True)

y1 = x*x
y1.backward(torch.ones(2, 2))
print(x.grad)

y2 = x*x*x
x.grad.data.zero_() # 再次反向传播前清零梯度,只有当次反向传播计算的梯度显示在结果中
y2.backward(torch.ones(2, 2))
print(x.grad)
# 输出如下:
tensor([[2., 2.],
[2., 2.]])
tensor([[3., 3.],
[3., 3.]])

临时关闭自动求导引擎。当把代码包装在 with torch.no_grad(): 块中时,PyTorch 会暂时关闭自动求导引擎。即使输入张量(如 x)设置了 requires_grad=True,在该代码块内部进行的所有数学运算,都不会被记录到计算图中。在该代码块内产生的新张量,其 requires_grad 属性强制为 False,且 .grad_fnNone

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import torch

# 1. 创建一个需要梯度的叶子张量 x
x = torch.tensor([2.0, 3.0], requires_grad=True)

print("=== 1. 常规模式(开启梯度追踪) ===")
y1 = x ** 2
print("x.requires_grad: ", x.requires_grad) # True
print("y1.requires_grad: ", y1.requires_grad) # True
print("y1.grad_fn: ", y1.grad_fn) # <PowBackward0 ...> (记录了计算图)

print("\n=== 2. no_grad 模式(关闭梯度追踪) ===")
with torch.no_grad():
y2 = x ** 2
print("y2.requires_grad: ", y2.requires_grad) # False (不再追踪)
print("y2.grad_fn: ", y2.grad_fn) # None (没有计算图)

print("\n=== 3. 尝试进行反向传播 ===")
# y1 有计算图,可以顺利求导
y1.sum().backward()
print("x.grad (由 y1 算出):", x.grad) # tensor([4., 6.])

# y2 没有计算图,尝试求导会直接报错
try:
y2.sum().backward()
except RuntimeError as e:
print("y2.backward() 失败,报错信息:", e)

# 输出如下:
=== 1. 常规模式(开启梯度追踪) ===
x.requires_grad: True
y1.requires_grad: True
y1.grad_fn: <PowBackward0 object at 0x7fb670e7df60>

=== 2. no_grad 模式(关闭梯度追踪) ===
y2.requires_grad: False
y2.grad_fn: None

=== 3. 尝试进行反向传播 ===
x.grad (由 y1 算出): tensor([4., 6.])
y2.backward() 失败,报错信息: element 0 of tensors does not require grad and does not have a grad_fn
  • 为什么需要 torch.no_grad()?

    在实际开发中,with torch.no_grad(): 最主要用于 模型评估 / 推理测试阶段(Model Evaluation / Inference) 和 网络参数更新:

    • 大幅节省显存与内存:训练时需要保存中间变量以便反向传播;推理时不需要求导,用 no_grad() 就不需要额外留存计算图和中间激活值,能节省大量显存。
    • 加速计算:不需要在后台维护计算图节点,提高前向传播的运行速度。

既想要修改 tensor 的数值,又不希望被 autograd 记录(即不会影响反向传播),那么就需要修改tensor.data 属性值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import torch

x = torch.ones(1,requires_grad=True)

print(x.data) # x.data也是一个tensor
print(x.data.requires_grad) # 但是已经是独立于计算图之外

y = 2 * x
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
"""
# 效果与如下相同
x.data *= 100 # 只改变了值,不会记录在计算图,所以不会影响梯度传播
y = 2 * x
"""

y.backward()
print(x) # 更改data的值也会影响tensor的值
print(x.grad)

# 输出如下:
tensor([1.])
False
tensor([100.], requires_grad=True)
tensor([2.])

2.3 并行计算简介

分布式数据并行:https://docs.pytorch.org/tutorials/intermediate/ddp_tutorial.html、https://docs.pytorch.ac.cn/tutorials/intermediate/ddp_tutorial.html

使用PyTorch进行深度学习时,由于数据集比较大(一个GPU显存加载不了,可以使用多个GPU来加载)或为了提升计算速度(将模型分成几个部分放到多个不同GPU上,或将数据集分成几个部分放到不同GPU上进行计算),此时就需要使用并行计算来处理。

2.3.1 为什么要做并行计算

单个GPU无法完成计算、为了提升计算速度。

2.3.2 为什么需要CUDA

CUDA是NVIDIA提供的一种GPU并行计算框架。对 GPU 本身编程,是用 CUDA C/C++(C/C++ 加 CUDA 扩展)配合 CUDA Runtime API 实现的。CUDA 并不是一门从零开始的语言,而是 NVIDIA 在 C/C++ 基础上扩展关键字/内建变量/启动语法后形成的“C/C++ 扩展集”,再加上 Runtime/Driver API 与 nvcc 编译工具链共同组成的编程体系。

在我们使用PyTorch编写深度学习代码时,使用的CUDA是另一个意思,在PyTorch使用 CUDA表示要开始要求我们的模型或者数据开始使用GPU了。我们并不使用CUDA语言、而是通过Python语言编码来操纵与使用GPU。

在编写程序中,当我们使用了 .cuda() 时,其功能是让我们的模型或者数据从CPU迁移到GPU上(默认是0号GPU)当中,通过GPU开始计算。


关于PyTorch中使用显卡的注意事项:

  • PyTorch 里用 .cuda() 而不是 .gpu(),是因为 CUDA 是 NVIDIA 私有的 GPU 编程栈,并非所有 GPU 都支持;命名沿用 Torch 历史,且刻意保留后端特异性(AMD 走 ROCm/HIP)。AMD 的 GPU 计算接口现在主推 ROCm(HIP 语言,语法接近 CUDA),PyTorch 官方已上游支持 ROCm 后端,AMD Instinct 等卡可直接跑 PyTorch。
  • 如非必要,尽量减少数据在CPU与GPU之间的复制操作。
  • 虽然GPU天生用来执行高并发性操作的,速度很快。但对于简单计算、数据量很小的计算,可以在CPU上执行,此时如果硬放到GPU上计算反而因数据在CPU与GPU间的复制、上下文切换而得不偿失。
  • 使用GPU时养成指定GPU编号的习惯,避免默认总是使用0号GPU造成其显存溢出。

2.3.3 常见的并行计算方法

2.3.3.1 拆分模型做并行计算

思路是,将一个模型的各个部分拆分,然后将不同的部分放入到不同GPU来做不同任务的计算。

又可以细分为两种:(1)网络分割(Network partitioning)。将模型的不同层或不同组件拆分开,放到不同GPU上做不同任务。(2)逐层划分(Layer-wise partitioning)。将同一层的模型做一个拆分,让不同的GPU去训练同一层模型的部分任务。

对于GPU之间的通信是一个考验。GPU的通信在这种密集任务中很难办到,所以这个方式慢慢淡出了视野。

2.3.3.2 拆分数据做并行计算

数据并行(Data parallelism)。不拆分模型,而是将输入的数据拆分多个部分分别放在不同的GPU上,同一个模型在不同的GPU上训练自己分到的那部分数据,然后将每个GPU上任务完成后得到的结果进行汇总、反向更新给每个GPU上的模型。

这是当前主流的并行计算方式。

2.3.4 使用CUDA加速训练

2.3.4.1 单卡训练

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# train-dp-snsg.py
# 执行: python train-dp-snsg.py

import torch
import torch.nn as nn
import torch.optim as optim

# 1. 设置使用的单张 GPU 设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

# 2. 定义大模型
class HeavyModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 10)
)
def forward(self, x):
return self.net(x)

# 3. 初始化模型并直接移至单卡
model = HeavyModel().to(device)

optimizer = optim.Adam(model.parameters(), lr=0.001)

print(f"单卡训练开始!使用的是设备: {device}。请立即在另一个终端查看 `watch -n 0.1 nvidia-smi`...")

# 4. 训练循环
for step in range(2000):
# 模拟数据也需要移至同一张卡
inputs = torch.randn(256, 4096).to(device)

# 前向与反向传播
outputs = model(inputs)
loss = outputs.sum()

optimizer.zero_grad()
loss.backward()
optimizer.step()

# 每 200 步打印一次进度
if step % 200 == 0:
print(f"步骤 [{step}/2000] - Loss: {loss.item():.4f}")

print("单卡训练完成!")

2.3.4.2 多卡训练

单机多卡DP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
# DP(DataParallel)代码示例
# train-dp-sndg.py
# 执行: python train-dp-sndg.py

import torch
import torch.nn as nn
import torch.optim as optim

# 1. 定义一个稍大些的模型(多层大线性层)
class HeavyModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 10)
)
def forward(self, x):
return self.net(x)

model = HeavyModel()
model = nn.DataParallel(model) # 包装为 DP
model = model.to("cuda")

optimizer = optim.Adam(model.parameters(), lr=0.001)

print("训练开始!请立即在另一个终端查看 `watch -n 0.1 nvidia-smi`...")

# 2. 引入训练循环(2000次迭代)
for step in range(2000):
# 增大 Batch Size (256) 和特征维度 (4096) 以增加 GPU 负载
inputs = torch.randn(256, 4096).to("cuda")

# 前向与反向传播
outputs = model(inputs)
loss = outputs.sum()

optimizer.zero_grad()
loss.backward()
optimizer.step()

# 每 200 步打印一次进度
if step % 200 == 0:
print(f"步骤 [{step}/2000] - Loss: {loss.item():.4f}")

print("训练完成!")
多机多卡DDP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# DDP(DistributedDataParallel)代码示例
# train-ddp-mnmg.py
# 执行: torchrun --nproc_per_node=GPU个数 train-ddp-mnmg.py

import os
import torch
import torch.distributed as dist
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP

# 1. 初始化分布式环境
dist.init_process_group(backend="nccl")
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)

# 2. 定义大模型
class HeavyModel(nn.Module):
def __init__(self):
super().__init__()
self.net = nn.Sequential(
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 4096),
nn.ReLU(),
nn.Linear(4096, 10)
)
def forward(self, x):
return self.net(x)

model = HeavyModel().to(local_rank)
model = DDP(model, device_ids=[local_rank])

optimizer = optim.Adam(model.parameters(), lr=0.001)

# 3. 构造大数据集与分布式采样器
class RandomDataset(Dataset):
def __len__(self):
return 10000 # 总样本数
def __getitem__(self, idx):
return torch.randn(4096), torch.zeros(10)

dataset = RandomDataset()
# DistributedSampler 会自动根据 rank 切分数据
sampler = DistributedSampler(dataset)
# 注意:DDP 中 DataLoader 的 batch_size 是【每个 GPU 单独的 batch】
dataloader = DataLoader(dataset, batch_size=128, sampler=sampler)

# 仅在主进程(rank 0)打印提示
if local_rank == 0:
print("DDP 训练开始!请在另一个终端查看 `watch -n 0.1 nvidia-smi`...")

# 4. 训练循环(跑 5 个 Epoch)
for epoch in range(5):
# 必须设置 sampler 的 epoch,保证数据打乱的随机种子每轮不同
sampler.set_epoch(epoch)

for step, (inputs, _) in enumerate(dataloader):
inputs = inputs.to(local_rank)

outputs = model(inputs)
loss = outputs.sum()

optimizer.zero_grad()
loss.backward()
optimizer.step()

# 仅让 rank 0 进程打印进度,避免日志混乱
if local_rank == 0 and step % 20 == 0:
print(f"Epoch [{epoch}/5] - Step [{step}/{len(dataloader)}] - Loss: {loss.item():.4f}")

if local_rank == 0:
print("DDP 训练完成!")

# ================= 显式加上这一行 =================
dist.destroy_process_group()
# ==================================================

PyTorch02-PyTorch基础知识
https://jiangsanyin.github.io/2026/07/28/PyTorch02-PyTorch基础知识/
作者
sanyinjiang
发布于
2026年7月28日
许可协议