想象你要为一场一千人的年夜饭准备食材。老办法是:后厨每位厨师都备一套完整食材——锅、碗、刀、勺,油盐酱醋样样齐全。十位厨师就要备十份一模一样的料,冰箱放不下,钱也烧不起。FSDP(Fully Sharded Data Parallel,全分片数据并行)的做法反着来:把整头猪、整袋米、整缸酱按部位切开,每位厨师只保管自己那几筐,谁做菜需要用到某个部位的料,就临时向保管它的伙伴借,用完再还回去。这就是“全分片”——把模型的参数、梯度、优化器状态,像切年货一样分剁到每张 GPU 上。本文带你走进这个分工厨房:学会怎么包装模型、选对分工方式、记录菜谱进度、调节传菜节奏,以及处理厨房里层出不穷的意外。读完后,你就能在多卡集群上把大模型“拆得开、装得回、收得齐”。

岗位关联

面试与工程中,大模型训练卡在显存是常态;掌握 FSDP 等于学会在多卡集群上把模型拆得开、装得回、收得齐,这是大模型工程师绕不开的基本功。

目录
  1. 可视化演示
  2. 核心原理
  3. 模型包装
  4. 分片策略
  5. 检查点管理
  6. 性能调优
  7. 报错排查
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示FSDP模型分片:多GPU(G0-G2)间分片Param/Grad/Opt三部分,All-Gather(AG)聚合→Reduce-Scatter(RS)分散,Ckpt检查点。

本节配有交互动效(进入视口后自动加载,约 3KB)。

FSDP实战核心 多GPU(G0-G2)间分片Param(参数)+Grad(梯度)+Opt(优化器状态),All-Gather(AG)聚合→Reduce-Scatter(RS)分散实现高效通信 配置要点:Wrap(模型包装)→Shard(分片策略)→Comm(通信优化)→Ckpt(激活检查点),避免配置错误导致性能下降 Memory_per_GPU = (Model + Optimizer + Activation) / N_GPUs

核心原理

年夜饭的备料环节,老传统“各备全套”最费冰箱:十位厨师各囤十份一样的猪肉和酱油,食材堆到冷库门口。FSDP 的做法像年夜饭的分工备料——整头猪按部位切好,A 师傅保管猪腿、B 师傅保管里脊、C 师傅掌勺调料,谁做菜需要某个部位,就喊一声让对方递过来。这套逻辑对应到深度学习里,就是“全分片”:把模型这套大食材拆开,分给参与训练的每一张 GPU 保管。

在 PyTorch 里,DDP(数据并行)让每张卡都保存一份完整模型副本,十张卡就是十份参数,显存被参数复制白白烧掉。FSDP 则把模型状态——参数、梯度、优化器状态——全部按卡切分。每张卡开始时只持有整个模型的 N 分之一(N 是卡数),需要算某一层时,再用 all-gather 操作把分片临时拼回全量;算完梯度后,又用 reduce-scatter 把梯度切分汇总回各卡。这就是“全分片”的含义:模型侧的三类状态统统均摊。

显存收益可以用一个粗略的账本算清楚。假设模型参数占 P 字节,梯度占 G 字节,优化器状态占 O 字节,激活值占 A 字节,N 张卡全分片后,单卡模型侧占用约为:

Mmodel=P+G+ON+A\displaystyle M_{\text{model}} = \frac{P + G + O}{N} + A

1. P(参数占用):模型权重本身占的字节数,等于参数量乘以每个参数的字节数。

P=num_params×bytes_per_param\displaystyle P = \text{num\_params} \times \text{bytes\_per\_param}

2. G(梯度占用):反向传播算出的梯度,尺寸与参数一样大,可近似认为 G ≈ P。

G≈P\displaystyle G \approx P

3. O(优化器状态):Adam 等优化器额外维护的一阶、二阶动量,通常约为参数的 2 倍。

O≈2P\displaystyle O \approx 2P

4. 除以 N(分片摊薄):全分片把参数、梯度、优化器状态三样均分到 N 张卡上。

P+G+ON\displaystyle \frac{P + G + O}{N}

