假设你住的小区要办一场三百人的百家宴。每家厨房都不大,谁也没本事把三十道菜的食材一次全塞进自家冰箱。于是居委会立下新规矩:每家只负责寄存一小部分食材——你管葱姜蒜,我管鸡鸭鱼,他管瓜果蔬菜。等到开席,大家把各自那部分端到长桌上,拼出完整的一道菜;这道菜用完,剩下的料立刻拆回各家冰箱,腾出桌面给下一道菜。这个“分着存、按需凑、用完散”的组织方式,就是今天的主角——FSDP(Fully Sharded Data Parallel,全分片数据并行)。它是 PyTorch 官方对 ZeRO-3 的实现,专门解决“模型太大、一块显卡装不下”的难题。本篇文章将带你认识参数如何被切开、前向反向如何自动聚合、混合精度如何配合,以及它与 DeepSpeed ZeRO 的异同,最后给出工程实践中的取舍建议。
掌握 FSDP,意味着你能在相同的 GPU 预算下训练更大的模型——这是大模型时代 AI 工程师最值钱的能力之一。
可视化演示
动态演示FSDP全分片数据并行的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FSDP全分片数据并行:FSDP FSDP的基本概念与ZeRO- 参数分片与前向反向自动聚合 核心机制可视化 FSDP全分片数据并行整体架构与关键组件
核心思想
想象小区要办一场三百人的百家宴,每家的厨房都只有巴掌大。如果沿用老办法——每家都独自准备一整桌菜,那么食材总量就得乘以户数,绝大多数人家连原料都堆不下。DDP(数据并行)正是这个老办法:每张 GPU 都复制一份完整模型。一个 7B 参数的模型,全精度参数就要 28GB,加上梯度和 Adam 优化器状态,总开销超过 100GB,任何单卡都装不下,这就是典型的“DDP 显存瓶颈”。
于是居委会想出新规矩:把整只鸡、整条鱼都拆开,每家只认领一小份食材存在自家冰箱里。N 户人家的冰箱加起来,正好装下全部三十道菜的原料。FSDP 的做法如出一辙:把模型参数均匀切成 N 份,每张卡只保存其中的 1/N,完成“参数全局分片”。
这样一来,每张卡需要常驻的参数显存就从 4Φ 降到了 4Φ/N。其中 Φ 是模型参数量(单位是“个”),4 是 FP32 格式每个参数占用的字节数,N 是参与训练的 GPU 数量。7B 模型用八张卡一摊,每卡只需背 3.5GB 的参数,显存压力瞬间卸掉大半。
食材是分出去了,可做菜时终归需要完整的鸡和鱼。怎样在开席的刹那把分散在各家的原料凑齐?这正是 FSDP 的第二招——分片机制。
- 参数量 Φ模型里所有权重和偏置的总个数。7B 模型就是 70 亿个参数。
- 字节数 4FP32 格式下每个参数占 4 字节内存。
- 均分 N 份把总字节数除以 GPU 数量,得到每卡需保存的部分。
分片机制
好厨师切菜讲究“顺着纹理下刀”,把一条鱼片成薄片要沿着鱼肉的肌理走,不能乱剁。FSDP 切参数也一样,不是把内存地址拦腰砍一刀,而是“按层切”:最小的分片单元通常是一个 Transformer Block(多头注意力层加前馈层),每一层内部的参数张量沿第一个维度均匀切成 N 片,各自存在对应卡上。
这样切有两个好处。第一,前向和反向都是逐层推进的,计算到某一层时只需临时凑齐“当前层”的参数,不必把整个模型的数据一次性搬到某张卡上;第二,未计算到的层继续保持分片状态,大大降低了峰值显存。但这套方案也有代价——每次凑齐一层都要做一次 all-gather,也就是让所有卡把自己的那一片广播给其他人。
就像百家宴上各家把鱼片端到长桌,拼成一条完整的鱼,桌面只占“一条鱼”的地方。分片粒度直接决定了这桌菜怎么摆、邻居之间要跑多少趟,这就是分片粒度与通信开销的权衡。
- 单层参数量 Φ_layer一个 Transformer Block 里的权重总个数,是分片和通信的基本单位。
- 缺的 N-1 份本卡只留了 1/N,需要从其余 N-1 张卡各取一份拼齐。
- 近似相等GPU 数 N 越大,(N-1)/N 越接近 1,通信量就越接近整层参数大小。
前反流程
百家宴的完整节奏是:开席前各家端菜上桌,吃完立刻撤桌腾地方,第二波菜再上来,散席后把剩菜打包分回各家。FSDP 的一次训练迭代也严格踩着这四拍:前向聚合参数 → 算完即释放 → 反向再聚参数 → 梯度归约回分。
前向计算第 i 层时,先执行 all-gather 把第 i 层的完整参数凑齐,随后计算这一层;该层前向一结束,完整参数副本立刻被丢弃,只保留本卡那 1/N 的分片。反向传播时同样需要再次聚合第 i 层参数——因为上一次的副本早已释放——求出该层梯度后执行 reduce-scatter:先把所有卡上的梯度求和归约,再按各家分片切回本卡存储,用于后续的优化器更新。
桌面永远只摆一道菜,其他食材都安心躺在各家冰箱——峰值显存被死死压在“单层参数加单层梯度”的水平,而不是整个模型。这套前反流程与内存生命周期,正是 FSDP 能在大模型训练中站稳脚跟的根本原因。
- 前向聚合 C_fwd每个 Transformer 层在前向计算前都要 all-gather 一次该层参数,总量约等于一个模型体积。
- 反向聚合 C_bwd反向传播时重新聚合同一层参数,因为前向后已被释放,又是一次约 Φ 的通信。
- 梯度回分 C_grad反向算完梯度后做 reduce-scatter,把梯度归约并按分片存回各卡,同样约 Φ 的通信量。
混合精度
好厨师不会把什么都塞进冰箱,也不会把什么都放在常温架上:贵的新鲜食材要冷藏,常用的大白菜放架子就好。FSDP 的后厨里也有两种食材:BF16/FP16 低精度副本负责前向和反向计算,省显存、跑得快;FP32 主权重负责优化器更新,保证数值精度不漂移。
主权重和 Adam 优化器状态(动量与方差)都以分片形式长期存储;低精度副本只在计算某一层时才临时凑齐,算完即弃。长期占用的是“分片后的主权重与优化器状态”,临时占用的是“单层低精度工作副本”。值得一提的是,BF16 的指数位宽与 FP32 相同,训练稳定性优于 FP16,这是它的精度收益所在。
冰箱放得下、架子也腾得开,加上分片存储让主权重和优化器状态不再奢侈,这便是百家宴后厨的从容。混合精度不是偷工减料,而是把“贵食材”和“常用菜”分门别类地安置。
- 主权重分片FP32 主权重每卡只存 1/N,长期驻留显存。
- 优化器状态分片Adam 的动量与方差各占 4Φ,同样全部均分到 N 张卡。
- 当前层工作副本BF16 每个参数占 2 字节,且只保留当前计算层,用完立即释放。
ZeRO对比
同一个百家宴想法,不同社区各有一本执行手册。DeepSpeed 的 ZeRO 把“分着存”分成三个阶段:ZeRO-1 只分片优化器状态,ZeRO-2 再分片梯度,ZeRO-3 连参数也全部按层分片。FSDP 本质上就是 PyTorch 官方实现的 ZeRO-3,两者算法高度一致。
两者的最大差异在“手感”。FSDP 使用 PyTorch 原生 API(torch.distributed.fsdp.FSDP),能直接与 torch 生态的 DCP 分布式检查点、torch.compile 等工具无缝衔接;DeepSpeed ZeRO 则提供了更细的调参旋钮,并附带推理引擎等周边能力。选型建议很直接:团队已在 PyTorch 生态就选 FSDP,深度绑定 DeepSpeed 基建就继续用 ZeRO。
菜谱可能不同,但“每家只做一部分菜”的精神是相通的。理解 ZeRO 三阶段的演进,就能明白 FSDP 在显存优化坐标系里的准确位置。
- 参数与梯度DDP 每卡各存 4Φ,分别是全精度参数副本和梯度缓冲。
- Adam 状态动量与方差两个状态合计 8Φ。
- 总数均摊DDP 单卡总计 16Φ;FSDP 把这三项全部除以 N。
工程实践
菜谱再完美,掌勺的手艺决定成败。FSDP 最基础的用法是给模型套一层 FSDP(model),但更专业的做法是用 auto_wrap_policy 让框架自动扫描模型结构,把每个 Transformer Block 识别为分片边界,这就是“自动包装策略”。
分片粒度是个典型的工程权衡:wrap 单元越大(比如整个模型只包一层),通信次数越少,但峰值显存越高;wrap 单元越小(每个子层都单独 wrap),显存越省,但通信往返越频繁。工程上默认按 Transformer Block 分片。为了隐藏通信延迟,还可以开启 forward_prefetch(前向预取下一层参数)和 backward_prefetch(反向预取),让聚合与计算重叠起来;保存模型时则使用 FSDP 自带的分布式检查点(DCP),避免保存出 N 份零散文件。
大火快炒还是小火慢炖,勺子的翻动节奏决定了这道“大模型硬菜”能否顺利出锅。掌握了自动包装、分片单元和预取这三件套,你就能在显存与带宽之间找到自己的平衡点。
- 当前层瞬态 4Φ_layerBF16 参数副本 2Φ_layer 加梯度缓冲 2Φ_layer,只活在计算当下。
- 持久分片 12Φ/NFP32 主权重 4Φ/N 加 Adam 状态 8Φ/N,长期以分片形式驻留。
适用场景
不是所有宴席都适合百家宴。三五好友小聚,自家厨房足够,何必大费周章。那么什么样的训练任务需要 FSDP?答案很直接:当模型总开销 16Φ 字节超过单卡显存时,就该考虑分片扩展了。
FSDP 的主场是“大模型预训练”与“全参数微调”:7B 乃至上百 B 的模型,单卡放不下,多卡摊薄后每卡负担可控。它同样适用于显存受限环境——比如实验室只有几张 24GB 的 RTX 4090,想把 7B 模型微调起来;参数分片配合激活检查点,能把峰值压进预算。在多机扩展方面,FSDP 跨节点的通信模式与单机一致,吞吐随卡数近似线性增长,是大规模训练的标准底座。
百人宴席办百家宴,十人小灶自己掌勺。先算清楚这道菜多大,再决定用哪套方案——这正是 FSDP 最朴素也最重要的应用准则。
- 总负担 16Φ参数、梯度、优化器状态的完整显存开销。
- 单卡显存 M_GPU一张 GPU 的可用显存字节数。
- 向上取整GPU 数量是整数,算出来有余数必须进位。
AI 应用场景
大模型预训练
阶段:模型训练 / 分布式训练显存优化
解决的问题:7B 模型按 DDP 需要 112GB 以上显存(参数 + 梯度 + Adam 状态),单张 A100-80G 直接 OOM。传统做法是换更大显存或牺牲 batch size,成本高且效率低。FSDP 将这 112GB 摊到 8 张卡上,每卡仅 14GB,为激活值、通信缓冲留出充足空间。
具体案例:某团队在 8×A100-80G 上用 PyTorch 2.x + transformers 从零预训练 7B LLM。核心代码只需一行 model = FSDP(model, auto_wrap_policy=transformer_auto_wrap_policy),配合 BF16 混合精度,吞吐从 DDP 的 0(OOM)提升到每卡约 1800 tokens/s。这就像百家宴把 8 户人家的小冰箱合起来,装下了原本需要超大厨房才能容纳的食材。
大模型全参微调
阶段:模型微调 / 迁移学习
解决的问题:微调 Qwen-14B 时单卡放不下全量参数与梯度,很多团队被迫降级到 LoRA,牺牲了全参微调的精度上限。FSDP 让 14B 模型在 4×A100-80G 上完成全参微调,参数、梯度、优化器状态全部按层分片,各卡只背 1/4。
具体案例:某算法团队在 4×A100-80G 上对 14B 模型做领域微调,开启 FSDP 后配合 activation checkpointing,batch size 从 1 提升到 8,收敛轮次减少近一半。这相当于百家宴上各家分工备菜,开席时再拼桌,既保证了“菜品”完整,又让每个灶台都忙得过来。
显存受限的科研环境
阶段:模型训练 / 资源受限场景
解决的问题:实验室只有 4×RTX 4090(24GB 显存),却要微调 7B 对话模型。DDP 会直接 OOM;FSDP 把 112GB 总开销分到 4 卡,每卡 28GB,再配合激活检查点把峰值压进 24GB,让科研实验得以在小机器上跑起来。
具体案例:一名研究生用 4×RTX 4090 微调 7B 模型做指令跟随实验,采用 FSDP + BF16 + 激活检查点,训练吞吐约 500 tokens/s。虽然不快,但足够支撑小规模消融实验的迭代。这就像小厨房里的百家宴——每家分一点,拼一拼,照样能做出一桌像样的菜。
Python 演示
"""MiniFSDP: 用 NumPy 模拟 FSDP 的分片、聚参、回分三步曲""" import numpy as np class MiniFSDP: """百家宴式参数管理: 每张卡只放参数的 1/world_size""" def __init__(self, world_size, layer_sizes): self.world_size = world_size self.layer_sizes = layer_sizes chunk_sizes = [s // world_size for s in layer_sizes] # 每家一个小冰箱, 存放各层参数的 1/world_size self.fridges = [[np.random.randn(c) * 0.01 for c in chunk_sizes] for _ in range(world_size)] def gather_layer(self, idx): """开席凑菜: 从所有卡凑齐第 idx 层完整参数""" return np.concatenate([self.fridges[g][idx] for g in range(self.world_size)]) def scatter_grad(self, idx, grad): """撤席分菜: 梯度按分片切回各卡, 并做一步 SGD""" pieces = np.array_split(grad, self.world_size) for g in range(self.world_size): self.fridges[g][idx] -= 0.01 * pieces[g] def peak_params(self): """单卡峰值: 本卡分片 + 当前层完整副本""" biggest = max(self.layer_sizes) return biggest // self.world_size + biggest def main(): layer_sizes = [4096, 4096, 4096, 11008, 4096] for w in (1, 2, 4, 8): model = MiniFSDP(world_size=w, layer_sizes=layer_sizes) full = model.gather_layer(0) # 前向: 凑齐第一层 grad = np.random.randn(full.size) # 反向: 算出梯度 model.scatter_grad(0, grad) # 回分: 梯度切回各家 print("world_size=%2d peak_params=%6d" % (w, model.peak_params())) if __name__ == "__main__": main()
chunk_sizes = [s // world_size for s in layer_sizes]把每一层参数均分成 world_size 块,对应“每家分到的食材份额”。self.fridges = [[np.random.randn(c) * 0.01 for c in chunk_sizes] ...]每张卡一个“小冰箱”,只存全部参数的 1/N。np.concatenate([self.fridges[g][idx] for g in range(self.world_size)])模拟 all-gather:从所有邻居冰箱拼出这一层的完整参数。pieces = np.array_split(grad, self.world_size)把梯度按分片切成 N 份,准备“回分”给各家。self.fridges[g][idx] -= 0.01 * pieces[g]每家只更新自己负责的那部分参数,完成一步 SGD。return biggest // self.world_size + biggest单卡峰值 = 本卡分片 + 当前整层副本;卡越多,分片部分越小。for w in (1, 2, 4, 8):对比不同卡数下的峰值参数量,直观验证“分片越多人均负担越小”。
常见误区
误区我以为 FSDP 就是 DDP 加了个省显存的开关,名字都叫 Data Parallel,训练出的指标也一样,只是内部把权重压缩了一下而已。
正解两者机制完全不同。DDP 每卡持有完整模型副本,每步用 all-reduce 同步完整梯度;FSDP 把参数、梯度、优化器状态全部按层分片,前向反向时临时 all-gather 聚合、算完释放、梯度再 reduce-scatter 回分,通信模式和内存生命周期都不一样。这就像“每家做一整桌菜”和“每家只存一把菜、开席再拼桌”,流程和盘子数量完全不同。
为何常见:两者都叫数据并行,损失曲线和精度指标常常完全一致,只看训练日志很难发现底层差异。
误区只要把模型包一层 FSDP(……),显存立刻省一大截,训练速度也不会受影响,纯赚不亏。
正解显存收益是真的,但通信开销也是真的:每步多出约 3Φ 的 all-gather / reduce-scatter 流量。要想把这笔账抹平,需要开启前向/反向预取、选择合适的 auto_wrap_policy,必要时配合激活检查点。省显存是用更多通信换来的,工程上必须主动“还债”。
为何常见:显存数字立竿见影,通信开销却藏在训练日志背后,被吞吐量稀释,初学者很容易只看见收益、看不见成本。
误区既然分片能省显存,那分得越细越好,最好每个参数都单独分片,一次省个痛快。
正解分片粒度是典型的权衡:单元越小,同一时刻需要聚合的参数越少,峰值显存越低;但每多一个分片单元就多一次聚合与释放的往返,通信次数随单元数线性上涨,颗粒过细会导致训练大幅变慢。工程共识是按 Transformer Block 分片,在显存与带宽之间取平衡。
为何常见:教科书只说“分片省显存”,没强调“聚合有成本”,直觉上的“少存一点 = 省一点”误导了初学者。
误区前向反向都用 BF16 计算,那 FP32 主权重纯属浪费,干脆也存成 2 字节,还能再省一半显存。
正解BF16 指数位与 FP32 相同、训练比 FP16 更稳,但它在很多中间结果上的精度仍不足。优化器更新时若直接用 BF16 数值累加,误差会不断累积甚至不收敛,因此必须保留 FP32 主权重用于更新。FSDP 的聪明之处在于:主权重和优化器状态本身也是分片的,单卡只需负担 12Φ/N,附带成本很小。
为何常见:看到前向计算用低精度,就误以为全程都是低精度,把“计算精度”和“存储精度”两件事混为一谈。
面试题
Q1. FSDP 的核心思想是什么?
- A.每张卡只存部分参数按需聚合← 正确答案
- B.每张卡保存一份完整模型副本
- C.用 CPU 内存缓存所有模型参数
- D.将模型量化到低精度来训练
答案:A。FSDP 的立身之本就是“分着存”:每个参数张量都切出 1/N 存在各卡,计算时再按需聚合。B 是 DDP 的做法,C 是 CPU offload,D 是量化压缩,都不是 FSDP 的核心机制。一句话记忆:全分片。
Q2. FSDP 前向计算某一层时需要什么操作?
- A.直接使用本卡全部模型参数
- B.先做 all-gather 再计算该层← 正确答案
- C.从 CPU 把整层参数拷回显存
- D.将梯度平均后复制到各张卡
答案:B。前向走到某一层时,本卡只握有该层参数的 1/N,必须先 all-gather 把 N 份拼成完整参数才能计算。C 是 CPU offload 的场景,D 说的是梯度同步,A 在 FSDP 中不存在——完整模型参数任何时刻都不常驻单卡。
Q3. FSDP 某层前向计算完成后,完整参数副本会怎样?
- A.永久保存便于下次使用
- B.立即释放以节省显存空间← 正确答案
- C.转变为梯度继续参与传播
- D.复制到其他卡上做备份
答案:B。FSDP 奉行“用完就撤桌”:该层前向算完,完整参数副本立即丢弃,只为下一步反向腾地方。B 正是这个设计,A 是 DDP 的保留方式,C、D 与内存管理无关。
Q4. FSDP 反向传播中梯度是如何同步的?
- A.全量梯度广播到所有 GPU 卡
- B.等价于 all-reduce 后再平均
- C.reduce-scatter 归约后按分片存← 正确答案
- D.每个 GPU 独立计算不通信
答案:C。反向的梯度同步走 reduce-scatter:先跨卡求和归约,再按原先分片切回各卡,一步完成“汇总 + 分片”。B 近似 DDP 的 all-reduce,A 是广播不是归约,D 完全错误——梯度必须同步。
Q5. 关于 FSDP 的说法正确的是?
- A.是 PyTorch 官方对 ZeRO-3 的实现← 正确答案
- B.每张卡只持有参数的 N 分之一← 正确答案
- C.不能与原生优化器混合使用
- D.完全不需要任何通信开销
答案:AB。FSDP 正是 PyTorch 官方对 ZeRO-3 的落地,每卡只持参数的 1/N(A、B 对)。它完全可以配合 torch 原生优化器与 DCP 使用(C 错);分片后仍有聚合与归约通信(D 错)。
Q6. FSDP 中 auto_wrap_policy 的作用是什么?
- A.自动修复损坏的模型梯度数据
- B.自动确定参数的保存与加载路径
- C.自动识别 Transformer 层作为分片单元← 正确答案
- D.自动调节学习率防止训练发散
答案:C。auto_wrap_policy 让框架自动扫描模型结构,把每个 Transformer Block 包成一个分片单元,从而在“省显存”和“少通信”之间取得平衡。它不是修梯度、存 checkpoint 或调学习率的工具。
Q7. 混合精度训练下 FSDP 中分片存储的内容包括?
- A.FP32 全精度主权重参数副本← 正确答案
- B.Adam 优化器的动量与方差← 正确答案
- C.当前层的完整参数量副本
- D.模型训练过程中的全部历史梯度
答案:AB。混合精度下长期分片保存的是 FP32 主权重与 Adam 的动量、方差(A、B 对);当前层完整参数只是临时聚合、用完即弃(C 错);历史梯度不会留存(D 错)。
Q8. 同一模型、同一批数据下,相比 DDP,FSDP 的通信量大致是多少?
- A.约一点五倍于 DDP 的通信量← 正确答案
- B.大约为 DDP 通信量的三倍
- C.恰好等于 DDP 的通信总量
- D.低于 DDP 因为梯度更小
答案:A。DDP 每步只有一次梯度 all-reduce,通信量约 2Φ;FSDP 要“前向聚一次、反向聚一次、梯度回分一次”,约 3Φ,故约为 DDP 的 1.5 倍。3 倍、相等、可忽略都不符合实际。
Q9. 以下哪种场景最适合用 FSDP?
- A.模型太大单卡显存放不下← 正确答案
- B.模型极小且数据量也很少
- C.单卡推理延迟要求不高
- D.只需单机且不使用 GPU
答案:A。FSDP 的价值在于用多卡分担单卡放不下的大模型。A 是它的主场;B、C、D 的场景小模型或无需多卡训练,杀鸡不用牛刀。
Q10. FSDP 相比 DeepSpeed ZeRO-3 的优势体现在哪些方面?
- A.贴合 PyTorch 原生编程习惯← 正确答案
- B.与 torch 生态工具天然兼容← 正确答案
- C.完全不需要理解分片的概念
- D.显存一定比 ZeRO-3 更省
答案:AB。FSDP 更贴合 PyTorch 原生习惯,与 torch 生态无缝衔接(A、B 对);使用任何分片方案都要理解分片概念(C 错);显存上它与 ZeRO-3 理论等价,不会天然更省(D 错)。