想象一支 8 人的登山队去雪山露营,每人只背得动 40 升的背包(GPU 显存),但每个人手里都有一套完整的装备:冲锋衣(模型参数)、旅行手账(梯度)、以及沉甸甸的私人药箱和营养包(优化器状态)。一开始大家图省心,每人照搬整套——8 个背包、8 份一模一样的 120 斤行李,还没到山脚就压垮了。这时领队 ZeRO 站出来,提出“分背”三阶段:先拆最重的药箱,再拆手账,最后连衣服也分开背。谁要用哪件,就向带它的人临时借,用完立刻还回去。到了 PyTorch 时代,FSDP 就是那个拿着对讲机、精确安排“谁何时借哪件衣服、用完放回哪个包”的智能领队。这篇文章就用这条登山主线,把 ZeRO 三阶段的显存公式、通信代价和 FSDP 配置一次讲透。

岗位关联

掌握 ZeRO 三阶段与 FSDP,是突破大模型显存瓶颈、从单卡走向多卡集群的必修课,直接决定你能否在有限硬件上训练百亿级模型。

目录
  1. 可视化演示
  2. 显存瓶颈探因
  3. 基准方案对比
  4. 阶段一:优化器状态分片
  5. 阶段二:梯度分片
  6. 阶段三:参数分片
  7. FSDP 工程实现
  8. 配置与实战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

通过训练显存占用构成、ZeRO三阶段渐进分片对比、通信代价与显存节省权衡三个模式,展示ZeRO优化如何通过分片优化器状态/梯度/参数大幅降低单卡显存占用。

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

训练显存占用四大构成 模型参数占20%每张卡完整持有前向反向计算必需 优化器状态占40%是显存大头Adam需存一阶二阶动量 Mem = Params(2P) + Grad(2P) + Optim(12P) + Act 优化器状态是显存消耗的主要来源

显存瓶颈探因

8 人登山队出发前清点装备:冲锋衣(参数)、旅行手账(梯度)、药箱和营养品(优化器状态)。每个人都想往 40 升的背包里塞下全套,结果拉链都拉不上。问题不是装备本身多到离谱——一套装备其实只有 120 斤——而是每个人都重复背负,8 份一模一样的 120 斤压在 8 个人身上,浪费得令人心疼。

训练深度模型时,GPU 显存里的情况如出一辙。混合精度训练下,围绕每个参数要维护三类“模型状态”:FP16 的模型参数、FP16 的梯度、以及 Adam 优化器为每个参数保存的 FP32 主权重、动量 m 和方差 v。前两类每参数各占 2 字节,第三类独占 12 字节。这就是“模型状态”的 16 字节总账。

一个 75 亿参数的 7.5B 模型,FP16“裸参数”只有 15GB,看起来不大;但加上梯度和 Adam 优化器全家桶后,模型状态膨胀到 120GB。这还没算前向计算产生的激活值、临时缓冲区和显存碎片。单卡 40GB 的 A100 根本装不下,这就是无数工程师卡在“模型太大”这道坎上的真正原因。

回到登山队:衣服 2 斤、手账 2 斤、药箱 12 斤——药箱竟是体量的 3/4!所以聪明的领队 ZeRO 第一眼就盯上了药箱,这也预示了优化的顺序。

