PyTorch 单机模型并行最佳实践

2025-06-19 10:16 更新

随着深度学习模型的不断增大和复杂化,传统的单 GPU 训练方式已难以满足模型对计算资源的需求。模型并行作为一种有效的解决方案,通过将模型的不同部分分配到多个 GPU 上进行计算,使得在单机环境下也能高效地训练大型模型。本文将深入探讨 PyTorch 中单机模型并行的最佳实践方法,帮助您在有限的硬件资源下实现模型的高效训练和推理。

一、模型并行的基本概念

模型并行的核心思想是将模型的不同子网络放置在不同的 GPU 上,并在训练过程中实现各子网络之间的高效通信。与数据并行(DataParallel)不同,模型并行将模型的不同部分分配到多个 GPU 上,而不是在每个 GPU 上复制整个模型。例如,假设一个模型包含 10 层神经网络,使用模型并行时,可以将前 5 层放在一个 GPU 上,后 5 层放在另一个 GPU 上。

通过这种方式,每个 GPU 只需处理模型的一部分,从而能够容纳更大规模的模型。然而,模型并行也带来了额外的通信开销,因为中间输出需要在 GPU 之间进行传输。因此,在实际应用中需要权衡模型并行的性能和通信成本。

二、实现简单的模型并行示例

接下来,我们将通过一个简单的玩具模型来演示如何在 PyTorch 中实现模型并行。

(一)定义模型

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


class ToyModel(nn.Module):
    def __init__(self):
        super(ToyModel, self).__init__()
        self.net1 = torch.nn.Linear(10, 10).to('cuda:0')  # 将第一层线性层放置在 GPU0 上
        self.relu = torch.nn.ReLU()
        self.net2 = torch.nn.Linear(10, 5).to('cuda:1')  # 将第二层线性层放置在 GPU1 上


    def forward(self, x):
        x = self.relu(self.net1(x.to('cuda:0')))  # 将输入数据移动到 GPU0
        return self.net2(x.to('cuda:1'))  # 将中间输出移动到 GPU1

(二)定义损失函数和优化器

model = ToyModel()
loss_fn = nn.MSELoss()  # 定义均方误差损失函数
optimizer = optim.SGD(model.parameters(), lr=0.001)  # 定义随机梯度下降优化器

(三)训练模型

optimizer.zero_grad()  # 清空梯度
outputs = model(torch.randn(20, 10))  # 生成随机输入数据并进行前向传播
labels = torch.randn(20, 5).to('cuda:1')  # 将标签数据移动到 GPU1 上
loss_fn(outputs, labels).backward()  # 计算损失并进行反向传播
optimizer.step()  # 更新模型参数

在上述代码中,我们将模型的不同部分分别放置在两个 GPU 上,并通过 .to(device) 方法将输入数据和中间输出在 GPU 之间进行传输。同时,损失函数和优化器的使用与单 GPU 训练时保持一致。

三、将模型并行应用于现有模块

对于已有的模型,我们可以通过继承原模型类并重新定义 forward 方法来实现模型并行。以下以 ResNet50 模型为例进行说明。

(一)定义模型并行的 ResNet50

from torchvision.models.resnet import ResNet, Bottleneck


num_classes = 1000


class ModelParallelResNet50(ResNet):
    def __init__(self, *args, **kwargs):
        super(ModelParallelResNet50, self).__init__(Bottleneck, [3, 4, 6, 3], num_classes=num_classes, *args, **kwargs)
        self.seq1 = nn.Sequential(
            self.conv1,
            self.bn1,
            self.relu,
            self.maxpool,
            self.layer1,
            self.layer2
        ).to('cuda:0')  # 将部分层序列放置在 GPU0 上


        self.seq2 = nn.Sequential(
            self.layer3,
            self.layer4,
            self.avgpool,
        ).to('cuda:1')  # 将剩余层序列放置在 GPU1 上


        self.fc.to('cuda:1')  # 将全连接层放置在 GPU1 上


    def forward(self, x):
        x = self.seq2(self.seq1(x).to('cuda:1'))  # 将中间输出从 GPU0 传输到 GPU1
        return self.fc(x.view(x.size(0), -1))  # 在 GPU1 上进行全连接层计算

(二)训练并行模型

import torchvision.models as models
import timeit
import matplotlib.pyplot as plt
import numpy as np


