动手学深度学习12.6. 多GPU的简洁实现-笔记&练习(PyTorch)

以下内容为结合李沐老师的课程和教材补充的学习笔记,以及对课后练习的一些思考,自留回顾,也供同学之人交流参考。

本节课程地址:简洁实现_哔哩哔哩_bilibili

本节教材地址:12.6. 多GPU的简洁实现 --- 动手学深度学习 2.0.0 documentation

本节开源代码:...>d2l-zh>pytorch>chapter_optimization>multiple-gpus-concise.ipynb


多GPU的简洁实现

每个新模型的并行计算都从零开始实现是无趣的。此外,优化同步工具以获得高性能也是有好处的。下面我们将展示如何使用深度学习框架的高级API来实现这一点。数学和算法与 12.5节 中的相同。本节的代码至少需要两个GPU来运行。

复制代码
import torch
from torch import nn
from d2l import torch as d2l

[简单网络]

让我们使用一个比 12.5节 的LeNet更有意义的网络,它依然能够容易地和快速地训练。我们选择的是 (He et al., 2016) 中的ResNet-18。因为输入的图像很小,所以稍微修改了一下。与 7.6节 的区别在于,我们在开始时使用了更小的卷积核、步长和填充,而且删除了最大汇聚层。

复制代码
#@save
def resnet18(num_classes, in_channels=1):
    """稍加修改的ResNet-18模型"""
    def resnet_block(in_channels, out_channels, num_residuals,
                     first_block=False):
        blk = []
        for i in range(num_residuals):
            if i == 0 and not first_block:
                blk.append(d2l.Residual(out_channels, use_1x1conv=True, strides=2))
            else:
                blk.append(d2l.Residual(out_channels))
        return nn.Sequential(*blk)

    # 该模型使用了更小的卷积核、步长和填充,而且删除了最大汇聚层
    net = nn.Sequential(
        nn.Conv2d(in_channels, 64, kernel_size=3, stride=1, padding=1),
        nn.BatchNorm2d(64),
        nn.ReLU())
    net.add_module("resnet_block1", resnet_block(64, 64, 2, first_block=True))
    net.add_module("resnet_block2", resnet_block(64, 128, 2))
    net.add_module("resnet_block3", resnet_block(128, 256, 2))
    net.add_module("resnet_block4", resnet_block(256, 512, 2))
    net.add_module("global_avg_pool", nn.AdaptiveAvgPool2d((1,1)))
    net.add_module("fc", nn.Sequential(nn.Flatten(),
                                       nn.Linear(512, num_classes)))
    return net

网络初始化

我们将在训练回路中初始化网络。请参见 4.8节 复习初始化方法。

复制代码
net = resnet18(10)
# 获取GPU列表
devices = d2l.try_all_gpus()
# 我们将在训练代码实现中初始化网络

[训练]

如前所述,用于训练的代码需要执行几个基本功能才能实现高效并行:

  • 需要在所有设备上初始化网络参数;
  • 在数据集上迭代时,要将小批量数据分配到所有设备上;
  • 跨设备并行计算损失及其梯度;
  • 聚合梯度,并相应地更新参数。

最后,并行地计算精确度和发布网络的最终性能。除了需要拆分和聚合数据外,训练代码与前几章的实现非常相似。

复制代码
def train(net, num_gpus, batch_size, lr):
    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    devices = [d2l.try_gpu(i) for i in range(num_gpus)]

    def init_weights(m):
        if type(m) in [nn.Linear, nn.Conv2d]:
            nn.init.normal_(m.weight, std=0.01)

    net.apply(init_weights)
    # 在多个GPU上设置模型,可以自动实现数据切分和并行计算
    net = nn.DataParallel(net, device_ids=devices)
    trainer = torch.optim.SGD(net.parameters(), lr)
    loss = nn.CrossEntropyLoss()
    timer, num_epochs = d2l.Timer(), 10
    animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
    for epoch in range(num_epochs):
        net.train()
        timer.start()
        for X, y in train_iter:
            trainer.zero_grad()
            X, y = X.to(devices[0]), y.to(devices[0])
            l = loss(net(X), y)
            l.backward()
            trainer.step()
        timer.stop()
        animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
    print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
          f'在{str(devices)}')

