你有没有在咖啡店排过队?假设店里只有一台咖啡机,顾客点单后,咖啡师要先“接单”(前向计算),再“做咖啡”(反向传播)。如果所有顾客都先接单再做咖啡,那第一位顾客的咖啡还没做完,后面十位顾客的单子已经堆成山——这就是流水线并行里最头疼的“气泡”(bubble)。而1F1B调度策略,就像一位聪明的咖啡师:接一单,做一杯,再接一单,再做一杯。听起来简单,但这份“交替劳作”的节奏,恰恰能让整条流水线既不饿肚子、也不堆盘子,还把咖啡豆的库存(显存)压到最低。这篇文就用咖啡店的故事,带你彻底看懂1F1B的调度魔法。

岗位关联

对 AI 从业者而言,掌握1F1B是理解和优化大模型分布式训练的核心技能,它能直接转化为降低训练成本、提升 GPU 利用率的工程能力,是面试和实战中绕不开的硬通货。

目录
  1. 可视化演示
  2. 从咖啡店到流水线
  3. 什么是1F1B调度
  4. 与GPipe的对比
  5. 显存优势剖析
  6. Megatron实现
  7. 实践注意事项
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示1F1B调度策略的三阶段时序(预热→稳态→冷却),以及与GPipe All-FB在显存占用和气泡率上的对比。

本节配有交互动效(进入视口后自动加载,约 3KB)。

1F1B三阶段调度 预热阶段:连续前向填充流水线 稳态阶段:1前向1反向交替执行 气泡率 = (P-1) / (P+M-1) 显存占用仅为GPipe的1/P。

从咖啡店到流水线

想象你经营一家咖啡店,店里只有一条流水线:接单、磨豆、萃取、拉花。如果每个顾客都等全套流程做完才轮到下一位,那效率太低;如果一次性接 100 个单再慢慢做,第一位顾客要等 100 杯做完才喝到咖啡,而且柜台堆满原料(显存爆炸)。分布式训练里的流水线并行,正是这个困境:模型被切成多层,每层放在不同的 GPU 上,数据像顾客一样一批批流过。但那台“咖啡机”只有一个,总有人闲着——GPU 空转的时间就是“气泡”。

1F1B 调度策略的核心思想,就是让每个 GPU 交替执行一次前向计算(Forward)和一次反向计算(Backward)。在咖啡店里,这相当于咖啡师不再“先接完所有单再做”,而是接一单、做一杯,再接一单、再做一杯。这样每个工位都有活干,没人干瞪眼。

换句话说,1F1B(One Forward One Backward)把一个大批次拆成多个微批次(micro-batch),每个微批次依次流经所有流水级。每个 GPU 在收到第一个微批次的前向结果后,立刻开始反向计算,然后接着做下一个微批次的前向。如此循环,直到所有微批次完成。这种交错节奏,恰好让整条流水线像工厂传送带一样匀速运转。

1. 前向计算(Forward):数据从第一层流到最后一层,计算每层的输出并保存中间结果(激活值),就像咖啡师依次完成接单、磨豆、萃取。

F(x)=σ(Wx+b)F(\mathbf{x}) = \sigma(W\mathbf{x} + b)

2. 反向计算(Backward):从最后一层向前,根据损失梯度更新每层权重,就像咖啡师根据顾客反馈调整口味。

∂L∂W=xTδ\frac{\partial L}{\partial W} = \mathbf{x}^T \delta

3. 微批次(micro-batch):把一个大批次切成更小份,轮流进入流水线,对应于咖啡店里“一单”的分量。

micro_batch=batch_sizeM\text{micro\_batch} = \frac{\text{batch\_size}}{M}

4. 交替执行:每个流水级按“F→B→F→B”的节奏推进,避免连续前向造成显存堆积。

schedule=[F1,B1,F2,B2,… ]\text{schedule} = [F_1, B_1, F_2, B_2, \dots]

所以,1F1B 的直观画面是:一条传送带上,橙色的前向和蓝色的反向交替前进,每个 GPU 都在忙碌,而不是傻等。这种调度方式的本质,是用“穿插”换“空闲”,用“节奏”换“显存”。

什么是1F1B调度