5. 加 A(激活值):前向传播的中间张量要在反向时复用,无法被分片,所以单独相加。

Mmodel=P+G+ON+A\displaystyle M_{\text{model}} = \frac{P + G + O}{N} + A

当然,天下没有免费的午餐。分片省下了冰箱空间,却多出了“借料”的跑腿成本:每次前向和反向都要把参数拼回全量,通信量比 DDP 更高。所以 FSDP 的定位很清晰——它不是让训练更快,而是让放不下的模型放得下。理解了“省显存、费通信”这笔账,后面所有坑的答案就藏在这八个字里。

模型包装

千人宴的后厨不能只有一个大通铺,否则所有人挤在一起抢料、找不到东西。聪明的做法是把后厨隔成若干小档口,每个档口独立保管自己的食材、独立记账。FSDP 里的“包装”正是这个隔断动作:你决定把模型的哪些层圈进同一个“档口”(FSDP 块),块内的参数一起分片、一起拼回。

PyTorch 中,最直接的包装入口是 FullyShardedDataParallel(model, ...),它把整个模型当作一个巨大的档口。但整包会让拼参时的峰值显存等于整个模型的参数总量,分片效果大打折扣。所以工程上更常用“自动包装”:通过 auto_wrap_policy 告诉 FSDP 按什么规则自动给每一层切档口。比如 size_based_auto_wrap_policy(min_num_params=阈值) 表示“参数量超过阈值的模块自动独立成块”;Transformer 架构则常用 transformer_auto_wrap_policy 按层类型切块。你也可以手动逐个包装子模块,精确控制每一刀切在哪里。

包装粒度直接决定显存峰值。设所有参数均摊后每卡占 P/N,而当前这一个档口的完整参数大小为 P_block,则前向拼参瞬间的单卡峰值约为:

Mpeak=PN+Pblock+A\displaystyle M_{\text{peak}} = \frac{P}{N} + P_{\text{block}} + A

1. P/N(常驻分片):所有参数均分后,每卡长期占用的那部分。

PN=各卡常驻参数\displaystyle \frac{P}{N} = \text{各卡常驻参数}

2. P_block(块内全量):单个 FSDP 包装块在拼参时需要的完整参数。

Pblock=max⁡(各包装块参数量)\displaystyle P_{\text{block}} = \max(\text{各包装块参数量})

3. 加 A(激活值):激活值显存仍然存在,且与包装粒度关系不大。

Mpeak=PN+Pblock+A\displaystyle M_{\text{peak}} = \frac{P}{N} + P_{\text{block}} + A

档口划分是一门平衡术:切得太粗,拼参峰值高,照样 OOM;切得太细,每次前向都要多次跨卡借料,通信时间直线上升。实践中的经验是:先按模型结构切(Transformer 的每个 block 一层),再用阈值策略微调,让每个档口大小适中。记住,包装决定的不是“能不能省”,而是“省多少、跑多快”。

分片策略

年夜饭的分工不止一种:最激进的方案是连菜带锅全摊开,每位厨师只保管自己那几筐料;温和一点的方案是大家各自备齐所有主菜食材,只把价格昂贵的调料集中保管;还有一种最省事的方案,干脆各备各的全套,互不借用。FSDP 的三种分片策略,恰好对应这三种厨房分工。

  • FULL_SHARD:参数、梯度、优化器状态全部按卡切分,最省显存,但每次前向反向都要 all-gather 拼回参数,通信最重。
  • SHARD_GRAD_OP:参数每卡保留全套(不切),只有梯度分片。省掉了梯度这块显存,通信量比 FULL_SHARD 低。
  • NO_SHARD:什么都不切,等价于 DDP,每卡一份完整模型,仅梯度做 all-reduce。

三种策略的通信代价可以量化对比。按每步训练计算,FSDP 全分片需要“前向拼参数一趟、反向拼参数一趟、梯度切分一趟”,共三趟模型大小的通信;而 DDP 只需要梯度 all-reduce 一趟(收和发合计两趟):

CFSDP=3P,CDDP=2P\displaystyle C_{\text{FSDP}} = 3P, \qquad C_{\text{DDP}} = 2P