接下来看看这在实践中是如何运作的。我们先[在单个GPU上训练网络]进行预热。

复制代码
train(net, num_gpus=1, batch_size=256, lr=0.1)

输出结果:

测试精度:0.90,47.1秒/轮,在[device(type='cuda', index=0)]

接下来我们[使用2个GPU进行训练 ]。与 12.5节 中评估的LeNet相比,ResNet-18的模型要复杂得多。这就是显示并行化优势的地方,计算所需时间明显大于同步参数需要的时间。因为并行化开销的相关性较小,因此这种操作提高了模型的可伸缩性。

复制代码
train(net, num_gpus=2, batch_size=512, lr=0.2)

输出结果:

测试精度:0.80,25.6秒/轮,在[device(type='cuda', index=0), device(type='cuda', index=1)]

小结

  • 神经网络可以在(可找到数据的)单GPU上进行自动评估。
  • 每台设备上的网络需要先初始化,然后再尝试访问该设备上的参数,否则会遇到错误。
  • 优化算法在多个GPU上自动聚合。

练习

  1. 本节使用ResNet-18,请尝试不同的迭代周期数、批量大小和学习率,以及使用更多的GPU进行计算。如果使用16个GPU(例如,在AWS p2.16xlarge实例上)尝试此操作,会发生什么?
    解:
    下面是尝试不同的迭代周期数、批量大小和学习率的实验,结果表明:
  • 测试精度基本上是随迭代周期数的增加而增加,epoch为10或20或30的耗时相差不大,但测试精度明显提高。

  • 测试精度随批量大小的增加先降后升,batch_size为512时的测试精度和耗时都最低,batch_size为1024时的测试精度最高,耗时还可以。

  • 测试精度随学习率的增加也是先降后升,但耗时从lr为0.1开始就几乎不变了,lr为0.4时的测试精度最高。 只有两个GPU,所以GPU数量对于测试精度和耗时的影响没有进行实验。

    def train(net, num_gpus, batch_size, lr, num_epochs):
    train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)
    devices = [d2l.try_gpu(i) for i in range(num_gpus)]
    def init_weights(m):
    if type(m) in [nn.Linear, nn.Conv2d]:
    nn.init.normal_(m.weight, std=0.01)
    net.apply(init_weights)
    # 在多个GPU上设置模型
    net = nn.DataParallel(net, device_ids=devices)
    trainer = torch.optim.SGD(net.parameters(), lr)
    loss = nn.CrossEntropyLoss()
    timer = d2l.Timer()
    animator = d2l.Animator('epoch', 'test acc', xlim=[1, num_epochs])
    for epoch in range(num_epochs):
    net.train()
    timer.start()
    for X, y in train_iter:
    trainer.zero_grad()
    X, y = X.to(devices[0]), y.to(devices[0])
    l = loss(net(X), y)
    l.backward()
    trainer.step()
    timer.stop()
    animator.add(epoch + 1, (d2l.evaluate_accuracy_gpu(net, test_iter),))
    print(f'测试精度:{animator.Y[0][-1]:.2f},{timer.avg():.1f}秒/轮,'
    f'在{str(devices)}')
    return animator.Y[0][-1], timer.avg()
    epochs = [5, 10, 20, 30]
    accs_1 = {"epochs": [], "accs": [], "times": []}

    for epoch in epochs:
    accs_1["epochs"].append(epoch)
    acc, time = train(net, num_gpus=2, batch_size=512, lr=0.2, num_epochs=epoch)
    accs_1["accs"].append(acc)
    accs_1["times"].append(time)

输出结果:
测试精度:0.93,25.6秒/轮,在[device(type='cuda', index=0), device(type='cuda', index=1)]

复制代码
fig, ax1 = plt.subplots(figsize=(6, 4))
ax1.plot(accs_1["epochs"], accs_1["accs"], marker='o', label='acc')

ax1.set_xlabel('epochs')
ax1.set_ylabel('accs')

ax2 = ax1.twinx()
ax2.plot(accs_1["epochs"], accs_1["times"], marker='o', label='time', color = 'orange')
ax2.set_ylabel('times')
ax1.legend(loc='upper left')
ax2.legend(loc='upper right')