模型状态=2Ψ⏟参数+2Ψ⏟梯度+12Ψ⏟优化器状态=16Ψ 字节\displaystyle \text{模型状态} = \underbrace{2\Psi}_{\text{参数}} + \underbrace{2\Psi}_{\text{梯度}} + \underbrace{12\Psi}_{\text{优化器状态}} = 16\Psi \ \text{字节}
Ψ 是模型参数量。每个参数平均要吃掉 16 字节,其中 12 字节属于优化器状态——药箱才是最重的那个包。
  1. 参数身位(2Ψ)混合精度训练保留一份 FP16 的“随身版”参数用于前向和反向,每个参数占 2 字节。
    2Ψ 字节2\Psi \ \text{字节}
  2. 梯度账本(2Ψ)反向传播得到的 FP16 梯度同样按每参数 2 字节记账,用于后续更新。
    2Ψ 字节2\Psi \ \text{字节}
  3. 优化器百宝箱(12Ψ)Adam 需要维护 FP32 主权重、动量 m、方差 v,各占 4Ψ,合计 12Ψ。
    4Ψ+4Ψ+4Ψ=12Ψ 字节4\Psi + 4\Psi + 4\Psi = 12\Psi \ \text{字节}
  4. 总账(16Ψ)三类相加,每个参数平均 16 字节,模型状态的固有体积就此锁定。
    16Ψ 字节16\Psi \ \text{字节}

基准方案对比

出发前没人敢轻信队友,于是决定各背各的:8 个背包,每包全套 120 斤。这是最省心也最浪费的方案,就像深度学习里最经典的 DDP(Distributed Data Parallel)——N 张 GPU 各持一份完整模型副本,前向各算各的数据,反向后通过 all-reduce 把梯度同步给所有人。

DDP 的单卡显存就是完整模型状态:16Ψ。用 7.5B 模型代入,120GB 的模型状态配上 40GB 的显存,结果只有一个——爆显存。更扎心的是这 120GB 在每张卡上都重复存了一份,8 卡就是 8×120GB,其中 7/8 是纯冗余。即便模型勉强塞进单卡,这种“人人全套”的背法也是巨大的浪费。

所有人都背着 120 斤还没走到营地就累垮了。队伍需要的不是更强的体力,而是一次彻底的重分配——这正是 ZeRO 诞生的起点。

MDDP=16Ψ\displaystyle M_{\text{DDP}} = 16\Psi
DDP 下每张卡都背负完整的 16Ψ,显存需求随模型规模线性膨胀,且 N 张卡之间完全重复。
  1. N 份全量数据并行下每张卡都保存一份完整模型状态,单卡占用即 16Ψ。
    MDDP=16ΨM_{\text{DDP}} = 16\Psi
  2. 代入算例7.5B 参数代入,16×7.5 = 120,单位是 GB。
    16×7.5 B=120 GB16 \times 7.5\text{ B} = 120\text{ GB}
  3. 超限对比单卡 A100 显存 40GB,120GB 远超上限,训练直接失败。
    120 GB≫40 GB120\text{ GB} \gg 40\text{ GB}

阶段一:优化器状态分片

领队 ZeRO 清点后大吃一惊:药箱占 12 斤,衣服和手账各才 2 斤!于是宣布方案一:把药箱拆成 8 份,每人只背 1.5 斤,到营地再互相补给;衣服和手账仍人手一套。行李从 120 斤骤降到 41 斤,几乎省掉了 2/3 的重量。

对应到技术里,这就是 ZeRO Stage1(优化器状态分片)。每张卡仍然保留完整的参数(前向/反向都要用)和完整的梯度(all-reduce 之后每卡都有全量),但只负责更新自己那 1/N 参数对应的优化器状态。用公式表达,单卡显存为参数 2Ψ、梯度 2Ψ 加优化器状态的 1/N,即 4Ψ + 12Ψ/N。

以 7.5B 模型 8 卡为例:30GB + 90GB/8 = 41.25GB,从 120GB 直落到 41.25GB。关键在于通信量纹丝不动——梯度同步还是 DDP 那个 all-reduce,路线没变,只是“药箱里的药”换了个存放位置。

方案一立竿见影,人均负重几乎砍半。但手账(梯度)每一页大家还在重复誊写——既然药箱能拆,手账为什么不能拆?