1. 前向 all-gather(C₁):前向计算前,把分片的参数从各卡拼回全量,通信量为 P。

C1=P\displaystyle C_1 = P

2. 反向 all-gather(C₂):反向算梯度时还要再用一次参数,所以再拼一趟,通信量为 P。

C2=P\displaystyle C_2 = P

3. reduce-scatter(C₃):梯度算完后,按分片规则切分并汇总回各卡,通信量为 P。

C3=P\displaystyle C_3 = P

4. DDP 的 all-reduce(C_DDP):梯度一次性全部规约,收与发合计约 2P。

CDDP=2P\displaystyle C_{\text{DDP}} = 2P

策略选择的决策树其实很朴素:显存快爆了,优先 FULL_SHARD;显存刚好够,想要更快收敛,试试 SHARD_GRAD_OP;模型不大、卡又多,NO_SHARD(DDP)反而省心。厨房里没有万能分工,只有“按食材多少、传菜通道宽窄”来定的方案——显存压力大就多摊一些,带宽紧张就少借几次料。

检查点管理

年夜饭从下午一直炖到深夜,中途最怕断电、锅翻、临时改菜单。聪明的厨师会每隔一小时记一次账:食材还剩多少、哪道菜炖到几成熟、火候怎么调的。这套“中途记账”在 FSDP 里就是检查点(checkpoint):把模型训练到一半的状态存下来,下次断电了从最近一笔账接着做,而不是从头再来。

但 FSDP 的账本有个特殊之处:每张卡只记得自己那一片食材的状态。直接 torch.save(model.state_dict(), "model.pt"),保存的只是本卡的分片,不是完整模型。正确做法是让 rank0 卡把所有分片聚合起来,拼成一份完整的“总账本”再落盘。保存时机的选择也有讲究:每隔固定步数存一次、或按 epoch 存一次;保存的频率越高,断电损失越小,但磁盘写入和聚合通信的开销越大。

聚合保存的本质,就是把 N 个分片状态按原顺序拼回完整状态。记第 i 张卡的状态为 S_i,则:

Sfull=∑i=1NSi\displaystyle S_{\text{full}} = \sum_{i=1}^{N} S_i

1. S_i(分片状态):第 i 张卡保存的状态,包含它保管的那部分参数、梯度和优化器状态。

Si=卡 i 的分片状态\displaystyle S_i = \text{卡 } i \text{ 的分片状态}

2. 求和符号 Σ(拼接):从第 1 张卡加到第 N 张卡,按分片顺序依次拼接。

∑i=1N\displaystyle \sum_{i=1}^{N}

3. S_full(完整状态):拼接结果等价于单卡全量模型的 state_dict,可直接用于单卡推理或后续微调。

Sfull=∑i=1NSi\displaystyle S_{\text{full}} = \sum_{i=1}^{N} S_i

版本兼容和恢复技巧同样重要:PyTorch 升级后,FSDP 的分片存储格式可能变化,加载旧检查点偶尔会报 unexpected key;跨版本训练时,建议只保存和加载模型权重,优化器状态则尽量同版本。恢复训练时,务必用与保存时完全相同的包装策略和分片策略重建模型,再 load_state_dict,否则等于拿着别人的钥匙开自己的锁。

性能调优

后厨出菜慢的瓶颈往往不在炒菜本事,而在传菜路上:每炒好一盘就叫一个人跑一趟,十分钟的菜全耗在走廊里。聪明的厨师会先把炒好的菜攒在传菜口,凑够一车再一起送。FSDP 里的“攒菜”就是梯度累积:本来每算一个 batch 就要同步一次梯度,现在连续算 K 个小 batch,梯度先攒在本地,攒够了再统一同步一次。

梯度累积直接降低了通信频率。设单卡单步的 batch 为 B,累积 K 步后再做一次优化器更新,等效 batch 变为:

Beff=B×K\displaystyle B_{\text{eff}} = B \times K

1. B(单步 batch):每张卡每个小步实际喂给模型的样本数。

B=单卡单步 batch\displaystyle B = \text{单卡单步 batch}