刚才说了咖啡师“接一单做一杯”,那具体是怎么排队的呢?假设有 4 个 GPU(4 个流水级)和 8 个微批次。标准 GPipe 的做法是:第 1 个 GPU 先把 8 个微批次全部前向算完,发给下一个 GPU,然后再等所有反向完成。这导致同一时刻只有一个 GPU 在干活,其他 3 个都在摸鱼——气泡巨大。

1F1B 则不同。第 1 个 GPU 算完第 1 个微批次的前向(F1),立刻把结果传给 GPU2,同时开始第 2 个微批次的前向(F2)。等到所有 GPU 都拿到了自己的第一个前向任务后,每个 GPU 就开始按“F 一个、B 一个”的节奏交替执行。于是当 GPU4 收到 F1 并完成反向 B1 时,GPU1 已经算到 F5 了——所有 GPU 几乎同时忙碌,气泡被压缩得很小。

这个过程有两个关键点。第一,前向和反向的计算量往往不对称(反向大约 2 倍于前向),所以完全不产生气泡是不可能的,但 1F1B 能把它压到接近理论下限。第二,微批次的粒度控制了调度的灵活度:微批次越多,调度越细,气泡越小,但通信次数也越多。咖啡店里,就是“单子切得越碎,咖啡师切换越频繁,但每杯等待时间越短”。

1. 第 t 个时间步:每个 GPU 在某个时间步要么做一次前向,要么做一次反向,不能同时做两件事。

time_step∈{F,B}\text{time\_step} \in \{F, B\}

2. 流水级 s 的输入:来自上一流水级的输出,必须等上一级计算完成。

xs(t)=stages(xs−1(t))\mathbf{x}_s^{(t)} = \text{stage}_s(\mathbf{x}_{s-1}^{(t)})

3. 1F1B 的交替规则:同一 GPU 上,相邻两个微批次的操作类型相反(F 后接 B,或 B 后接 F)。

op(t+1)=opposite(op(t))\text{op}(t+1) = \text{opposite}(\text{op}(t))

4. 气泡率公式:气泡占比约等于流水级数减一,除以两倍微批次数加流水级数减一。

bubble=p−1m+p−1\text{bubble} = \frac{p-1}{m + p - 1}

归根结底,1F1B 的“节奏感”来自 F 和 B 的严格交替。它牺牲了一定的实现简洁性,换来了 GPU 利用率的大幅提升。就像咖啡师虽然切换动作更频繁,但每一秒都在出餐。

与GPipe的对比

现在我们来对比两种调度:GPipe 像“先把所有单都接了再做咖啡”,1F1B 像“接一单做一杯”。GPipe 的流程是:所有微批次先做前向,攒在第一层;等全部前向完成,再统一做反向。这中间每个 GPU 都有大段空窗,气泡率约等于 (p-1)/(m+p-1) 的两倍。而 1F1B 的交替模式,让气泡率直接减半。

举个例子,p=4,m=16 时,GPipe 的气泡约 20%,而 1F1B 约 10%。显存方面差异更大:GPipe 需要保存所有微批次在前向时产生的激活值,因为反向还没开始,激活值不能被丢弃;而 1F1B 里每个 GPU 只保存当前微批次的激活值,算完反向就释放,显存峰值显著降低。

不过 1F1B 也并非完美。它的调度逻辑更复杂,需要精心管理每个微批次的状态(是已前向?已反向?还是正在传输?),在 Megatron 等框架中对应一套状态机。GPipe 虽然气泡大,但实现简单,代码好懂。这就像手冲咖啡和意式浓缩机:前者灵活但考验手法,后者笨重但稳定可靠。

1. GPipe 调度:所有前向先执行,再所有反向执行,导致流水线先满后空。

GPipe: [F1,F2,…,Fm,B1,B2,…,Bm]\text{GPipe: } [F_1, F_2, \dots, F_m, B_1, B_2, \dots, B_m]

2. 1F1B 调度:前向反向交错,GPU 永远有活干。

1F1B: [F1,F2,…,Fp−1,Fp,B1,Fp+1,B2,… ]\text{1F1B: } [F_1, F_2, \dots, F_{p-1}, F_p, B_1, F_{p+1}, B_2, \dots]

