数据并行和模型并行的区别

此文翻译自[1],[1]对数据并行和模型并行进行了很好地区分,因此这里推荐给大家。


介绍

现在深度学习模型的参数量已经变得越来越多了,数据集的尺寸也随之疯狂地增长。为了在一个巨大的数据集上训练一个复杂的深度学习模型,我们不得不使用多节点的并行方式,否则我们永远不可能达到这个目的。这里谈到的并行,通常指的有两种,或者它们各自的混合:

  1. 数据并行 (Data Parallel)
  2. 模型并行 (Model Parallel)
  3. 数据模型并行(Data-Model Parallel)

鉴于第三种方式是前面两种方式的结合,在此我们只讨论前两种并行方式的区别。

数据并行

在现代深度学习中,因为数据集越来越大,以至于我们很难将其把它全部载入内存,我们通常使用所谓的随机梯度下降法[2],对数据集的每个批次(batch)进行梯度求导。举例而言,如果我们的数据集有10K个数据点,每次我们只从中取出16个数据点去计算根据这个批次得到的对梯度的估计,如果我们尝试一次性去计算所有数据点的梯度,我们的GPU显存极可能无法容纳那么多的数据。

然而,随机梯度下降法的缺点在于其对梯度的估计, 相比于用整个数据集进行梯度计算得到的真实梯度来说,可能不够精确。因此,随机梯度下降法通常需要更多的训练时间去达到模型收敛。

有个自然的做法就是在一个更大的批次尺寸上进行更为精确的梯度估计,甚至我们可以使用整个的数据集大小的批次。为了实现这个目的,我们把这个大批次分割为很多小批次,在每个GPU上计算一个小批次,若干个GPU的梯度估计结果进行汇总后,进行加权平均,最终求和就得到了最终的大批次的梯度估计结果。

而这个过程,可以用数学过程进行保证,如:
Lw=[1ni=1nf(xi,yi)]w=1ni=1nf(xi,yi)w=m1n[1m1i=1m1f(xi,yi)]w+m2n[1m2i=m1+1m1+m2f(xi,yi)]w++mkn[1mki=mk1+1mk1+mkf(xi,yi)]w=m1nl1w+m2nl2w++mknlkw(1) \begin{aligned} \dfrac{\partial \mathcal{L}}{ \partial w} &= \dfrac{\partial [\frac{1}{n} \sum_{i=1}^n f(x_i,y_i)]}{\partial w} \\ &= \dfrac{1}{n} \sum_{i=1}^n \dfrac{\partial f(x_i,y_i)}{\partial w} \\ &= \dfrac{m_1}{n} \dfrac{\partial [\frac{1}{m_1} \sum_{i=1}^{m_1} f(x_i,y_i)]}{\partial w} + \dfrac{m_2}{n} \dfrac{\partial [\frac{1}{m_2} \sum_{i={m_1+1}}^{m_1+m_2} f(x_i,y_i)]}{\partial w} + \cdots \\ & +\dfrac{m_k}{n} \dfrac{\partial [\frac{1}{m_k} \sum_{i={m_{k-1}+1}}^{m_{k-1}+m_k} f(x_i,y_i)]}{\partial w} \\ &= \dfrac{m_1}{n} \dfrac{\partial l_1}{\partial w} + \dfrac{m_2}{n} \dfrac{\partial l_2}{\partial w} + \cdots + \dfrac{m_k}{n} \dfrac{\partial l_k}{\partial w} \end{aligned} \tag{1}
其中:
ww是模型的参数;

Lw\dfrac{\partial \mathcal{L}}{\partial w}是采用尺寸为n的大批次的计算得到的真实梯度;

lkw\dfrac{\partial l_k}{\partial w}是节点k的梯度;

xix_iyiy_i是数据点i的特征和标签;

f(xi,yi)f(x_i, y_i)是对于数据点i,在前向传播过程中计算得到的损失函数;

nn是整个数据集的大小, kk是节点数,mkm_k是分配到节点k的数据量,其中有:
m1+m2++mk=nm_1+m_2+\cdots+m_k = n

如果我们对每个节点的数据量进行平分,我们有:
m1=m2==mk=nkLw=1k[l1w+l2w++lkw](2) \begin{aligned} m_1 &= m_2 = \cdots = m_k = \dfrac{n}{k} \\ \dfrac{\partial \mathcal{L}}{\partial w} &= \dfrac{1}{k} \left[ \dfrac{\partial l_1}{\partial w} + \dfrac{\partial l_2}{\partial w} + \cdots + \dfrac{\partial l_k}{\partial w} \right] \end{aligned} \tag{2}
对于每个节点来说,我们使用相同的模型参数进行前向传播,我们把整个大批次数据分割成很多个小批次数据,发送到不同的节点上,每个节点都正常地计算其梯度,计算完结后返回其梯度计算结果到主节点(译者:我们称之为output_device)。这一步通常是异步的,因为每个节点的速度可能都稍有差别。一旦我们得到了所有的梯度,我们计算这些梯度的平均值(也就是平均加权),并且使用这个平均加权的梯度去更新整个模型的参数。接着,我们就继续下个迭代。

模型并行

模型并行性乍一听挺唬人的,但是其实和令人生畏的数学没太大关系。模型并行更多的是一种对计算机资源的分配问题。有时候我们的模型可能太大了,甚至大到不能把整个模型载入一个GPU中,因为其中有着太多的层,太多的参数。因此,我们可以考虑把整个模型按层分解成若干份,把每一份(其中的层是连续的)载入不同的节点中,也即是每个不同的节点计算着整个模型的不同的层,计算着不同的层的梯度。通过这种方法,单个节点的参数量就减少了,并且使得用更为精确的梯度进行计算提供了可能性。

举例而言,假如我们有10个GPU节点,我们想要训练一个ResNet50网络(其中有50层)。我们可以将前5层分配给GPU#1, 下一个5层分给GPU #2,如此类推,最后的5层自然是分配给了GPU #10。在训练过程中,在每个迭代中,前向传播首先在GPU #1进行,接下来是GPU #2, #3等等。这个过程是串行的,后面的节点必须等待前面的节点运算完之后才能接着运算,但是,反过来说,后面节点在进行运算的时候,并不妨碍前面的节点进行下一个批次的运算,这个其实就是一个流水线(pipeline)的结构了。当涉及到反向传播时,我们首先计算GPU #10的梯度,并且根据此,更新网络参数。然后我们继续计算前一节点GPU #9的梯度,并且更新,以此类推。每个节点就像是一个工厂中的生产部门,所有节点组成了一个流水线。

简评

在我看来,模型并行的名字其实有点误导性,因为我们发现模型并行有时候并不是一个并行计算的良好例子。一个更为精确的名字应该类似于“模型串行化”,因为它使用了一种串行化的方法而不是一种并行的方法。然而,在一些模型场景中,神经网络中的一些层的确可以做到并行化,比如siamese network,其不同的分支是可以看成完全的并行的。在这种场景中,模型并行可以表现得和真正的并行计算一样。数据并行,的确是100%的并行计算了。

Reference

[1]. https://leimao.github.io/blog/Data-Parallelism-vs-Model-Paralelism/
[2]. https://blog.csdn.net/LoseInVain/article/details/78243051

posted @ 2020-04-28 13:15  FesianXu  阅读(176)  评论(0编辑  收藏  举报