12.7. 参数服务器
Open the notebook in Colab
Open the notebook in Colab
Open the notebook in Colab

当我们从一个 GPU 迁移到多个 GPU 时,以及再迁移到包含多个 GPU 的多个服务器时(可能所有服务器的分布跨越了多个机架和多个网络交换机),分布式并行训练算法也需要变得更加复杂。通过细节可以知道,一方面是不同的互连方式的带宽存在极大的区别(例如,NVLink 可以通过设置实现跨 \(6\) 条链路的高达 100 GB/s 的带宽,16 通道的 PCIe 4.0 提供 32 GB/s 的带宽,而即使是高速 100 GbE 以太网也只能提供大约10 GB/s 的带宽);另一方面是期望开发者既能完成统计学习建模还精通系统和网络也是不切实际的。

参数服务器的核心思想首先是由 [Smola & Narayanamurthy, 2010] 在分布式隐变量模型的背景下引入的。然后,在 [Ahmed et al., 2012] 中描述了 Push 和 Pull 的语义,又在 [Li et al., 2014] 中描述了系统和开源库。下面,我们将介绍用于提高计算效率的组件。

12.7.1. 数据并行训练

让我们回顾一下在分布式架构中数据并行的训练方法,因为在实践中它的实现相对简单,因此本节将排除其他内容只对其进行介绍。由于当今的 GPU 拥有大量的显存,因此在实际场景中(不包括图深度学习)只有数据并行这种并行训练策略值得推荐。图 图12.7.1 描述了在 12.5节 中实现的数据并行的变体。其中的关键是梯度的聚合需要在 GPU 0 上完成,然后再将更新后的参数广播给所有 GPU。

../_images/ps.svg

图12.7.1 左图:单GPU训练。右图:多GPU训练的一个变体:(1)计算损失和梯度,(2)所有梯度聚合在一个GPU上,(3)发生参数更新,并将参数重新广播给所有GPU。

回顾来看,选择 GPU 0 进行聚合似乎是个很随便的决定,当然也可以选择 CPU 上聚合,事实上只要优化算法支持,在实际操作中甚至可以在某个 GPU 上聚合其中一些参数,而在另一个 GPU 上聚合另一些参数。例如,如果有四个与参数向量相关的梯度 \(\mathbf{g}_1, \ldots, \mathbf{g}_4\),还可以一个 GPU 对一个 \(\mathbf{g}_i (i = 1, \ldots, 4\)) 地进行梯度聚合。

这样的推断似乎是轻率和武断的,毕竟数学应该是逻辑自洽的。但是,我们处理的是如 12.4节 中所述的真实的物理硬件,其中不同的总线具有不同的带宽。考虑一个如 12.4节 中所述的真实的 \(4\) 路 GPU 服务器。如果它的连接是特别完整的,那么可能拥有一个 100 GbE 的网卡。更有代表性的数字是 1-10GbE 范围内,其有效带宽为 100 MB/s 到 1 GB/s。因为 CPU 的 PCIe 通道太少(例如,消费级的 Intel CPU 有 \(24\) 个通道),所以无法直接与所有的 GPU 相连接,因此需要 multiplexer。CPU 在 16x Gen3 链路上的带宽为 16 GB/s,这也是每个 GPU 连接到交换机的速度,这意味着 GPU 设备之间的通信更有效。

../_images/bw-hierarchy.svg

图12.7.2 一个4路GPU服务器

为了便于讨论,我们假设所有梯度共需 160 MB。在这种情况下,将其中 \(3\) 个 GPU 的梯度发送到第 \(4\) 个 GPU 上需要 \(30\) 毫秒(每次传输需要 \(10\) 毫秒 = 160 MB/16 GB/s)。再加上 \(30\) 毫秒将权重向量传输回来,得到的结果是总共需要 \(60\) 毫秒。如果将所有的数据发送到 CPU,总共需要 \(80\) 毫秒,其中将有 \(40\) 毫秒的惩罚,因为 \(4\) 个 GPU 每个都需要将数据发送到 CPU。最后,假设能够将梯度分为 \(4\) 个部分,每个部分为 \(40\) MB,现在可以在不同的 GPU 上同时聚合每个部分。因为 PCIe 交换机在所有链路之间提供全带宽操作,所以传输需要 \(2.5\times 3=7.5\) 毫秒,而不是 \(30\) 毫秒,因此同步操作总共需要 \(15\) 毫秒。简而言之,一样的参数同步操作基于不同的策略时间可能在 \(15\) 毫秒到 \(80\) 毫秒之间。 图12.7.3 描述了交换参数的不同策略。