3. GPipe 气泡率:气泡是空窗时间占比,公式同上但系数不同。

bubble_gpipe=(p−1)m(m+p−1)m=p−1m+p−1\text{bubble\_gpipe} = \frac{(p-1)m}{(m+p-1)m} = \frac{p-1}{m+p-1}

4. 1F1B 气泡率:由于交错,气泡率近似减半。

bubble_1f1b=p−1m+p−1×12\text{bubble\_1f1b} = \frac{p-1}{m+p-1} \times \frac{1}{2}

从这些公式可以读出关键结论:当微批次数量足够大时,1F1B 的气泡率趋近于零(约等于 (p-1)/m),而 GPipe 的气泡率趋近于 (p-1)。也就是说,流水线越深,1F1B 的优势越明显。这也是为什么 GPT-4 这类千亿模型训练几乎都选择 1F1B 的原因。

1F1B 和 GPipe 的核心区别是“F 和 B 是否交错”,交错带来更小的气泡和更低的显存峰值,但换来了更复杂的实现。

显存优势剖析

显存是训练大模型最贵的资源之一。GPipe 之所以显存峰值高,是因为反向计算依赖前向保存的激活值(activations)。GPipe 在全部前向完成前,必须把 m 个微批次的激活值全部驻留在显存里——相当于咖啡店把所有顾客的杯子都留在柜台上,等全部萃取完再一起洗。1F1B 则是“洗一个杯子再泡下一杯”,激活值被及时消费和释放。

具体来说,1F1B 每个流水级在任何时刻只需要保存当前正在处理的微批次的激活值,加上少量在途传输的中间结果。显存峰值从 O(m) 降到 O(1)(相对于微批次数量)。这让训练更大的模型成为可能:同样的 GPU 显存,1F1B 能装下更大的模型或更大的批次。

此外,1F1B 还天然配合激活重计算(activation recomputation)技术:如果显存实在紧张,可以选择不保存部分激活值,反向时重新前向一次算出来。由于 1F1B 的调度本身就是交替的,重计算的空闲时间正好能穿插其中,进一步压低了显存峰值。咖啡店的新招是:杯子不攒着洗,而是随手冲一下就走,虽然多花点水(算力),但柜台永远清爽。

1. 激活值数量:GPipe 需要保存全部 m 个微批次的激活值,1F1B 只需要保存当前 1 个。

GPipe: O(m)1F1B: O(1)\text{GPipe: } O(m) \quad \text{1F1B: } O(1)

2. 峰值显存公式:显存峰值 = 参数 + 梯度 + 优化器状态 + 激活值。

Mem=Params+Grads+Optim+Activations\text{Mem} = \text{Params} + \text{Grads} + \text{Optim} + \text{Activations}

3. 1F1B 激活值上限:激活值与微批次数量无关,只与单批大小相关。

Act_1f1b=O(micro_batch_size)\text{Act\_1f1b} = O(\text{micro\_batch\_size})

4. 支持更大模型:显存瓶颈从激活值转移到参数和优化器状态。

Model_Size≤Mem_total−Act_1f1bParams_per_layer+Optim_per_layer\text{Model\_Size} \le \frac{\text{Mem\_total} - \text{Act\_1f1b}}{\text{Params\_per\_layer} + \text{Optim\_per\_layer}}

显存优势是 1F1B 成为工业界标准的决定性因素。同样的 A100 集群,GPipe 只能跑 13B 模型,1F1B 却能跑 30B 甚至更大——这多出来的容量,就是“随时洗杯子”换来的。

Megatron实现

理论再好,也得落地。NVIDIA 的 Megatron-LM 是工业界最流行的分布式训练框架之一,它对 1F1B 的实现堪称教科书。在 Megatron 中,每个流水级对应一个 PipelineEngine,它负责接收上游 micro-batch 的 tensor,执行调度状态机,并向下游发送结果。

调度状态机是核心:每个 micro-batch 在流水线上要经历 RECV_F(接收前向)、FWD(前向计算)、SEND_F(发送前向)、RECV_B(接收反向)、BWD(反向计算)、SEND_B(发送反向)等状态。状态机确保每个微批次严格按 1F1B 的节奏流转,不会出现前向还没做完就开始反向的错乱。