2. K(累积步数):连续计算 K 个小 batch 的梯度,累加但不更新参数。

K=累积步数\displaystyle K = \text{累积步数}

3. 乘积(等效规模):累积 K 步后做一次更新,等效于一次喂入 K 倍样本。

Beff=B×K\displaystyle B_{\text{eff}} = B \times K

除了梯度累积,还有几招能进一步压通信:一是通信与计算重叠,FSDP 在算当前层时,后台提前向其他卡请求下一层的参数,边炒边传;二是混合精度,用 BF16 传梯度,通信量直接减半;三是调整超参——打包粒度、分片策略、梯度累积步数之间互相牵制,往往要组合调优。调优的顺序建议是:先看显存够不够(不够就切更细或换 FULL_SHARD),再看通信占比高不高(高就累积梯度、开重叠、上混合精度),最后才动学习率这些收敛参数。

报错排查

再熟练的后厨也会遇到事故:食材不够了、传菜超时了、调料过期了。FSDP 训练同样有一批“高频事故”,先分清是哪一类,再对症下药。

第一类是显存爆掉(OOM)。它通常不是玄学,而是账没算清:包装粒度太粗导致前向拼参峰值过高,或 batch 太大导致激活值爆表。排查时先按公式估算 M_peak = P/N + P_block + A,确认是哪一项超了;然后对症:把档口切细、减小 batch、开混合精度、或改用 FULL_SHARD。第二类是通信超时。NCCL 的集合通信如果等待太久,会抛超时异常。这往往是机器间带宽不足、某张卡掉队拖慢整体、或 timeout 参数设得太小。解决思路是:检查网络拓扑、用 torch.distributed.barrier() 定位掉队卡、调大 init_process_group(timeout=...)。第三类是版本与加载问题:PyTorch 升级后分片格式变化、保存和加载时包装配置不一致,都会报 unexpected key 或加载失败。

通信超时的判定逻辑可以写成一条不等式:

Tcomm>Ttimeout⇒Failure\displaystyle T_{\text{comm}} > T_{\text{timeout}} \Rightarrow \text{Failure}

1. T_comm(通信耗时):一次集合通信从发起到完成的总用时,受带宽、卡数、数据量影响。

Tcomm=单次集合通信耗时\displaystyle T_{\text{comm}} = \text{单次集合通信耗时}

2. T_timeout(超时阈值):初始化进程组时通过 timeout 参数设置的上限。

Ttimeout=timeout 设置值\displaystyle T_{\text{timeout}} = \text{timeout 设置值}

3. 大于号右移(触发失败):等待时间一旦越过阈值,进程组主动报错,避免无限挂起。

Tcomm>Ttimeout⇒Failure\displaystyle T_{\text{comm}} > T_{\text{timeout}} \Rightarrow \text{Failure}

排查的通用心法是“先环境、后代码”:先看卡数、带宽、PyTorch 和 torch.distributed 版本,再看包装与分片配置,最后才怀疑模型本身。厨房里出了事,先查是不是停水停电,别急着怪菜谱——大多数 FSDP 报错,都是配置和环境问题,不是算法问题。

AI 应用场景

多卡小显存集群微调大模型

阶段:模型微调(训练阶段)

解决的问题:8 张 24GB 显存的消费级卡,连 7B 模型的参数都放不下,更别说训练。传统 DDP 每卡要装全套参数,直接 OOM。用 FSDP 全分片,把参数、梯度、优化器状态摊到 8 张卡上,每卡只承担约 1/8 的模型状态,再配合混合精度和梯度累积,就能在大众显卡上微调百亿参数以内的模型。

具体案例:某创业团队只有 4 张 4090,要在本地微调 7B 对话模型做垂直领域助手。他们用 FULL_SHARD + size_based_auto_wrap_policy 包装,每个 Transformer 块独立分片,激活值用 gradient checkpointing 压缩,最终把 batch 跑起来,显存占用稳定在 21GB 左右。

长训练任务的中断恢复

阶段:分布式训练运维

解决的问题:大模型训练动辄几天几周,机房断电、机器故障、抢占式实例回收都可能导致训练中断。没有检查点就得从头再来,有了合理的检查点策略,最多损失最近一个保存间隔的进度。