MStage1=4Ψ+12ΨN\displaystyle M_{\text{Stage1}} = 4\Psi + \frac{12\Psi}{N}
优化器状态是最大的开销(12Ψ),把它按 GPU 数均分后,单卡显存立刻从 16Ψ 降到 4Ψ+12Ψ/N,通信量却不增加。
  1. 全量保留参数 2Ψ 与梯度 2Ψ 每卡依旧完整,两者合计 4Ψ,是分片后单卡的“固定底数”。
    2Ψ+2Ψ=4Ψ2\Psi + 2\Psi = 4\Psi
  2. 拆分药箱优化器状态 12Ψ 由 N 张卡均分,每卡只留 12Ψ/N。
    12ΨN\frac{12\Psi}{N}
  3. 单卡公式固定底数加上分摊后的优化器状态,即 Stage1 的单卡占用。
    MStage1=4Ψ+12ΨNM_{\text{Stage1}} = 4\Psi + \frac{12\Psi}{N}

阶段二:梯度分片

第二阶段,领队宣布手账也拆着写:每人只负责其中 1/8 的章节,每天写完后用“分工汇总”的方式合订——这个过程叫 reduce-scatter。衣服依旧每人都有一套,因为任何时候都可能要穿。

这就是 ZeRO Stage2(梯度分片)。反向传播算出的梯度不再每卡保存全量,而是通过 reduce-scatter 在聚合的同时完成分摊:每卡只拿到自己负责那 1/N 的梯度切片,并用它更新自己手上的优化器状态。前向计算需要的参数依然每卡完整,所以“前向时的显存”与 Stage1 相同,省下的部分来自梯度的去重。

单卡公式变为 2Ψ + 14Ψ/N。7.5B 模型 8 卡:15GB + 105GB/8 = 28.125GB,比 Stage1 又降了约 1/3。通信量依然维持在 2Ψ——reduce-scatter 把“同步”和“分摊”合并成了一次集体行动,没有额外喊话。

药箱和手账都拆完了,人均从 120 斤降到 28 斤。但每个人仍抱着一麻袋衣服——那是参数,下一步连衣服都要拆。

MStage2=2Ψ+14ΨN\displaystyle M_{\text{Stage2}} = 2\Psi + \frac{14\Psi}{N}
梯度也按 N 均分后,单卡只剩参数 2Ψ 是固定的;优化器与梯度合计 14Ψ 被 N 张卡分摊。
  1. 只留参数参数 2Ψ 每卡完整保留,这是前向计算和反向“借参数”的基础。
    2Ψ2\Psi
  2. 梯度摊分梯度 2Ψ 被切成 N 片,每卡只留 2Ψ/N。
    2ΨN\frac{2\Psi}{N}
  3. 优化器摊分优化器状态维持 Stage1 的均分结果 12Ψ/N。
    12ΨN\frac{12\Psi}{N}
  4. 合并公式三者相加,参数独占 2Ψ,其余 14Ψ 全部摊进分母。
    MStage2=2Ψ+14ΨNM_{\text{Stage2}} = 2\Psi + \frac{14\Psi}{N}

阶段三:参数分片

终极方案来了:衣服也按 1/8 分给每个人。问题随之而来——今天要穿红羽绒服,带它的人却在队尾。于是队伍约定:谁需要哪件,就向持有者喊一嗓子借来穿(all-gather),穿完立刻还回去(reshard)。借还的喊话让通信变多了,但每个人背包只剩 15 斤。

这就是 ZeRO Stage3(参数分片):参数、梯度、优化器状态三类全部切分。前向计算某一层时,先 all-gather 取回该层完整参数,算完立刻释放;反向时再次取回该层参数计算梯度,再用 reduce-scatter 把梯度切片归并回去。每一层的“借—用—还”精确得像一场接力赛。

单卡公式简化为 16Ψ/N。7.5B 模型 8 卡:120GB/8 = 15GB——这是三个阶段中唯一能让 7.5B 模型塞进 24GB 消费级显卡的方案。代价明码标价:通信量从 DDP 的 2Ψ 涨到 3Ψ,整整 1.5 倍。这是一笔“用通信换显存”的生意。