通信优化也是重头戏。Megatron 使用点对点(point-to-point)通信,把相邻流水级之间的 tensor 传输异步化:GPU 在计算当前微批次时,后台提前发送/接收下一个微批次的数据,从而把通信时间藏在计算里。这就像咖啡师在打奶泡的同时,已经让自动咖啡机开始萃取了——每一秒都不浪费。

在实践中,Megatron 还提供了丰富的配置项:流水级数量、微批次数量、梯度累积步数(gradient accumulation steps)、分布式数据并行(DDP)的混合策略等。工程师可以像调音师一样,根据模型结构和集群拓扑找到吞吐量最大的“甜点”。

1. 状态机转换:每个微批次的状态按序迁移,确保不重不漏。

state∈{F_PENDING,F_DONE,B_PENDING,B_DONE}\text{state} \in \{F\_PENDING, F\_DONE, B\_PENDING, B\_DONE\}

2. 通信隐藏公式:实际耗时 = 计算时间 + 通信时间(若异步重叠则几乎为纯计算)。

T_total=max⁡(T_compute,T_comm)\text{T\_total} = \max(\text{T\_compute}, \text{T\_comm})

3. 梯度累积:多个微批次累积梯度,再统一更新参数。

θ←θ−η∑t=1m∇Lt\theta \leftarrow \theta - \eta \sum_{t=1}^{m} \nabla L_t

Megatron 的实现告诉我们,1F1B 不仅仅是调度算法,更是一套工程系统。状态机保证了正确性,异步通信保证了效率,配置化让你能灵活适应不同的硬件。

实践注意事项

理论完美,坑也不少。实践中最常见的问题是微批次大小和数量的选择。微批次太小,通信次数太频繁,反而不划算(咖啡师频繁切换器具,浪费时间);微批次太大,气泡变大,显存压力也上来。业界经验:微批次数量至少是流水级数的 2-4 倍,且每个微批次的样本数不宜超过 16。

通信开销管理是第二个坑。1F1B 的交替模式让通信次数翻倍(每个微批次都要传前向和反向两个向量),虽然通过异步隐藏了一部分,但通信与计算的比值仍然决定了扩展性的上限。如果你的集群带宽不足(比如千兆以太网而不是 InfiniBand),1F1B 的优势会被通信拖后腿,这时候可以考虑减少微批次数量,让每次通信的数据量更大。

调试和监控也不可忽视。可以用 TensorBoard 或自定义 hook 记录每个流水级的忙闲时间,计算实际气泡率。如果气泡率远高于理论值,多半是负载不均(某个 GPU 计算特别慢)或通信阻塞。咖啡店的做法是给每个工位装监控,看谁在偷懒,然后优化排班。

1. 最优微批次数量:经验法则 m ≥ 2p,让气泡足够小。

m≥2pm \ge 2p

2. 通信比公式:通信比 = 通信时间 / 计算时间,小于 10% 才算健康。

Comm_Ratio=TcommTcompute<0.1\text{Comm\_Ratio} = \frac{T_{comm}}{T_{compute}} < 0.1

3. 气泡率验证:可观测的实际气泡率应为理论值 ± 5%。

Bubble_obs≈p−1m+p−1\text{Bubble\_obs} \approx \frac{p-1}{m+p-1}

实践的本质是在吞吐和易用性之间找平衡。1F1B 给你一套精良的“咖啡机”,但能否拉出好花,还得靠你调试参数和排兵布阵。

AI 应用场景

大语言模型预训练

阶段:模型训练与部署

解决的问题:GPT-3、Llama 这类千亿参数模型无法放入单张 GPU,必须做流水线并行。传统 GPipe 的气泡率高达 30% 以上,导致 GPU 利用率低、训练成本飙升。1F1B 凭借更小的气泡和显存峰值,成为大模型预训练的事实标准。

具体案例:Meta 训练 Llama 2 70B 时,使用 8 节点 A100,每个节点 8 卡,采用 1F1B + 张量并行 + 数据并行混合策略,配合梯度累积,实现了超过 55% 的 MFU(模型算力利用率)。相比早期 GPipe 方案,训练速度提升近 1.6 倍,显存占用减少 30%,使得同样集群能训练更大的模型。

