2.2 连续批处理与推测解码


文档摘要

2.2 连续批处理与推测解码 本节摘要:PagedAttention 解决了显存碎片,但要榨干 GPU 利用率还差两个关键优化。连续批处理(continuous batching)打破了传统静态批处理"一个请求慢、整个 batch 等"的困局——请求在 token 级别动态加入和退出,GPU 始终保持满载,吞吐提升两到四倍。推测解码(speculative decoding)走另一条路降延迟:用一个小而快的草稿模型先猜几个候选 token,大模型一次性并行验证这些猜测,猜对的直接采纳,把 decode 的串行过程变成部分并行。两者一个提吞吐、一个降延迟,可以叠加使用。

2.2 连续批处理与推测解码

本节摘要:PagedAttention 解决了显存碎片,但要榨干 GPU 利用率还差两个关键优化。连续批处理(continuous batching)打破了传统静态批处理"一个请求慢、整个 batch 等"的困局——请求在 token 级别动态加入和退出,GPU 始终保持满载,吞吐提升两到四倍。推测解码(speculative decoding)走另一条路降延迟:用一个小而快的草稿模型先猜几个候选 token,大模型一次性并行验证这些猜测,猜对的直接采纳,把 decode 的串行过程变成部分并行。两者一个提吞吐、一个降延迟,可以叠加使用。

学习目标

阅读完本节,你应当能够:

  1. 区分静态批处理和连续批处理,说明后者解决的核心痛点
  2. 描述连续批处理中请求如何动态加入和退出 batch
  3. 复述推测解码的"草稿模型猜、目标模型验"两阶段流程
  4. 解释推测解码为什么能加速,以及加速来自哪里
  5. 判断什么场景适合用连续批处理、什么场景适合推测解码

一、问题与直觉

假设你用一个 batch=8 的静态批处理服务。8 个请求一起开始生成,但它们的长短天差地别——请求 A 只生成 20 token 就结束了,请求 H 要生成 800 token。问题来了:A 结束后,它的 GPU 算力和显存空着,但因为 batch 是静态的,要等 H 也结束才能开下一批。结果就是 GPU 在大部分时间里只跑了 1–2 个请求,利用率可能不到 20%。

这就是静态批处理(static batching)的核心痛点:木桶效应——最长的请求决定整个 batch 的时长,短的请求结束后资源闲置。batch 越大,浪费越严重;batch 小了又吃不饱。

连续批处理(continuous batching,也叫 iteration-level scheduling 或 dynamic batching)打破了这个困局。它的核心思想是:不在请求级别批处理,而在 token 级别。每当某个请求生成完一个 token,调度器检查有没有新请求在等,有就立刻把它加进 batch。某个请求生成结束,立刻移出 batch,资源给新请求。这样 batch 永远是满的,GPU 利用率拉满。

而推测解码解决的是另一个问题。即使 GPU 利用率满了,单个请求的 decode 还是串行的——一次只能生成一个 token,因为下一个 token 依赖上一个的输出。推测解码用一个"作弊"办法打破这个串行:让小模型先猜接下来 4 个 token,大模型一次性验证这 4 个猜测(因为是并行验证,只花一次前向传播的时间),猜对的直接采纳。如果小模型猜得准,相当于一次decode 生成了多个 token。

二、核心原理

2.1 静态 vs 连续批处理

先看两者的时序对比,差异一目了然:

静态批处理的生命周期是"组 batch → 全部跑完 → 组下一批",中间不接新请求。连续批处理的生命周期是"持续运行,每个 token 迭代后检查并调整 batch 成员"。后者能保持高利用率的关键是:得益于 PagedAttention 的分块显存管理,加一个新请求只是从块池领几块、改一下页表,不需要预留连续大空间,也不影响正在跑的其他请求。

2.2 连续批处理的调度逻辑

连续批处理的调度器每个迭代步做三件事:

这里有个细节:新加入的请求要先做 prefill(处理它的整个 prompt),这和正在 decode 的请求的计算模式不同。早期的连续批处理实现会让 prefill 和 decode 分开做(要么先处理完所有 prefill 再 decode,要么轮流),但这会造成卡顿。后来的改进(比如 vLLM 的 chunked prefill)把长 prompt 的 prefill 切成小块,和 decode 混在一起迭代,让延迟更平滑。

