英文缩写

  • Data Parallelism(DP)数据并行
  • Distributed Data Parallelism(DDP)分布式数据并行
  • Zero Redundancy Optimizer(ZeRO)零冗余优化器
  • FWD:前传
  • BWD:后传

背景

  • PP并行的原理:当模型太大,一块GPU放不下时,流水线并行将模型的不同执行阶段(一般为不同层)放到不同的GPU上,通过将mini-batch切割成更细粒度的micro-batch,实现对训练数据的流水线处理,提升GPU计算通讯比。同时通过re-materialization机制降低显存消耗。
  • DP并行的优势(更强易用性应用更加广泛):在实际应用中,流水线并行并不特别流行,主要原因是模型能否均匀切割,这影响了整体计算效率,也需要算法工程师做手调。因此,来介绍一种应用最广泛,最易于理解的并行范式:数据并行。
  • 数据并行的核心思想是:在各个GPU上都拷贝一份完整模型,各自吃一份数据,算一份梯度,最后对梯度进行累加来更新整体模型。理念不复杂,但到了大模型场景,巨大的存储和GPU间的通讯量,就是系统设计要考虑的重点了。在本文以及后续文章中,我们将递进介绍三种主流数据并行的实现方式:
    • DP:最早的数据并行模式,一般采用参数服务器这一编程框架。实际中多用于单机多卡
    • DDP:分布式数据并行,采用Ring AllReduce的通讯方式,实际中多用于多机场景
    • ZeRO:零冗余优化器。由微软推出并应用于其DeepSpeed框架中。严格来讲ZeRO采用数据并行+张量并行的方式,旨在降低存储

1 数据并行(DP)

1.1 整体架构

image-20241229184009649

一个经典数据并行的过程如下:

  • 若干块计算GPU(Worker):如图中GPU0~GPU2
  • 1块梯度收集GPU(Server):如图中AllReduce操作所在GPU
  • 在每块计算GPU上都拷贝一份完整的模型参数 W W W
  • 把一份数据 X X X(例如一个batch)均匀分给不同的计算GPU:如 X 0 ∼ X 2 X0\sim X2 X0X2
  • 每块计算GPU做一轮FWD和BWD后,算得一份梯度 G G G,如 G 0 ∼ 2 G0\sim2 G02
  • 每块计算GPU将自己的梯度push给梯度收集GPU,做聚合操作。这里的聚合操作一般指梯度累加。当然也支持用户自定义
  • 梯度收集GPU聚合完毕后,计算GPU从它那pull下完整的梯度结果,用于更新模型参数 W W W。更新完毕后,计算GPU上的模型参数依然保持一致
  • 聚合再下发梯度的操作,称为AllReduce

前文说过,实现DP的一种经典编程框架叫“参数服务器”,在这个框架里,计算GPU称为Worker,梯度聚合GPU称为Server。在实际应用中,为了尽量减少通讯量,一般可选择一个Worker同时作为Server。比如可把梯度全发到GPU0上做聚合。需要再额外说明几点:

  • 1个Worker或者Server下可以不止1块GPU,可以是多个GPU的集群。
  • Server可以只做梯度聚合,也可以梯度聚合+全量参数更新一起做

在参数服务器的语言体系下,DP的过程又可以被描述下图:

image-20241229184902700

1.2 通讯量分析

假设模型参数 W W W的大小为 Φ \Phi Φ ,GPU个数为 N N N 。则梯度大小也为 Φ \Phi Φ ,每个梯度块的大小为 Φ N \frac{\Phi}{N} NΦ

对单个GPU(Worker)来说:

  • push的过程需要完成每一个GPU上完整梯度(大小为 Φ \Phi Φ)上送
  • pull的过程需要完成每一个GPU上完整新权重(大小为 Φ \Phi Φ)下拉
  • 单卡总通讯量为 2 Φ 2\Phi 2Φ(包含上送和下拉)
  • 全Worker卡总通讯量为 2 N Φ 2N\Phi 2NΦ