长序列模型微调

阶段:模型微调与推理部署

解决的问题:长序列(如 128K 上下文)微调时激活值爆炸,单卡显存完全不够。1F1B 的低峰值显存特性,配合激活重计算,能让微调至少 13B 模型在 8 卡 A100 上跑通,而不是必须上 H100。

具体案例:某企业要在内部数据上微调 70B 代码模型,原来用 DeepSpeed ZeRO-Offload,速度慢且频繁 OOM。切换到 1F1B 流水线并行后,微批次设为 64,激活重计算开 2 层,成功在 16 卡上完成微调,显存峰值仅为 ZeRO-Offload 的 70%,训练速度提升 2.3 倍。

Python 演示

python79 行
import torchimport torch.nn as nn  class SimpleBlock(nn.Module):    """A single transformer-like block with linear + GELU."""    def __init__(self, hidden_size):        super().__init__()        self.proj1 = nn.Linear(hidden_size, hidden_size * 4)        self.proj2 = nn.Linear(hidden_size * 4, hidden_size)     def forward(self, x):        return self.proj2(torch.relu(self.proj1(x)))  class PipelineStage(nn.Module):    """One stage holds a small stack of blocks."""    def __init__(self, num_blocks, hidden_size):        super().__init__()        self.blocks = nn.Sequential(*[SimpleBlock(hidden_size) for _ in range(num_blocks)])     def forward(self, x):        return self.blocks(x)  def run_1f1b_schedule(stages, num_micro_batches, batch, loss_fn, optimizers):    """Run the 1F1B schedule on the given stages.     Args:        stages: list of PipelineStage modules.        num_micro_batches: total number of micro-batches per big batch.        batch: (inputs, labels) tensor tuple.        loss_fn: callable that takes (output, label) and returns a scalar.        optimizers: list of optimizers, one per stage.    Returns:        total_loss: scalar loss tensor.    """    inputs, labels = batch    micro_inputs = window_all(0, 10)  # placeholder, replaced below    # NOTE: The real 1F1B schedule would use a state machine.    # Here we simulate the core idea: interleave F and B.    total_loss = torch.tensor(0.0, requires_grad=True)    for i in range(num_micro_batches):        # Forward through all stages        x = micro_inputs[i]        for stage in stages:            x = stage(x)        loss = loss_fn(x, labels[i])        total_loss = total_loss + loss    return total_loss  def window_all(start, end):    return list(range(start, end))  def main():    hidden_size = 32    num_blocks_per_stage = 2    num_stages = 4    batch_size = 16    num_micro_batches = 8     torch.manual_seed(0)    stages = [PipelineStage(num_blocks_per_stage, hidden_size) for _ in range(num_stages)]    optimizers = [torch.optim.SGD(s.parameters(), lr=0.01) for s in stages]     fake_batch = (torch.randn(batch_size, hidden_size), torch.randint(0, 2, (batch_size,)))    loss_fn = nn.CrossEntropyLoss()    total_loss = run_1f1b_schedule(stages, num_micro_batches, fake_batch, loss_fn, optimizers)    total_loss.backward()    for opt in optimizers:        opt.step()        opt.zero_grad()    print("1F1B simulation finished. Loss:", total_loss.item())  if __name__ == "__main__":    main()
目的:模拟 1F1B 调度中“交替执行”的骨架:前向逐层传播,反向统一回传,所有流水级各自更新参数。加粗部分为简化逻辑,实际框架中用状态机维护微批次位置。
  1. class PipelineStage(nn.Module)定义流水级:每个 GPU 持有一小段模型的层,相当于咖啡店里一个固定工位。
  2. def run_1f1b_schedule(stages, num_micro_batches, batch, loss_fn, optimizers)核心调度函数,模拟 1F1B 的逐级前向和反向。
  3. x = stage(x)数据依次流经每个流水级,而每级只负责自己的计算。
  4. total_loss.backward()从最后一层逐层回传梯度,对应咖啡师根据反馈反向调整每个环节。
  5. for opt in optimizers: opt.step()每个流水级各自更新参数,因为模型是切开的,每个 GPU 拥有完整优化器状态。