../_images/ps-distributed.svg

图12.7.3 参数同步策略

请注意,我们还可以使用另一个工具来改善性能:在深度网络中,从顶部到底部计算所有梯度需要一些时间,因此即使还在忙着为某些参数计算梯度时,就可以开始为准备好的参数同步梯度了。想了解详细信息可以参见 [Sergeev & DelBalso, 2018] ,想知道如何操作可参考 Horovod

12.7.2. 环同步(Ring Synchronization)

当谈及现代深度学习硬件的同步问题时,我们经常会遇到大量的定制的网络连接。例如,AWS p3.16xlarge 和 NVIDIA DGX-2 实例中的连接都使用了 图12.7.4 中的结构。每个 GPU 通过 PCIe 链路连接到主机 CPU,该链路最多只能以 16 GB/s 的速度运行。此外,每个 GPU 还具有 \(6\) 个 NVLink 连接,每个 NVLink 连接都能够以 300 Gbit/s 进行双向传输。这相当于每个链路每个方向约 \(300\div 8\div 2\approx 18 \mathrm{GB/s}\)。简言之,聚合的 NVLink 带宽明显高于 PCIe 带宽,问题是如何有效地使用它。

../_images/nvlink.svg

图12.7.4 在 8 台 V100 GPU 服务器上连接 NVLink(图片由英伟达提供)

[Wang et al., 2018] 的研究结果表明最优的同步策略是将网络分解成两个环,并基于两个环直接同步数据。 图12.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

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

考虑下面的思维试验:给定由 \(n\) 个计算节点(或 GPU)组成的一个环,梯度可以从第一个节点发送到第二个节点,在第二个结点将本地的梯度与传送的梯度相加并发送到第三个节点,依此类推。在 \(n-1\) 步之后,可以在最后访问的节点中找到聚合梯度。也就是说,聚合梯度的时间随节点数线性增长。但如果照此操作,算法是相当低效的。归根结底,在任何时候都只有一个节点在通信。如果我们将梯度分为 \(n\) 个块,并从节点 \(i\) 开始同步块 \(i\),会怎么样?因为每个块的大小是 \(1/n\),所以总时间现在是 \((n-1)/n \approx 1\)。换句话说,当我们增大环的大小时,聚合梯度所花费的时间不会增加。这是一个相当惊人的结果。 图12.7.6 说明了\(n=4\)个节点上的步骤顺序。

../_images/ringsync.svg

图12.7.6 跨4个节点的环同步。每个节点开始向其左邻居发送部分梯度,直到在其右邻居中找到聚合的梯度。

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

注意到有一种常见的误解认为环同步与其他同步算法在本质上是不同的,实际上与简单的树算法相比其唯一的区别是同步路径稍微精细一些。

12.7.3. 多机训练

新的挑战出现在多台机器上进行分布式训练:我们需要服务器之间相互通信,而这些服务器又只通过相对较低的带宽结构连接,在某些情况下这种连接的速度可能会慢一个数量级,因此跨设备同步是个棘手的问题。毕竟,在不同机器上运行训练代码的速度会有细微的差别,因此如果想使用分布式优化的同步算法就需要 同步(synchronize)这些机器。 图12.7.7 说明了分布式并行训练是如何发生的。

  1. 在每台机器上读取一组(不同的)批量数据,在多个 GPU 之间分割数据并传输到 GPU 的显存中。基于每个 GPU 上的批量数据分别计算预测和梯度。

  2. 来自一台机器上的所有的本地 GPU 的梯度聚合在一个 GPU 上(或者在不同的 GPU 上聚合梯度的某些部分)。

  3. 每台机器的梯度被发送到其本地 CPU 中。

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

  5. 然后使用聚合后的梯度来更新参数,并将更新后的参数广播回各个 CPU 中。

  6. 更新后的参数信息发送到本地一个(或多个)GPU 中。

  7. 所有 GPU 上的参数更新完成。