## 定义训练函数
def train(model):
    model.train(True)
    loss_fn = nn.MSELoss()
    optimizer = optim.SGD(model.parameters(), lr=0.001)
    one_hot_indices = torch.LongTensor(batch_size).random_(0, num_classes).view(batch_size, 1)


    for _ in range(num_batches):
        inputs = torch.randn(batch_size, 3, image_w, image_h)
        labels = torch.zeros(batch_size, num_classes).scatter_(1, one_hot_indices, 1)


        optimizer.zero_grad()
        outputs = model(inputs.to('cuda:0'))  # 输入数据移动到 GPU0
        labels = labels.to(outputs.device)  # 标签数据移动到输出所在 GPU
        loss_fn(outputs, labels).backward()
        optimizer.step()


## 设置训练参数
num_batches = 3
batch_size = 120
image_w = 128
image_h = 128
num_repeat = 10


## 测试模型并行的 ResNet50
model = ModelParallelResNet50()
mp_run_times = timeit.repeat("train(model)", globals=globals(), number=1, repeat=num_repeat)
mp_mean, mp_std = np.mean(mp_run_times), np.std(mp_run_times)


## 测试单 GPU 的 ResNet50
model = models.resnet50(num_classes=num_classes).to('cuda:0')
rn_run_times = timeit.repeat("train(model)", globals=globals(), number=1, repeat=num_repeat)
rn_mean, rn_std = np.mean(rn_run_times), np.std(rn_run_times)


## 绘制执行时间对比图
def plot(means, stds, labels, fig_name):
    fig, ax = plt.subplots()
    ax.bar(np.arange(len(means)), means, yerr=stds, align='center', alpha=0.5, ecolor='red', capsize=10, width=0.6)
    ax.set_ylabel('ResNet50 Execution Time (Second)')
    ax.set_xticks(np.arange(len(means)))
    ax.set_xticklabels(labels)
    ax.yaxis.grid(True)
    plt.tight_layout()
    plt.savefig(fig_name)
    plt.close(fig)


plot([mp_mean, rn_mean], [mp_std, rn_std], ['Model Parallel', 'Single GPU'], 'mp_vs_rn.png')

从实验结果可以看出,模型并行的实现虽然能够解决模型过大无法放入单个 GPU 的问题,但其执行时间通常会长于单 GPU 实现,这是由于 GPU 之间的通信开销所致。

四、通过流水线输入加速模型并行

为了进一步提升模型并行的训练效率,可以采用流水线技术对输入数据进行划分和并行处理。

(一)定义流水线并行模型

class PipelineParallelResNet50(ModelParallelResNet50):
    def __init__(self, split_size=20, *args, **kwargs):
        super(PipelineParallelResNet50, self).__init__(*args, **kwargs)
        self.split_size = split_size


    def forward(self, x):
        splits = iter(x.split(self.split_size, dim=0))  # 将输入数据按批次划分
        s_next = next(splits)
        s_prev = self.seq1(s_next).to('cuda:1')  # 将第一个批次的数据传入 GPU1
        ret = []


        for s_next in splits:
            s_prev = self.seq2(s_prev)  # 在 GPU1 上计算当前批次
            ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))


            s_prev = self.seq1(s_next).to('cuda:1')  # 将下一个批次的数据传入 GPU1,与 GPU1 的计算并行进行


        s_prev = self.seq2(s_prev)
        ret.append(self.fc(s_prev.view(s_prev.size(0), -1)))


        return torch.cat(ret)

(二)测试流水线并行模型

## 测试流水线并行的 ResNet50
model = PipelineParallelResNet50(split_size=20)
pp_run_times = timeit.repeat("train(model)", globals=globals(), number=1, repeat=num_repeat)
pp_mean, pp_std = np.mean(pp_run_times), np.std(pp_run_times)


## 绘制包含流水线并行的执行时间对比图
plot([mp_mean, rn_mean, pp_mean], [mp_std, rn_std, pp_std], ['Model Parallel', 'Single GPU', 'Pipelining Model Parallel'], 'mp_vs_rn_vs_pp.png')

流水线技术通过将输入数据划分为多个批次,并在多个 GPU 上并行处理这些批次,能够显著提高模型并行的训练效率。

五、总结与展望

通过本文,您已经学习了如何在 PyTorch 中实现单机模型并行,包括基本的模型并行概念、实现方法以及通过流水线技术加速模型并行训练的技巧。尽管模型并行在处理大型模型时具有显著优势,但其通信开销也可能成为性能瓶颈。在实际应用中,需要根据模型结构和硬件资源合理选择并行策略,并通过实验优化相关参数。

未来,您可以进一步探索分布式模型并行训练、多机多 GPU 并行等更高级的并行技术,以应对更大规模的模型和更复杂的训练任务。编程狮将持续为您带来更多深度学习模型并行训练的优质教程,助力您在高性能计算领域不断前行。

以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号