概述
Pipeline Parallelism(PP) 是一种将模型按层(layer)切分的并行策略。整个模型被纵向划分成 p 个 pipeline stage,每个 GPU(或一组 GPU)仅持有其中一段连续的 Transformer 层。Stage 之间通过 P2P 通信传递激活值(前向)和梯度(反向)。
PP 的核心挑战在于:如果简单串行执行——所有 stage 先做完 forward 再做 backward——后续 stage 等前面 stage 完成时会大量空闲,形成"bubble"(气泡)。Megatron-LM 通过精心设计的调度算法来最小化 bubble 时间。
| Stage 0 GPU 0 |
→ | Stage 1 GPU 1 |
→ | Stage 2 GPU 2 |
→ | Stage 3 GPU 3 |
|
|---|---|---|---|---|---|---|---|
| 模型层 |
Embedding Layer 0–5 |
激活值 send/recv |
Layer 6–11 | 激活值 send/recv |
Layer 12–17 | 激活值 send/recv |
Layer 18–23 Output Layer |
| 职责 | 读入数据 first stage |
中间计算 | 中间计算 | 计算 loss last stage |
反向传播时方向相反:last stage 率先计算梯度,并将梯度(input_tensor_grad)通过 P2P 向前传递,直到 first stage。
| 术语 | 含义 |
|---|---|
| pipeline stage | 一个物理 GPU(或 TP 组)所持有的连续模型层,也称"PP stage" |
| micro-batch (m) | 将一个 global batch 切成 m 份,流水线的最小调度单元 |
| bubble | 流水线中 GPU 因等待数据而空闲的时间段,直接浪费算力 |
| warmup | 1F1B 开始前的纯前向阶段,各 stage 依次拿到第一批激活值 |
| cooldown | 1F1B 结束后的纯反向阶段,清空所有积压的反向传播 |
| virtual stage (VPP) | 每个物理 stage 持有多段不连续层,减小 bubble ratio |
朴素流水线 vs 1F1B 调度
朴素流水线的 bubble 问题
最简单的调度策略是:先让 m 个 micro-batch 全部完成前向,再从 last stage 开始反向。Stage 0 的时间线如下:
| 时间 → | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Stage 0 (first) | F0 | F1 | F2 | F3 | · | · | · | · | · | · | B0 | B1 | B2 | B3 | ← bubble 6步 |
| Stage 1 | F0 | F1 | F2 | F3 | · | · | · | · | B0 | B1 | B2 | B3 | ← bubble 4步 | ||
| Stage 2 | F0 | F1 | F2 | F3 | · | · | B0 | B1 | B2 | B3 | ← bubble 2步 | ||||
| Stage 3 (last) | F0 | F1 | F2 | F3 | B0 | B1 | B2 | B3 | ← bubble 0步 |
p=4, m=4 · Stage 0 bubble = 2(p−1) = 6 步 · 气泡率 \(\dfrac{p-1}{m+p-1} = \dfrac{3}{7} \approx 43\%\)
bubble ratio(气泡占比)为 \(\dfrac{p-1}{m+p-1}\)。当 pipeline 深度 p 较大或 m 较小时,气泡严重。
1F1B 调度:三阶段结构
1F1B(One Forward One Backward) 通过在 warmup 后交替执行 F 和 B 来消除朴素流水线的中段空等:
① Warmup 阶段
每个 stage s 连续执行 p - s - 1 个 micro-batch 的前向,将激活值缓存起来。Stage 0 需要 warmup p-1 个 micro-batch;last stage 不需要 warmup,直接进入 1F1B。
② Steady State(1F1B 稳态)
每个 stage 每轮做一次前向(F)接一次反向(B)。所有 GPU 始终处于忙碌状态,bubble 集中在最前和最后。
③ Cooldown 阶段
处理 warmup 阶段积压的 micro-batch 的反向传播(纯 backward,不再有新 forward)。Stage 0 需要 cooldown p-1 个 micro-batch。
标准 1F1B 调度(PP=4, microbatches=8)
上半:标准 1F1B — 粉色为 Forward,黄色为 Backward,空白为 bubble。
同一 microbatch 的 Backward 在各 Stage 上时间错开,梯度逐级回传。下半:Seq1F1B(论文提出的改进方法)。
图源:Seq1F1B, arXiv:2406.03488
- 颜色 = 操作类型:粉色 = Forward,黄色 = Backward,空白 = bubble
- 数字 = microbatch ID:同一个 microbatch 先在 Stage 1 前向,再逐级传到 Stage 4
- Warmup 阶段:Stage 1 需连续做 p−1 个 Forward 才能填满 pipeline
- 稳态(1F1B):每个 Stage 交替执行 1 次 Forward + 1 次 Backward,无空闲
- Cooldown 阶段:Stage 1 最后需连续做 p−1 个 Backward 排空积压
1F1B 的 bubble ratio 改善为 \(\dfrac{p-1}{m}\)。当 m 足够大(通常要求 \(m \geq p\))时,bubble 可以忽略不计。
在 forward_backward_pipelining_without_interleaving 中(schedules.py:2230):
num_warmup_microbatches = pp_group.size() - pp_group.rank() - 1,这正是 \(p - s - 1\)。warmup 循环结束后进入 for i in range(num_microbatches_remaining) 的 1F1B 主循环。
Interleaved 1F1B(VPP)
标准 1F1B 在 p 较大时 bubble ratio \(\frac{p-1}{m}\) 仍然可观。Interleaved 1F1B(又称 VPP,Virtual Pipeline Parallelism)通过引入"virtual stage"进一步压缩 bubble。
虚拟 stage 的层分配
设物理 stage 数 p=4,virtual stage 数 v=2,则总共有 p×v = 8 个 virtual stage。每个物理 GPU 持有 v 段不连续的模型层(model chunk):
| GPU 0 (stage 0) | GPU 1 (stage 1) | GPU 2 (stage 2) | GPU 3 (stage 3) | |
|---|---|---|---|---|
| Chunk 0 virtual 0–3 |
Layer 0–2 | Layer 3–5 | Layer 6–8 | Layer 9–11 |
| Chunk 1 virtual 4–7 |
Layer 12–14 | Layer 15–17 | Layer 18–20 | Layer 21–23 |
| 调度策略 | Bubble Ratio | P2P 通信量 | 显存峰值(激活值) |
|---|---|---|---|
| 朴素流水线 | \(\dfrac{p-1}{m+p-1}\) | 1× | 低(一次只缓存少量) |
| 1F1B | \(\dfrac{p-1}{m}\) | 1× | warmup 期缓存 \(p{-}s{-}1\) 个激活 |
| Interleaved 1F1B (VPP) | \(\dfrac{p-1}{m \cdot v}\) | v× | 缓存量约为标准 1F1B 的 v 倍 |
1F1B-I / VPP 交错调度(PP=4, V=2, 每 GPU 持有 2 个 virtual stage)
上半:1F1B-I(即 Megatron 的 VPP)— 粉色/蓝色区分 Stage 1 和 Stage 2 的 Forward,
黄色/紫色区分 Backward。每个 Device 在两个 virtual stage 间交替执行,bubble 显著减少。
下半:Seq1F1B-I(论文提出的改进方法)。
图源:Seq1F1B, arXiv:2406.03488
- 颜色 = virtual stage:粉色 = Chunk 0(前半层),蓝色 = Chunk 1(后半层)
- 数字 = microbatch ID:同一 microbatch 先经过 Chunk 0 再经过 Chunk 1
- Warmup 更长但更细粒度:Device 1 需要更多 warmup forward,但每次只算部分层
- Bubble 更小:对比两张图空白区域,1F1B-I 的 bubble 明显少于标准 1F1B
- 代价:virtual stage 切换时额外的 P2P 通信(通信量 ×v 倍)
VPP 的代价是 P2P 通信量乘以 v 倍。每次 virtual stage 的边界都需要一次 send/recv,v=2 时通信量翻倍。因此需要确保 P2P 通信带宽足够,或者使用 overlap_p2p_comm=True 将通信与计算重叠。
配置 VPP 只需设置 virtual_pipeline_model_parallel_size=v(即 --num-layers-per-virtual-pipeline-stage),Megatron 会自动切换到 forward_backward_pipelining_with_interleaving。
VPP 模型构建:完整调用链追踪源码追踪
以 8层 Transformer、2块 GPU、vp_size=2 为例,从 python pretrain_gpt.py 那一行开始,完整追踪每一次函数调用、每次传参、每次返回值。
--num-layers 8 --pipeline-model-parallel-size 2 # pp_size = 2 --num-layers-per-virtual-pipeline-stage 2 # vp_size = 2,即每个 GPU 持有 2 个 chunk
调用链总览
pretrain_gpt.py __main__
pretrain(model_provider=partial(model_provider_fn, gpt_builder), ...) ← A
setup_model_and_optimizer(model_provider_func=partial(...), ...) ← B
get_model(model_provider_func=partial(...), ...) ← C
for vp_stage in [0, 1]: ← VPP 循环,每次建一个 chunk
model_provider_func(pre_process=?, post_process=?, vp_stage=?) ← D
model_provider_fn(gpt_builder, pre_process, post_process, vp_stage) ← E
gpt_builder(args, pre_process, post_process, vp_stage) ← F
GPTModel(config, ..., pre_process, post_process, vp_stage) ← G
TransformerBlock(config, ..., vp_stage) ← H
get_num_layers_to_build(config, vp_stage) → 2 ← I
_build_layers() ← J
get_transformer_layer_offset(config, vp_stage, pp_rank) → offset
for n in [1, 2]: global_n = n + offset
TransformerLayer(config, layer_number=global_n)
← 返回 GPTModel 对象,append 进 model list
← 返回 model = [GPTModel(vp=0), GPTModel(vp=1)]
A — pretrain_gpt.py:__main__ 把什么传进去
model_provider 参数不是一个类,而是用 partial 打包出来的函数对象。调用它时会自动把 gpt_builder 作为第一个参数带上:
from model_provider import model_provider as model_provider_fn # E 步的函数
pretrain(
train_valid_test_datasets_provider,
partial(model_provider_fn, gpt_builder), # ← 打包成 model_provider_func
... # 以后调用它只需传 pre/post_process, vp_stage
)
B/C — pretrain → setup_model_and_optimizer → get_model:原封不动地传下去
这两层只是透传,没有对 model_provider_func 做任何修改:
model, optimizer, opt_param_scheduler = setup_model_and_optimizer(
model_provider, # 就是 partial(model_provider_fn, gpt_builder)
...
)
model = get_model(model_provider_func, model_type)
# model_provider_func 还是 partial(model_provider_fn, gpt_builder)
D — get_model:VPP 路径,循环调用 model_provider_func
这里是 PP 和 VPP 的分叉点。VPP 路径会循环 vp_size 次,每次把当前 vp_stage 对应的 pre_process/post_process 算出来,再调用 model_provider_func:
def build_model():
if pp_world_size > 1 and args.virtual_pipeline_model_parallel_size is not None:
# VPP 路径
model = []
for i in range(vp_size): # i = 0, 1
# 问:这个 chunk 需要 Embedding 吗?需要 Output Layer 吗?
pre_process = mpu.is_pipeline_first_stage(ignore_virtual=False, vp_stage=i)
post_process = mpu.is_pipeline_last_stage(ignore_virtual=False, vp_stage=i)
# ↑ 两个函数的返回值见下面
chunk = model_provider_func(pre_process=pre_process,
post_process=post_process,
vp_stage=i)
# 调用 partial(model_provider_fn, gpt_builder)(pre_process, post_process, vp_stage=i)
# 等价于 model_provider_fn(gpt_builder, pre_process, post_process, vp_stage=i)
model.append(chunk)
else:
# 普通 PP 路径:只建一个模型对象
pre_process = mpu.is_pipeline_first_stage()
post_process = mpu.is_pipeline_last_stage()
model = model_provider_func(pre_process=pre_process, post_process=post_process)
return model
is_pipeline_first_stage(ignore_virtual=False, vp_stage=i) 的逻辑(parallel_state.py:1715):
- 如果
vp_stage != 0,直接返回False(不是第一个 virtual chunk,不可能有 Embedding) - 否则返回
pp_rank == 0(还要是物理流水线的第一个 GPU)
两块 GPU 循环两次的结果:
| vp_stage=0 (i=0) | vp_stage=1 (i=1) | |
|---|---|---|
| GPU 0(pp=0) | pre=True, post=False | pre=False, post=False |
| GPU 1(pp=1) | pre=False, post=False | pre=False, post=True |
E — model_provider_fn:把 gpt_builder 当参数再调一层
model_provider.py 里的 model_provider 函数签名是 (model_builder, pre_process, post_process, vp_stage),它只是把参数原封转给 gpt_builder:
def model_provider(model_builder, pre_process=True, post_process=True, vp_stage=None):
# model_builder = gpt_builder(由 partial 在 A 步绑定好的)
# pre_process / post_process / vp_stage 由 D 步的循环传入
args = get_args()
# ... 一些内存追踪、ModelOpt 判断 ...
return model_builder(args, pre_process, post_process, vp_stage)
# 调用 gpt_builder(args, pre_process, post_process, vp_stage)
F — gpt_builder:把 args 转成 config,然后 new 一个 GPTModel
def gpt_builder(args, pre_process, post_process, vp_stage=None, config=None):
if config is None:
config = core_transformer_config_from_args(args)
# config.num_layers = 8
# config.pipeline_model_parallel_size = 2
# config.virtual_pipeline_model_parallel_size = 2
transformer_layer_spec = get_gpt_layer_local_spec(...)
# transformer_layer_spec 描述每一层用什么模块实现(Attention/MLP 用哪个类)
return GPTModel(
config=config,
transformer_layer_spec=transformer_layer_spec,
vocab_size=args.padded_vocab_size,
pre_process=pre_process, # ← 从 D 步传来
post_process=post_process, # ← 从 D 步传来
vp_stage=vp_stage, # ← 从 D 步传来(0 或 1)
)
G — GPTModel.__init__:按标志决定建哪些组件
def __init__(self, config, transformer_layer_spec, vocab_size,
pre_process=True, post_process=True, vp_stage=None, ...):
self.pre_process = pre_process
self.post_process = post_process
self.vp_stage = vp_stage
# ── 只有 pre_process=True 的 chunk 才建 Embedding ──
if self.pre_process:
self.embedding = Embedding(vocab_size=vocab_size, hidden_size=config.hidden_size, ...)
# 仅 GPU0/chunk0 有这个属性
# ── 每个 chunk 都建 TransformerBlock,但层数和全局编号不同(见 H)──
self.decoder = TransformerBlock(
config=config,
spec=transformer_layer_spec,
pre_process=pre_process,
post_process=post_process,
vp_stage=vp_stage, # ← 传下去,决定层偏移
)
# ── 只有 post_process=True 的 chunk 才建 Output Layer ──
if self.post_process:
self.output_layer = Linear(config.hidden_size, vocab_size, bias=False)
# 仅 GPU1/chunk1 有这个属性
H/I/J — TransformerBlock:算层数、算偏移、建层
TransformerBlock.__init__ 先调 get_num_layers_to_build() 知道建几层,再调 _build_layers() 在里面用 get_transformer_layer_offset() 算出每层的全局编号:
class TransformerBlock(MegatronModule):
def __init__(self, config, spec, pre_process, post_process, vp_stage=None, ...):
self.vp_stage = vp_stage
# I: 这个 chunk 要建几层?
self.submodules = _get_block_submodules(config, spec, vp_stage, pp_rank)
# _get_block_submodules 内部调 get_num_layers_to_build(config, vp_stage, pp_rank)
# → 8 // 2 // 2 = 2 层(所有 chunk 都一样)
self._build_layers() # J
self.num_layers_per_pipeline_rank = len(self.layers) # = 2
def _build_layers(self):
def build_layer(layer_spec, layer_number): # layer_number = 1 或 2(本地编号)
# J: 算全局编号
global_layer_number = layer_number + get_transformer_layer_offset(
self.config, self.vp_stage, pp_rank
)
# get_transformer_layer_offset 公式:
# offset = vp_stage * (num_layers // vp_size) + pp_rank * (num_layers // pp_size // vp_size)
# = vp_stage * 4 + pp_rank * 2
return build_module(layer_spec, config=config, layer_number=layer_number, ...)
# TransformerLayer 内部会用 global_layer_number 初始化 RoPE 等
self.layers = torch.nn.ModuleList([
build_layer(spec, i + 1) # i+1 = 1, 2
for i, spec in enumerate(self.submodules.layer_specs)
])
四个 chunk 的 offset 计算结果(offset = vp_stage×4 + pp_rank×2):
| vp_stage=0 | vp_stage=1 | |
|---|---|---|
| GPU 0(pp=0) | offset=0 → 全局层 1, 2 | offset=4 → 全局层 5, 6 |
| GPU 1(pp=1) | offset=2 → 全局层 3, 4 | offset=6 → 全局层 7, 8 |
最终:两块 GPU 的内存里各有什么
get_model() 返回后,每块 GPU 持有一个长度为 2 的 list,调度器后续用 model[chunk_id] 选 chunk:
GPU 0: model = [
# model[0] gpt_builder(pre_process=True, post_process=False, vp_stage=0) 的返回值
GPTModel {
embedding: Embedding(50257→1024) ← pre_process=True,有
decoder.layers[0]: TransformerLayer(global_layer_number=1)
decoder.layers[1]: TransformerLayer(global_layer_number=2)
output_layer: (无) ← post_process=False
},
# model[1] gpt_builder(pre_process=False, post_process=False, vp_stage=1) 的返回值
GPTModel {
embedding: (无)
decoder.layers[0]: TransformerLayer(global_layer_number=5)
decoder.layers[1]: TransformerLayer(global_layer_number=6)
output_layer: (无)
},
]
GPU 1: model = [
# model[0] gpt_builder(pre_process=False, post_process=False, vp_stage=0)
GPTModel {
embedding: (无)
decoder.layers[0]: TransformerLayer(global_layer_number=3)
decoder.layers[1]: TransformerLayer(global_layer_number=4)
output_layer: (无)
},
# model[1] gpt_builder(pre_process=False, post_process=True, vp_stage=1)
GPTModel {
embedding: (无)
decoder.layers[0]: TransformerLayer(global_layer_number=7)
decoder.layers[1]: TransformerLayer(global_layer_number=8)
output_layer: Linear(1024→50257) ← post_process=True,有
},
]
调度时:model[chunk_id] 选 chunk,P2P 传激活值
VPP 调度的每次 forward 都是 forward_step(model[chunk_id], ...),chunk_id 由调度器根据当前 microbatch 编号计算(schedules.py:1160):
def forward_step_helper(virtual_microbatch_id, ...):
chunk_id = get_model_chunk_id(virtual_microbatch_id, forward=True)
# chunk_id = 0 或 1,对应 vp_stage
output = forward_step(
forward_step_func, # pretrain_gpt.py 里的 forward_step()
data_iterator[chunk_id], # 每个 chunk 有自己的数据迭代器
model[chunk_id], # ← 拿对应 chunk 的 GPTModel
...
)
# output = hidden_states([seq_len, batch, hidden]),
# 如果是最后一层则是 loss scalar
执行顺序(pp=2,vp=2,m=2 个 microbatch,标注 GPU 和 chunk_id):
前向 warmup: GPU0 model[0] mb0 →P2P→ GPU1 model[0] mb0 (层1,2 → 层3,4) GPU0 model[0] mb1 →P2P→ GPU1 model[0] mb1 (层1,2 → 层3,4) GPU0 model[1] mb0 →P2P→ GPU1 model[1] mb0 (层5,6 → 层7,8+loss) ... 稳态 1F1B:交替做 chunk[0] forward 和 chunk[1] backward ... 反向:chunk_id 反序(先 chunk1 再 chunk0),梯度逐 GPU 往回传
VPP warmup 微批次数计算源码解析
在 get_pp_rank_microbatches(schedules.py:780)中,VPP 的 warmup 数为:
# forward_backward_pipelining_with_interleaving
num_warmup_microbatches = (pipeline_parallel_size - pipeline_parallel_rank - 1) * 2
num_warmup_microbatches += (num_model_chunks - 1) * microbatch_group_size_per_vp_stage
其中 num_model_chunks = v(virtual stage 数),microbatch_group_size_per_vp_stage 是每个 VP stage 一组的 micro-batch 数。前一项 2×(p-s-1) 对应相邻通信所需的额外 warmup;后一项 (v-1)×group_size 是多个 virtual stage 之间切换的 warmup 开销。
forward_backward_func 入口
所有调度逻辑的统一入口是 get_forward_backward_func(),它根据当前并行配置返回一个函数,由训练主循环调用。三种调度函数的选择逻辑如下:
返回的函数统一接受如下参数(通过 forward_backward_func(...) 调用):
| 参数 | 类型 | 说明 |
|---|---|---|
forward_step_func | Callable | 用户定义的前向步骤,返回 (output, loss_func) |
data_iterator | Iterator / List[Iterator] | VPP 时为 Iterator 列表(每个 model chunk 一个) |
model | nn.Module / List[nn.Module] | VPP 时为模型列表(每个 model chunk 一个) |
num_microbatches | int | 每次 train_step 的 micro-batch 数量 m |
seq_length | int | 序列长度,用于推算 P2P 张量形状 |
micro_batch_size | int | 单个 micro-batch 的 batch size |
decoder_seq_length | int, optional | Encoder-Decoder 架构中 decoder 的序列长度 |
forward_only | bool | True 时跳过 backward(推理 / 验证) |
collect_non_loss_data | bool | True 时收集非 loss 输出(如推理 logits) |
get_forward_backward_func 源码schedules.py:46
def get_forward_backward_func(pp_size=None, vp_size=None):
if pp_size is None and vp_size is None:
pp_size = parallel_state.get_pipeline_model_parallel_world_size()
vp_size = parallel_state.get_virtual_pipeline_model_parallel_world_size()
if pp_size > 1:
if vp_size is not None:
forward_backward_func = forward_backward_pipelining_with_interleaving
else:
forward_backward_func = forward_backward_pipelining_without_interleaving
else:
forward_backward_func = forward_backward_no_pipelining
return forward_backward_func
P2P 通信协议
P2P 通信封装在 P2PCommunicator 类(p2p_communication.py)中。每个 stage 通过 prev_rank(上一 stage)和 next_rank(下一 stage)与邻居通信。
| Stage 0 | Stage 1 | Stage 2 | Stage 3 | |||
|---|---|---|---|---|---|---|
| forward →send_next | → | forwardrecv_prev | forward →send_next | → | forwardrecv_prev | |
| ← backwardrecv_prev | ← | ← backwardsend_prev | ← backwardrecv_prev | ← | ← backward (loss)send_prev |
高层 API:六个便捷方法
调度函数通过以下方法与邻居通信,内部均调用 _communicate():
| 方法 | 方向 | 说明 |
|---|---|---|
recv_forward() | ← prev | 从上一 stage 接收激活值(前向输入) |
send_forward() | → next | 向下一 stage 发送激活值(前向输出) |
recv_backward() | ← next | 从下一 stage 接收梯度(反向输入) |
send_backward() | → prev | 向上一 stage 发送梯度(反向输出) |
send_forward_recv_backward() | → next / ← next | 稳态中批量合并,减少同步点 |
send_backward_recv_forward() | → prev / ← prev | 稳态中批量合并,减少同步点 |
P2P 通信时序(1F1B 稳态单步)
isend → next_rank S1 ->> S2: send_forward(act_m3)
isend → next_rank S2 ->> S1: send_backward(grad_m3)
isend → prev_rank S1 ->> S0: send_backward(grad_m2)
isend → prev_rank Note over S1: send_forward_recv_backward()
合并:发 act,收 grad,一次 P2P
同步 vs 异步通信
底层 _communicate() 支持三种模式:
batch_p2p_comm=True(默认):用torch.distributed.batch_isend_irecv批量提交所有 P2P 操作,然后统一wait()。吞吐最优,但仍需等待完成。use_ring_exchange_p2p=True:用ring_exchange同时交换上下游,适合 NVLink 环形拓扑。overlap_p2p_comm=True(仅 VPP):wait_on_reqs=False,不立即等待,将 P2P 通信与下一段计算重叠。等到真正用到数据时再wait(),最大化 GPU 利用率。
死锁风险与奇偶交替规避
当 batch_p2p_comm=False 时,每个 P2P 操作独立调用 isend/irecv。若所有 rank 都先 send 再 recv,而 send 缓冲区满、recv 没人接收,就会发生死锁。
保证总有一方准备接收,不会死锁
_p2p_ops 奇偶逻辑源码p2p_communication.py:54
if group.rank() % 2 == 0:
# 偶数 rank:先发送,再接收
if tensor_send_next is not None:
reqs["send_next"] = isend(tensor_send_next, dst=next_pipeline_rank, ...)
if tensor_recv_prev is not None:
reqs["recv_prev"] = irecv(tensor_recv_prev, src=prev_pipeline_rank, ...)
if tensor_send_prev is not None:
reqs["send_prev"] = isend(tensor_send_prev, dst=prev_pipeline_rank, ...)
if tensor_recv_next is not None:
reqs["recv_next"] = irecv(tensor_recv_next, src=next_pipeline_rank, ...)
else:
# 奇数 rank:先接收,再发送
if tensor_recv_prev is not None:
reqs["recv_prev"] = irecv(tensor_recv_prev, src=prev_pipeline_rank, ...)
if tensor_send_next is not None:
reqs["send_next"] = isend(tensor_send_next, dst=next_pipeline_rank, ...)
if tensor_recv_next is not None:
reqs["recv_next"] = irecv(tensor_recv_next, src=next_pipeline_rank, ...)
if tensor_send_prev is not None:
reqs["send_prev"] = isend(tensor_send_prev, dst=prev_pipeline_rank, ...)
当 PP=2 时,两个 rank 互为邻居。代码使用 两个不同的 communicator(even_send_odd_recv_group 用 pp_group,even_recv_odd_send_group 用全局 WORLD group)来允许两个独立通信重叠。UCC backend 除外(需相同 communicator)。
变长序列:形状预通信
当 config.variable_seq_lengths=True 时(如 packing 数据集中不同 micro-batch 序列长度不同),接收方不知道张量形状。_communicate_shapes() 会在传数据前,先用一次 3 维 int64 张量交换形状信息,接收方据此分配内存后再接收实际激活值。
激活值显存与 Activation Checkpointing 的配合
在标准 1F1B 中,warmup 阶段缓存的激活值是 PP 显存开销的主要来源。Stage s 在 warmup 阶段会完成 p - s - 1 个 micro-batch 的前向传播,并将这些激活值保存在显存中,等待后续反向传播使用。
随着 p 增大,first stage 需要缓存的激活值越来越多,显存压力显著。Megatron-LM 提供三种策略应对:
① 全层 Recompute(recompute_granularity=full)
不缓存任何中间激活,反向时重新执行完整 forward 计算激活值。显存最省,但计算量增加约 33%。每个 micro-batch 只保存前向的输入张量(即 P2P 接收的激活),不保存层间中间结果。
② 选择性 Recompute(recompute_granularity=selective)
仅重计算 attention 的 softmax/dropout 等内存占用大但计算便宜的操作,保留 MLP 层激活。显存节省约 70%,计算开销约增加 10%。
③ 部分 Micro-batch Checkpointing(num_microbatches_with_partial_activation_checkpoints)
精细化控制:对一个 window 内部分 micro-batch 执行 recompute,其余正常保留激活值。window 大小 = num_warmup_microbatches + 1,确保最大并发反向不超过此值,由此平衡显存与重计算开销。
Partial checkpointing 的 window 逻辑schedules.py:2244
# max_outstanding_backprops = warmup 数 + 1(当前正在跑的 forward)
max_outstanding_backprops = None
if config.num_microbatches_with_partial_activation_checkpoints is not None:
max_outstanding_backprops = num_warmup_microbatches + 1
# Warmup 阶段决定是否对当前 micro-batch 做 full recompute
for i in range(num_warmup_microbatches):
if max_outstanding_backprops is not None:
checkpoint_activations_microbatch = (
i % max_outstanding_backprops
>= config.num_microbatches_with_partial_activation_checkpoints
)
else:
checkpoint_activations_microbatch = None
# forward_step 内部检查 checkpoint_activations_microbatch
# 不为 None 时传入 forward_step_func 以触发 gradient checkpointing
逻辑:在大小为 max_outstanding_backprops 的 window 中,前 num_microbatches_with_partial_activation_checkpoints 个 micro-batch 正常保留激活(或仅做 selective recompute),其余做 full recompute。window 在 warmup 和稳态各滑动一次,later stage 的 window 更小(因为 warmup 数少),自动保留更多激活。
当 config.deallocate_pipeline_outputs=True 时,stage 在将激活值发送给下一 stage 之后,立刻将 output_tensor.data 替换为一个标量张量(torch.empty((1,))),释放激活数据占用的显存。此时 output_tensor 仍持有 .grad_fn(autograd 图节点),反向时通过 custom_backward() 直接调用 C++ 引擎跳过 PyTorch 的形状检查,避免报错。