总的来说,分布式训练分为这几类:
模型并行 vs 数据并行
假设我们有n张GPU:
- 模型并行:不同的GPU输入相同的数据,运行模型的不同部分,比如多层网络的不同层;
- 数据并行:不同的GPU输入不同的数据,运行相同的完整的模型。
当模型非常大,一张GPU已经存不下的时候,可以使用模型并行,把模型的不同部分交给不同的机器负责,但是这样会带来很大的通信开销,而且模型并行各个部分存在一定的依赖,规模伸缩性差。因此,通常一张可以放下一个模型的时候,会采用数据并行的方式,各部分独立,伸缩性好。
同步更新 vs 异步更新
对于数据并行来说,由于每个GPU负责一部分数据,那就涉及到如果更新参数的问题,分为同步更新和异步更新两种方式。
-
同步更新:每个batch所有GPU计算完成后,再统一计算新权值,然后所有GPU同步新值后,再进行下一轮计算。
-
异步更新:每个GPU计算完梯度后,无需等待其他更新,立即更新整体权值并同步。
一般我们使用的都是数据并行+同步更新。因此本文主要讲解数据并行+同步更的细节。
分别以单机单卡和单机4卡,batchsize=64为例。
在单机单卡的模式下,一次性输入一个batch的数据进行forward。在得到网络的output后,由output和target进行loss的计算。但要注意,这里的loss要对bacth中的样本量(batchsize)取平均。
在单机4卡的模式下,因为batchsize=64。因此分配给每块GPU的size为16。各块GPU上模型一致并共享参数,只是数据输入不同。4块GPU分别进行forward,各GPU上loss的计算依旧要根据本卡上的样本量(size=16)取平均。在梯度计算上,我看到过两个版本,一种是将多卡loss收集到主GPU上再次按卡数进行平均。随后再进行梯度回传并更新参数,将更新后的参数分发给其他GPU。另外一种是各GPU先分别进行梯度回传,再将梯度gather到主GPU上取平均,随后更新参数并分发给其他GPU。两种方法最终的结果实际上是一致的,只是步骤不同。因此这里不再深究。
从上面的分析可以看出,无论是单卡训练还是多卡训练,最终用来计算梯度的loss值,是要对样本量取平均的。因此在借助Pytorch累加梯度的特性,变相增加batchsize的过程中,需要将loss除以累加迭代的次数。
那么问题来了,单机单卡、单机多卡、借助梯度累加变相增加batchsize三者是否完全等价???
除去训练速度以及不同GPU随机种子设定不同导致的细微差别,单从forward、backward、parameter update三方面来看,是没有什么差别的。
但是,有个东西叫Bach Normalization。大家都知道,这神器对模型训练大有脾益,有了他不但减小了对dropout的依赖,更削弱了参数初始化的影响。可是,这玩意儿是基于batch来计算的。在单卡,多卡、变相增大batchsize的情况下,Bach Normalization单次处理的数据量可就不一样了。
单卡时,每次处理的数据量等于batchsize。而在多卡情况下,由于数据分发,每次处理的数据量等于batchsize/卡数。在变相增大batchsize的情况下,每次处理的数据量与不进行这种操作是相等的,也就是说采用这种方法,其loss计算和梯度计算与增大的batchsize是一致的。但是在Bach Normalization上依旧是从前那个少年!