具体案例:某大厂训练团队在 64 卡集群上微调 13B 模型,每小时保存一次聚合检查点。某次机器故障后,他们从最近检查点恢复训练,仅损失 40 分钟进度;恢复时用与保存时相同的 FSDP 包装配置加载模型,再单独加载优化器状态,全程无人值守。

多机多卡算力池的弹性调度

阶段:训练资源调度

解决的问题:云上 GPU 实例规格不一,卡数、显存、带宽都可能不同。FSDP 的分片策略和包装粒度可以按实际资源调整:显存小的机器分得更细,带宽高的机器通信更频繁也不怕,从而把异构算力池用起来。

具体案例:某高校实验室把 8 张 A100 和 16 张 3090 混编成一个训练池,通过 FSDP 按节点显存量差异配置不同的分片策略,让大模型训练任务在异构集群上正常跑通,资源利用率提升了约 35%。

Python 演示

python60 行
import osimport torchimport torch.nn as nnimport torch.distributed as distfrom torch.distributed.fsdp import FullyShardedDataParallel as FSDPfrom torch.distributed.fsdp import ShardingStrategyfrom torch.distributed.fsdp.wrap import size_based_auto_wrap_policy  def init_dist():    torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))    dist.init_process_group(backend="nccl")  class TinyModel(nn.Module):    def __init__(self):        super().__init__()        self.fc1 = nn.Linear(512, 512)        self.fc2 = nn.Linear(512, 512)        self.fc3 = nn.Linear(512, 512)     def forward(self, x):        x = torch.relu(self.fc1(x))        x = torch.relu(self.fc2(x))        return self.fc3(x)  def main():    init_dist()    rank = dist.get_rank()    model = TinyModel()    wrap_policy = size_based_auto_wrap_policy(min_num_params=2048)    model = FSDP(        model,        auto_wrap_policy=wrap_policy,        sharding_strategy=ShardingStrategy.FULL_SHARD,    )    opt = torch.optim.Adam(model.parameters(), lr=2e-4)    accum = 4    micro_bs = 64    data = torch.randn(micro_bs, 512)     for epoch in range(3):        opt.zero_grad()        for step in range(accum):            out = model(data.to(rank))            loss = out.sum() / accum            loss.backward()        opt.step()        if rank == 0:            print("epoch", epoch, "loss", loss.item())     if rank == 0:        torch.save(model.state_dict(), "fsdp_model.pt")    dist.barrier()    dist.destroy_process_group()  if __name__ == "__main__":    main()
目的:演示一个最小可跑的 FSDP 训练脚本——自动包装模型、FULL_SHARD 全分片、梯度累积降低通信频率、最后在 rank0 保存聚合后的完整检查点。用 torchrun --nproc_per_node=4 script.py 即可启动。
  1. torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))每个进程绑定自己那张卡,避免多进程抢卡。
  2. dist.init_process_group(backend="nccl")建立多卡通信组,NCCL 负责 GPU 间的集合通信。
  3. size_based_auto_wrap_policy(min_num_params=2048)参数量超过阈值的模块自动切成独立 FSDP 块。
  4. FSDP(model, auto_wrap_policy=..., sharding_strategy=FULL_SHARD)包装模型并启用全分片。
  5. loss = out.sum() / accum把损失按累积步数缩放,等效于扩大 batch 又不炸显存。
  6. data.to(rank)直接把 rank 当作设备号,把数据送到本进程对应的卡上。
  7. torch.save(model.state_dict(), "fsdp_model.pt")在 rank0 聚合各卡分片后保存完整检查点。

常见误区

误区我用了 FSDP 之后应该立刻变快,如果变慢说明是我代码写错了。

正解FSDP 的首要目标是省显存,不是提速。它把参数摊到各卡,省下显存才能塞下更大模型;但每次前向都要 all-gather 拼参数,通信量约为 DDP 的 1.5 倍。小模型或卡间带宽不足时,DDP 往往更快。花钱买大冰箱不等于出菜更快,FSDP 的意义是“装得下”,提速要靠梯度累积和通信重叠。

