中文版
数据并行、模型并行与张量并行:深度学习中的并行计算策略
随着深度学习模型的不断增大,单个计算节点(例如单个 GPU)的计算和内存能力逐渐成为了限制训练效率和模型规模的瓶颈。为了应对这些挑战,深度学习社区提出了多种并行计算策略,其中包括数据并行(Data Parallelism)、模型并行(Model Parallelism)和张量并行(Tensor Parallelism)。
这些并行策略的核心思想是通过在多个计算设备之间分配计算负载,来加速模型训练,尤其是当模型规模超出单个设备的计算能力时,能够有效提升训练效率和扩展性。
在本篇博客中,我们将通俗易懂地解释这些并行策略的概念,并通过简单的示例代码来帮助大家理解。我们还会加入数学公式来帮助阐明这些概念。
1. 数据并行(Data Parallelism)
数据并行是最常见的并行训练策略之一,它通过将数据集拆分成多个小批次(mini-batch),并将这些小批次分配给不同的计算设备(如不同的 GPU),以此来加速训练。
数据并行的基本思路:
- 模型复制:每个计算设备都有一个完整的模型副本。
- 数据划分:训练数据被划分成多个小批次,每个设备处理不同的小批次。
- 梯度聚合:各个设备计算出的梯度会被合并(通常使用求平均或求和),并同步更新模型的参数。
假设我们有一个总的数据集 ( D = { x 1 , x 2 , . . . , x n } D = \{x_1, x_2, ..., x_n\} D={x1,x2,...,xn} ),将其拆分成 ( P P P ) 个小批次,分配给 ( P P P ) 个 GPU。每个 GPU 上运行相同的模型,计算出对应小批次的梯度,并最终将梯度合并更新模型参数。
数学公式:
对于每个设备 (
i
i
i ),计算损失函数 (
L
(
θ
i
,
x
i
)
L(\theta_i, x_i)
L(θi,xi) ):
L
(
θ
i
,
x
i
)
=
Loss
(
f
(
x
i
;
θ
i
)
)
L(\theta_i, x_i) = \text{Loss}(f(x_i; \theta_i))
L(θi,xi)=Loss(f(xi;θi))
其中 (
f
(
x
i
;
θ
i
)
f(x_i; \theta_i)
f(xi;θi) ) 是模型的输出,(
θ
i
\theta_i
θi ) 是设备 (
i
i
i ) 上的模型参数。
计算完成后,所有设备计算出的梯度 (
∇
θ
i
L
\nabla \theta_i L
∇θiL ) 会聚合:
1
P
∑
i
=
1
P
∇
θ
i
L
\frac{1}{P} \sum_{i=1}^{P} \nabla \theta_i L
P1i=1∑P∇θiL
然后,通过这种方式同步更新所有设备上的模型参数。
示例代码:
import torch
import torch.nn as nn
import torch.optim as optim
from torch.nn.parallel import DataParallel
# 创建一个简单的神经网络
class SimpleNN(nn.Module):
def __init__(self):
super(SimpleNN, self).__init__()
self.fc1 = nn.Linear(100, 50)
self.fc2 = nn.Linear(50, 10)
def forward(self, x):
x = torch.relu(self.fc1(x))
x = self.fc2(x)
return x
# 假设我们有两个GPU
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = SimpleNN()
# 使用DataParallel包装模型,进行数据并行训练
model = DataParallel(model) # 自动分配到多个GPU
model.to(device)
# 假设数据
input_data = torch.randn(64, 100).to(device) # 一个批次的数据
target = torch.randn(64, 10).to(device)
# 定义损失函数和优化器
criterion = nn.MSELoss()
optimizer = optim.SGD(model.parameters(), lr=0.01)
# 训练循环
for epoch in range(10):
optimizer.zero_grad()
output = model(input_data)
loss = criterion(output, target)
loss.backward()
optimizer.step()
2. 模型并行(Model Parallelism)
当模型的规模非常大,以至于无法在单个设备的内存中存储时,可以采用模型并行策略。与数据并行不同,模型并行通过将模型的不同部分分配到多个设备上进行计算,而不是将数据拆分。
模型并行的基本思路:
- 模型拆分:将模型划分为多个部分,每个部分被放置到不同的设备上。
- 设备间通信:各部分之间通过设备间的通信进行数据传递。
假设我们的模型包含两个大层 ( L 1 L_1 L1 ) 和 ( L 2 L_2 L2 ),由于 ( L L L ) 的参数过大,无法全部存放在单个 GPU 上,我们可以将 ( L 1 L_1 L1 ) 放在 GPU 1 上,将 ( L 2 L_2 L2 ) 放在 GPU 2 上,进行计算。
数学公式:
对于模型的不同部分,假设我们有两个设备 ( G P U 1 GPU_1 GPU1 ) 和 ( G P U 2 GPU_2 GPU2 ),计算流程如下:
- 在 ( G P U 1 GPU_1 GPU1 ) 上计算第一部分的输出 ( h 1 = f 1 ( x ) h_1 = f_1(x) h1=f1(x) )。
- 将 ( h 1 h_1 h1 ) 传输到 ( G P U 2 GPU_2 GPU2 ),在 ( G P U 2 GPU_2 GPU2 ) 上计算第二部分 ( h 2 = f 2 ( h 1 ) h_2 = f_2(h_1) h2=f2(h1) )。
- 最终输出 ( y = h 2 y = h_2 y=h2 )。
示例代码:
import torch
import torch.nn as nn
class Part1(nn.Module):
def __init__(self):
super(Part1, self).__init__()
self.fc1 = nn.Linear(100, 200)
def forward(self, x):
return torch.relu(self.fc1(x))
class Part2(nn.Module):
def __init__(self):
super(Part2, self).__init__()
self.fc2 = nn.Linear(200, 10)
def forward(self, x):
return self.fc2(x)
device1 = torch.device("cuda:0")
device2 = torch.device("cuda:1")
model_part1 = Part1().to(device1)
model_part2 = Part2().to(device2)
# 假设输入数据
input_data = torch.randn(64, 100).to(device1)
# 在GPU 1上计算第一部分
h1 = model_part1(input_data)
# 将数据传输到GPU 2并计算第二部分
h1 = h1.to(device2)
output = model_part2(h1)
3. 张量并行(Tensor Parallelism)
张量并行是一种细粒度的并行策略,它通过将张量的计算切分成多个部分,并在多个设备上并行计算来加速训练过程。张量并行通常与数据并行和模型并行结合使用。
张量并行的基本思路:
- 张量切分:将模型中的大张量(例如权重矩阵)分割成多个小张量,并分配给不同的设备。
- 并行计算:不同设备并行计算各自的部分,然后将结果合并。
假设我们有一个大型矩阵 ( A A A ) 需要进行矩阵乘法。我们可以将 ( A A A ) 切分成几个小矩阵,每个小矩阵分配给一个设备,进行并行计算。
数学公式:
假设我们要计算矩阵乘法 ( C = A ⋅ B C = A \cdot B C=A⋅B ),其中 ( A A A ) 是一个 ( m × n m \times n m×n ) 的矩阵,( B B B ) 是一个 ( n × p n \times p n×p ) 的矩阵。
在张量并行中,我们将矩阵 ( A A A ) 切分成若干个子矩阵 ( A 1 , A 2 , . . . , A k A_1, A_2, ..., A_k A1,A2,...,Ak ),并将每个子矩阵 ( A i A_i Ai ) 分配到不同的 GPU 上进行计算。
C
=
(
A
1
⋅
B
)
+
(
A
2
⋅
B
)
+
⋯
+
(
A
k
⋅
B
)
C = \left( A_1 \cdot B \right) + \left( A_2 \cdot B \right) + \dots + \left( A_k \cdot B \right)
C=(A1⋅B)+(A2⋅B)+⋯+(A