https://www.cnblogs.com/rossiXYZ/p/15815013.html
目录
我们在前文介绍过,微软 ZeRO 可以对一个万亿参数模型可以使用 8 路模型并行、64 路管道并行和 8 路数据并行在 4,096 个 NVIDIA A100 GPU 上进行扩展。
而FSDP(Fully Sharded Data Parallel)是Facebook 深度借鉴微软ZeRO之后提出的PyTorch DDP升级版本,可以认为是对标微软 ZeRO,其本质是 parameter sharding。Parameter sharding 就是把模型参数等切分到各个GPU之上。我们会以 Google,微软和 Facebook 的论文,博客以及代码来进行学习分析。
本系列其他文章如下:
[源码解析] PyTorch 分布式之 ZeroRedundancyOptimizer
[论文翻译] 分布式训练 Parameter sharding 之 ZeRO
[论文翻译] 分布式训练 Parameter Sharding 之 Google Weight Sharding
大规模训练人工智能模型并不容易。除了需要大量的计算能力和资源外,训练非常大的模型背后还有相当大的工程复杂性。Facebook人工智能研究(FAIR)工程部一直致力于构建工具和基础设施,以使大型人工智能模型的培训变得更容易。
Fully Sharded Data Parallel(FSDP)是FAIR引入的最新工具。它将AI模型的参数在数据并行worker之间进行切分,并且可以选择将部分训练计算卸载到CPU。顾名思义,FSDP是一种数据并行训练算法。尽管参数被分片到不同的GPU,但每个微批次数据的计算对于每个GPU worker来说仍然是本地的。这种概念上的简单性使FSDP更易于理解,并且更适用于各种使用场景(与层内并行和流水线并行相比)。与optimizer state+gradient sharding数据并行方法相比,FSDP在训练过程中通过通信和计算重叠对模型参数进行更均匀的切分,具有更好的性能。
FSDP可以使用更少的GPU更有效地训练数量级更大的模型。FSDP已在FairScale库中实现,允许工程师和开发人员使用简单的API扩展和优化其模型的培训。在Facebook,FSDP已经被整合并测试,用于训练一些NLP和Vision模型。
大规模模型训练需要大量的计算资源,比如OpenAI的GPT-3 拥有1750亿个参数。其训练估计需要355年的GPU时间,相当于1000个GPU连续工作4个月以上。
除了需要大量计算和工程资源外,大多数的训练扩展方法都会带来额外的通信成本,并且需要工程师仔细评估内存使用和计算效率之间的权衡。例如,典型的数据并行培训要求在每个GPU上维护模型的冗余副本,而模型并行培训为在worker(GPU)之间移动激活引入了额外的通信成本。
相比之下,FSDP相对而言没有做任何权衡。它通过在GPU上分割模型参数、梯度和优化器状态来提高内存效率,并通过分解通信并将其与前向和后向过程重叠来提高计算效率。FSDP产生与标准分布式数据并行(DDP)培训相同的结果,并提供易于使用的接口,该接口是PyTorch分布式数据并行模块的替代品。Facebook 的早期测试表明,FSDP可以扩展到数万亿个参数。
在标准DDP训练中,每个worker处理一个单独的批次,并使用all-reduce对worker之间的梯度进行汇总。虽然DDP已经变得非常流行,但它占用的GPU内存比它实际需要的要多,因为模型权重和优化器状态在所有DDP worker中都有一个副本。
减少副本的一种方法是应用全参数分片( full parameter sharding)的过程,其中仅提供局部计算所需的模型参数、梯度和优化器的子集。这种方法的一个实现 ZeRO-3 已经被微软所普及。
解锁全参数切分的关键是:我们可以把DDP之中的all reduce操作分解为独立的 reduce-scatter 和 all-gather 操作。
图来自 :https://engineering.fb.com/wp-content/uploads/2021/07/FSDP-graph-2a.png?w=1024
“All-reduce”是“reduce-scatter”和“all-gather”的组合。聚合梯度的标准 “All-reduce”操作可以分解为两个单独的阶段:“reduce-scatter”和“all-gather”。
通过重新安排reduce scatter和all gather,每个DDP worker只需要存储一个参数分片和优化器状态。
下图显示了标准DDP训练(上半部分)和FSDP训练(下半部分):
在标准的数据并行训练方法中,每个GPU上都有一个模型副本,向前和向后传递的序列只在自己的数据分片上进行运行。在这些局部计算之后,每个局部过程的参数和优化器与其他GPU共享,以便计算全局权重更新。
在FSDP中:
为了最大限度地提高内存效率,我们可以在每层向前传播后丢弃全部权重,为后续层节省内存。这可以通过将FSDP包装应用于网络中的每一层来实现(通过设置reshard_after_forward=True)。
下面是伪代码实现:
FSDP forward pass:
for layer_i in layers:
all-gather full weights for layer_i # 权重
forward pass for layer_i
discard full weights for layer_i # 权重
FSDP backward pass:
for layer_i in layers:
all-gather full weights for layer_i # 权重
backward pass for layer_i
discard full weights for layer_i # 权重
reduce-scatter gradients for layer_i # 梯度
我们结合论文思路再来梳理一下 FSDP。
论文思路如下:
总结一下:因为模型参数被分区,所以参数梯度(在框架实现中,梯度往往是参数的成员变量)自然就被分区了。分区的参数被设置到优化器之中,所以优化器只会优化本分区的参数,所以优化器状态自然就是分区之后的。注意,在前向传播和后向传播时候,每个GPU都是用全部模型来计算,得到的梯度也是全部的梯度,只是存储时候只存储自己分区对应的部分。
我们再来展示一下具体流程。假设数据并行度为 n,则有 n 个GPU,那么每个GPU之上保存总模型参数的 1/n,同时梯度,优化器状态就自然被分区了,每个GPU之上还有数据并行。
目前,FAIR 提供四种解决方案来使用FSDP,以适应不同的需求。
对于语言模型,可以在通过以下新参数,在 fairseq framework 之中支持 FSDP:
具体请参阅fairseq教程。
对于计算机视觉模型, VISSL 中可以支持FSDP,并在regnet架构上进行了测试。像BatchNorm和ReLU这样的层已经被无缝地处理并已经测试过其收敛性。可以使用下面选项来启用 FSDP。
在如下链接可以继续研究 this section 。
为了更容易地与更通用的用例集成,PyTorch Lightning已经将FSDP作为beta功能。[此教程](https://pytorch-lightning.readthedocs.io/en/latest/advanced/advanced_gpu.html#fully-sharded training) 包含一个关于如何将FSDP插件与PyTorch Lightning一起使用的详细示例。如下所示,添加plugins='fsdp'可以激活它。
model = MyModel()
trainer = Trainer(gpus=4, plugins='fsdp', precision=16)
trainer.fit(model)
trainer.test()
trainer.predict()
FSDP的主要开发库是FairScale.。您可以通过以下示例直接使用FairScale的FSDP,只需更换DDP。
from fairscale.nn.data_parallel import FullyShardedDataParallel as FSDP
...
# sharded_module = DDP(my_module)
sharded_module = FSDP(my_module)
optim = torch.optim.Adam(sharded_module.parameters(), lr=0.0001)
for sample, label in dataload.next_batch:
out = sharded_module(x=sample, y=3, z=torch.Tensor([1]))
loss = criterion(out, label)
loss.backward()
optim.step()
FairScale中的FSDP库为大规模训练的许多重要方面提供了选项。当你希望使用FSDP的全部功能,你可以自行研究如下方面。
我们接下来看看FSDP如何管理内存。
FairScale提供了受ZeRO
启发的算法:当使用数据并行训练时,您需要在计算/通信效率方面权衡内存的使用。另一方面,在使用模型并行训练时,需要为了内存而权衡计算/通信。
模型训练的内存使用通常分为两类:
模型状态:优化器状态、梯度、参数。
剩余状态:激活、临时缓冲区、碎片内存。
为了减少模型状态下的冗余,ZeRO提出了三种不同的算法。这些在FairScale中实现为优化器状态分片(Optimizer State Sharding,即OSS)、分片数据并行(Sharded Data Parallel,即SDP)和最终完全分片数据并行(Fully Sharded Data Parallel,即FSDP)。让我们深入了解每一个算法的实际机制,并理解它们为什么能够节省内存。
FairScale已经实现了与优化器内存相关的内存优化 OSS。
像Adam这样的优化器通常需要保持动量、方差。即使可以使用FP16精度的参数和梯度进行训练,参数和梯度也需要保存为FP32精度。当每个rank更新完整模型时,这意味着相当大一部分内存被优化器状态的冗余表示所占用。
为了克服这种冗余,优化器状态分片需要将模型优化步骤划分在不同的rank之间,以便每个rank只负责更新模型的对应分片。这反过来又确保优化器状态在每个rank上小得多,并且它不包含跨rank的冗余信息。
训练流程可以从DDP的执行流程做如下修改:
wrapped optimizer根据参数大小(而不是使用顺序)以贪心算法方式来分割优化器状态。这是为了确保每个rank具有几乎相同的优化器内存占用。
训练过程类似于PyTorch的分布式数据并行(DDP)的过程。在每个rank上完成前向传播,然后是向后传播。在后向传播过程中,使用allreduce同步梯度。
每个rank只更新它负责的优化器分配状态参数,然后丢弃其余的。
更新后,将执行broadcast或allgather操作,以确保所有rank都收到最新更新的参数值。
当您使用具有附加状态的优化器(如Adam)时,OSS非常有用。如果您使用的是SGD或任何内存占用有限的优化器,那么在使用多个节点时,由于步骤4中的额外通信,您可能会看到速度减慢。在第2步的allreduce过程中,也有一些用于存储梯度的浪费内存,这些内存随后被丢弃。
OSS公开了一个broadcast_fp16 flag,您可能应该在多节点作业中使用它。在单节点实验中通常不需要这样做。
如果您的模型在大小方面极不平衡(例如,存在一个巨大的张量),那么这种方法将不会有很大帮助,而张量切分选项,如'fairscale.nn.FullyShardedDataParallel'将更可取。
3.OSS应该是DDP环境中的一个临时解决方案,其与大多数DDP功能保持兼容。
在单个节点上,OSS应该总是比vanilla PyTorch快,内存节省会因使用的优化器而异
当使用多个节点时,OSS也可以比vanilla PyTorch快或慢,具体取决于所使用的优化器和可选标志(如上文提到的broadcast_fp16、梯度压缩、梯度累积)
如果您的实验可以使用更大的batch size,则采取更大的batch size并减少所涉及的rank数通常是有益的,或者使用梯度累积,因为这样可以降低通信成本。
虽然OSS解决了优化器中的冗余问题,但依然存在梯度聚合计算的重复以及存在用于梯度的额外内存。为了克服冗余梯度内存,我们可以使用梯度分片或ZeRO-2
。这已由FairScale中的分片数据并行(SDP)API实现。
为了启用梯度分片,每个 rank 都被分配一组参数,它们负责管理优化器状态以及梯度聚合。通过将一个模型分片分配给一个给定的rank,我们确保梯度被规约到特定的rank,而这些rank又负责相应的更新。因此这减少了通信和内存使用。
训练过程如下:
与之前一样,包装的优化器在不同的列组中分割参数。
该模型现在使用分片数据并行(SDP)包装器进行包装,该包装器允许我们在训练过程中添加适当的hook并维护状态。
SDP关注于可训练的参数,并为每个参数添加了一个反向hook。
在反向传播过程中,梯度将规约到指定rank,rank是在 1 中作为切分过程的一部分指定的。使用reduce op代替allreduce op,从而减少通信开销。
每个rank更新其负责的参数。
更新后,将执行广播或allgather,以确保所有rank都收到最新更新的参数值。
OSS和SDPAPI都允许您减少用于梯度和优化器状态的内存,但是如果网络缓慢,则可能存在额外的通信成本。当遇到内存不足(OOM)问题时,可以把OSS和SDP作为第一步尝试。
如果使用多个节点,请通过指定reduce_buffer_size
参数确保SDP正在使用reduce buffers。改变它们的大小可能是一个优化目标,最佳配置可能取决于互连状况。
如果在单个节点上,通常最好不要使用'reduce_buffer_size',因为它会带来延迟成本,但不会增加内存。将此值设置为0表示不使用此功能。
如果您的实验可以使用更大的batch size,则采取更大的batch size并减少所涉及的rank数通常是有益的,或者使用梯度累积,因为这样可以降低通信成本。
为了进一步优化训练并实现更大的内存节省,我们需要启用参数切分。
参数切分类似于梯度和优化器状态,即,每个数据并行rank负责模型参数的一个分片。FairScale通过完全分片数据并行(FSDP)API实现参数分片,该API深受 ZeRO-3
的启发。
参数分片有两个如下关键点:
Allreduce操作可以分为reduce和allgather,类似于以前的分片技术(优化器状态和梯度)。
可以使用FSDP API包装各个层,该API允许我们在给定实例中将单个层所需的所有参数引入给定GPU,计算前向传递,然后丢弃不属于该rank的参数。
使用FSDP很简单,只需要在代码中简单地替换原来的DDP即可。注意:FSDP目前要求模型是一个nn.Sequential
模型。
from torch.utils.data.dataloader import DataLoader
from torchvision.datasets import FakeData
from torchvision.transforms import ToTensor
from fairscale.experimental.nn.offload import OffloadModel
num_inputs = 8
num_outputs = 8
num_hidden = 4
num_layers = 2
batch_size = 8
transform = ToTensor()
dataloader = DataLoader(
FakeData(
image_size=(1, num_inputs, num_inputs),
num_classes=num_outputs,
transform=transform,
),
batch_size=batch_size,
)
model = torch.nn.Sequential(
torch.nn.Linear(num_inputs * num_inputs, num_hidden),
*([torch.nn.Linear(num_hidden, num_hidden) for _ in range(num_layers)]),
torch.nn.Linear(num_hidden, num_outputs),
)
具体训练过程如下:
在开始计算特定层之前,allgather
模型每个层的正向传播所需的参数。
计算向前计算。
在特定层开始反向传递之前,allgather
模型每个层反向传播所需的参数。
计算向后传播。
规约梯度,以便在负责相应参数的rank上累积聚合梯度。
让每个rank使用聚合梯度更新已分配给它的参数。
有了FSDP,在使用API进行检查点设置和保存优化器状态时,需要做一些小的更改。鉴于优化器状态和参数的分片性质,任何旨在保存模型状态以供训练或推理的API都需要考虑保存所有worker的权重。FSDP实现所需的管道(required plumbing)以保存所有worker的权重、保存单个worker的权重以及保存所有worker的优化器状态。
FSDP还支持混合精度训练,其中计算和通信均以FP16精度进行。如果要减少在FP32中执行的操作(这是DDP的默认行为),则必须设置 fp32_reduce_scatter=True
。
为了进一步节省内存,FSDP支持将当前未使用的参数和梯度卸载到CPU上。这可以通过将“move_params_to_cpu”和“move_grads_to_cpu”设置为True来启用。
对于FSDP,最好使用 model.zero_grad(set_to_none=True)
,因为它在单步执行后节省了大量内存。
torch.cuda.amp.autocast
与FSDP完全兼容。您需要将'mixed_precision'arg设置为True。
如果与激活检查点相结合,则最好使用 FSDP(checkpoint_wrapper(module))而不是checkpoint_wrapper(FSDP(module)).。后者将导致更多的通信,速度也会变慢。
FSDP与使用pointwise优化器的DDP兼容,例如Adam、AdamW、ADADDelta、Adamax、SGD等。当使用non-pointwise优化器(例如Adagrad、Adafactor、LAMB等)时,sharding将导致略有不同的结果。
为了获得最佳内存效率,请使用“auto_wrap”将网络中的每一层用FSDP进行封装,并将 reshard_after_forward
设置为True。这样速度会慢,但是显存开销最小。
为了获得最佳训练速度,请将 reshard_after_forward
设置为False(不需要包装每一层,但如果设置,则会进一步提高速度)。
支持,FSDP基本原理和如何使用我们已经介绍完毕,下一篇我们介绍其代码细节,看看究竟如何做到最大程度减少内存使用。
Fully Sharded Data Parallel: faster AI training with fewer GPUs
ZeRO & DeepSpeed:可以让训练模型拥有超过1000亿个参数的优化(微软)
Fully Sharded Data Parallel: faster AI training with fewer GPUs
https://github.com/microsoft/DeepSpeed
ZeRO: Memory Optimizations Toward Training Trillion Parameter Models
Automatic Cross-Replica Sharding of Weight Update in Data-Parallel Training