对单个GPU(Server)来说:

  • push的过程需要完成每一个Worker上完整梯度上送的收集(大小为 N Φ N\Phi NΦ
  • pull的过程需要完成对每一个Worker上完整新权重的下发(大小为 N Φ N\Phi NΦ
  • 单卡总通讯量为 2 N Φ 2N\Phi 2NΦ

1.3 通讯瓶颈与梯度异步更新

DP的框架理解起来不难,但实战中确有两个主要问题:

  • 存储开销大:每块GPU上都存了一份完整的模型,造成冗余。关于这一点的优化,在后文ZeRO部分做讲解
  • 通讯开销大:Server需要和每一个Worker进行梯度传输。当Server和Worker不在一台机器上时,Server的带宽将会成为整个系统的计算效率瓶颈

对通讯开销再做详细说明。如果将传输比作一条马路,带宽就是马路的宽度,它决定每次并排行驶的数据量。例如带宽是100GB/s,但每秒却推给Server 1000GB的数据,消化肯定需要时间10s。那么当Server在搬运数据,计算梯度的时候,Worker们就在休息。于是梯度异步更新诞生了。

image-20241229185835089

上图刻画了在梯度异步更新的场景下,某个Worker的计算顺序为:

  • 在第10轮计算中,该Worker正常计算梯度,并向Server发送push&pull梯度请求。
  • 但是,该Worker并不会实际等到把聚合梯度拿回来,更新完参数获得第10轮更新结果 W 10 W_{10} W10后再做计算。而是直接拿旧的权重比如 W 9 W_9 W9,吃新的数据,一边穿一边继续第11轮的计算。这样就保证在通讯的时间里,Worker也在马不停蹄做计算,提升计算通讯比。
  • 当然,异步也不能太过份。只计算梯度,不更新权重,那模型就无法收敛。图中刻画的是延迟为1的异步更新,也就是在开始第12轮的计算时,必须保证 W W W已经用第10轮的梯度做完更新了。

参数服务器的框架下,延迟的步数也可以由用户自己决定,下图分别刻划了几种延迟情况:

image-20241229190235867

  • (1) 无延迟
  • (2) 延迟但不指定延迟步数。也即在迭代2时,用的可能是老权重,也可能是新权重,听天由命
  • (3) 延迟且指定延迟步数为1。例如做迭代3时,可以不拿回迭代2的梯度,但必须保证迭代0、1的梯度都已拿回且用于参数更新

总结一下,异步很香,但对一个Worker来说,只是等于 W W W不变,batch的数量增加了而已,在SGD下,会减慢模型的整体收敛速度。异步的整体思想是,比起让Worker闲着,倒不如让它多吃点数据,虽然反馈延迟了,但只要它在干活在学习就行。

2 分布式数据并行(DDP)

受通讯负载不均的影响,DP一般用于单机多卡场景。因此,DDP作为一种更通用的解决方案出现了,既能多机,也能单机。

DDP首先要解决的就是通讯问题:将Server上的通讯压力均衡转到各个Worker上。实现这一点后,可以进一步去Server,留Worker。

前文我们说过,聚合梯度 + 下发梯度这一轮操作,称为AllReduce。接下来我们介绍目前最通用的AllReduce方法:Ring-AllReduce。它由百度最先提出,非常有效地解决了数据并行中通讯负载不均的问题,使得DDP得以实现。

2.1 Ring-AllReduce(reduce-scatter + all-gather)

如下图,假设有4块GPU,每块GPU上的数据也对应被切成4份。AllReduce的最终目标,就是让每块GPU上的数据都变成箭头右边汇总的样子。

image-20250104010215971

Ring-ALLReduce则分两大步骤实现该目标:

  • Reduce-Scatter
  • All-Gather
2.1.1 Reduce-Scatter(散射规约)

定义网络拓扑关系,使得每个GPU只和其相邻的两块GPU通讯。每次发送对应位置的数据进行累加。每一次累加更新都形成一个拓扑环,因此被称为Ring。看到这觉得困惑不要紧,我们用图例把详细步骤画出来。

image-20250104011321823

image-20250104011405603

一次累加完毕后,蓝色位置的数据块被更新,被更新的数据块将成为下一次更新的起点,继续做累加操作。

image-20250104011436407

image-20250104011454203

3次更新之后,每块GPU上都有一块数据拥有了对应位置完整的聚合(图中红色)。此时,Reduce-Scatter阶段结束。进入All-Gather阶段。目标是把红色块的数据广播到其余GPU对应的位置上。

2.1.2 All-Gather

如名字里Gather所述的一样,这操作里依然按照“相邻GPU对应位置进行通讯”的原则,但对应位置数据不再做相加,而是直接替换。All-Gather以红色块作为起点。

image-20250104011609976

image-20250104011827663

以此类推,同样经过3轮迭代后,使得每块GPU上都汇总到了完整的数据,变成如下形式:

img

2.2 Ring-AllReduce通讯量分析

假设模型参数 W W W的大小为 Φ \Phi Φ ,GPU个数为 N N N 。则梯度大小也为 Φ \Phi Φ ,每个梯度块的大小为 Φ N \frac{\Phi}{N} NΦ(如前文的例子,分成了四份数据,单卡需要完成的数据散射和归并是模型任务量是1/4)。

对单个GPU来说:

  • Reduce-Scatter阶段,通讯量为 ( N − 1 ) Φ N \frac{(N−1)\Phi}{N} N(N1)Φ(每一个数据块大小 Φ N \frac{\Phi}{N} NΦ,ring上走 N − 1 N-1 N1次就可以在某一个GPU上完成归并)(如果考虑发送与收到则需要两份如此通讯量)
  • All-Gather阶段,通讯量为 ( N − 1 ) Φ N \frac{(N−1)\Phi}{N} N(N1)Φ(每一个数据块大小 Φ N \frac{\Phi}{N} NΦ,ring上走 N − 1 N-1 N1块数据次就可以把某一块GPU上完成归并的数据块下发至每一个GPU)(如果考虑发送与收到则需要两份如此通讯量)
  • 单卡单向总通讯量为 2 ( N − 1 ) Φ N \frac{2(N−1)\Phi}{N} N2(N1)Φ,随着 N N N的增大,可以近似为 2 Φ 2\Phi 2Φ,双向则为 4 Φ 4\Phi 4Φ
  • 全卡单向总通讯量为 2 N Φ 2N\Phi 2NΦ,双向则为 4 N Φ 4N\Phi 4NΦ

一般互联带宽为双向带宽,即同时实现相同带宽的收与发。假设收与发的带宽均为 B B B

DP 收发通讯时间
Server(单卡) N Φ N\Phi NΦ N Φ N\Phi NΦ N Φ B \frac{N\Phi}{B} BNΦ
Worker(单卡) Φ \Phi Φ Φ \Phi Φ Φ B \frac{\Phi}{B} BΦ
集群视角(以server为瓶颈) N Φ N\Phi NΦ N Φ N\Phi NΦ N Φ B \frac{N\Phi}{B} BNΦ
DDP 收发通讯时间
单卡Reduce-Scatter阶段 Φ \Phi Φ Φ \Phi Φ Φ B \frac{\Phi}{B} BΦ
单卡All-Gather阶段 Φ \Phi Φ Φ \Phi Φ Φ B \frac{\Phi}{B} BΦ
单卡All-Reduce 2 Φ 2\Phi 2Φ 2 Φ 2\Phi 2Φ 2 Φ B \frac{2\Phi}{B} B2Φ
集群视角All-Reduce阶段 2 N Φ 2N\Phi 2NΦ 2 N Φ 2N\Phi 2NΦ 2 N Φ N B = 2 Φ B \frac{2N\Phi}{NB}=\frac{2\Phi}{B} NB2NΦ=B2Φ

DP中Server为瓶颈,搬运数据量均阻塞在Server的通讯能力上。DDP把通讯量均衡负载到了每一时刻的每个Worker上,当越来越多的GPU分布在距离较远的机器上时,DP的通讯时间是会增加的,但是DDP可以基本不变。

但这并不说明参数服务器不能打(有很多文章将参数服务器当作old dinosaur来看)。事实上,参数服务器也提供了多Server方法,如下图:

image-20250104013603020

在多Server的模式下,进一步,每个Server可以只负责维护和更新某一块梯度(也可以某块梯度+参数一起维护),此时虽然每个Server仍然需要和所有Worker通讯,但它的带宽压力会小非常多。经过调整设计后,依然可以用来做DDP。虽然这篇文章是用递进式的方式来介绍两者,但不代表两者间一定要决出优劣。我想表达的观点是,方法是多样性的。对参数服务器有兴趣的朋友,可以阅读参考的第1个链接。

最后,请大家记住Ring-AllReduce的方法,因为在之后的ZeRO,Megatron-LM中,它将频繁地出现,是分布式训练系统中重要的算子。

3 总结

  • 在DP中,每个GPU上都拷贝一份完整的模型,每个GPU上处理batch的一部分数据,所有GPU算出来的梯度进行累加后,再传回各GPU用于更新参数
  • DP多采用参数服务器这一编程框架,一般由若个计算Worker和1个梯度聚合Server组成。Server与每个Worker通讯,Worker间并不通讯。因此Server承担了系统所有的通讯压力。基于此DP常用于单机多卡场景。
  • 异步梯度更新是提升计算通讯比的一种方法,延迟更新的步数大小决定了模型的收敛速度。
  • Ring-AllReduce通过定义网络环拓扑的方式,将通讯压力均衡地分到每个GPU上,使得跨机器的数据并行(DDP)得以高效实现。
  • DP和DDP的总通讯量相同,但因负载不均的原因,DP需要耗费更多的时间搬运数据

4 参考

Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