13.7. 参数服务器
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 Colab 中打开 Notebook
在 SageMaker Studio Lab 中打开 Notebook

随着我们从单个GPU发展到多个GPU,再到包含多个GPU的多台服务器(这些服务器可能分布在多个机架和网络交换机上),我们的分布式和并行训练算法需要变得更加复杂。细节至关重要,因为不同的互连设备具有非常不同的带宽(例如,在适当的设置中,NVLink可以通过6个链路提供高达100 GB/s的带宽,PCIe 4.0(16通道)提供32 GB/s,而即使是高速100GbE以太网也仅能达到10 GB/s)。同时,期望一个统计建模人员成为网络和系统专家也是不合理的。

参数服务器的核心思想最初由 Smola and Narayanamurthy (2010) 在分布式潜在变量模型的背景下提出。随后 Ahmed et al. (2012) 对推(push)和拉(pull)语义进行了描述,Li et al. (2014) 对该系统和开源库进行了描述。下面,我们将阐述为提高效率所需的组件。

13.7.1. 数据并行训练

让我们回顾一下分布式训练中的数据并行方法。本节中我们将只讨论这种方法,因为它在实践中实现起来要简单得多。除了在图上进行深度学习等少数情况外,几乎没有其他并行策略比它更受青睐,因为如今的GPU内存充足。 图 13.7.1 描述了我们在 13.5节 中实现的数据并行的一种变体。其关键之处在于,在更新后的参数被重新广播到所有GPU之前,梯度的聚合是在单个GPU(GPU 0)上进行的。

../_images/ps.svg

图 13.7.1 左:单GPU训练。右:多GPU训练的一种变体:(1)我们计算损失和梯度,(2)所有梯度在一个GPU上聚合,(3)进行参数更新,然后将参数重新分发到所有GPU。

回想起来,在GPU 0上聚合的决定似乎有些随意。毕竟,我们也可以在CPU上聚合。事实上,我们甚至可以决定在某个GPU上聚合一部分参数,在另一个GPU上聚合另一部分。只要优化算法支持,这样做并没有什么真正的障碍。例如,如果我们有四个参数向量及其对应的梯度 \(\mathbf{g}_1, \ldots, \mathbf{g}_4\),我们可以为每个 \(\mathbf{g}_i\)\(i = 1, \ldots, 4\))在不同的GPU上聚合梯度。

这种推理似乎既随意又轻率。毕竟,整个过程中的数学原理都是相同的。然而,我们正在处理的是真实的物理硬件,如 13.4节 中所讨论的,不同的总线具有不同的带宽。考虑一个如 图 13.7.2 中所述的真实的4路GPU服务器。如果它连接得特别好,可能有一张100 GbE的网卡。更典型的情况是在1–10 GbE范围内,有效带宽为100 MB/s到1 GB/s。由于CPU没有足够的PCIe通道直接连接所有GPU(例如,消费级Intel CPU有24个通道),我们需要一个多路复用器。CPU在16x Gen3链路上的带宽是16 GB/s。这也是*每个*GPU连接到交换机的速度。这意味着在设备之间进行通信更有效。

../_images/bw-hierarchy.svg

图 13.7.2 一个4路GPU服务器。

为了论证,我们假设梯度大小为160 MB。在这种情况下,将梯度从其余3个GPU发送到第4个GPU需要30毫秒(每次传输需要10毫秒 = 160 MB / 16 GB/s)。再加上将权重向量传回的30毫秒,总共需要60毫秒。如果我们将所有数据发送到CPU,会产生40毫秒的延迟,因为*每个*GPU都需要将数据发送到CPU,总共需要80毫秒。最后,假设我们能够将梯度分成4个40 MB的部分。现在我们可以在不同的GPU上*同时*聚合每个部分,因为PCIe交换机在所有链路之间提供了全带宽操作。这不再需要30毫秒,而是7.5毫秒,使得同步操作的总时间为15毫秒。简而言之,根据我们同步参数的方式,同样的操作可能需要15到80毫秒不等。 图 13.7.3 描述了交换参数的不同策略。

../_images/ps-distributed.svg

