📑 本页目录(点开跳转)
17 · 连续批处理与 PagedAttention
⏱ 38 分钟 | ⭐ vLLM 快 20 倍的两个原因
🎯 一句话
静态批处理让快的请求等慢的,朴素显存管理浪费 60-80% 的 KV Cache。
这一章的两个技术分别解决这两件事 ——
它们合起来就是现代推理引擎和 model.generate() 之间的全部差距。
🐌 一、问题一:静态批处理在浪费什么
流程图
🔑 问题的根源:LLM 的输出长度是不可预测的。 一个请求可能输出 10 个 token,另一个 2000 个 —— 差 200 倍。 静态批处理让整批的效率被最长的那个请求决定。
⭐ 二、连续批处理(Continuous Batching)
流程图
对照
时间 →
槽位1 [A A A A][E E E E E E E E E] ← A 完成后 E 立刻补上 ⭐
槽位2 [B B B B B B B B][F F F F F]
槽位3 [C C C C C C C C C C C C C C]
槽位4 [D D D][G G G G G][H H H H H]
↑ 随时进出,GPU 一直满载
| 收益 | 说明 |
|---|---|
| 吞吐 | 提升 5-20 倍(取决于输出长度的方差)⭐ |
| 排队延迟 | 从"等一整批"降到"等一个 token 的时间" |
| GPU 利用率 | 从锯齿状变成持续满载 |
💡 为什么收益这么大:输出长度方差越大,静态批处理浪费越多。 真实流量里长短请求混杂,方差极大 —— 所以连续批处理的收益在生产环境里比 benchmark 还明显。
⚠️ Prefill 和 Decode 混在一起的麻烦
结果对照
⭐ P/D 分离是 2024 年之后大规模部署的重要趋势: Prefill 和 Decode 的硬件需求完全不同(第 15 章)—— Prefill 要算力,Decode 要带宽。分开部署可以各自用最合适的卡和并行策略。
💸 P/D 分离的那笔账:KV 要跨机搬
上面那句"分开部署"听起来是免费的午餐,它不是。
因果链
搬多少?直接用第 16 章的公式接着算。以 Llama-3 70B(80 层、GQA-8、head_dim 128、BF16) 为例:
算一算
每 token 每层 = 2 × 8 × 128 × 2 字节 = 4 KB
× 80 层 = 320 KB / token ⭐
→ 4000 token 的输入:320 KB × 4000 ≈ 1.28 GB 要搬这么多
再看这条链路有多宽(附录 A 的带宽表):
| 链路 | 带宽 | 搬 1.28 GB 要多久 |
|---|---|---|
| HBM(同一张卡内,不用搬) | 2000 GB/s | — |
| NVLink(同一台机器) | 450–900 GB/s | 1.4–2.8 ms |
| InfiniBand(跨机)⚠️ | 25–50 GB/s | 26–51 ms 💀 |
| 以太网 | 1–12 GB/s | 107 ms – 1.3 s 💀💀 |
⚠️ 注意 InfiniBand 那一行:它只有 HBM 的约 1/60。 这就是 P/D 分离真正的代价 —— 你把一件"本来在显存里现成就有"的事,变成了一次跨机传输。 这条链路直接加在 TTFT 上:算完 Prefill 还要再等几十毫秒才能开始吐第一个字。
所以它什么时候划算、什么时候不划算:
| 情况 | 判断 | 为什么 |
|---|---|---|
| 输入长、输出也长 | ✅ 划算 | 传输是一次性的,摊到几百个 decode step 上可以忽略 |
| 超大规模、Prefill 和 Decode 各自要几十上百张卡 | ✅ 划算 ⭐ | 两边能各自选卡型和并行度,省下的钱远大于传输代价 |
| 输入短(几百 token) | ❌ 别分 | 搬运的固定开销比省下来的还多 |
| 输出短(比如只出几十个 token) | ❌ 别分 | 传输摊不开,直接把 TTFT 拉高 |
| 低并发 / 单机就够 | ❌ 别分 ⭐ | 分块 Prefill 已经能解决 TPOT 毛刺,别为了架构好看付这笔钱 |
| 只有以太网 | ❌ 别分 💀 | 上表最后一行,几百毫秒起步 |
⭐ 顺序不要搞反:先上分块 Prefill(一个参数的事), 只有当规模大到"Prefill 和 Decode 想用不同卡型 / 不同并行度"时,P/D 分离才开始值。
💡 业界有专门做这层 KV 传输的组件(Mooncake、LMCache 这类), 核心思路都是把 KV 当成一份可以跨节点存取和复用的缓存: 传输和计算重叠(一层算完就开始传这一层)、复用已有前缀(不用重传)、 必要时经 CPU 内存中转。⚠️ 但它们改变的是常数,不是量级 —— 上面那张带宽表是物理事实,先看你的输入输出长度配不配得上这笔搬运费。
📄 三、PagedAttention:把虚拟内存搬过来
信息关系
结果对照
信息关系
⭐ 意外的收益:写时复制的共享
流程图
🔗 上面 ②③ 两条都是「解码策略」的名字,这一章只把它们当负载形态用,没讲它们各自在干什么: 《大模型全景导论》06b · 解码策略。
⭐ 去那里主要为了两件对容量规划很实际的事:
① Beam Search 和「并行采样 n 个候选」看着都是 n 条路,用途完全相反: beam 是全局按累计对数概率排序取前 k,目标是「最可能的那一句」,所以 k 条 beam 常常只差一两个词; 并行采样是 n 次独立抽签,目标恰恰是「不一样的 n 个结果」。 ⚠️ 那一章的明确结论:要 n 个不同的候选就采样 n 次,别用 beam search。
② beam 的成本是 k 份 KV Cache——这正是块共享在救的东西,也是它值得在这一节被点名的原因;
但块共享只能省掉公共前缀那部分,分歧之后每条 beam 的显存还是各算各的。
那一章还有一个真实的搬运事故:把翻译服务调好的 num_beams=5 原样搬到「生成营销文案」上。
🔑 这是 PagedAttention 最漂亮的地方: 它解决碎片问题的同时,顺带让"共享前缀"变成几乎免费的。 而共享前缀在 Agent、多候选生成、树搜索里到处都是。
🧰 四、实际怎么用
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-3.1-8B-Instruct",
tensor_parallel_size=2, # 张量并行(第 12 章)
gpu_memory_utilization=0.90, # ⭐ 留 10% 余量,调高能装更多 KV
max_model_len=8192, # ⭐ 别设太大,它决定预留空间
enable_prefix_caching=True, # ⭐ 前缀缓存(第 16 章)
kv_cache_dtype="fp8", # ⭐ KV 量化,能开 2 倍 batch
enable_chunked_prefill=True, # ⭐ 避免长 prefill 造成 TPOT 毛刺
)
outs = llm.generate(prompts, SamplingParams(temperature=0.7, max_tokens=512))
四个最该调的参数:
| 参数 | 作用 | 调法 |
|---|---|---|
gpu_memory_utilization |
留给 KV Cache 的显存比例 | 从 0.90 起,OOM 就降 ⭐ |
max_model_len |
最大上下文 | 按实际需求设,设大了白白预留 ⭐ |
max_num_seqs |
最大并发请求数 | 吞吐/延迟的权衡旋钮 |
enable_chunked_prefill |
分块 prefill | 有长输入时必开 |
📊 五、调度策略:吞吐和延迟的旋钮
信息关系
| 策略 | 适合 |
|---|---|
| FCFS(先来先服务) | 公平,默认 |
| 优先级队列 | 区分付费/免费用户 |
| 最短作业优先 | 降低平均延迟(但需要预测输出长度,难) |
限制 max_num_seqs |
⭐ 保证 P99 延迟的最直接手段 |
💡 一个实用建议:用
max_num_seqs来守 SLO。 先压测出"并发数 → P99 TPOT"的曲线, 找到满足 SLO 的最大并发数,把它设成上限 —— 超出的排队,而不是拖垮所有人。
🔗 和站内其他章的关系
| 相关的地方 | 这里的位置 |
|---|---|
| 第 15 章 批处理提吞吐 | 本章是怎么在动态请求下做到 ⭐ |
| 第 15 章 Prefill/Decode 性质不同 | 混合调度的麻烦 + P/D 分离 |
| 第 16 章 碎片浪费 60-80% | PagedAttention 解决它 ⭐ |
| 第 16 章 前缀缓存 | 靠 PagedAttention 的块共享实现 |
| 第 20 章 SLO | 调度策略的目标 |
| 《大模型全景导论》06 推理优化 PagedAttention 一节 | 那边只说"借鉴了虚拟内存";块表、写时复制、抢占这三件事在这里 ⭐ |
| 《模型上线之后》18 成本与容量 3 秒抖动变成 22 分钟不可用 | 本章的"抢占"正是这种排队雪崩的起点 —— 所以 max_num_seqs 要按压测设死 ⚠️ |
✅ 检查点
- 静态批处理浪费在哪?根源是什么?
- 连续批处理的核心思想是什么?吞吐提升多少?
- 为什么真实生产环境里连续批处理的收益比 benchmark 还明显?
- Prefill 插进 Decode 批次里会造成什么问题?三种解法是什么?
- P/D 分离的动机是什么?
- P/D 分离的代价是什么?70B 模型 4000 token 的输入,要搬多少 KV?跨机走 IB 大概要多久?
- 什么时候【不该】上 P/D 分离?该先做什么?
- PagedAttention 借鉴了什么思想?它怎么消除两种碎片?
- KV Cache 利用率从多少提到多少?
- PagedAttention 的"意外收益"是什么?举三个受益场景。
- 守 P99 延迟最直接的手段是什么?
👀 答案
- 浪费在已完成的请求还占着槽位空转,新请求要等整批结束。根源:LLM 的输出长度不可预测(可能差 200 倍),整批效率被最长的请求决定。
- 以迭代为单位调度而不是以请求为单位——每生成一个 token 后就检查完成的踢出、新来的加入。吞吐提升 5-20 倍。
- 因为输出长度方差越大浪费越多,而真实流量里长短请求混杂、方差极大。
- 长 Prefill(算力密集)插进来会让这一步变很慢,所有正在 decode 的请求都卡一下 → TPOT 毛刺。解法:①分块 prefill ②P/D 分离 ③限制每步 prefill token 预算。
- 因为 Prefill 要算力、Decode 要带宽,硬件需求完全不同,分开部署可以各自用最合适的卡和并行策略。
- 代价是 Prefill 算出来的 KV Cache 必须跨机搬到 Decode 节点。70B(80 层、GQA-8、head_dim 128、BF16)每 token 320 KB,4000 token 就是 约 1.28 GB;InfiniBand 只有 25–50 GB/s(HBM 的约 1/60)→ 26–51 ms,而且这段时间直接加在 TTFT 上。
- 输入短、输出短、低并发、只有以太网这四种情况都不该分。该先做的是分块 Prefill——它只是一个参数,已经能解决 TPOT 毛刺;只有规模大到"Prefill 和 Decode 想用不同卡型/不同并行度"时,P/D 分离才开始值。
- 借鉴操作系统的虚拟内存分页。KV Cache 切成固定大小的块,每个请求一张块表记录逻辑块→物理块映射,物理块不需要连续。内部碎片最多浪费一个块(<16 token),外部碎片因为块大小统一而完全消除。
- 从 20-40% 提到 90%+,能开的 batch 大 2-4 倍。
- 块共享(写时复制)——因为是按块管理+引用计数。三个场景:前缀共享(相同系统提示词)、并行采样(同一 prompt 生成 n 个候选,省 50%+ 显存)、Beam Search(各 beam 共享公共前缀)。
- 限制
max_num_seqs。先压测出"并发数→P99 TPOT"曲线,找到满足 SLO 的最大并发设成上限,超出的排队而不是拖垮所有人。
🛑 可以停在这里
⚡ 走神救援
⭐ vLLM 比
model.generate()快一个量级就靠这两个技术。问题一是静态批处理:凑齐一批跑完再收下一批,做完的请求占着槽位空转。根源是 ⭐ 输出长度不可预测(可能差两个数量级),整批效率被最长的那个请求决定。连续批处理把调度单位从「请求」换成「迭代」——每生成一个 token 就踢出完成的、放进新的。⭐ 真实流量方差大,所以生产收益比 benchmark 上还明显。
⚠️ 代价是 Prefill 插进 Decode 批次会造成 TPOT 毛刺。三个解法里 ⭐ 顺序千万别搞反:先上分块 Prefill(一个参数的事,已经能解决毛刺)。💸 P/D 分离不是免费的:Prefill 算出的 KV 要跨机搬——七十亿级模型几千 token 的输入就是 GB 量级,而机间带宽比显存低约两个数量级,这段时间直接加在 TTFT 上。⭐ 所以只有输入长且输出也长、或规模大到两边想用不同卡型时才划算。
问题二是显存碎片(利用率只有两三成)。PagedAttention 把操作系统的虚拟内存分页搬过来:KV Cache 切成固定块、每请求一张块表,⭐ 物理块不需要连续——内部碎片最多浪费一个块、外部碎片完全消除。
⭐ 意外收益是块共享加写时复制:前缀共享、并行采样、Beam Search 共享公共前缀几乎免费。
⭐ 守 P99 最直接的手段是限制最大并发序列数——让超出的排队,而不是拖垮所有人。 ⚠️ 另外
max_model_len设大了是白白预留显存。
下一节 👉 18-量化.md