plt.show()
复制代码
batchs = [128, 256, 512, 1024]
accs_2 = {"batchs": [], "accs": [], "times": []} 

for batch in batchs:
    accs_2["batchs"].append(batch)
    acc, time = train(net, num_gpus=2, batch_size=batch, lr=0.2, num_epochs=10)
    accs_2["accs"].append(acc)
    accs_2["times"].append(time)

输出结果:
测试精度:0.92,27.1秒/轮,在[device(type='cuda', index=0), device(type='cuda', index=1)]

复制代码
fig, ax1 = plt.subplots(figsize=(8, 4))
ax1.plot(accs_2["batchs"], accs_2["accs"], marker='o', label='acc',)

ax1.set_xlabel('batchs')
ax1.set_ylabel('accs')
ax1.legend(loc='upper left')

ax2 = ax1.twinx()
ax2.plot(accs_2["batchs"], accs_2["times"], marker='o', label='time', color = 'orange')
ax2.set_ylabel('times')
ax2.legend(loc='upper right')

plt.show()
复制代码
lrs = [0.05, 0.1, 0.2, 0.3, 0.4]
accs_3 = {"lrs": [], "accs": [], "times": []} 

for lr in lrs:
    accs_3["lrs"].append(lr)
    acc, time = train(net, num_gpus=2, batch_size=512, lr=lr, num_epochs=10)
    accs_3["accs"].append(acc)
    accs_3["times"].append(time)

输出结果:
测试精度:0.88,25.6秒/轮,在[device(type='cuda', index=0), device(type='cuda', index=1)]

复制代码
fig, ax1 = plt.subplots(figsize=(8, 4))
ax1.plot(accs_3["lrs"], accs_3["accs"], marker='o')

ax1.set_xlabel('lrs')
ax1.set_ylabel('accs')
ax1.legend(['acc'], loc='upper left')

ax2 = ax1.twinx()
ax2.plot(accs_3["lrs"], accs_3["times"], marker='o', color = 'orange')
ax2.set_ylabel('times')
ax2.legend(['time'], loc='upper right')

plt.show()
  1. 有时候不同的设备提供了不同的计算能力,我们可以同时使用GPU和CPU,那应该如何分配工作?为什么?
    解:
    根据GPU和CPU的计算能力和特性,可以如下分配:
    1)数据预处理和后处理分配给CPU,包括:数据加载、数据预处理(如归一化、裁剪、翻转等)、数据后处理。因为CPU在处理I/O和控制流方面更高效,而数据预处理和后处理通常涉及大量的I/O操作和简单的数值计算,所以这些任务更适合在CPU上运行。
    2)模型训练分配给GPU,包括:模型的前向传播、后向传播和权重更新。因为GPU在处理大规模矩阵运算和并行计算方面具有显著优势,能够加速模型的训练过程。
相关推荐
kailp25 分钟前
云渲染技术解析与渲酷平台深度测评:如何实现高效3D创作?
人工智能·深度学习·3d·渲染·gpu算力
摆烂..32 分钟前
博客摘录「 unity 协程 多线程 进程 纤程 区别和介绍」2025年5月22日
笔记
Tech Synapse2 小时前
深度解析3D模型生成器:基于StyleGAN3与PyTorch3D的多风格生成工具开发实战
人工智能·pytorch·深度学习·3d
旺仔溜溜没2 小时前
Autodl训练Faster-RCNN网络(自己的数据集)
人工智能·深度学习·神经网络·cnn
Stuomasi_xiaoxin3 小时前
FFmpeg 超级详细安装与配置教程(Windows 系统)
python·深度学习·ai·ffmpeg
z人间防沉迷k4 小时前
高效查询:位图、B+树
开发语言·数据结构·笔记·python·算法
尬尬_4 小时前
【计算机网络】TCP如何保障传输可靠性_笔记
服务器·笔记·tcp/ip·计算机网络·考研
孤寂大仙v4 小时前
【Linux笔记】——网络基础
linux·网络·笔记
缘友一世7 小时前
PyTorch可视化工具——使用Visdom进行深度学习可视化
人工智能·pytorch·深度学习