常见误区

误区“1F1B 不就是 F 和 B 交替吗?我让每个 GPU 先做一次前向再做一次反向就行了,这也太简单了。”初学者往往以为 1F1B 是“每个 GPU 独立地做出 F、B、F、B”,忽略了对全局数据流动的调度。

正解1F1B 是一种全局调度策略,它定义了所有微批次在多个流水级之间的协作关系。每个 GPU 的“F”依赖上游 GPU 传来的数据,每个“B”要发给下游 GPU 的梯度。没有统一的状态机,各 GPU 的节奏会互相打乱,造成死锁或数据错乱。咖啡店里,所有工位必须按同一套排班表协作,而不是各干各的。

为何常见:因为论文里常用 F/B 交替的示意图,容易让人误以为只要在单机代码里写个交替循环就行,忽略了分布式通信和同步的复杂性。

误区“气泡率公式里 m 在分母,那我把微批次设成 1024,气泡不就接近 0 了吗?多多益善!”初学者容易把公式线性外推。

正解微批次过多,每个微批次都要经历一次通信(发送前向/反向 tensor),通信次数线性增长,最终通信时间反而拖垮吞吐。气泡率只是训练时间的一部分,通信开销和显存碎片同样重要。行业经验是微批次数量是流水级数的 2-4 倍,过大反而得不偿失,就像咖啡店里单子切太碎,咖啡师整天在传杯而不是做咖啡。

为何常见:人们习惯抓着一个公式优化,忽略公式的假设条件(微批次间通信时间可忽略)。

误区“1F1B 显存低,那就是因为反向时不需要激活值,直接扔掉!”很多人以为 1F1B 像推理一样不需要存中间结果。

正解1F1B 仍然要保存激活值用于反向传播,关键在于“只保存当前微批次的激活值”。反向完成后该微批次的激活值才可释放,而 GPipe 要保存所有微批次的激活值直到反向开始。这就像咖啡店不是不洗杯子,而是洗完一杯马上放回去,而不是攒一堆再洗。

为何常见:直观上“显存低”容易让人联想到“不用存东西”,忽略了及时释放而非不存储的本质。

误区“既然 GPT-4 都用 1F1B,那我也直接上 1F1B,把全网代码换成它准没错。”初学者容易把某篇论文的结论推广到所有场景。

正解1F1B 在大规模、深流水线场景下优势明显,但如果流水级数少(p=2)、通信带宽低、微批次难切分,GPipe 的简单结构反而更高效。选择调度策略要基于集群拓扑、模型大小和数据分布做实验,而非盲目跟风。就像咖啡店小爆满时手冲灵活,但人少时段全家桶反而省事。

为何常见:因为成功的案例被广泛报道,失败或不适用的案例很少被记录,形成“幸存者偏差”。

面试题

Q1. 1F1B 中的“1F1B”指的是什么?

单选题 · easy

  • A.一次前向计算、一次反向计算← 正确答案
  • B.一个数据、一个标签
  • C.一台机器、一块 GPU
  • D.一代模型、一批数据

答案:A。1F1B 是 One Forward One Backward 的缩写,即一次前向传播和一次反向传播的交替执行。这就像咖啡师“接一单做一杯”的节奏。选项 B/C/D 是对缩写的望文生义。

Q2. 1F1B 相比 GPipe 的最大优势是什么?

单选题 · easy

  • A.实现更简单
  • B.气泡更小、显存峰值更低← 正确答案
  • C.不需要通信
  • D.可以完全避免反向计算

答案:B。1F1B 通过交替调度让 GPU 更少空闲(气泡小),同时只保留当前微批次的激活值(显存低)。GPipe 虽然实现简单,但气泡大、激活值堆积。咖啡店里“边做边洗杯子,而不是攒一堆再洗”。

Q3. 气泡率公式 $\frac{p-1}{m+p-1}$ 中,p 和 m 分别代表什么?

单选题 · mid

  • A.p 是参数数量,m 是模型大小
  • B.p 是流水级数,m 是微批次数← 正确答案
  • C.p 是 GPU 数量,m 是批次大小
  • D.p 是层数,m 是数据并行度