三阶段全部完成:8 个人只带一套装备上山,谁要用什么就临时借。人均 15 斤,轻装上阵;但“借还”的喊话声此起彼伏——现场急需一个智能领队来打理。

MStage3=16ΨN\displaystyle M_{\text{Stage3}} = \frac{16\Psi}{N}
三类状态全部摊分,单卡显存降到 16Ψ/N。N 越大省得越多,但“借还参数”的通信成本也随之上升。
  1. 全部摊分参数、梯度、优化器状态全部按 N 均分,每卡只留总量的 1/N。
    MStage3=16ΨNM_{\text{Stage3}} = \frac{16\Psi}{N}
  2. 借衣服前向或反向需要第 l 层参数时,先 all-gather 借回完整副本 Ψ_l。
    all-gather: Ψl→本卡\text{all-gather: } \Psi_l \to \text{本卡}
  3. 还衣服计算完毕立即释放该副本,避免长期占用显存。
    用完即弃: Ψl→释放\text{用完即弃: } \Psi_l \to \text{释放}
  4. 代入算例7.5B 模型在 8 卡下,120GB/8 = 15GB。
    120 GB8=15 GB\frac{120\text{ GB}}{8} = 15\text{ GB}

FSDP 工程实现

靠人肉管理借还衣服,队伍早就乱套。FSDP(Fully Sharded Data Parallel)就是那个拿着对讲机、精确安排“谁何时借哪件、用完放回哪个包”的领队。它把 ZeRO Stage3 变成了一套开箱即用的自动化流程。

FSDP 的第一招是“参数扁平化”:把嵌套模型里所有参数拉直成一根巨大的长绳(flat parameter),按 GPU 数切成 N 段分发。训练时执行 unshard(取回当前段)→ 前向/反向 → reshard(释放),像领队按时按点地拆包、发衣服、收衣服。

第二招是“自动包裹策略”(auto_wrap_policy):领队不会让全队 120 斤衣服一次性摊开,而是按 Transformer 层或参数规模把模型切成一个个 FSDP 单元,每次只 unshard 正在计算的那一层。配合“懒初始化”(先登记元数据、再分配真实显存),峰值显存被压得更加平滑;若开启 CPUOffload,最重且不常用的“备用补给”还能寄存在 CPU 内存里,随时取用。

有了领队,8 人的“分背—借还”变成一套整齐的行程表:谁的衣服何时拆包、何时归位、要不要寄放 CPU,全部自动执行。接下来只差一件事——选哪套方案上路。

M峰值≈16ΨN+max⁡l (2Ψl+Al)\displaystyle M_{\text{峰值}} \approx \frac{16\Psi}{N} + \max_{l}\,\left(2\Psi_l + A_l\right)
FSDP 的显存峰值约等于“分片常驻的模型状态”加上“当前层临时借来的参数”和“该层激活值”。借来的衣服不算行李,但穿在身上那会儿确实占地方。
  1. 常驻行李分片后的模型状态每卡 16Ψ/N,是背包里固定的重量。
    16ΨN\frac{16\Psi}{N}
  2. 当前层借用处理第 l 层时,该层参数 2Ψ_l 被临时 all-gather 到本卡,用完即还。
    2Ψl2\Psi_l
  3. 临时激活该层前向产生的激活值 A_l 只存在于本卡,属于“剩余状态”,不被分摊。
    AlA_l
  4. 峰值合计峰值约等于常驻量加上所有层里最大的“借用+激活”,这正是 FSDP 按层管理的原因。
    max⁡l (2Ψl+Al)\max_{l}\,(2\Psi_l + A_l)

配置与实战

领队提供了四种负重方案:不改(NO_SHARD)、只拆药箱(SHARD_GRAD_OP)、全套拆光(FULL_SHARD)、以及“小组内拆光、小组间各背各的”(HYBRID_SHARD)。选哪套,取决于山路是宽是窄(网络带宽)和队员体质(GPU 显存)。

