2. 基础知识

流水线并行(Pipeline Parallelism)是把一个大型神经网络按层切分成多个阶段(stage),每个阶段部署在不同的 GPU 上,前向计算像工厂流水线一样从第一个阶段流向最后一个阶段,反向梯度再逆流回传。它的核心价值在于:当单卡显存放不下整个模型时,用多卡按层分摊参数、激活和优化器状态,同时通过微批次让各阶段尽量保持忙碌,从而提升整体吞吐。

2.1 流水线并行的基本原理

假设模型有 8 层,切分为 4 个阶段,每阶段 2 层,分别放在 GPU0~GPU3 上。一个批次的数据先经过 GPU0 的两层,产生的中间激活通过点对点通信(P2P)发送给 GPU1,依次传递到 GPU3 完成前向;损失计算后,梯度从 GPU3 反向传回 GPU0。这个过程中,任意时刻只有一个阶段在计算,其余阶段处于等待,这就是最朴素的“朴素流水线”,其设备利用率极低。

为了量化效率,引入两个关键指标:气泡率(bubble ratio)表示流水线中空闲时间占比,吞吐量表示单位时间完成的样本数。气泡主要来自流水线的填充(warm-up)和排空(drain)阶段,阶段数越多、微批次越少,气泡越明显。

4阶段流水线:微批次填充与气泡示意 时间 → GPU0 GPU1 GPU2 GPU3 F0 F1 F2 F3 B3 B2 B1 B0 空闲 F0 F1 F2 F3 B3 B2 B1 B0 空闲 空闲 F0 F1 F2 F3 B3 B2 B1 B0 空闲 空闲 空闲 F0 F1 F2 F3 B3 B2 B1 前向 F 反向 B 气泡/空闲 F0~F3 为 4 个微批次;填充与排空阶段产生气泡

2.2 微批次(Micro-batch)概念

微批次是流水线并行的“燃料”。把一个全局批次(global batch)拆成若干个更小的微批次,让它们像流水线上的零件一样连续进入各阶段。例如全局批次为 32,拆成 8 个微批次,每个微批次 4 条样本。这样 GPU0 完成微批次 0 的前向后,立刻可以开始微批次 1 的前向,同时 GPU1 正在处理微批次 0,流水线被填满,气泡显著减少。

微批次数量(通常记为 gradient_accumulation_steps 或 micro_batches)越多,气泡占比越小,但每个微批次的通信开销和调度开销会累积。实践中需要在吞吐与显存之间折中:微批次太小会导致 P2P 通信频繁、GPU 利用率下降;微批次太大则显存吃紧、气泡变多。

气泡率近似公式: 气泡率 ≈ (P − 1) / (M + P − 1),其中 P 为流水线阶段数,M 为微批次数量。当 M 远大于 P 时,气泡率趋近于 0。

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 实现机制之上。