图 13.7.3 参数同步策略。

请注意,在提高性能方面,我们还有一个工具:在深度网络中,从顶层到底层计算所有梯度需要一些时间。我们可以在计算其他参数组的梯度时,就开始同步某些参数组的梯度。关于如何在Horovod中实现这一点,详情请参见 Sergeev and Del Balso (2018)

13.7.2. 环形同步

当谈到在现代深度学习硬件上进行同步时,我们经常会遇到高度定制的网络连接。例如,AWS p3.16xlarge和NVIDIA DGX-2实例共享了 图 13.7.4 的连接结构。每个GPU通过PCIe链路连接到主CPU,其最大速度为16 GB/s。此外,每个GPU还拥有6个NVLink连接,每个连接能够双向传输300 Gbit/s的数据。这相当于每个方向每个链路约18 GB/s。简而言之,NVLink的总带宽远高于PCIe带宽。问题是如何最有效地利用它。

../_images/nvlink.svg

图 13.7.4 8台V100 GPU服务器上的NVLink连接(图片由NVIDIA提供)。

事实证明,最佳的同步策略是将网络分解成两个环,并利用它们直接同步数据 (Wang et al., 2018)图 13.7.5 展示了网络可以分解为一个具有双倍NVLink带宽的环(1-2-3-4-5-6-7-8-1)和一个具有常规带宽的环(1-4-6-3-5-8-2-7-1)。在这种情况下设计一个高效的同步协议并非易事。

../_images/nvlink-twoloop.svg

图 13.7.5 将NVLink网络分解为两个环。

考虑以下思想实验:给定一个由 \(n\) 个计算节点(或GPU)组成的环,我们可以将梯度从第一个节点发送到第二个节点。在那里,梯度会与本地梯度相加,然后发送到第三个节点,依此类推。经过 \(n-1\) 步后,聚合后的梯度会出现在最后访问的节点上。也就是说,聚合梯度的时间与节点数量成线性增长。但如果我们这样做,算法效率会很低。毕竟,在任何时候只有一个节点在进行通信。如果我们把梯度分成 \(n\) 块,并从节点 \(i\) 开始同步块 \(i\) 呢?由于每个块的大小是 \(1/n\),总时间现在是 \((n-1)/n \approx 1\)。换句话说,随着环大小的增加,聚合梯度所需的时间*不会增长*。这是一个相当惊人的结果。图 13.7.6 展示了在 \(n=4\) 个节点上的步骤序列。

../_images/ringsync.svg

图 13.7.6 在4个节点上的环形同步。每个节点开始向其左侧邻居传输部分梯度,直到聚合后的梯度出现在其右侧邻居上。

如果我们使用相同的例子,在8个V100 GPU上同步160 MB的数据,我们大约得到 \(2 \cdot 160 \textrm{MB} / (3 \cdot 18 \textrm{GB/s}) \approx 6 \textrm{ms}\)。这比使用PCIe总线要好,即使我们现在使用了8个GPU。请注意,在实践中,这些数字会稍差一些,因为深度学习框架通常无法将通信组装成大的突发传输。

请注意,一个常见的误解是,环形同步与其它同步算法有根本的不同。唯一的区别是,与简单的树形结构相比,同步路径更为精细。

13.7.3. 多机训练

在多台机器上进行分布式训练带来了另一个挑战:我们需要与那些仅通过相对较低带宽网络连接的服务器通信,在某些情况下,这些网络的速度可能慢一个数量级以上。跨设备同步是棘手的。毕竟,运行训练代码的不同机器会有细微的速度差异。因此,如果我们想使用同步分布式优化,就需要*同步*它们。 图 13.7.7 说明了分布式并行训练是如何进行的。

  1. 在每台机器上读取一个(不同的)批次数据,将其分割到多个GPU上,并传输到GPU内存中。在那里,每个GPU批次分别计算预测和梯度。

  2. 所有本地GPU的梯度在一个GPU上聚合(或者部分梯度在不同GPU上聚合)。

  3. 梯度被发送到CPU。

  4. CPU将梯度发送到一个中央参数服务器,该服务器聚合所有梯度。

  5. 聚合后的梯度随后用于更新参数,更新后的参数被广播回各个CPU。

  6. 信息被发送到一个(或多个)GPU。

  7. 更新后的参数被分发到所有GPU。