FSDP 的核心配置就落在三个旋钮上:sharding_strategy 控制分片策略,FULL_SHARD 对应 Stage3、SHARD_GRAD_OP 对应 Stage2、NO_SHARD 退化为 DDP、HYBRID_SHARD 在节点内 FULL_SHARD、跨节点保持整副本,适合机间带宽有限的多机训练;auto_wrap_policy 用 transformer_auto_wrap_policy 自动包裹每个 Transformer block;cpu_offload 开启后,优化器状态与参数可驻留 CPU 内存。

来一组 7.5B 模型 × 16 卡的实战账本:DDP 基线 120GB 直接出局;Stage1 为 30 + 90/16 = 35.6GB;Stage2 为 15 + 105/16 = 21.6GB;Stage3 直达 120/16 = 7.5GB,16GB 显卡也能从容训练。通信账同步算清:DDP 的 all-reduce 为 2Ψ,Stage3 因“借参数 + 还梯度”涨到 3Ψ。GPU 越多分得越散,通信占比越高,所以大集群常选 HYBRID_SHARD 折中。

选对方案,这支登山队才能在“显存”与“通信”两座山之间走出最优路线——而 FSDP 把这个选择权,装进了三行配置代码里。

VStage3=3ΨvsVDDP=2Ψ\displaystyle V_{\text{Stage3}} = 3\Psi \quad \text{vs} \quad V_{\text{DDP}} = 2\Psi
通信量的增长速度远低于显存的下降速度:显存从 16Ψ 降到 16Ψ/N(N 倍),通信只涨 1.5 倍——这笔账怎么算都划算。
  1. DDP 基线all-reduce 同步梯度,一次集体喊话的通信量约 2Ψ。
    VDDP=2ΨV_{\text{DDP}} = 2\Psi
  2. Stage3 三项前向借参数 all-gather 花 Ψ、反向再借一次花 Ψ、梯度 reduce-scatter 花 Ψ。
    VStage3=Ψ+Ψ+Ψ=3ΨV_{\text{Stage3}} = \Psi + \Psi + \Psi = 3\Psi
  3. 倍率3Ψ 除以 2Ψ,通信量是 DDP 的 1.5 倍。
    3Ψ2Ψ=1.5\frac{3\Psi}{2\Psi} = 1.5

AI 应用场景

千亿参数大模型预训练

阶段:模型训练与分布式执行

解决的问题:传统数据并行下,70B 模型需要 1.2TB 模型状态,8×80GB 的 A100 集群也塞不下;用 FSDP 的 FULL_SHARD 把参数、梯度、优化器状态全部摊到 16 张卡上,单卡负担降到 1/16,训练才能跑起来。

具体案例:某 LLM 团队预训练 13B 模型,用 transformer_auto_wrap_policy 按层包裹,配合激活检查点,在 16×A100 上把单卡显存峰值压到 40GB 以内,吞吐较 DDP 提升 3 倍。

单卡微调大模型

阶段:微调与推理部署

解决的问题:个人开发者只有一张 24GB 的 RTX 4090,7B 模型裸参数就要 14GB,加上梯度和优化器必然爆显存;FSDP 即使只有 1 张卡,也能通过 CPU 卸载把优化器状态“寄放”到内存,腾出显存跑微调。

具体案例:用 FSDP(model, cpu_offload=CPUOffload(offload_params=True)) 包装 7B 模型,配合 LoRA 只训练少量参数,在单卡上完成指令微调,batch size 开到 4 而不爆显存。

多机多卡集群训练平台

阶段:平台架构与资源调度

解决的问题:机间带宽(如 25Gbps RDMA)远低于机内 NVLink,FULL_SHARD 的全局 all-gather 会把跨机通信打满;HYBRID_SHARD 在节点内做完整分片、节点间保留整副本,把通信压回可控范围。

