你有没有在咖啡店排过队?假设店里只有一台咖啡机,顾客点单后,咖啡师要先“接单”(前向计算),再“做咖啡”(反向传播)。如果所有顾客都先接单再做咖啡,那第一位顾客的咖啡还没做完,后面十位顾客的单子已经堆成山——这就是流水线并行里最头疼的“气泡”(bubble)。而1F1B调度策略,就像一位聪明的咖啡师:接一单,做一杯,再接一单,再做一杯。听起来简单,但这份“交替劳作”的节奏,恰恰能让整条流水线既不饿肚子、也不堆盘子,还把咖啡豆的库存(显存)压到最低。这篇文就用咖啡店的故事,带你彻底看懂1F1B的调度魔法。
对 AI 从业者而言,掌握1F1B是理解和优化大模型分布式训练的核心技能,它能直接转化为降低训练成本、提升 GPU 利用率的工程能力,是面试和实战中绕不开的硬通货。
可视化演示
展示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):数据从第一层流到最后一层,计算每层的输出并保存中间结果(激活值),就像咖啡师依次完成接单、磨豆、萃取。
2. 反向计算(Backward):从最后一层向前,根据损失梯度更新每层权重,就像咖啡师根据顾客反馈调整口味。
3. 微批次(micro-batch):把一个大批次切成更小份,轮流进入流水线,对应于咖啡店里“一单”的分量。
4. 交替执行:每个流水级按“F→B→F→B”的节奏推进,避免连续前向造成显存堆积。
所以,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 在某个时间步要么做一次前向,要么做一次反向,不能同时做两件事。
2. 流水级 s 的输入:来自上一流水级的输出,必须等上一级计算完成。
3. 1F1B 的交替规则:同一 GPU 上,相邻两个微批次的操作类型相反(F 后接 B,或 B 后接 F)。
4. 气泡率公式:气泡占比约等于流水级数减一,除以两倍微批次数加流水级数减一。
归根结底,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 调度:所有前向先执行,再所有反向执行,导致流水线先满后空。
2. 1F1B 调度:前向反向交错,GPU 永远有活干。
3. GPipe 气泡率:气泡是空窗时间占比,公式同上但系数不同。
4. 1F1B 气泡率:由于交错,气泡率近似减半。
从这些公式可以读出关键结论:当微批次数量足够大时,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 个。
2. 峰值显存公式:显存峰值 = 参数 + 梯度 + 优化器状态 + 激活值。
3. 1F1B 激活值上限:激活值与微批次数量无关,只与单批大小相关。
4. 支持更大模型:显存瓶颈从激活值转移到参数和优化器状态。
显存优势是 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. 状态机转换:每个微批次的状态按序迁移,确保不重不漏。
2. 通信隐藏公式:实际耗时 = 计算时间 + 通信时间(若异步重叠则几乎为纯计算)。
3. 梯度累积:多个微批次累积梯度,再统一更新参数。
Megatron 的实现告诉我们,1F1B 不仅仅是调度算法,更是一套工程系统。状态机保证了正确性,异步通信保证了效率,配置化让你能灵活适应不同的硬件。
实践注意事项
理论完美,坑也不少。实践中最常见的问题是微批次大小和数量的选择。微批次太小,通信次数太频繁,反而不划算(咖啡师频繁切换器具,浪费时间);微批次太大,气泡变大,显存压力也上来。业界经验:微批次数量至少是流水级数的 2-4 倍,且每个微批次的样本数不宜超过 16。
通信开销管理是第二个坑。1F1B 的交替模式让通信次数翻倍(每个微批次都要传前向和反向两个向量),虽然通过异步隐藏了一部分,但通信与计算的比值仍然决定了扩展性的上限。如果你的集群带宽不足(比如千兆以太网而不是 InfiniBand),1F1B 的优势会被通信拖后腿,这时候可以考虑减少微批次数量,让每次通信的数据量更大。
调试和监控也不可忽视。可以用 TensorBoard 或自定义 hook 记录每个流水级的忙闲时间,计算实际气泡率。如果气泡率远高于理论值,多半是负载不均(某个 GPU 计算特别慢)或通信阻塞。咖啡店的做法是给每个工位装监控,看谁在偷懒,然后优化排班。
1. 最优微批次数量:经验法则 m ≥ 2p,让气泡足够小。
2. 通信比公式:通信比 = 通信时间 / 计算时间,小于 10% 才算健康。
3. 气泡率验证:可观测的实际气泡率应为理论值 ± 5%。
实践的本质是在吞吐和易用性之间找平衡。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 演示
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()
class PipelineStage(nn.Module)定义流水级:每个 GPU 持有一小段模型的层,相当于咖啡店里一个固定工位。def run_1f1b_schedule(stages, num_micro_batches, batch, loss_fn, optimizers)核心调度函数,模拟 1F1B 的逐级前向和反向。x = stage(x)数据依次流经每个流水级,而每级只负责自己的计算。total_loss.backward()从最后一层逐层回传梯度,对应咖啡师根据反馈反向调整每个环节。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”指的是什么?
- A.一次前向计算、一次反向计算← 正确答案
- B.一个数据、一个标签
- C.一台机器、一块 GPU
- D.一代模型、一批数据
答案:A。1F1B 是 One Forward One Backward 的缩写,即一次前向传播和一次反向传播的交替执行。这就像咖啡师“接一单做一杯”的节奏。选项 B/C/D 是对缩写的望文生义。
Q2. 1F1B 相比 GPipe 的最大优势是什么?
- A.实现更简单
- B.气泡更小、显存峰值更低← 正确答案
- C.不需要通信
- D.可以完全避免反向计算
答案:B。1F1B 通过交替调度让 GPU 更少空闲(气泡小),同时只保留当前微批次的激活值(显存低)。GPipe 虽然实现简单,但气泡大、激活值堆积。咖啡店里“边做边洗杯子,而不是攒一堆再洗”。
Q3. 气泡率公式 $\frac{p-1}{m+p-1}$ 中,p 和 m 分别代表什么?
- A.p 是参数数量,m 是模型大小
- B.p 是流水级数,m 是微批次数← 正确答案
- C.p 是 GPU 数量,m 是批次大小
- D.p 是层数,m 是数据并行度
答案:B。p 是流水线级数(stage 数量),m 是微批次数(micro-batch count)。公式显示:微批次越大,气泡越小;流水级越多,气泡越大。这是 1F1B 调参的基础。
Q4. 为什么 GPipe 的显存峰值比 1F1B 高?
- A.因为 GPipe 计算量更大
- B.因为 GPipe 需要保存所有前向微批次的激活值← 正确答案
- C.因为 GPipe 不释放梯度
- D.因为 GPipe 使用更大的批次
答案:B。GPipe 等所有前向完成后才开始反向,因此必须同时保存所有微批次的激活值;而 1F1B 边前向边反向,激活值及时释放。这就是咖啡店“攒杯子”和“洗一再用一”的区别。
Q5. 下面哪些策略常用在 Megatron 中配合 1F1B 使用?
- A.激活重计算← 正确答案
- B.异步点对点通信← 正确答案
- C.梯度累积← 正确答案
- D.直接全量批次反传
答案:ABC。激活重计算降低显存峰值,异步通信隐藏通信耗时,梯度累积让多个微批次拼一个大更新。GPipe 那套“全量批次反传”在 1F1B 下不成立,因为 F/B 已经交错,所以 D 错。
Q6. 当流水级数 p=4、微批次数 m=32 时,1F1B 的气泡率约为多少?
- 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,会发生什么?
- A.气泡率大幅上升← 正确答案
- B.显存自动减少
- C.调度自动退化为 GPipe
- D.通信消失
答案:A。当 m 很小时,气泡率公式 $(p-1)/(m+p-1)$ 的分子相对分母占比巨大,GPU 空闲时间增多。极端情况 m=1 时,1F1B 退化为朴素流水线,气泡最严重。这就像咖啡店只接一单,其他工位全闲着。
Q8. 1F1B 实践中遇到实际气泡率远高于理论值,可能原因有哪些?
- A.各流水级计算量不均匀← 正确答案
- B.通信带宽不足使等待时间拉长← 正确答案
- C.微批次过多导致通信频繁拥堵← 正确答案
- D.GPU 频率过高
答案:ABC。负载不均会让某个 GPU 成为瓶颈,带宽不足会拉长每次 tensor 传输的等待,微批次过多会让通信请求排队。GPU 频率过高只会加快计算,不会增大气泡。
Q9. 激活值(中间结果)在 1F1B 中什么时候被释放?
- A.前向计算完成后立即释放
- B.反向计算完成后释放← 正确答案
- C.优化器更新后释放
- D.从来不会释放
答案:B。激活值要留到反向阶段计算梯度时用,所以前向结束不能删,反向算完梯度后生命周期才终结。咖啡店是“杯子洗完才收走”,不是一泡完就扔。
Q10. 1F1B 适合以下哪些场景?
- A.模型层数多、流水级深← 正确答案
- B.单卡显存紧张但集群较大← 正确答案
- C.追求最高 GPU 利用率← 正确答案
- D.只有 2 个 GPU 且网络极差
答案:ABC。深度流水线下 1F1B 气泡优势显著;显存紧张时它的低峰值优势突出;高利用率追求者自然首选 1F1B。网络差 + 极浅流水线时,通信开销可能抵消收益,此时 GPipe 反而更稳。