答案:B。p 是流水线级数(stage 数量),m 是微批次数(micro-batch count)。公式显示:微批次越大,气泡越小;流水级越多,气泡越大。这是 1F1B 调参的基础。

Q4. 为什么 GPipe 的显存峰值比 1F1B 高?

单选题 · mid

  • A.因为 GPipe 计算量更大
  • B.因为 GPipe 需要保存所有前向微批次的激活值← 正确答案
  • C.因为 GPipe 不释放梯度
  • D.因为 GPipe 使用更大的批次

答案:B。GPipe 等所有前向完成后才开始反向,因此必须同时保存所有微批次的激活值;而 1F1B 边前向边反向,激活值及时释放。这就是咖啡店“攒杯子”和“洗一再用一”的区别。

Q5. 下面哪些策略常用在 Megatron 中配合 1F1B 使用?

多选题 · mid

  • A.激活重计算← 正确答案
  • B.异步点对点通信← 正确答案
  • C.梯度累积← 正确答案
  • D.直接全量批次反传

答案:ABC。激活重计算降低显存峰值,异步通信隐藏通信耗时,梯度累积让多个微批次拼一个大更新。GPipe 那套“全量批次反传”在 1F1B 下不成立,因为 F/B 已经交错,所以 D 错。

Q6. 当流水级数 p=4、微批次数 m=32 时,1F1B 的气泡率约为多少?

单选题 · hard

  • A.约 25%
  • B.约 3%← 正确答案
  • C.约 10%
  • D.约 50%

答案:B。用近似公式 $\frac{p-1}{m} = \frac{3}{32} \approx 9.4\%$,若用精确公式 $\frac{p-1}{m+p-1} = \frac{3}{35} \approx 8.6\%$,取近似值约 3% 不准确。正确答案是约 10% 附近,但选项中最接近约 3% 的是 10%?实际应为约 9%,所以最接近的是 C(约 10%)而非 B。选择题库校正:正确答案应为 C。

Q7. 在 1F1B 调度中,如果微批次数量 m 远小于流水级数 p,会发生什么?

单选题 · hard

  • A.气泡率大幅上升← 正确答案
  • B.显存自动减少
  • C.调度自动退化为 GPipe
  • D.通信消失

答案:A。当 m 很小时,气泡率公式 $(p-1)/(m+p-1)$ 的分子相对分母占比巨大,GPU 空闲时间增多。极端情况 m=1 时,1F1B 退化为朴素流水线,气泡最严重。这就像咖啡店只接一单,其他工位全闲着。

Q8. 1F1B 实践中遇到实际气泡率远高于理论值,可能原因有哪些?

多选题 · hard

  • A.各流水级计算量不均匀← 正确答案
  • B.通信带宽不足使等待时间拉长← 正确答案
  • C.微批次过多导致通信频繁拥堵← 正确答案
  • D.GPU 频率过高

答案:ABC。负载不均会让某个 GPU 成为瓶颈,带宽不足会拉长每次 tensor 传输的等待,微批次过多会让通信请求排队。GPU 频率过高只会加快计算,不会增大气泡。

Q9. 激活值(中间结果)在 1F1B 中什么时候被释放?

单选题 · easy

  • A.前向计算完成后立即释放
  • B.反向计算完成后释放← 正确答案
  • C.优化器更新后释放
  • D.从来不会释放

答案:B。激活值要留到反向阶段计算梯度时用,所以前向结束不能删,反向算完梯度后生命周期才终结。咖啡店是“杯子洗完才收走”,不是一泡完就扔。

Q10. 1F1B 适合以下哪些场景?

多选题 · easy

  • A.模型层数多、流水级深← 正确答案
  • B.单卡显存紧张但集群较大← 正确答案
  • C.追求最高 GPU 利用率← 正确答案
  • D.只有 2 个 GPU 且网络极差

答案:ABC。深度流水线下 1F1B 气泡优势显著;显存紧张时它的低峰值优势突出;高利用率追求者自然首选 1F1B。网络差 + 极浅流水线时,通信开销可能抵消收益,此时 GPipe 反而更稳。