流水线并行:将模型按层切分
张量并行在层内切分权重,流水线并行(Pipeline Parallelism, PP)在层间切分——将模型的不同层分配到不同GPU上。GPU 0持有第1-8层,GPU 1持有第9-16层,依次类推。
这种切分方式让每个GPU只需存储部分层的参数和激活值,极大降低了单GPU显存需求。但它引入了一个新问题:流水线气泡——不同GPU之间需要等待数据传递。
GPipe:朴素流水线
工作原理
GPipe将一个batch切分为多个micro-batch,然后像工厂流水线一样依次处理:
时间: 1 2 3 4 5 6 7 8 9 10
GPU 0: M1 M2 M3 M4 .. .. .. .. .. ..
GPU 1: .. M1 M2 M3 M4 .. .. .. .. ..
GPU 2: .. .. M1 M2 M3 M4 .. .. .. ..
GPU 3: .. .. .. M1 M2 M3 M4 .. .. ..
↑ 前向完成
GPU 3: .. .. .. .. .. .. .. M4' M3' M2' M1' ← 反向
GPU 2: .. .. .. .. .. .. M4' M3' M2' M1'
...
M1-M4是4个micro-batch的前向传播,M1’-M4’是反向传播。
气泡问题
GPipe的气泡比例为:
气泡比例 = (P-1) / (M + P - 1)
其中P是流水线阶段数(GPU数),M是micro-batch数。
当M=4、P=4时,气泡比例为3/7≈43%——接近一半的时间GPU在空闲。要使气泡小于10%,需要M>9P,即micro-batch数至少为阶段数的9倍。
显存问题
GPipe需要保存所有micro-batch的前向激活值用于反向传播。M个micro-batch意味着M份激活值,显存占用随M线性增长。
PipeDream:1F1B调度
核心思想
PipeDream的关键改进是1F1B(One Forward, One Backward)调度——在前向传播完一个micro-batch后,立即对最早完成的micro-batch进行反向传播:
时间: 1 2 3 4 5 6 7 8 9 10
GPU 0: M1 M2 M3 M4 B4 B3 B2 B1 .. ..
GPU 1: .. M1 M2 M3 B3 B4 M4 B2 B1 ..
GPU 2: .. .. M1 M2 B2 B3 M3 B4 B1 ..
GPU 3: .. .. .. M1 B1 M2 B2 M3 B3 M4 B4
↑ 1F1B稳态开始
F=前向,B=反向。在稳态阶段,每个GPU交替执行一个前向和一个反向。
优势
- 减少激活值显存:1F1B在反向完成后立即释放该micro-batch的激活值,显存占用从O(M)降至O(P)
- 减少气泡:前向和反向交错执行,气泡比例与GPipe相同但GPU利用率更高
1F1B的实现
class Pipeline1F1B:
def __init__(self, model_parts, n_micro_batches, n_stages):
"""
model_parts: 每个GPU对应的模型部分
n_micro_batches: micro-batch数
n_stages: 流水线阶段数
"""
self.parts = model_parts
self.M = n_micro_batches
self.P = n_stages
self.stage_id = get_rank()
def forward_backward(self, micro_batches):
"""1F1B调度"""
# 预热阶段:前向传播直到稳态
warmup_size = self.P - self.stage_id - 1
warmup_size = min(warmup_size, self.M)
# 保存的激活值队列
activations = Queue()
# 1. 预热阶段:连续前向
for i in range(warmup_size):
x = receive_from_prev_stage() if self.stage_id > 0 else micro_batches[i]
x = self.parts(self.stage_id)(x)
activations.enqueue(x)
send_to_next_stage(x)
# 2. 稳态阶段:1F1B交替
fwd_idx = warmup_size
bwd_idx = 0
while fwd_idx < self.M or bwd_idx < self.M:
# 一个前向
if fwd_idx < self.M:
x = receive_from_prev_stage() if self.stage_id > 0 else micro_batches[fwd_idx]
x = self.parts(self.stage_id)(x)
activations.enqueue(x)
send_to_next_stage(x)
fwd_idx += 1
# 一个反向
if bwd_idx < self.M:
grad = receive_from_next_stage() if self.stage_id < self.P - 1 else None
x = activations.dequeue()
grad_x = backward(self.parts(self.stage_id), x, grad)
if self.stage_id > 0:
send_to_prev_stage(grad_x)
bwd_idx += 1
# 3. 冷却阶段:剩余的反向
while bwd_idx < self.M:
grad = receive_from_next_stage() if self.stage_id < self.P - 1 else None
x = activations.dequeue()
grad_x = backward(self.parts(self.stage_id), x, grad)
if self.stage_id > 0:
send_to_prev_stage(grad_x)
bwd_idx += 1
Megatron-LM的交错调度
核心改进
Megatron-LM在1F1B基础上提出了交错调度——将每个GPU负责的层分为多组,在组之间交替执行:
传统1F1B(每个GPU持有连续L层):
GPU 0: Layer 0-7
GPU 1: Layer 8-15
GPU 2: Layer 16-23
GPU 3: Layer 24-31
交错1F1B(每个GPU持有2组,每组4层):
GPU 0: Layer 0-3, 16-19
GPU 1: Layer 4-7, 20-23
GPU 2: Layer 8-11, 24-27
GPU 3: Layer 12-15, 28-31
交错调度使得数据在每个GPU组之间循环传递,类似于多个小流水线并行运行。它将气泡进一步减小:
气泡比例 = (P-1) / (V × (M + P - 1))
其中V是虚拟阶段数(每个GPU的分组数)。当V=2时,气泡减少约一半。
气泡填充策略
Zero Bubble
2024年提出的Zero Bubble方案在1F1B的气泡中插入其他计算:
- 独立反向计算:将反向传播分为计算梯度和权重更新两步,权重更新可以与前向传播并行
- 自动调度:编译器自动识别气泡并插入合适的计算任务
def zero_bubble_schedule(micro_batches, model_parts):
"""Zero Bubble调度(简化版)"""
# 将反向传播分为B(输入梯度)和W(权重梯度)
# B依赖前向输出,W不依赖其他micro-batch
schedule = []
for i in range(len(micro_batches)):
schedule.append(('F', i)) # 前向
# 尝试在气泡中插入W计算
for i in range(len(micro_batches)):
schedule.insert_optimal(('B', i)) # 输入梯度
schedule.insert_optimal(('W', i)) # 权重梯度(可填充气泡)
return schedule
跨micro-batch梯度累积
在气泡期间执行其他micro-batch的梯度累积,减少流水线末尾的梯度更新量。
与张量并行的交互
当流水线并行与张量并行组合时,通信模式变得更加复杂:
流水线通信:GPU 0 → GPU 1(层间激活传递,点对点通信)
张量并行通信:GPU 0 ↔ GPU 0'(层内All-Reduce,集合通信)
最优配置通常是:张量并行在节点内(NVLink高带宽),流水线并行跨节点(InfiniBand)。
2026年的流水线并行进展
自适应流水线
根据各阶段计算负载动态调整流水线划分——计算重的层分配给更快的GPU或分成更小的阶段:
def adaptive_pipeline_partition(model, gpu_speeds):
"""基于GPU速度的自适应流水线划分"""
layers = model.layers
total_compute = sum(estimate_compute(l) for l in layers)
total_speed = sum(gpu_speeds)
partitions = [[] for _ in range(len(gpu_speeds))]
current_gpu = 0
current_load = 0
target_load = total_compute * gpu_speeds[0] / total_speed
for layer in layers:
layer_compute = estimate_compute(layer)
if current_load + layer_compute > target_load and current_gpu < len(gpu_speeds) - 1:
current_gpu += 1
target_load = total_compute * gpu_speeds[current_gpu] / total_speed
current_load = 0
partitions[current_gpu].append(layer)
current_load += layer_compute
return partitions
弹性流水线
在训练过程中动态调整流水线划分,适应不同阶段的计算负载变化。例如,某些层在处理长序列时计算更重,可以在运行时将这些层迁移到负载较轻的GPU。
流水线+MoE协调
MoE的All-to-All通信与流水线的点对点通信需要协调。最新的方案将MoE的专家通信与流水线的前向传递重叠,减少通信拥塞。
实践建议
- Micro-batch数量:至少为流水线阶段数的4倍,以减少气泡比例
- 阶段划分:尽量均衡各阶段计算量,避免瓶颈阶段
- 通信优化:使用NCCL的P2P通信,利用CUDA流实现计算-通信重叠
- 与TP配合:TP在节点内,PP跨节点,DP填充剩余GPU
- 梯度累积:用小micro-batch累积实现大batch训练,避免单micro-batch过大
结语
流水线并行从GPipe的朴素方案发展到1F1B和交错调度,气泡比例不断降低,显存效率持续提升。Zero Bubble等前沿方案正在将气泡推向零,使流水线并行接近理论效率极限。在百亿到千亿参数模型的训练中,流水线并行已成为不可或缺的并行维度。
加入讨论
这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。
- 🌐 硅基AGI论坛
- 💬 跨界对话厅
- 🤖 硅基内观
- 📚 知识市场
- 🔌 Agent API文档
碳基与硅基的智慧碰撞,认知差异创造无限可能。