2. 基础知识
流水线并行(Pipeline Parallelism)是把一个大型神经网络按层切分成多个阶段(stage),每个阶段部署在不同的 GPU 上,前向计算像工厂流水线一样从第一个阶段流向最后一个阶段,反向梯度再逆流回传。它的核心价值在于:当单卡显存放不下整个模型时,用多卡按层分摊参数、激活和优化器状态,同时通过微批次让各阶段尽量保持忙碌,从而提升整体吞吐。
2.1 流水线并行的基本原理
假设模型有 8 层,切分为 4 个阶段,每阶段 2 层,分别放在 GPU0~GPU3 上。一个批次的数据先经过 GPU0 的两层,产生的中间激活通过点对点通信(P2P)发送给 GPU1,依次传递到 GPU3 完成前向;损失计算后,梯度从 GPU3 反向传回 GPU0。这个过程中,任意时刻只有一个阶段在计算,其余阶段处于等待,这就是最朴素的“朴素流水线”,其设备利用率极低。
为了量化效率,引入两个关键指标:气泡率(bubble ratio)表示流水线中空闲时间占比,吞吐量表示单位时间完成的样本数。气泡主要来自流水线的填充(warm-up)和排空(drain)阶段,阶段数越多、微批次越少,气泡越明显。
2.2 微批次(Micro-batch)概念
微批次是流水线并行的“燃料”。把一个全局批次(global batch)拆成若干个更小的微批次,让它们像流水线上的零件一样连续进入各阶段。例如全局批次为 32,拆成 8 个微批次,每个微批次 4 条样本。这样 GPU0 完成微批次 0 的前向后,立刻可以开始微批次 1 的前向,同时 GPU1 正在处理微批次 0,流水线被填满,气泡显著减少。
微批次数量(通常记为 gradient_accumulation_steps 或 micro_batches)越多,气泡占比越小,但每个微批次的通信开销和调度开销会累积。实践中需要在吞吐与显存之间折中:微批次太小会导致 P2P 通信频繁、GPU 利用率下降;微批次太大则显存吃紧、气泡变多。
2.3 同步与异步流水线
同步流水线(Synchronous Pipeline)要求每个微批次的前向和反向严格按顺序推进,所有阶段在每个时钟步内处理同一个微批次,参数更新在整批梯度累积完成后统一进行。它的优点是语义清晰、与数据并行容易组合、收敛稳定;缺点是填充和排空阶段存在不可避免的气泡。
异步流水线(Asynchronous Pipeline)允许各阶段不等待,前向一完成就立即开始下一个微批次,甚至允许参数在不同阶段间存在版本差异。它几乎消除了气泡,但引入了“陈旧梯度”(stale gradient)问题:反向传播时使用的参数可能已经不是前向时的版本,导致收敛不稳定,需要额外的同步或补偿机制。
| 对比维度 | 同步流水线 | 异步流水线 |
|---|---|---|
| 气泡 | 存在填充/排空气泡 | 几乎无气泡 |
| 参数一致性 | 严格一致 | 可能存在版本滞后 |
| 收敛稳定性 | 高 | 较低,需调参 |
| 实现复杂度 | 低 | 高 |
| 典型应用 | DeepSpeed Pipe、Megatron-LM | PipeDream、部分研究型框架 |
2.4 DeepSpeed 中的流水线并行实现
DeepSpeed 的流水线并行由 PipelineModule 和 DeepSpeedEngine 协同完成。用户需要把模型按层切分,并用 LayerSpec 描述每一层的构建方式,然后交给 PipelineModule 包装。DeepSpeed 会自动插入 P2P 通信、管理微批次的调度,并与 ZeRO 优化器状态分片协同工作。
import deepspeed
from deepspeed.pipe import PipelineModule, LayerSpec
class Block(nn.Module):
def __init__(self, dim):
super().__init__()
self.linear = nn.Linear(dim, dim)
def forward(self, x):
return self.linear(x)
layers = [LayerSpec(Block, dim=1024) for _ in range(8)]
model = PipelineModule(
layers=layers,
num_stages=4, # 4 个流水线阶段
loss_fn=nn.CrossEntropyLoss(),
partition_method='uniform',
)
ds_config = {
"train_batch_size": 32,
"train_micro_batch_size_per_gpu": 4, # 每卡微批次大小
"gradient_accumulation_steps": 8, # 微批次数量
"zero_optimization": {"stage": 1},
"fp16": {"enabled": True},
}
engine, _, _, _ = deepspeed.initialize(
model=model, model_parameters=model.parameters(), config=ds_config
)
关键参数说明:num_stages 决定流水线深度;train_micro_batch_size_per_gpu 与 gradient_accumulation_steps 共同决定全局批次;partition_method 支持 uniform(均匀切分)和 parameters(按参数量均衡切分)。当各层参数量差异较大时,均匀切分会导致阶段负载不均,此时应使用按参数量切分,或结合后续章节的负载均衡策略。
gradient_accumulation_steps ≥ 4 × num_stages,可把气泡率压到 20% 以下;再根据显存余量逐步增大微批次大小,直到接近 OOM 边界。
2.5 常见误区与复盘
- 误区一:微批次越多越好。 微批次过多会放大 P2P 通信次数和调度开销,实际吞吐可能不升反降,应结合带宽和阶段数做实测。
- 误区二:忽略阶段负载差异。 若 GPU0 分到 6 层、GPU3 只分到 2 层,慢阶段会成为瓶颈,其他阶段频繁等待,气泡率远高于公式估算。
- 误区三:把流水线并行当数据并行用。 流水线并行按层切分,数据并行按样本切分,两者可组合,但通信模式和显存分布完全不同,配置时需分别设置。
- 复盘要点: 观察每卡利用率、P2P 通信耗时、微批次耗时分布,判断瓶颈在计算、通信还是负载不均,再决定调整阶段划分还是微批次数量。
理解以上基础后,才能进一步讨论流水线负载均衡与动态调度:负载均衡解决“阶段划分是否公平”,动态调度解决“运行时如何让各阶段尽量不空闲”。这两者都建立在本章的同步流水线、微批次和 DeepSpeed 实现机制之上。