大模型参数规模持续膨胀,从几十亿到上千亿,单张GPU的显存早已无法承载完整模型,更不用说训练过程中还要保存梯度和优化器状态。FSDP(Fully Sharded Data Parallel)和流水线并行(Pipeline Parallelism)是解决这个问题的两条互补路径:前者切分显存占用,后者切分计算图。把两者组合起来,就是所谓3D并行的其中两个维度,也是当前训练百亿级以上模型时非常常见的工程选择。本文详细拆解这两种并行方式的原理、组合时的难点以及具体的实现方法。

一、FSDP与流水线并行各自解决什么问题
FSDP的核心思想来源于ZeRO Stage 3,即把模型参数、梯度和优化器状态沿数据并行维度切分,每张卡只保存完整参数的几分之一。在前向计算到某一层时,FSDP会通过all-gather临时聚合该层的完整参数,计算完成后立刻释放;反向传播时再次聚合参数,计算完梯度后用reduce-scatter把梯度切分回各个rank。这样单卡显存占用从3倍参数量级别降低到参数量除以并行度,使得在有限显存上训练大模型成为可能。
流水线并行走的是另一条路。它把模型按层切分成多个stage,每个stage放在不同的设备上,数据像流水线一样依次流过各个stage。单卡显存占用取决于所分配的层数,而不是整个模型,因此天然适合纵向扩展。但朴素的流水线存在严重的气泡问题:在填满和排空阶段,大部分设备处于空闲状态,利用率低。
两者的互补性很明显:FSDP解决的是“单层参数太大”的问题,流水线解决的是“层数太多、模型纵向太深”的问题。当模型既有很宽的层(如大隐藏维度的Transformer层)又有很深的堆叠结构时,单一策略都会碰到瓶颈,混合使用才能在显存和利用率之间取得平衡。
二、混合并行的拓扑设计与通信分析
组合两种并行首先要回答的问题是:设备分组怎么排。常见的做法是按流水线stage分组,每个stage内部再组建一个FSDP进程组。例如8台机器、每台8卡,共64卡,划分为4个stage,每个stage占用16卡,这16卡内部组成数据并行组。前向时,每个stage内部的16卡各自持有不同的micro-batch,通过all-gather聚合本stage的参数完成计算,然后把激活值通过点对点通信发送给下一个stage的第一张卡。
通信开销是混合并行最需要仔细权衡的地方。FSDP的通信量与参数量和数据并行度成正比,流水线的通信量主要是stage之间传递的激活值,与batch大小和隐藏维度成正比。一个重要经验是:数据并行通信尽量走机器内的高带宽NVLink,流水线通信可以容忍跨机网络,因为点对点传输激活值的数据量相对较小,且可以与计算重叠。
因此拓扑编排上有个实用技巧:把同一个FSDP组的卡放在同一台机器内或同一高速互联域内,把流水线维度暴露到跨机网络。PyTorch的device mesh机制正是为此设计,可以显式声明多维网格,让框架知道哪一维对应哪种并行,从而自动创建对应的进程组。
三、基于PyTorch的代码实现
PyTorch从2.x版本开始提供了对混合并行的原生支持,核心是DeviceMesh配合fully_shard API(即新的DTensor FSDP2接口)。下面给出一个典型的组合示例:4卡流水线、每stage内4卡FSDP,总共16卡。
import torch
import torch.distributed as dist
from torch.distributed.device_mesh import init_device_mesh
from torch.distributed.pipelining import pipeline, SplitPoint, PipelineStage
from torch.distributed.fsdp import fully_shard, CPUOffloadPolicy
# 初始化进程组和二维mesh:dp维在机器内,pp维跨机器
dist.init_process_group("nccl")
mesh = init_device_mesh("cuda", (4, 4), mesh_dim_names=("pp", "dp"))
pp_rank = mesh.get_local_rank("pp")
dp_group = mesh.get_group("dp")
# 构建一个8层Transformer,切分为4个stage,每个stage包含2层
def build_stage_layers():
layers = [TransformerLayer(dim=4096, heads=32) for _ in range(2)]
for layer in layers:
# 对每个子层应用FSDP,参数在本stage的dp组内切分
fully_shard(layer, mesh=dp_group)
return layers
model_layers = build_stage_layers()
stage = PipelineStage(
model_layers,
stage_index=pp_rank,
num_stages=4,
device=torch.device("cuda"),
)
# 使用micro-batch填充流水线,降低气泡比例
pipe = pipeline(
stage,
n_microbatches=8,
)
optimizer = torch.optim.AdamW(stage.parameters(), lr=1e-4)
这段代码的关键点有三个。第一,mesh的第一维是pp,第二维是dp,framework会据此在正确的通信域上执行对应的集合通信。第二,fully_shard是逐层调用的,建议对每个Transformer子模块单独shard,这样前向时参数聚合可以按层流水进行,避免一次性聚合整个stage的参数导致显存峰值过高。第三,n_microbatches决定了流水线调度的填充度,通常设为stage数的2到4倍,气泡率大约为(p-1)/(m+p-1),其中p是stage数、m是micro-batch数。
如果是较老版本的PyTorch,也可以手动组合:用torch.distributed.pipeline.sync.Pipe或第三方库如DeepSpeed、Megatron-LM。DeepSpeed里对应的是在配置文件中同时设置pipeline_stages和ZeRO-3,原理完全一致,只是stage划分和调度器的实现方式不同。
四、调优要点与常见坑
第一个坑是stage划分不均衡。如果某个stage包含的层数或参数量明显多于其他stage,整个流水线会被最慢的stage拖住。Transformer各层结构相同,均衡划分容易,但模型首尾的embedding层和输出头往往被忽略,建议把embedding显式分配给第一个stage,lm_head分配给最后一个stage,并单独调整各stage层数让显存和计算量尽量对齐。
第二个坑是显存峰值。即使FSDP已经切分了参数,聚合时的临时buffer和流水线缓存的激活值依然可观。缓解手段包括:开启activation checkpointing,用重计算换显存;对FSDP设置更细的reshard_after_forward策略;必要时用CPUOffloadPolicy把未使用的参数卸载到内存。注意activation checkpointing会增加约30%的计算量,需要通过实测决定开启范围。
第三个坑是调度顺序的选择。1F1B调度在填满后实现一次前向配一次反向,激活值缓存最少,是默认选择;GPipe式调度则等所有micro-batch前向完成再统一反向,实现简单但显存压力大。PyTorch的pipelining模块默认支持1F1B,如果显存极度紧张,还可以考虑interleaved 1F1B,让每个stage持有不连续的多个层块,进一步缩小气泡,代价是通信次数成倍增加。
最后是数值与正确性验证。混合并行下bug往往表现为loss缓慢发散或某些rank梯度不一致,建议先用极小模型和数据做单步前向反向对齐实验,将混合并行的结果与单卡结果比对梯度范数,确认无误后再放大规模。同时开启TORCH_DISTRIBUTED_DEBUG=DETAIL可以在早期发现进程组和集合通信的配置错误。
五、小结
FSDP与流水线并行的组合,本质上是在显存、通信和利用率三者之间做工程权衡:FSDP压平了每个stage内部的显存墙,流水线把深度模型拆解到多个设备域,micro-batch调度填补了流水线空隙。落地时抓住三个关键即可:拓扑上让FSDP通信留在高带宽域、划分上让各stage负载均衡、调优上用micro-batch数量和重计算策略控制气泡与显存峰值。掌握这些要点后,再叠加张量并行扩展到完整的3D并行,也只是在这个骨架上多加一个维度而已。
FSDPPipeline并行混合并行修改时间:2026-09-11 00:06:45