LLM 流水线气泡管理:你花钱买的 GPU 正在空转
原文:https://dev.to/shrsv/pipeline-bubble-management-for-llms-the-gpus-you-paid-for-are-waiting-28hg(作者 @shrsv)
你可能坐拥八块昂贵的 GPU、一个充分优化的模型、高速网络、FlashAttention、连续批处理(continuous batching),再加上一套精心调校过的运行时。
却仍然把机器的很大一部分算力浪费在彻头彻尾的空转上。
罪魁祸首往往是流水线气泡(pipeline bubble)。
流水线气泡,是流水线各阶段之间的依赖关系造成的闲置容量。一块 GPU 已经准备好干活,但它需要的数据还没送到;又或者分配给某个阶段的工作已经做完,这个阶段只能干等着。
这是一类随着 LLM 规模变大而愈发重要的系统问题。
有意思的是,解决方案很少是"买更快的 GPU"。
解决方案通常是:
让流水线始终保持满载。
1. 直觉理解:像工厂一样思考
想象一家有四名工人的工厂:
Worker 1 -> Worker 2 -> Worker 3 -> Worker 4
每件产品都必须经过这四名工人之手。
如果只有一件产品:
Time ---> W1: [A] W2: [A] W3: [A] W4: [A]
三名工人大部分时间都在等待。
现在把工作拆成若干个小批次:
Time ---> W1: [A] [B] [C] [D] [E] W2: [A] [B] [C] [D] [E] W3: [A] [B] [C] [D] [E] W4: [A] [B] [C] [D] [E]
工人们就可以并发干活了。
工厂执行单个操作的速度并没有变快。
它变强的地方在于让操作相互重叠。
这正是深度学习中流水线并行(pipeline parallelism)背后的核心思想。
这段历史脉络值得记住。GPipe 由 Google 的 Yanping Huang 及其同事发表在 NeurIPS 2019 上,它通过把批次切分成微批次(micro-batch),再依次推入网络的不同分区,让流水线并行在超大规模神经网络上变得切实可行。他们的演示包括一个 60 亿参数、128 层的 Transformer,训练覆盖 100 多种语言。([Google Research][1])
问题在于,即便是流水线,也有启动和收尾的开销。
这些开销就是气泡。
2. 气泡从何而来
假设你有:
p = 4 pipeline stages m = 4 micro-batches
暂且忽略通信开销,一个简化后的调度示意如下:
Time Stage 1: A A A A Stage 2: A A A A Stage 3: A A A A Stage 4: A A A A
一开始,只有阶段 1 有活可干。
接着阶段 2 苏醒。
然后是阶段 3。
再然后是阶段 4。
这段启动期就是填充(fill)。
结束时则发生相反的过程。工作从流水线中逐渐排空,各阶段相继闲置。
对于经典的 GPipe 式调度,气泡占比有一个很好用的近似公式:
bubble_fraction = (p - 1) / (m + p - 1)
其中:
p = number of pipeline stages m = number of micro-batches
假设你用 8 块 GPU 和 8 个微批次:
bubble = (8 - 1) / (8 + 8 - 1)
= 7 / 15
= 46.7%
这样的利用率堪称灾难。
把微批次增加到 64:
bubble = 7 / (64 + 8 - 1)
= 7 / 71
= 9.9%
GPU 并没有变快。
你只是给了流水线更多可以相互重叠的工作。([arXiv][2])
由此可以得到一条实用的工程法则:
micro-batches >> pipeline stages
一个有 32 个阶段却只有 8 个微批次的流水线,在结构上就很难保持忙碌。
而一个 8 个阶段、128 个微批次的流水线,可重叠的机会要多得多。
3. LLM 让问题变得更有意思
LLM 推理引入了另一个不均衡来源。
一个 LLM 请求包含两个本质上截然不同的阶段:
Prefill -> Decode -> Decode -> Decode -> ...
Prefill(预填充)
模型会一次性消费整个输入提示词。
对于一个 4000 token 的提示词,它可能并行处理数千个 token。
这个阶段计算密集,通常能高效利用 GPU。
Decode(解码)
模型每次只生成一个新 token。
因此一个请求的行为大致如下:
Prefill: 4000 tokens Decode: 1 token Decode: 1 token Decode: 1 token ...
两个阶段的硬件特性因此完全不同。
Prefill 偏向计算密集型。
Decode 则往往受内存搬运和 KV cache 访问的制约,算术强度可能低得多。
现在想象把这些请求放进流水线。
某个 micro-batch 可能装着一次大规模 prefill:
████████████████████
另一个可能只包含几个 decode 步骤:
██
各个 stage 的工作量不再相等。
于是会出现第二种气泡:
Stage 1: ████████████████████ Stage 2: ████████████ Stage 3: █████ Stage 4: ███
从技术上讲,流水线里确实塞满了请求。
它只不过是负载严重失衡而已。
这正是 SARATHI 要解决的问题。SARATHI 是 2023 年由微软印度研究院与佐治亚理工学院的 Amey Agrawal、Ashish Panwar、Jayashree Mohan 及其同事提出的系统。它没有把 prefill 和 decode 当作互不相关的工作负载,而是将大的 prefill 切分成块,并把一个 prefill 块与多个 decode 请求组合在一起。核心思想是让连续多个 micro-batch 的工作量更加均匀。([arXiv][3])
他们的实验很好地提醒我们:“更多批处理”这种说法太含糊了。
真正重要的问题是:
批次的构成应该是什么样的?
4. 第一个真正的杠杆:micro-batch 大小
假设模型让一个 micro-batch 通过一个流水线 stage 大约需要:
T = 10 ms
同时你有:
p = 8 stages m = 8 micro-batches
有效工作量大约是:
8 micro-batches * 10 ms = 80 ms
但流水线需要先填充再排空。
理想情况下的总耗时正比于:
m + p - 1 = 8 + 8 - 1 = 15 time units
如果改成:
m = 64 m + p - 1 = 64 + 8 - 1 = 71
问题就会大幅缩小。
填充成本并没有变。
你只是把它摊销到了更多的有效工作上。
这与许多排队系统背后的基本原理相同:
fixed overhead / amount of work
随着有效工作量增加,这个比值会变小。
但这里有个坑。
Micro-batch 会消耗内存。
在执行期间,对于仍在流水线上处理中的工作,中间激活值或 KV cache 状态必须一直驻留在内存里。
于是我们遇到了一个系统层面的权衡:
more micro-batches
|
+--> smaller bubbles
|
+--> more memory pressure
而且还有另一个问题:
smaller micro-batches
|
+--> worse GPU kernel efficiency
GPU 通常需要足够多的工作量,才能形成大型、高效的矩阵运算。
所以工程目标不是:
maximize micro-batches
而是:
maximize useful overlap subject to memory + kernel-efficiency + latency constraints
5. 第二个杠杆:让各 stage 负载均衡
想象四块 GPU 分别承担以下负载:
GPU 0: 100 units GPU 1: 100 units GPU 2: 100 units GPU 3: 160 units
整条流水线的吞吐量由 GPU 3 决定。
其余 GPU 会不断重复这样的状态:
done waiting done waiting done waiting
这相当于你买了一台 160 单位的机器,又给它外挂了三台 100 单位的机器。
这正是流水线切分(partitioning)重要的原因。
如果工作可以重新分配:
Before: 100 | 100 | 100 | 160 After: 115 | 115 | 115 | 115
最慢的 stage 就从 160 降到了 115。
整条流水线都会因此受益。
用数学语言来说,稳态吞吐量的一个简单近似是:
throughput ~= 1 / max(T1, T2, ..., Tp)
其中 Ti 是第 i 个 stage 的执行时间。
这个 max() 很关键。
流水线不在乎平均每个 stage 耗时 112 ms。
它在乎的是最慢的 stage 耗时 160 ms。
因此 DeepSpeed 提供了显式的机制来跨流水线 stage 切分模型,包括基于参数量和基于层数的切分方式。它的文档也直接点明了实操要点:流水线性能高度依赖负载均衡。([DeepSpeed][4])
对 LLM 而言这一点尤其关键,因为在真实系统里各层并不总是完全相同的。
不同层可能有不同的内存行为。
注意力密集的组件与 MLP 密集的组件表现可能不同。
通信开销也会因层的放置位置而异。
一个按参数量看很均衡的切分方案,按实际运行时间衡量可能仍然不均衡。
正确的衡量指标通常是:
wall-clock stage time
而不是:
number of layers
6. 第三个杠杆:改变调度
流水线系统的历史中还藏着另一条洞见。
最朴素的策略是:
前向计算所有 micro-batch 反向计算所有 micro-batch
GPipe 的 fill-drain(填充-排空)方案就是这么工作的。
另一种做法是把前向和反向操作交错起来:
1F1B 1 次前向 1 次反向 1 次前向 1 次反向 ...
PipeDream 与 Aaron Harlap、Deepak Narayanan、Amar Phanishayee、Vivek Seshadri 等人相关,它在分布式 DNN 训练中探索了这种批间流水线方式。该系统明确以更好的重叠和更高的加速器利用率为目标。([arXiv][5])
这里值得吸取的系统级经验,比具体算法本身更宽泛:
调度本身就是一种资源分配策略。
想一想:
GPU 0:
F1 F2 F3 F4 F5 F6
GPU 1:
F1 F2 F3 F4 F5 F6
GPU 2:
F1 F2 F3 F4 F5 F6
再对比一个只要依赖关系允许、就立刻启动反向计算的调度方案。
你改变的是:内存在何时分配、通信在何时发生、以及每块 GPU 在何时空闲下来。
在推理系统中,同一个思想以不同的形式出现。
你可能有:
prefill 队列 decode 队列 等待中的请求 KV-cache 内存 GPU 算力
由调度器决定下一项进入流水线的工作是什么。
这让调度变成了一个优化问题。
一个粗糙的目标函数大概长这样:
最大化: GPU 利用率 + 吞吐量 - 延迟惩罚 - 内存压力 - 调度开销
真实系统要复杂得多,但这个心智模型很有用。
7. 经济学视角:流水线气泡就是闲置的现金
假设你租用了:
8 块 GPU 每块 $3/小时
你的基础设施成本是:
8 * $3 = $24/小时
现在假设你的有效利用率是 60%。
粗略地说,你付出的是:
$24/小时
换来的却更接近:
8 * 0.60 = 4.8 块满负荷 GPU 等效
也就是说:
每块满负荷 GPU 的有效成本 = $24 / 4.8 = $5/小时
在不新购任何东西的前提下,把利用率从 60% 提升到 80%:
8 * 0.80 = 6.4 块满负荷 GPU 等效
现在:
$24 / 6.4 = $3.75/小时
你实际上把每单位有效算力的基础设施成本降低了大约:
1 - 3.75/5 = 25%
这就是为什么流水线气泡管理不仅是一个 GPU 编程问题,同样是一个经济学问题。
同样的推理也适用于延迟。
设想一个这样的服务:
GPU 计算 = 30 ms 流水线等待 = 20 ms 网络 = 5 ms 排队 = 10 ms
用户实际体验到的大约是:
65 ms
而其中只有 30 ms 是真正的模型计算。
把矩阵乘法提速 10%,得到的是:
30 -> 27 ms 总计: 27 + 20 + 5 + 10 = 62 ms
消除 20 ms 的流水线气泡,得到的是:
30 + 0 + 5 + 10 = 45 ms
效果更大的那项优化,不是动 GPU kernel 的那个。
而是动 GPU kernel 周围的系统 的那个。
这才是流水线管理更深层的一课。
当你的 LLM 系统逐渐变大时,先问:
工作在哪里等待?
再去问:
我怎样才能让计算更快?
8. 实战中如何调试流水线气泡
最容易犯的错误,是只看 GPU 的整体利用率。
假设你的监控面板显示:
GPU 利用率:72%
这并不能告诉你是否存在流水线问题。
你需要的是一条时间线。
例如:
GPU 0: ███████████████████████████████ GPU 1: █████████████████████████████ GPU 2: █████████████████████████ GPU 3: █████████████████████
对比:
GPU 0: ███████████████████████████████ GPU 1: ███████████████████████████████ GPU 2: ███████████████████████████████ GPU 3: ███████████████████████████████
第二种才是你想要的。
对一个真实的 LLM 服务,至少要对以下环节埋点:
请求到达 队列等待 prefill 开始/结束 decode 开始/结束 stage 开始/结束 通信开始/结束 KV-cache 分配 KV-cache 驱逐 请求完成
然后计算:
stage_utilization_i = busy_time_i / wall_clock_time
以及:
pipeline_efficiency = useful_work / total_pipeline_work
还要测量各 stage 执行时间的波动程度:
CV = standard_deviation(stage_time) / mean(stage_time)
变异系数(CV)偏高往往是一个警示:名义上均衡的负载,实际上并不均衡。
对推理来说,prefill 和 decode 要分开测量。
一个服务可能整体吞吐量漂亮,尾延迟却很糟糕,因为长 prefill 会一次次打断 decode。
这正是促使 SARATHI 尝试让 micro-batch 更均匀的那种负载间相互作用。([Microsoft][6])
9. 值得记住的心智模型
真正需要调节的旋钮其实只有四个:
Pipeline Performance
|
+--------------+--------------+
| | |
Micro-batches Stage balance Schedule
| | |
fill/drain bottleneck ordering
|
Memory
而对现代 LLM 推理来说,还有第五个:
Work composition prefill <-> decode
核心方程简单得近乎令人尴尬:
pipeline throughput ~= 1 / slowest_stage_time
而核心直觉还要更简单:
idle hardware is lost capacity
这就是为什么流水线气泡管理值得被看作 LLM 工程的一门一等公民学科。
LLM 著名的扩展(scaling)叙事通常围绕 FLOPs、参数量、内存带宽、attention 内核、量化和互连展开。
这些当然重要。
但一旦你拥有一个足够大的分布式系统,另一个问题就变得同样重要:
每一时刻,这台机器到底有多少比例在做有用的工作?
GPipe 证明了微批处理(micro-batching)能让超大模型变得切实可行;PipeDream 展示了调度与流水线执行如何让分布式加速器保持高效产出;SARATHI 则表明,在 LLM 推理中,连微批次的构成都能决定气泡会膨胀到多大。([Google Research][1])
因此,现代 LLM 工程师面对的优化问题略有不同。
不再是:
"How fast is my GPU?"
而是:
"For every millisecond of my GPU bill, how many milliseconds contain useful work?"
这就是流水线气泡管理。
你在实践中遇到过哪种流水线瓶颈:微批次气泡、各阶段负载不均、prefill/decode 相互干扰,还是通信停顿?
[1]: https://research.google/pubs/gpipe-efficient-training-of-giant-neural-networks-using-pipeline-parallelism/ "GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism"
[2]: https://arxiv.org/abs/1811.06965 "GPipe: Efficient Training of Giant Neural Networks using Pipeline Parallelism"
[3]: https://arxiv.org/abs/2308.16369 "SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills"
[4]: https://www.deepspeed.ai/tutorials/pipeline/ "Pipeline Parallelism - DeepSpeed"
[5]: https://arxiv.org/abs/1806.03377 "PipeDream: Fast and Efficient Pipeline Parallel DNN Training"
[6]: https://www.microsoft.com/en-us/research/publication/sarathi-efficient-llm-inference-by-piggybacking-decodes-with-chunked-prefills/?msockid=1115aefb038768df34d0b8ae025b690c&utm_source=chatgpt.com "SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills - Microsoft Research"
原文:https://dev.to/shrsv/pipeline-bubble-management-for-llms-the-gpus-you-paid-for-are-waiting-28hg(作者 @shrsv)