为何常见:名字里有“并行”,直觉上容易以为并行就等于加速;很多教程只强调省显存带来的吞吐提升,没提通信开销这笔账。

误区反正写了 `FSDP(model)` 就开始分片了,FULL_SHARD、SHARD_GRAD_OP 这些细节不用管。

正解三种策略的显存和通信特性差别很大。FULL_SHARD 最省显存但通信最重;SHARD_GRAD_OP 参数不切、只切梯度,省一半梯度显存;NO_SHARD 就是 DDP。选错策略会出现“显存还是爆”或“慢得离谱”的情况。就像三种厨房分工,各有各的适用场景,不能闭眼选。

为何常见:官方默认值恰好是 FULL_SHARD,很多人在小模型上跑通了,就误以为默认配置在任何场景都是最优解。

误区训练完每张卡都 `torch.save(model.state_dict())`,随便拿一张卡的文件就当作最终模型。

正解FSDP 下每张卡的 state_dict 只是分片,单卡文件不是完整模型。要在 rank0 做聚合(使用 full state dict 模式)再保存;加载时也要先按相同包装配置重建 FSDP 模型,再 load_state_dict,否则会出现键对不上、参数缺失。分片保存的账本,必须拼起来才是一本完整的账。

为何常见:单卡训练和 DDP 训练中每卡都有一份完整模型,这种惯性思维被直接带到了 FSDP 场景。

误区一看到 timeout 或 OOM 就疯狂检查模型结构,觉得一定是我把网络写错了。

正解大量 FSDP 报错来自环境与配置:NCCL 超时多半是带宽不足或 timeout 设置太小;OOM 可能是包装粒度太粗或 batch 太大;PyTorch 升级后加载旧检查点会出现 unexpected key。排查应按报错分类:先查卡数、带宽、版本、包装策略,再查模型代码。厨房出了事,先查是不是停水停电,别急着怪菜谱。

为何常见:报错信息长而专业,小白容易把所有问题归因到“自己写错”,而忽略了分布式训练里环境因素占比极高。

面试题

Q1. FSDP 中的“全分片”主要指什么?

单选题 · easy

  • A.把训练数据均匀分给每张卡
  • B.把模型状态均分到所有卡上← 正确答案
  • C.把激活值全部丢弃只留梯度
  • D.把数据集按类别重新分组

答案:B。全分片指参数、梯度、优化器状态这三类模型状态都按卡切分,每卡只存 1/N。数据分片是数据并行的通用做法,DDP 也做,不是 FSDP 的独特之处;激活值在反向时还要用,不能丢弃。

Q2. FSDP 与 DDP 在显存占用上的最大不同是什么?

单选题 · easy

  • A.FSDP 会自动压缩梯度的数值精度
  • B.FSDP 每卡只保存部分模型参数← 正确答案
  • C.DDP 每卡保存的模型参数更小
  • D.DDP 会自动释放优化器状态

答案:B。FSDP 的核心是把模型参数、梯度、优化器状态分片存储,每卡只持有一部分;而 DDP 每卡保存一份完整模型副本,所以显存占用大约是 FSDP 的 N 倍。压缩精度和释放状态都不是两种方法的固有区别。

Q3. FSDP 训练时,前向传播之前需要做什么?

单选题 · mid

  • A.先把各卡分片的参数拼回全量← 正确答案
  • B.先把所有梯度全部归零重置
  • C.先把优化器状态同步到主卡
  • D.先把激活值广播给所有机器

答案:A。因为每卡只有参数的一部分,前向计算某层前必须用 all-gather 把该层的全量参数从各卡拼回来,计算完再释放。梯度归零是每步优化器更新前要做的事,优化器状态并不需要同步到主卡,激活值也不是训练前广播的。

Q4. 三种分片策略中,“参数每卡全套、只分片梯度”对应的是?

单选题 · mid

  • A.FULL_SHARD
  • B.SHARD_GRAD_OP← 正确答案
  • C.NO_SHARD
  • D.MIXED_SHARD