2.3 推测解码的两阶段流程

推测解码(也叫 speculative sampling 或 draft-verify)的核心是利用一个事实:大模型一次前向传播算多个 token 的概率分布,和算一个的成本差不多(只要它们在一次 attention 里)。所以与其让大模型一个个串行生成,不如让它一次验证多个候选。

加速来自哪里?假设草稿模型猜 4 个 token,平均接受 2.5 个。那么大模型每做一次前向传播,平均推进了 2.5 个 token(而不是 1 个)。如果草稿模型足够快(它的前向成本远低于大模型),整体 decode 速度能提升 1.5 到 2.5 倍。

2.4 接受率和什么有关

推测解码的收益高度依赖"接受率"——草稿模型猜对的概率。这取决于草稿模型和目标模型的分布有多接近。理想情况是草稿模型是目标模型的蒸馏版,二者分布接近,接受率高。如果草稿模型太弱(和目标分布差很远),接受率低,加上草稿模型本身的前向开销,可能反而变慢。

草稿模型选择 接受率 草稿开销 净收益
目标模型的小型蒸馏版 高(70%+) 明显加速
同家族更小参数模型 中(50%) 有收益
任意小模型 低(20%) 收益微薄甚至倒退
n-gram 统计模型 不稳定 极低 简单文本有效

⚠️ 常见坑:别拿一个和目标模型完全不同家族的小模型当草稿。比如目标是某主流大模型,草稿用一个不相干的小模型,二者分布差异巨大,接受率可能只有 10–20%,草稿的前向开销都赚不回来。草稿模型最好和目标同源,或者直接用目标模型前面几层的浅版本。

三、工程实践要点

3.1 连续批处理的关键参数

落地连续批处理,几个参数要调好:

最大 batch 大小:决定同时能服务多少请求。受显存池大小约束(每个请求占若干 KV Cache 块)。设太大显存溢出,设太小吞吐上不去。建议从显存允许的最大值开始,压测观察延迟,找到吞吐和延迟的平衡点。

最大等待 token 数(max num batched tokens):限制单次迭代处理的 token 总数(包括新请求的 prefill)。这是为了防止单个超长 prompt 的 prefill 把整个 batch 卡住。常见值几千到一万。

prefill 调度策略:新请求来时是立刻 prefill 还是攒一批。立刻 prefill 延迟低但可能打断正在 decode 的请求;攒一批吞吐高但首字延迟高。多数场景用混合策略——短 prompt 立刻处理,长 prompt 切块混入。

# 概念性调度器骨架 class ContinuousBatcher: def __init__(self, max_batch, max_tokens): self.max_batch = max_batch self.max_tokens = max_tokens self.running = [] # 正在decode的请求 self.waiting = [] # 待prefill的请求 self.engine = InferenceEngine() def step(self): # 1. 移除已完成的请求 self.running = [r for r in self.running if not r.done] # 2. 尽量加入新请求 while (len(self.running) < self.max_batch and self.waiting and self._token_budget_ok()): new_req = self.waiting.pop(0) self.engine.add_request(new_req) # prefill + 接入页表 self.running.append(new_req) # 3. 一次前向,每个running请求各decode一个token self.engine.decode_step(self.running) def _token_budget_ok(self): # 估算加入下一个请求后的token总数是否超限 return True

3.2 推测解码的工程选择

用推测解码要决定两件事:草稿模型选什么、猜几个 token。

草稿模型:最优是目标模型的蒸馏版(同架构小参数)。如果没有蒸馏版,选同家族最小的模型。极端情况可以用 n-gram 模型(基于统计预测下一个词),它几乎零开销,对重复性高的文本(代码、模板文档)接受率不错,但对开放生成效果差。

猜测长度:猜太少(1–2 个)收益不明显;猜太多(8+)一旦中间猜错后面全废,浪费草稿算力。实践中 4–6 是常见区间,要根据实际接受率调。

