Run this notebook online:\ |Binder| or Colab: |Colab| .. |Binder| image:: https://mybinder.org/badge_logo.svg :target: https://mybinder.org/v2/gh/deepjavalibrary/d2l-java/master?filepath=chapter_computational-performance/multiple-gpus.ipynb .. |Colab| image:: https://colab.research.google.com/assets/colab-badge.svg :target: https://colab.research.google.com/github/deepjavalibrary/d2l-java/blob/colab/chapter_computational-performance/multiple-gpus.ipynb .. _sec_multi_gpu: 多GPU训练 ========= 到目前为止,我们讨论了如何在CPU和GPU上高效地训练模型,同时在 :numref:`sec_auto_para`\ 中展示了深度学习框架如何在CPU和GPU之间自动地并行化计算和通信,还在 :numref:`sec_use_gpu`\ 中展示了如何使用\ ``nvidia-smi``\ 命令列出计算机上所有可用的GPU。 但是我们没有讨论如何真正实现深度学习训练的并行化。 是否一种方法,以某种方式分割数据到多个设备上,并使其能够正常工作呢? 本节将详细介绍如何从零开始并行地训练网络, 这里需要运用小批量随机梯度下降算法(详见 :numref:`sec_minibatch_sgd`\ )。 后面我还讲介绍如何使用高级API并行训练网络(请参阅 :numref:`sec_multi_gpu_concise`\ )。 .. _fig_alexnet_original: .. _fig_splitting: 问题拆分 -------- 我们从一个简单的计算机视觉问题和一个稍稍过时的网络开始。 这个网络有多个卷积层和汇聚层,最后可能有几个全连接的层,看起来非常类似于LeNet :cite:`LeCun.Bottou.Bengio.ea.1998`\ 或AlexNet :cite:`Krizhevsky.Sutskever.Hinton.2012`\ 。 假设我们有多个GPU(如果是桌面服务器则有\ :math:`2`\ 个,AWS g4dn.12xlarge上有\ :math:`4`\ 个,p3.16xlarge上有\ :math:`8`\ 个,p2.16xlarge上有\ :math:`16`\ 个)。 我们希望以一种方式对训练进行拆分,为实现良好的加速比,还能同时受益于简单且可重复的设计选择。 毕竟,多个GPU同时增加了内存和计算能力。 简而言之,对于需要分类的小批量训练数据,我们有以下选择。 第一种方法,在多个GPU之间拆分网络。 也就是说,每个GPU将流入特定层的数据作为输入,跨多个后续层对数据进行处理,然后将数据发送到下一个GPU。 与单个GPU所能处理的数据相比,我们可以用更大的网络处理数据。 此外,每个GPU占用的\ *显存*\ (memory footprint)可以得到很好的控制,虽然它只是整个网络显存的一小部分。 然而,GPU的接口之间需要的密集同步可能是很难办的,特别是层之间计算的工作负载不能正确匹配的时候, 还有层之间的接口需要大量的数据传输的时候(例如:激活值和梯度,数据量可能会超出GPU总线的带宽)。 此外,计算密集型操作的顺序对于拆分来说也是非常重要的,这方面的最好研究可参见 :cite:`Mirhoseini.Pham.Le.ea.2017`\ ,其本质仍然是一个困难的问题,目前还不清楚研究是否能在特定问题上实现良好的线性缩放。 综上所述,除非存框架或操作系统本身支持将多个GPU连接在一起,否则不建议这种方法。 第二种方法,拆分层内的工作。 例如,将问题分散到\ :math:`4`\ 个GPU,每个GPU生成\ :math:`16`\ 个通道的数据,而不是在单个GPU上计算\ :math:`64`\ 个通道。 对于全连接的层,同样可以拆分输出单元的数量。 :numref:`fig_alexnet_original`\ 描述了这种设计,其策略用于处理显存非常小(当时为2GB)的GPU。 当通道或单元的数量不太小时,使计算性能有良好的提升。 此外,由于可用的显存呈线性扩展,多个GPU能够处理不断变大的网络。 |由于GPU显存有限,原有AlexNet设计中的模型并行| 然而,我们需要大量的同步或\ *屏障操作*\ (barrier operation),因为每一层都依赖于所有其他层的结果。 此外,需要传输的数据量也可能比跨GPU拆分层时还要大。 因此,基于带宽的成本和复杂性,我们同样不推荐这种方法。 最后一种方法,跨多个GPU对数据进行拆分。 这种方式下,所有GPU尽管有不同的观测结果,但是执行着相同类型的工作。 在完成每个小批量数据的训练之后,梯度在GPU上聚合。 这种方法最简单,并可以应用于任何情况,同步只需要在每个小批量数据处理之后进行。 也就是说,当其他梯度参数仍在计算时,完成计算的梯度参数就可以开始交换。 而且,GPU的数量越多,小批量包含的数据量就越大,从而就能提高训练效率。 但是,添加更多的GPU并不能让我们训练更大的模型。 |在多个GPU上并行化。从左到右:原始问题、网络并行、分层并行、数据并行| :numref:`fig_splitting`\ 中比较了多个GPU上不同的并行方式。 总体而言,只要GPU的显存足够大,数据并行是最方便的。 有关分布式训练分区的详细描述,请参见 :cite:`Li.Andersen.Park.ea.2014`\ 。 在深度学习的早期,GPU的显存曾经是一个棘手的问题,然而如今除了非常特殊的情况,这个问题已经解决。 下面我们将重点讨论数据并行性。 .. _fig_data_parallel: 数据并行性 ---------- 假设一台机器有\ :math:`k`\ 个GPU。 给定需要训练的模型,虽然每个GPU上的参数值都是相同且同步的,但是每个GPU都将独立地维护一组完整的模型参数。 例如, :numref:`fig_data_parallel`\ 演示了在\ :math:`k=2`\ 时基于数据并行方法训练模型。 |利用两个GPU上的数据,并行计算小批量随机梯度下降| 一般来说,\ :math:`k`\ 个GPU并行训练过程如下: - 在任何一次训练迭代中,给定的随机的小批量样本都将被分成\ :math:`k`\ 个部分,并均匀地分配到GPU上。 - 每个GPU根据分配给它的小批量子集,计算模型参数的损失和梯度。 - 将\ :math:`k`\ 个GPU中的局部梯度聚合,以获得当前小批量的随机梯度。 - 聚合梯度被重新分发到每个GPU中。 - 每个GPU使用这个小批量随机梯度,来更新它所维护的完整的模型参数集。 在实践中请注意,当在\ :math:`k`\ 个GPU上训练时,需要扩大小批量的大小为\ :math:`k`\ 的倍数,这样每个GPU都有相同的工作量,就像只在单个GPU上训练一样。 因此,在16-GPU服务器上可以显著地增加小批量数据量的大小,同时可能还需要相应地提高学习率。 还请注意, :numref:`sec_batch_norm`\ 中的批量规范化也需要调整,例如,为每个GPU保留单独的批量规范化参数。 小结 ---- - 有多种方法可以在多个GPU上拆分深度网络的训练。拆分可以在层之间、跨层或跨数据上实现。前两者需要对数据传输过程进行严格编排,而最后一种则是最简单的策略。 - 数据并行训练本身是不复杂的,它通过增加有效的小批量数据量的大小提高了训练效率。 - 在数据并行中,数据需要跨多个GPU拆分,其中每个GPU执行自己的前向传播和反向传播,随后所有的梯度被聚合为一,之后聚合结果向所有的GPU广播。 - 小批量数据量更大时,学习率也需要稍微提高一些。 练习 ---- 1. 在\ :math:`k`\ 个GPU上进行训练时,将批量大小从\ :math:`b`\ 更改为\ :math:`k \cdot b`\ ,即按GPU的数量进行扩展。 2. 比较不同学习率时模型的精确度,随着GPU数量的增加学习率应该如何扩展? 3. 实现一个更高效的\ ``allreduce``\ 函数用于在不同的GPU上聚合不同的参数?为什么这样的效率更高? 4. 实现模型在多GPU下测试精度的计算。 .. |由于GPU显存有限,原有AlexNet设计中的模型并行| image:: https://d2l.ai/_images/alexnet-original.svg .. |在多个GPU上并行化。从左到右:原始问题、网络并行、分层并行、数据并行| image:: https://d2l.ai/_images/splitting.svg .. |利用两个GPU上的数据,并行计算小批量随机梯度下降| image:: https://d2l.ai/_images/data-parallel.svg