# 分布式训练技术原理 - 数据并行 - FSDP - FSDP算法是由来自DeepSpeed的ZeroRedundancyOptimizer技术驱动的,但经过修改的设计和实现与PyTorch的其他组件保持一致。FSDP将模型实例分解为更小的单元,然后将每个单元内的所有参数扁平化和分片。分片参数在计算前按需通信和恢复,计算结束后立即丢弃。这种方法确保FSDP每次只需要实现一个单元的参数,这大大降低了峰值内存消耗。(数据并行+Parameter切分) - DDP - DistributedDataParallel (DDP), **在每个设备上维护一个模型副本,并通过向后传递的集体AllReduce操作同步梯度,从而确保在训练期间跨副本的模型一致性** 。为了加快训练速度, **DDP将梯度通信与向后计算重叠** ,促进在不同资源上并发执行工作负载。 - ZeRO - Model state - Optimizer->ZeRO1 - 将optimizer state分成若干份,每块GPU上各自维护一份 - 每块GPU上存一份完整的参数W,做完一轮foward和backward后,各得一份梯度,对梯度做一次 **AllReduce(reduce-scatter + all-gather)** , **得到完整的梯度G,由于每块GPU上只保管部分optimizer states,因此只能将相应的W进行更新,对W做一次All-Gather** - Gradient+Optimzer->ZeRO2 - 每个GPU维护一块梯度 - 每块GPU上存一份完整的参数W,做完一轮foward和backward后, **算得一份完整的梯度,对梯度做一次Reduce-Scatter,保证每个GPU上所维持的那块梯度是聚合梯度,每块GPU用自己对应的O和G去更新相应的W。更新完毕后,每块GPU维持了一块更新完毕的W。同理,对W做一次All-Gather,将别的GPU算好的W同步到自己这来** - Parameter+Gradient+Optimizer->ZeRO3 - 每个GPU维护一块模型状态 - 每块GPU上只保存部分参数W,做forward时,对W做一次 **All-Gather** ,取回分布在别的GPU上的W,得到一份完整的W, **forward做完,立刻把不是自己维护的W抛弃,做backward时,对W做一次All-Gather,取回完整的W,backward做完,立刻把不是自己维护的W抛弃. 做完backward,算得一份完整的梯度G,对G做一次Reduce-Scatter,从别的GPU上聚合自己维护的那部分梯度,聚合操作结束后,立刻把不是自己维护的G抛弃。用自己维护的O和G,更新W。由于只维护部分W,因此无需再对W做任何AllReduce操作** - Residual state - activation->Partitioned Activation Checkpointing - 每块GPU上只维护部分的activation,需要时再从别的地方聚合过来就行。需要注意的是,activation对显存的占用一般会远高于模型本身,通讯量也是巨大的 - temporary buffer->Constant Size Buffer - 提升带宽利用率。当GPU数量上升,GPU间的通讯次数也上升,每次的通讯量可能下降(但总通讯量不会变)。数据切片小了,就不能很好利用带宽了。所以这个buffer起到了积攒数据的作用:等数据积攒到一定大小,再进行通讯。 - 使得存储大小可控。在每次通讯前,积攒的存储大小是常量,是已知可控的。更方便使用者对训练中的存储消耗和通讯时间进行预估 - unusable fragment->Memory Defragmentation - 对碎片化的存储空间进行重新整合,整出连续的存储空间。防止出现总存储足够,但连续存储不够而引起的存储请求fail - offload - ZeRO-Offload - **forward和backward计算量高** ,因此和它们相关的部分,例如参数W(fp16),activation,就全放入GPU - **update的部分计算量低** ,因此和它相关的部分,全部放入CPU中。例如W(fp32),optimizer states(fp32)和gradients(fp16)等 - ZeRO-Offload 分为 Offload Strategy 和 Offload Schedule 两部分,前者解决如何在 GPU 和 CPU 间划分模型的问题,后者解决如何调度计算和通信的问题 - ZeRO-Infinity - 一是将offload和 ZeRO 的结合从 ZeRO-2 延伸到了 ZeRO-3,解决了模型参数受限于单张 GPU 内存的问题 - 二是解决了 ZeRO-Offload 在训练 batch size 较小的时候效率较低的问题 - 三是除 CPU 内存外,进一步尝试利用 NVMe 的空间 - 模型并行 - tensor-wise parallelism - MLP切分 - 对第一个线性层按列切分,对第二个线性层按行切分 - ![图片](./img/分布式训练技术原理-幕布图片-36114-765327.jpg) - ![图片](./img/分布式训练技术原理-幕布图片-392521-261326.jpg) - ![图片](./img/分布式训练技术原理-幕布图片-57107-679259.jpg) - self-attention切分 - attention的多头计算天然适合tensor并行,因为每个头上都可以独立计算最后再将结果concat起来,从而 **可以把每个头的参数放到一块GPU上** - 对线性层, **按照“行切割”** 。切割的方式和MLP层基本一致,其forward与backward原理也一致 - 输入层Embedding切分 - 对positional embedding来说,max_s本身不会太长,因此每个GPU上都拷贝一份,对显存的压力也不会太大 - 将word embedding拆分到不同GPU上,每块GPU维护一分部词表。当输入X去GPU上查找时,能找到的词,就正常返回词向量,找到不到就把词向量中的全部全素都置0。按此方式查找完毕后,每块GPU上的数据做一次AllReduce,就能得到最终的输入。 - ![图片](./img/分布式训练技术原理-幕布图片-220157-552735.jpg) - 输出层Embedding切分 - **输入层和输出层共用一个word embeding** - **当模型的输入层到输入层都在一块GPU上时(即流水线并行深度=1),我们不必担心这点(实践中大部分用Megatron做并行的项目也是这么做的)。但若模型输入层和输出层在不同的GPU上时,我们就要保证在权重更新前,两块GPU上的word embedding梯度做了一次AllReduce** 。 - ![图片](./img/分布式训练技术原理-幕布图片-42284-124759.jpg) - cross-entroy - ![图片](./img/分布式训练技术原理-幕布图片-124076-270516.jpg) - ![图片](./img/分布式训练技术原理-幕布图片-838373-426344.jpg) - [pipeline paralelism]("https://zhuanlan.zhihu.com/p/629637468") - GPipe - PipeDream - 1F1B - 每个 GPU 以交替的方式执行每个 micro batch 的正向和反向过程,以尽早释放其占用的显存,进而减少显存占用 - ![图片](./img/分布式训练技术原理-幕布图片-20096-279847.jpg) - 1F1B 并不能减少 bubble time, **为了进一步减少 bubble time,Megatron 又提出了 interleaved 1F1B 模式** 。也就是原本每个 GPU 负责连续 4 个层的计算,现在变成负责连续两个层的计算,只有原来的一半,从而 bubble time 也变成了原来的一半,即把一个设备上连续的层划分为若干不连续的层,负责的数量不变,但顺序变了。 - ![图片](./img/分布式训练技术原理-幕布图片-618350-869132.jpg) - DAPPLE - ![图片](./img/分布式训练技术原理-幕布图片-906937-836104.jpg) - layer-wise parallelism - sequence parallelism - Sequence 并行的好处在于不会增加通信量,并且可以大大减少显存占用 - Layer-norm 和 Dropout 沿着序列的维度是独立的,因此可以按照 Sequence 维度进行拆分 - 使用了 Sequence 并行之后,对于超大规模的模型而言,其实显存占用量还是很大的。因此,Megatron 又引入了激活重计算技术,找到一些计算量很少但显存占用很大的算子,比如 Attention 里的 Softmax、Dropout 等算子,对这些算子进行激活重计算就可以显著减少显存,并且计算开销增加不大 - MoE - 核心思想:将大模型拆分成多个小模型。每个样本只需要激活部分专家模型进行计算,从而大大节省计算资源。 **MoE 的基本思路是通过宽度换取深度,因为模型深度越深,计算层数越多,进而推理时间越长** - Hard Gate MoE - Sparse MoE