到目前为止,我们讨论了如何在 CPU 和 GPU 上高效地训练模型。在13.3 节中,我们甚至展示了深度学习框架如何允许人们在它们之间自动并行计算和通信 。我们还在6.7 节中展示了如何使用nvidia-smi
命令列出计算机上所有可用的 GPU。我们没有讨论的是如何真正并行化深度学习训练。相反,我们暗示传递一个会以某种方式将数据拆分到多个设备并使其工作。本节填写详细信息并展示如何从头开始并行训练网络。有关如何利用高级 API 中的功能的详细信息归入 第 13.6 节. 我们假设您熟悉 minibatch 随机梯度下降算法,例如12.5 节中描述的算法。
13.5.1。拆分问题
让我们从一个简单的计算机视觉问题和一个稍微陈旧的网络开始,例如,具有多层卷积、池化,最后可能还有一些完全连接的层。也就是说,让我们从一个看起来与 LeNet (LeCun等人,1998 年)或 AlexNet (Krizhevsky等人,2012 年)非常相似的网络开始。给定多个 GPU(如果是桌面服务器则为 2 个,在 AWS g4dn.12xlarge 实例上为 4 个,在 p3.16xlarge 上为 8 个,或在 p2.16xlarge 上为 16 个),我们希望以实现良好加速的方式对训练进行分区同时受益于简单且可重现的设计选择。毕竟,多个 GPU 会增加内存和计算能力。简而言之,给定我们要分类的小批量训练数据,我们有以下选择。
首先,我们可以跨多个 GPU 划分网络。也就是说,每个 GPU 将流入特定层的数据作为输入,跨多个后续层处理数据,然后将数据发送到下一个 GPU。与单个 GPU 可以处理的数据相比,这使我们能够使用更大的网络处理数据。此外,可以很好地控制每个 GPU 的内存占用量(它只占网络总占用量的一小部分)。
然而,层(以及 GPU)之间的接口需要紧密同步。这可能很棘手,特别是如果层与层之间的计算工作负载没有正确匹配。对于大量 GPU,问题会更加严重。层与层之间的接口也需要大量的数据传输,例如激活和梯度。这可能会超出 GPU 总线的带宽。此外,计算密集型但顺序的操作对于分区来说并不重要。参见例如Mirhoseini等人。( 2017 年)在这方面尽最大努力。这仍然是一个难题,尚不清楚是否有可能在非平凡问题上实现良好的(线性)缩放。我们不推荐它,除非有出色的框架或操作系统支持将多个 GPU 链接在一起。
其次,我们可以分层拆分工作。例如,与其在单个 GPU 上计算 64 个通道,不如将问题拆分到 4 个 GPU,每个 GPU 生成 16 个通道的数据。同样,对于全连接层,我们可以拆分输出单元的数量。 图 13.5.1(取自 Krizhevsky等人(2012 年))说明了这种设计,其中这种策略用于处理内存占用非常小(当时为 2 GB)的 GPU。如果通道(或单元)的数量不太小,这就可以在计算方面实现良好的缩放。此外,由于可用内存线性扩展,多个 GPU 可以处理越来越大的网络。
然而,我们需要大量的同步或屏障操作,因为每一层都依赖于所有其他层的结果。此外,需要传输的数据量可能比跨 GPU 分布层时更大。因此,由于带宽成本和复杂性,我们不推荐这种方法。
最后,我们可以跨多个 GPU 对数据进行分区。这样,所有 GPU 都执行相同类型的工作,尽管观察结果不同。在每个小批量训练数据之后,梯度在 GPU 之间聚合。这是最简单的方法,适用于任何情况。我们只需要在每个小批量之后进行同步。也就是说,非常希望在其他仍在计算的同时开始交换梯度参数。此外,更大数量的 GPU 会导致更大的小批量大小,从而提高训练效率。然而,添加更多 GPU 并不能让我们训练更大的模型。
图 13.5.2描绘了多 GPU 上不同并行化方式的比较。总的来说,数据并行是最方便的方法,前提是我们可以访问具有足够大内存的 GPU。另请参阅 ( Li et al. , 2014 )以了解分布式训练分区的详细描述。在深度学习的早期,GPU 内存曾经是一个问题。到目前为止,除了最不寻常的情况外,所有问题都已解决。下面我们重点介绍数据并行性。
13.5.2。数据并行
假设有k机器上的 GPU。给定要训练的模型,每个 GPU 将独立维护一组完整的模型参数,尽管 GPU 之间的参数值是相同且同步的。例如,图 13.5.3说明了在以下情况下使用数据并行性进行训练k=2.
一般来说,训练过程如下:
-
在训练的任何迭代中,给定一个随机小批量,我们将批量中的示例分成k部分并将它们均匀地分布在 GPU 上。
-
每个 GPU 根据分配给它的小批量子集计算模型参数的损失和梯度。
-
每个的局部梯度kGPU 被聚合以获得当前的小批量随机梯度。
-
聚合梯度被重新分配给每个 GPU。
-
每个 GPU 使用这个小批量随机梯度来更新它维护的完整模型参数集。
请注意,在实践中我们增加了小批量大小k-训练时折叠kGPU 这样每个 GPU 都有相同数量的工作要做,就好像我们只在单个 GPU 上训练一样。在 16-GPU 服务器上,这会大大增加小批量大小,我们可能不得不相应地增加学习率。另请注意,第 8.5 节中的批量归一化需要进行调整,例如,通过为每个 GPU 保留一个单独的批量归一化系数。下面我们将使用玩具网络来说明多 GPU 训练。
%matplotlib inline
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
13.5.3。玩具网络
我们使用7.6 节中介绍的 LeNet (稍作修改)。我们从头开始定义它以详细说明参数交换和同步。
# Initialize model parameters
scale = 0.01
W1 = torch.randn(size=(20, 1, 3, 3)) * scale
b1 = torch.zeros(20)
W2 = torch.randn(size=(50, 20, 5, 5)) * scale
b2 = torch.zeros(50)
W3 = torch.randn(size=(800, 128)) * scale
b3 = torch.zeros(128)
W4 = torch.randn(size=(128, 10)) * scale
b4 = torch.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]
# Define the model
def lenet(X, params):
h1_conv = F.conv2d(input=X, weight=params[0], bias=params[1])
h1_activation = F.relu(h1_conv)
h1 = F.avg_pool2d(input=h1_activation, kernel_size=(2, 2), stride=(2, 2))
h2_conv = F.conv2d(input=h1, weight=params[2], bias=params[3])
h2_activation = F.relu(h2_conv)
h2 = F.avg_pool2d(input=h2_activation, kernel_size=(2, 2), stride=(2, 2))
h2 = h2.reshape(h2.shape[0], -1)
h3_linear = torch.mm(h2, params[4]) + params[5]
h3 = F.relu(h3_linear)
y_hat = torch.mm(h3, params[6]) + params[7]
return y_hat
# Cross-entropy loss function
loss = nn.CrossEntropyLoss(reduction='none')
# Initialize model parameters
scale = 0.01
W1 = np.random.normal(scale=scale, size=(20, 1, 3, 3))
b1 = np.zeros(20)
W2 = np.random.normal(scale=scale, size=(50, 20, 5, 5))
b2 = np.zeros(50)
W3 = np.random.normal(scale=scale, size=(800, 128))
b3 = np.zeros(128)
W4 = np.random.normal(scale=scale, size=(128, 10))
b4 = np.zeros(10)
params = [W1, b1, W2, b2, W3, b3, W4, b4]
# Define the model
def lenet(X, params):
h1_conv = npx.convolution(data=X, weight=params[0], bias=params[1],
kernel=(3, 3), num_filter=20)
h1_activation = npx.relu(h1_conv)
h1 = npx.pooling(data=h1_activation, pool_type='avg', kernel=(2, 2),
stride=(2, 2))
h2_conv = npx.convolution(data=h1, weight=params[2], bias=params[3],
kernel=(5, 5), num_filter=50)
h2_activation = npx.relu(h2_conv)
h2 = npx.pooling(data=h2_activation, pool_type='avg', kernel=(2, 2),
stride=(2, 2))
h2 = h2.reshape(h2
评论
查看更多