01

概述

pipeline stage micro-batch P2P 通信 bubble

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 因等待数据而空闲的时间段,直接浪费算力
warmup1F1B 开始前的纯前向阶段,各 stage 依次拿到第一批激活值
cooldown1F1B 结束后的纯反向阶段,清空所有积压的反向传播
virtual stage (VPP)每个物理 stage 持有多段不连续层,减小 bubble ratio
02

朴素流水线 vs 1F1B 调度

bubble ratio warmup steady state cooldown

朴素流水线的 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步
Forward Backward Bubble(GPU 空转)

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 schedule

上半:标准 1F1B — 粉色为 Forward,黄色为 Backward,空白为 bubble。 同一 microbatch 的 Backward 在各 Stage 上时间错开,梯度逐级回传。下半:Seq1F1B(论文提出的改进方法)。
图源:Seq1F1B, arXiv:2406.03488

如何阅读标准 1F1B 调度图
  • 颜色 = 操作类型:粉色 = 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 主循环。

03

Interleaved 1F1B(VPP)

virtual pipeline stage bubble ratio P2P × v

标准 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 对比
调度策略Bubble RatioP2P 通信量显存峰值(激活值)
朴素流水线 \(\dfrac{p-1}{m+p-1}\) 低(一次只缓存少量)
1F1B \(\dfrac{p-1}{m}\) warmup 期缓存 \(p{-}s{-}1\) 个激活
Interleaved 1F1B (VPP) \(\dfrac{p-1}{m \cdot v}\) 缓存量约为标准 1F1B 的 v 倍

1F1B-I / VPP 交错调度(PP=4, V=2, 每 GPU 持有 2 个 virtual stage)

1F1B-I (VPP) schedule

上半:1F1B-I(即 Megatron 的 VPP)— 粉色/蓝色区分 Stage 1 和 Stage 2 的 Forward, 黄色/紫色区分 Backward。每个 Device 在两个 virtual stage 间交替执行,bubble 显著减少。 下半:Seq1F1B-I(论文提出的改进方法)。
图源:Seq1F1B, arXiv:2406.03488

如何阅读 1F1B-I 调度图
  • 颜色 = 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 作为第一个参数带上:

pretrain_gpt.pyL309–L314
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 做任何修改:

training.py · pretrainL909–L910
model, optimizer, opt_param_scheduler = setup_model_and_optimizer(
    model_provider,   # 就是 partial(model_provider_fn, gpt_builder)
    ...
)
training.py · setup_model_and_optimizerL1357
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

training.py · get_model → build_modelL1134–L1148
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=Falsepre=False, post=False
GPU 1(pp=1)pre=False, post=Falsepre=False, post=True

E — model_provider_fn:把 gpt_builder 当参数再调一层

model_provider.py 里的 model_provider 函数签名是 (model_builder, pre_process, post_process, vp_stage),它只是把参数原封转给 gpt_builder

model_provider.py · model_providerL24–L67
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

gpt_builders.py · gpt_builderL27–L97
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__:按标志决定建哪些组件

gpt_model.py · GPTModel.__init__L90–L232
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() 算出每层的全局编号:

transformer_block.py · TransformerBlock.__init__ + _build_layersL272–L375
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=0vp_stage=1
GPU 0(pp=0)offset=0 → 全局层 1, 2offset=4 → 全局层 5, 6
GPU 1(pp=1)offset=2 → 全局层 3, 4offset=6 → 全局层 7, 8

最终:两块 GPU 的内存里各有什么

get_model() 返回后,每块 GPU 持有一个长度为 2 的 list,调度器后续用 model[chunk_id] 选 chunk:

GPU 内存中的 model list(get_model 返回值)
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):

schedules.py · forward_step_helper(简化)L1301–L1328
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_microbatchesschedules.py:780)中,VPP 的 warmup 数为:

schedules.py · get_pp_rank_microbatchesL780–L792
# 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 开销。

04

forward_backward_func 入口

get_forward_backward_func pp_size vp_size

所有调度逻辑的统一入口是 get_forward_backward_func(),它根据当前并行配置返回一个函数,由训练主循环调用。三种调度函数的选择逻辑如下:

flowchart TD A["get_forward_backward_func()"] --> B{"pp_size > 1?"} B -- "否 pp=1" --> C["forward_backward_no_pipelining\n所有层在单 GPU 上\n直接 F→B 逐 micro-batch"] B -- "是" --> D{"vp_size is not None?"} D -- "否 标准 1F1B" --> E["forward_backward_pipelining\n_without_interleaving\n经典三阶段:warmup→1F1B→cooldown"] D -- "是 VPP" --> F["forward_backward_pipelining\n_with_interleaving\nInterleaved 1F1B\nbubble÷v"] style C fill:#1f2937,stroke:#3fb950,color:#e6edf3 style E fill:#1f2937,stroke:#58a6ff,color:#e6edf3 style F fill:#1f2937,stroke:#d2a8ff,color:#e6edf3

返回的函数统一接受如下参数(通过 forward_backward_func(...) 调用):

参数类型说明
forward_step_funcCallable用户定义的前向步骤,返回 (output, loss_func)
data_iteratorIterator / List[Iterator]VPP 时为 Iterator 列表(每个 model chunk 一个)
modelnn.Module / List[nn.Module]VPP 时为模型列表(每个 model chunk 一个)
num_microbatchesint每次 train_step 的 micro-batch 数量 m
seq_lengthint序列长度,用于推算 P2P 张量形状
micro_batch_sizeint单个 micro-batch 的 batch size
decoder_seq_lengthint, optionalEncoder-Decoder 架构中 decoder 的序列长度
forward_onlyboolTrue 时跳过 backward(推理 / 验证)
collect_non_loss_databoolTrue 时收集非 loss 输出(如推理 logits)
get_forward_backward_func 源码schedules.py:46
schedules.py · get_forward_backward_funcL46–L146
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
05

P2P 通信协议

P2PCommunicator isend / irecv 死锁规避 奇偶交替

P2P 通信封装在 P2PCommunicator 类(p2p_communication.py)中。每个 stage 通过 prev_rank(上一 stage)和 next_rank(下一 stage)与邻居通信。

前向 / 反向 的 send-recv 方向
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 稳态单步)

sequenceDiagram participant S0 as Stage 0 participant S1 as Stage 1 participant S2 as Stage 2 (last) Note over S0,S2: 稳态一个 1F1B 步骤(Stage 1 视角) S0 ->> S1: send_forward(act_m4)
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 没人接收,就会发生死锁

sequenceDiagram participant Even as 偶数 rank(0, 2, ...) participant Odd as 奇数 rank(1, 3, ...) Note over Even,Odd: _p2p_ops:奇偶交替避免死锁 Even ->> Odd: isend(send_next) ①先发 Odd -->> Even: irecv(recv_prev) ①先收 Even -->> Odd: irecv(recv_prev) ②再收 Odd ->> Even: isend(send_next) ②再发 Note over Even,Odd: 偶数先发后收 / 奇数先收后发
保证总有一方准备接收,不会死锁
_p2p_ops 奇偶逻辑源码p2p_communication.py:54
p2p_communication.py · _p2p_opsL78–L127
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_group.size() == 2

当 PP=2 时,两个 rank 互为邻居。代码使用 两个不同的 communicatoreven_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 张量交换形状信息,接收方据此分配内存后再接收实际激活值。

06

激活值显存与 Activation Checkpointing 的配合

activation memory recompute partial checkpointing

在标准 1F1B 中,warmup 阶段缓存的激活值是 PP 显存开销的主要来源。Stage s 在 warmup 阶段会完成 p - s - 1 个 micro-batch 的前向传播,并将这些激活值保存在显存中,等待后续反向传播使用。

p=4 时,各 stage 需缓存的最大激活值数量(warmup 结束时): Stage 0 缓存 3 个 micro-batch 的激活 ████████████████████████ ← 最多(first stage) Stage 1 缓存 2 个 micro-batch 的激活 ████████████████ Stage 2 缓存 1 个 micro-batch 的激活 ████████ Stage 3 缓存 0 个 micro-batch 的激活 ← 无需缓存(last stage,立即 backward) 公式:max_buffered_activations[s] = p - s - 1 = num_warmup_microbatches[s]

随着 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
schedules.py · forward_backward_pipelining_without_interleavingL2244–L2290
# 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 数少),自动保留更多激活。

deallocate_pipeline_outputs

config.deallocate_pipeline_outputs=True 时,stage 在将激活值发送给下一 stage 之后,立刻将 output_tensor.data 替换为一个标量张量(torch.empty((1,))),释放激活数据占用的显存。此时 output_tensor 仍持有 .grad_fn(autograd 图节点),反向时通过 custom_backward() 直接调用 C++ 引擎跳过 PyTorch 的形状检查,避免报错。