具体案例:某推荐系统团队用 HYBRID_SHARD 在 4 机 32 卡上训练千亿级 CTR 模型,单卡显存峰值下降 70%,有效吞吐提升 1.8 倍。

Python 演示

python62 行
import osimport torchimport torch.nn as nnimport torch.distributed as distfrom torch.distributed.fsdp import (    FullyShardedDataParallel as FSDP,    CPUOffload,    ShardingStrategy,)from torch.distributed.fsdp.wrap import size_based_auto_wrap_policy  def init_process_group():    os.environ.setdefault("MASTER_ADDR", "127.0.0.1")    os.environ.setdefault("MASTER_PORT", "29500")    os.environ.setdefault("WORLD_SIZE", "2")    os.environ.setdefault("RANK", "0")    dist.init_process_group(backend="gloo")  class ToyNet(nn.Module):    def __init__(self):        super(ToyNet, self).__init__()        self.layers = nn.Sequential(            nn.Linear(1024, 2048),            nn.ReLU(),            nn.Linear(2048, 2048),            nn.ReLU(),            nn.Linear(2048, 16),        )     def forward(self, x):        return self.layers(x)  def train():    init_process_group()    model = ToyNet()    wrap_policy = size_based_auto_wrap_policy(min_num_params=2_000_000)    model = FSDP(        model,        sharding_strategy=ShardingStrategy.FULL_SHARD,        auto_wrap_policy=wrap_policy,        cpu_offload=CPUOffload(offload_params=True),    )    optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)    loss_fn = nn.MSELoss()    for step in range(3):        data = torch.randn(32, 1024)        target = torch.randn(32, 16)        optimizer.zero_grad()        output = model(data)        loss = loss_fn(output, target)        loss.backward()        optimizer.step()        if dist.get_rank() == 0:            print("step=%d loss=%.4f" % (step, loss.item()))    dist.destroy_process_group()  if __name__ == "__main__":    train()
目的:演示 PyTorch FSDP 的最小训练闭环——用 FULL_SHARD 把玩具模型切分到多卡上,跑通“前向取参—反向还梯度—优化器只更新本片”的完整流程。
  1. from torch.distributed.fsdp import (引入 FSDP、CPU 卸载和分片策略组件,相当于领取领队的对讲机。
  2. model = FSDP(model,用 FSDP 包装模型,把普通模型改造成“分背版”,参数自动按卡切分。
  3. sharding_strategy=ShardingStrategy.FULL_SHARD,选择 ZeRO 阶段三:参数、梯度、优化器全部摊分。
  4. auto_wrap_policy=wrap_policy,超过 200 万参数的内层子模块会被单独包裹,实现按层取回,避免整模型一把梭。
  5. cpu_offload=CPUOffload(offload_params=True),允许把暂时不用的参数与优化器状态寄放到 CPU 内存,进一步腾空显存。
  6. loss.backward()反向时 FSDP 自动完成“借参数”和 reduce-scatter 梯度切片,代码层面零感知。
  7. optimizer.step()每张卡只更新自己手上那片优化器状态,与 Stage1 的“各管各的药箱”完全一致。

常见误区

误区很多人看到单卡显存从 16Ψ 降到 16Ψ/N,就以为 ZeRO 像量化一样把每个参数占的字节变小了,模型被压缩了。

正解ZeRO 没有改变任何数值精度,它做的是“存储布局重规划”。模型状态的固有体积仍然是 16Ψ,但不再让每张卡各存一份(DDP 的 N×16Ψ),而是全队只背一套(16Ψ)。如同 8 人不再各背全套 120 斤,改成共背一套 120 斤——装备没变小,只是不再重复携带。单卡从 16Ψ 降到 16Ψ/N,集群总存储也从 N×16Ψ 降到 16Ψ,降的是“冗余复制”而非“数据体积”。

为何常见:显存数字下降得过于直观,让人误以为是数据被压缩;“分摊”与“压缩”一字之差,本质却完全不同。

误区觉得 FSDP 也是数据并行,梯度同步该怎么做还怎么做,通信量应该和 DDP 差不多。

正解DDP 一次 all-reduce 的通信量约 2Ψ;FSDP 的 Stage3 除了梯度 reduce-scatter(Ψ),还要在前向和反向各做一次参数 all-gather(各 Ψ),合计约 3Ψ,是 DDP 的 1.5 倍。这多出来的 1 倍就是“借衣服”的喊话成本。正因为通信涨了,工程上才需要 auto_wrap_policy 按层取回,把通信打散到计算中隐藏。

为何常见:只看到“都是数据并行”的标签,没细算 all-gather 与 all-reduce 的次数差异。

误区以为参数、梯度、优化器都分片了,单卡显存只剩 16Ψ/N,长序列大模型就能随便跑了。

正解16Ψ/N 只覆盖“模型状态”;激活值(activation)、临时缓冲区和显存碎片仍由每卡独享,长序列训练时激活显存可能比模型状态更大。FSDP 的显存峰值公式里专门加了 max(2Ψ_l + A_l) 一项,正是提醒激活值这个“隐藏的重量”。实际训练仍需配合激活检查点(activation checkpointing)来压峰值。

为何常见:公式只算了模型状态,忽略了“剩余状态”里的激活开销,把分片当成了万能药。

误区既然单卡显存随 N 线性下降,那就不停加卡,显存省了速度肯定也上去了。

正解Stage3 的通信量固定在 3Ψ,不随 N 变化。N 增大后单卡计算量变小,通信占比迅速上升,最终被互连带宽卡死。所以大集群常用 HYBRID_SHARD:节点内 FULL_SHARD、节点间整副本,用局部冗余换全局通信的下降。显存优化是必要不充分条件,速度还要看计算/通信重叠做得好不好。

为何常见:把“显存装得下”直接等同于“训练跑得快”,忽略了分布式系统里通信才是常客。

面试题

Q1. ZeRO 的第一个阶段首先在 GPU 之间分片哪类状态?

单选题 · easy

  • A.模型参数分片
  • B.梯度信息分片
  • C.优化器状态分片← 正确答案
  • D.激活值压缩存储

答案:C。ZeRO 按“从重到轻”的顺序优化:先分片最重的优化器状态(12Ψ),再分梯度(2Ψ),最后分参数(2Ψ)。A 是阶段三、B 是阶段二的职责;激活值不在 ZeRO 的分片范围内。类比登山队先拆 12 斤的药箱,而不是 2 斤的衣服。

Q2. 使用 Adam 优化器时,一个参数对应的模型状态总字节数是?

单选题 · easy

  • A.大约12Ψ字节
  • B.大约14Ψ字节
  • C.大约16Ψ字节← 正确答案
  • D.大约20Ψ字节

答案:C。模型状态 = 参数 2Ψ + 梯度 2Ψ + 优化器状态 12Ψ = 16Ψ。12Ψ 来自 Adam 的 FP32 主权重、动量 m、方差 v 各 4Ψ。14Ψ 和 20Ψ 都是混淆了分片后单卡公式与总账公式的产物。

Q3. N 卡训练下,ZeRO 阶段二每卡的模型状态显存表达式是?

单选题 · mid

  • A.4Ψ+12Ψ/N
  • B.2Ψ+14Ψ/N← 正确答案
  • C.4Ψ+14Ψ/N
  • D.2Ψ+12Ψ/N

答案:B。阶段二只保留参数 2Ψ 每卡完整,梯度和优化器状态合计 2Ψ+12Ψ=14Ψ 被 N 分摊,故为 2Ψ+14Ψ/N。A 是阶段一表达式;C 和 D 分别混淆了参数是否完整、优化器是否分片。

Q4. ZeRO 阶段三在计算某一层前向时,需要先做什么?

单选题 · mid

  • A.AllGather 借回该层参数← 正确答案
  • B.ReduceScatter 同步该层梯度
  • C.Broadcast 广播该层参数
  • D.Alltoall 交换该层参数

答案:A。阶段三的参数分散在各卡,前向计算某层前必须 all-gather(借衣服)取回该层完整参数,用完即 reshard。ReduceScatter 是反向时的动作;Broadcast 和 Alltoall 都不是 FSDP 的取数方式。

Q5. 以下哪些属于 FSDP 的关键工程机制?

多选题 · mid

  • A.参数扁平化为一维张量← 正确答案
  • B.前向取回用完即弃← 正确答案
  • C.自动包裹策略按层切分← 正确答案
  • D.激活值跨卡动态共享

答案:ABC。FSDP 的三个核心机制正是参数扁平化、unshard/reshard 循环、auto_wrap_policy 按层包裹。激活值不属于模型状态,FSDP 不分片激活值,D 错误。

Q6. 使用 8 张 GPU 训练 7.5B 参数模型,ZeRO 阶段二的单卡模型状态约为?

单选题 · hard

  • A.约 15 GB
  • B.约 28 GB← 正确答案
  • C.约 41 GB
  • D.约 120 GB

答案:B。Stage2 单卡 = 2Ψ + 14Ψ/N = 2×7.5 + 14×7.5/8 = 15 + 13.125 ≈ 28GB。15GB 是 Stage3(16Ψ/N),41GB 是 Stage1(4Ψ+12Ψ/N),120GB 是 DDP 基线。

Q7. 相比 DDP 的通信量 2Ψ,ZeRO 阶段三通信量约为?

单选题 · hard

  • A.与 DDP 完全一致
  • B.约为一点五倍← 正确答案
  • C.约为两倍数量
  • D.约为三倍数量

答案:B。Stage3 通信 = 前向 all-gather Ψ + 反向 all-gather Ψ + 梯度 reduce-scatter Ψ = 3Ψ,是 DDP(2Ψ)的 1.5 倍。这正是“用通信换显存”的代价,也是 HYBRID_SHARD 存在的理由。

Q8. 下列哪些做法可以进一步压降 FSDP 训练的显存峰值?

多选题 · hard

  • A.开启 CPUOffload 卸载← 正确答案
  • B.使用激活检查点技术← 正确答案
  • C.按小粒度自动包裹分片← 正确答案
  • D.成倍增大训练批量大小

答案:ABC。CPUOffload 把优化器状态寄放 CPU,激活检查点省去大量激活值,小粒度自动包裹让“借用”更精细——三者都能降峰值。增大 batch size 只会在单位时间内累积更多激活值,反而推高峰值显存。

Q9. FSDP 的 FULL_SHARD 分片策略对应 ZeRO 的哪个阶段?

单选题 · easy

  • A.阶段零无任何分片
  • B.阶段一优化器分片
  • C.阶段二梯度分片
  • D.阶段三全状态分片← 正确答案

答案:D。FULL_SHARD 对三类模型状态全部切分,即 ZeRO 阶段三;SHARD_GRAD_OP 对应阶段二;NO_SHARD 等价于 DDP。命名里的 FULL 指“全状态分片”,不是“全参数”。

Q10. 关于 ZeRO 显存优化,下列说法正确的是?

多选题 · mid

  • A.总存储下降源于消除冗余← 正确答案
  • B.单卡显存负载明显下降← 正确答案
  • C.集群通信量略有增长← 正确答案
  • D.激活值也会被一并分片

答案:ABC。ZeRO 消除的是 DDP 里 N 份重复副本,集群总存储从 N×16Ψ 降到 16Ψ,单卡从 16Ψ 降到 16Ψ/N,代价是通信从 2Ψ 涨到 3Ψ。激活值不属于模型状态,ZeRO 不分片激活值,D 错误。