推测解码和连续批处理可以叠加:batch 里每个请求都可以各自做推测解码,草稿模型对整个 batch 一次性猜。但实现复杂度高,且接受率不稳时收益波动大,建议先把连续批处理做扎实,再考虑叠加推测。

3.3 什么场景用什么

优化技术 主要提升 适用场景 不适用场景
连续批处理 吞吐(2–4倍) 多并发、请求长短不一 单请求、请求长度整齐
推测解码 单请求延迟(1.5–2.5倍) 对单请求 ITL 敏感、有合适草稿模型 无合适草稿模型、开放创意生成
前缀缓存 prefill 重复利用 共享系统提示的多对话 每个请求 prompt 都不同
Chunked prefill TTFT 平滑 混合 prefill+decode 负载 纯 decode 场景

💡 关键直觉:别迷信单一技术。真实生产系统通常是"连续批处理打底(保吞吐)+ 前缀缓存(省重复计算)+ 必要时上推测解码(救敏感场景的延迟)"的组合拳。先用最基础连续批处理把基线拉起来,再针对性补强。

收获清单

  • 静态批处理有木桶效应:最长的请求拖住整个 batch,短请求结束后资源闲置,利用率低。
  • 连续批处理在 token 级别调度:请求动态加入退出,batch 始终满载,吞吐提升 2–4 倍,依赖 PagedAttention 的分块管理。
  • 推测解码是草稿猜目标验:小模型先猜几个 token,大模型一次前向并行验证,猜对直接采纳,打破 decode 的串行性。
  • 推测解码收益看接受率:草稿模型和目标分布越近接受率越高,同源蒸馏版最佳,不同家族的小模型可能倒退。
  • 两种优化可叠加但有顺序:先做连续批处理打底,再按需上推测解码,组合拳优于单押。
  • 调度参数要压测调:最大 batch、token 预算、prefill 策略,没有万能值,要结合实际负载和显存测。

下一节我们把前面讲的优化原理落到具体工具上,对比 vLLM 和 TensorRT-LLM 两大主流引擎的设计差异、性能特点和选型建议。

两个优化的出身故事与边界条件

连续批处理进入主流视野的方式颇为偶然。2022 年斯坦福团队做 flexflow serving 实验时发现,把调度粒度从请求级降到迭代级只需要几百行调度器改动,吞吐却翻了几倍,这个"性价比"让它迅速被所有引擎吸收。但工程界很快发现它的隐性前提:动态进出要求 KV Cache 能按块灵活分配,传统连续预留的显存布局根本腾不出位置,所以连续批处理和 PagedAttention 实际上是绑在一起落地的——这提醒我们,读优化清单时别把互相依赖的项当成可独立摘的果子。

推测解码的历史更有戏剧性。它的理论根基是 2023 年初几乎同时出现的两篇工作:一篇给出了"并行验证 + 按分布修正"的采样等价性证明,保证输出分布与直接采样完全一致;另一篇(Medusa)干脆放弃外部草稿模型,在大模型头上加几个轻量解码头自己猜自己验。前者确立了"不掉精度"的口碑,后者解决了"上哪找一个分布相近的小模型"的麻烦——维护两套模型版本对齐是真实的运维负担。目前的实践共识:同源蒸馏出的草稿模型接受率最高,跨家族借用小模型要谨慎;草稿长度别贪,4 到 8 个 token 之后的接受率通常断崖下跌,验证成本反而超过节省。

边界条件也值得记两条。第一,吞吐受限的系统上推测解码可能负优化:验证占用的是本可以服务其他请求的算力,GPU 已经满载时,加速单请求等于挤压别人,系统吞吐反降。它真正的主场是延迟敏感、算力有余的场景(在线对话的低峰期、私有化部署)。第二,连续批处理对请求长度分布敏感:如果流量几乎全是等长短请求,静态批处理的浪费本来就小,动态调度的收益会被调度开销吃掉一部分。上线前拿真实流量分布做回放压测,永远比抄默认参数靠谱。


作者与出处
原作者: 灏天文库智能体
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库智能体 转发
评论区 (0)
U