../_images/ps-multimachine.svg

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

以上这些操作似乎都相当简单,而且事实上它们可以在一台机器内高效地执行,但是当我们考虑多台机器时,就会发现中央的参数服务器成为了瓶颈。毕竟,每个服务器的带宽是有限的,因此对于 \(m\) 个工作节点来说,将所有梯度发送到服务器所需的时间是 \(\mathcal{O}(m)\)。我们也可以通过将参数服务器数量增加到 \(n\) 来突破这一障碍。此时,每个服务器只需要存储 \(\mathcal{O}(1/n)\) 个参数,因此更新和优化的总时间变为 \(\mathcal{O}(m/n)\)。这两个数字的匹配会产生稳定的伸缩性,而不用在乎我们需要处理多少工作节点。在实际应用中,我们使用同一台机器既作为工作节点还作为服务器。设计说明请参考 图12.7.8(技术细节请参考 [Li et al., 2014] )。特别是,确保多台机器只在没有不合理延迟的情况下工作是相当困难的。我们在下面忽略了关于阻塞的细节,只简单介绍一下同步和异步(unsynchronized)更新。

../_images/ps-multips.svg

图12.7.8 上图:单参数服务器是一个瓶颈,因为它的带宽是有限的。下图:多参数服务器使用聚合带宽存储部分参数。

12.7.4. 键值存储

在实践中,实现分布式多 GPU 训练所需要的步骤绝非易事。这就是公共抽象值得使用的原因,公共抽象即重新定义具有更新语义的 键-值存储(key-value store)的抽象。

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

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

其中 \(\mathbf{g}_{ijk}\) 是在工作节点 \(k\) 的 GPU \(j\) 上拆分的梯度 \(i\) 的一部分。这个运算的关键在于它是一个 交换归约(commutative reduction),也就是说,它把许多向量变换成一个向量,而运算顺序在完成向量变换时并不重要。这对实现我们的目标来说是非常好的,因为不需要为何时接收哪个梯度进行细粒度的控制。此外,请注意,这个操作在不同的 \(i\) 之间是独立的。

这就允许我们定义下面两个操作:push(用于累积梯度)和 pull(用于取得聚合梯度)。因为我们有很多层,也就有很多不同的梯度集合,因此需要用一个键 \(i\) 来对梯度建索引。这个与 Dynamo [DeCandia et al., 2007] 中引入的“键-值存储”之间存在相似性并非巧合。它们两个定义都拥有许多相似的性质,特别是在多个服务器之间分发参数时。

“键-值存储”的 push 与 pull 操作描述如下:

  • push(key,value) 将特定的梯度值从工作节点发送到公共存储,在那里通过某种方式(例如,相加)来聚合值。

  • pull(key,value) 从公共存储中取得某种方式(例如,组合来自所有工作节点的梯度)的聚合值。

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

12.7.5. 小结

  • 同步需要高度适应特定的网络基础设施和服务器内的连接,这种适应会严重影响同步所需的时间。

  • 环同步对于 p3 和 DGX-2 服务器是最佳的,而对于其他服务器则未必。

  • 当添加多个参数服务器以增加带宽时,分层同步策略可以工作的很好。

12.7.6. 练习

  1. 你能进一步提高环同步的性能吗?(提示:你可以双向发送消息。)

  2. 在计算仍在进行中,可否允许执行异步通信?它将如何影响性能?

  3. 怎样处理在长时间运行的计算过程中丢失了一台服务器这种问题?尝试设计一种容错机制来避免重启计算这种解决方案?

Discussions