../_images/ps-multimachine.svg

图 13.7.7 多机多GPU分布式并行训练。

这些操作中的每一个看起来都相当直接。而且,确实,它们可以高效地在*单台*机器内执行。然而,一旦我们考虑多台机器,就会发现中央参数服务器成了瓶颈。毕竟,每台服务器的带宽是有限的,因此对于 \(m\) 个工作节点,将所有梯度发送到服务器所需的时间是 \(\mathcal{O}(m)\)。我们可以通过将服务器数量增加到 \(n\) 来突破这个障碍。此时,每个服务器只需要存储 \(\mathcal{O}(1/n)\) 的参数,因此更新和优化的总时间变为 \(\mathcal{O}(m/n)\)。将这两个数字匹配起来,无论我们处理多少个工作节点,都能实现恒定的扩展。在实践中,我们使用*相同*的机器既作为工作节点也作为服务器。图 13.7.8 展示了这种设计(详情亦可参见 (Li et al., 2014))。特别是,确保多台机器在没有不合理延迟的情况下工作并非易事。

../_images/ps-multips.svg

图 13.7.8 上:单个参数服务器是瓶颈,因为其带宽有限。下:多个参数服务器存储部分参数,总带宽增加。

13.7.4. 键值存储

在实践中,实现分布式多GPU训练所需的步骤并非易事。因此,使用一个通用的抽象,即重新定义了更新语义的*键值存储*,是值得的。

在许多工作节点和许多GPU上,梯度 \(i\) 的计算可以定义为

(13.7.1)\[\mathbf{g}_{i} = \sum_{k \in \textrm{workers}} \sum_{j \in \textrm{GPUs}} \mathbf{g}_{ijk},\]

其中 \(\mathbf{g}_{ijk}\) 是梯度 \(i\) 在工作节点 \(k\) 的GPU \(j\) 上的一部分。这个操作的关键在于它是一个*可交换的归约*,也就是说,它将多个向量变成一个,并且操作的应用顺序不影响结果。这对我们的目的来说非常好,因为我们不需要(也不必)对何时接收到哪个梯度进行精细控制。此外,请注意,这个操作在不同的 \(i\) 之间是独立的。

这使我们能够定义以下两个操作:*push*(推),用于累积梯度;和*pull*(拉),用于检索聚合后的梯度。由于我们有许多不同的梯度集合(毕竟,我们有很多层),我们需要用一个键 \(i\) 来索引梯度。这种与键值存储(例如Dynamo (DeCandia et al., 2007) 中介绍的)的相似性并非巧合。它们也满足许多类似的特性,特别是在将参数分布到多个服务器方面。

键值存储的推(push)和拉(pull)操作描述如下:

  • push(key, value) 从一个工作节点向一个公共存储发送一个特定的梯度(值)。在那里,值被聚合,例如通过求和。

  • pull(key, value) 从公共存储中检索一个聚合值,例如,在合并了所有工作节点的梯度之后。

通过将所有关于同步的复杂性隐藏在一个简单的推和拉操作之后,我们可以将统计建模人员(他们希望能够用简单的术语表达优化)和系统工程师(他们需要处理分布式同步固有的复杂性)的关注点分离开来。

13.7.5. 小结

  • 同步需要高度适应服务器内特定的网络基础设施和连接性。这对同步所需的时间有显著影响。

  • 对于p3和DGX-2服务器,环形同步可能是最优的。对于其他服务器,可能并非如此。

  • 当为增加带宽而添加多个参数服务器时,分层同步策略效果很好。

13.7.6. 练习

  1. 你能否进一步提升环形同步的效率?提示:你可以双向发送消息。

  2. 是否可以允许异步通信(在计算仍在进行时)?它如何影响性能?

  3. 如果在一个长时间运行的计算过程中,我们丢失了一台服务器怎么办?我们如何设计一个*容错*机制来避免完全重新启动计算?

讨论