答案:B。SHARD_GRAD_OP 只对梯度做分片,参数每卡保留全套,显存省一半梯度空间,通信量比 FULL_SHARD 低。FULL_SHARD 是参数和梯度都切,NO_SHARD 是都不切(等价 DDP),不存在 MIXED_SHARD 这个标准策略名。

Q5. 梯度累积的主要目的是什么?

单选题 · mid

  • A.减少前向传播的计算精度损失
  • B.降低通信频率并扩大等效 batch← 正确答案
  • C.让每张卡保存更多副本参数
  • D.替代混合精度训练的全部功能

答案:B。梯度累积把 K 个小 batch 的梯度攒起来再统一同步和更新,等效 batch 变为 B×K,同步频率降到 1/K,通信开销显著下降。它不改变计算精度,不会增加参数副本,也不能替代混合精度——两种手段通常配合使用。

Q6. 关于 FSDP 的通信量,下列说法正确的是?

单选题 · hard

  • A.FSDP 每步通信量只有 DDP 的一半
  • B.FSDP 每步通信量约为模型参数的 3 倍← 正确答案
  • C.FSDP 每步通信量与模型参数无关
  • D.FSDP 与 DDP 的通信量完全相同

答案:B。FSDP 全分片每步要做前向 all-gather、反向 all-gather、reduce-scatter 三趟,每趟通信量约等于模型参数总量 P,合计约 3P;DDP 的梯度 all-reduce 约 2P。通信量和参数总量正相关,并不与模型参数无关,两者数值也不相同。

Q7. FSDP 训练保存检查点时,推荐做法是什么?

单选题 · hard

  • A.每张卡各自加载再覆盖主卡结果
  • B.主卡聚合各卡分片后再统一保存← 正确答案
  • C.只保存 GPU0 上的参数分片即可
  • D.训练结束后丢弃优化器状态再存

答案:B。各卡只持有分片,直接 torch.save 只能存本卡的那部分。正确做法是在 rank0 用完整状态模式聚合所有分片后再保存,得到等价于单卡全量模型的 state_dict,便于后续推理或微调。只存 GPU0 的分片会丢失大部分参数,丢弃优化器状态则是功能缺失而非推荐做法。

Q8. 下列哪些做法有助于降低 FSDP 训练时的显存峰值?

多选题 · mid

  • A.使用全参数分片策略 FULL_SHARD← 正确答案
  • B.增加包装粒度让每个 FSDP 块更小← 正确答案
  • C.把每一步的批次大小调得更大
  • D.合理使用混合精度训练← 正确答案

答案:ABD。FULL_SHARD 把参数、梯度、优化器状态全部均摊到各卡,显存最省;更小的包装块降低前向拼参的峰值;混合精度用 BF16 存储和传输,显存和通信同时减半。C 选项相反,更大的 batch 会增加激活值显存,是显存压力上升的原因之一。

Q9. FSDP 训练报“通信超时”,可能的原因和应对做法有哪些?

多选题 · hard

  • A.机器间带宽不足或 NCCL 配置不当← 正确答案
  • B.某张卡的训练速度严重拖慢整体← 正确答案
  • C.及时调大 init_process_group 的 timeout← 正确答案
  • D.立即删掉所有检查点再重跑训练

答案:ABC。带宽不足会让集合通信等待过久,掉队卡会拖慢 all-gather 整体进度,调大 timeout 可以缓解偶发慢卡导致的误报。删除检查点反而丢了恢复机会,和通信超时没有关系,属于错误操作。

Q10. 关于 FSDP 与 DDP 的对比,下列说法正确的有哪些?

多选题 · easy

  • A.FSDP 能显著降低模型状态的显存占用← 正确答案
  • B.DDP 的每个进程都保存完整模型副本← 正确答案
  • C.FSDP 适合超大模型的多卡训练场景← 正确答案
  • D.FSDP 在小模型上总是比 DDP 更快

答案:ABC。A、B 是两者的基本事实:FSDP 分片省显存,DDP 每卡全套。C 正确,模型越大,分片收益越明显。D 错误,小模型下 FSDP 的拼参通信开销可能超过省显存的收益,速度上反而可能输给 DDP。