> For the complete documentation index, see [llms.txt](https://yeasy.gitbook.io/llm_internals/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://yeasy.gitbook.io/llm_internals/di-san-bu-fen-tui-li-yu-bu-shu-pian/11_serving/11.2_continuous_batching.md).

# 11.2 连续批处理与 PagedAttention

连续批处理和 PagedAttention 是现代推理引擎实现高吞吐量的两项核心技术。11.1.3 的组件清单停在最后一行「KV 缓存管理器：使用 PagedAttention 等技术高效管理显存」——PagedAttention 在那里只是一个名字，同一份清单里的批处理管理器也只得了一句话。本节把这两个方框拆开。

## 11.2.1 静态批处理的低效

传统的**静态批处理**（Static Batching）将一批请求打包在一起处理，等待所有请求都完成生成后才处理下一批。

问题在于不同请求的生成长度差异巨大——有些可能只需生成 10 个词元，有些可能需要 2000 个。短请求完成后，其占用的 GPU 计算资源和显存在长请求完成之前一直空闲。生成长度差异越大，这种“等最慢者”造成的算力闲置就越严重。

## 11.2.2 连续批处理的工作原理

**连续批处理**（Continuous Batching，也称 Iteration-level Batching）在**每个解码步**检查批次状态：

1. 如果有请求完成生成（输出了结束标记或达到最大长度），立即将其移出批次
2. 如果有等待中的新请求，立即将其插入空出的位置
3. 执行一步解码，所有活跃请求并行计算

这样可以在队列充足且显存预算允许时显著提高 GPU 利用率，但并不保证任何时刻都满载，也无法消除长 Prefill、低到达率或显存耗尽造成的等待。在请求到达率较高的场景下，连续批处理的吞吐量可以是静态批处理的 **2-10 倍**。

连续批处理的核心不是“批次更大”，而是“批次边解码边换人”。下面的调度图把这个循环展开了。

```mermaid
flowchart LR
    queue["waiting queue"] --> admit["fill free slots"]
    admit --> decode["decode one token for all active requests"]
    decode --> check{"finished request?"}
    check -->|yes| evict["evict finished request<br/>release slot"]
    check -->|no| keep["keep request in batch"]
    evict --> admit
    keep --> check_wait{"new request waiting?"}
    check_wait -->|yes| admit
    check_wait -->|no| decode
```

图 11-2：连续批处理在每个解码步动态换入和换出请求

## 11.2.3 PagedAttention 的设计

即使有了连续批处理，KV 缓存的显存管理仍然是瓶颈。传统做法是为每个请求**预分配**足够容纳最大生成长度的连续显存块。由于实际生成长度通常远小于最大值，这导致了严重的**内存浪费和碎片化**。

**PagedAttention**（Kwon 等人，2023 年）借鉴了操作系统中**虚拟内存**的页式管理思想：

1. 将物理显存划分为固定大小的“页”（vLLM 默认每页容纳 16 个词元的 KV 缓存，不同引擎常见 16-64）
2. 每个请求维护一个逻辑-物理页面映射表
3. 当请求需要更多 KV 缓存空间时，动态分配新页面
4. 请求完成时，释放其占用的所有页面

关键优势：

* **消除碎片化**：页面可以在物理显存中不连续分布
* **按需分配**：不预分配最大长度，只在实际生成时分配
* **高效共享**：多个请求可以共享相同的前缀页面（如系统提示），通过写时复制（Copy-on-Write）避免重复存储

PagedAttention 将既有系统中 60-80% 的碎片化和预留浪费压低到最后一个块内的少量空洞；vLLM 报告实践中 KV 缓存浪费低于约 4%，因此能显著增加可同时服务的并发请求数。

PagedAttention 的关键结构不是“更大的缓存池”，而是每个请求维护一张逻辑块到物理页的映射表。

![PagedAttention 中逻辑块到物理页的映射示意图](/files/YEmSHzxMfKaKWZzw9zyI)

图 11-3：PagedAttention 的逻辑块表与物理 KV 页映射

## 11.2.4 前缀缓存

**前缀缓存**（Prefix Caching）是 PagedAttention 的自然延伸——如果多个请求有相同的系统提示或共享的对话上下文前缀，它们的 KV 缓存前缀部分完全相同。前缀缓存将这些共享部分的 KV 缓存只存储一份（存放在 PagedAttention 的共享页面中），所有共享该前缀的请求直接通过虚拟内存映射引用这些页面，无需复制。

前缀 KV 缓存复用要求**词元级精确前缀一致**，缓存 key 至少应纳入 tenant、auth scope、model/version、system prompt、tool state、私有文档标记和隐私标签。默认不应跨租户、跨权限或跨私有语料复用 KV 缓存；否则可能把不可见上下文变成侧信道。

在生产环境中（如客服机器人，所有对话共享同一个长系统提示），前缀缓存可以大幅减少 KV 缓存显存占用——共享前缀越长、并发越高，节省比例越大。更重要的是，在分离式架构中（11.3 节），Prefill 集群可以预计算常见前缀（如系统提示、热门文档）的 KV 缓存并缓存到高速存储（如 GPU 显存或 NVMe），当 Decode 集群接收新请求时可直接查询，大幅减少 Prefill→Decode 的 KV 缓存转移量。

## 11.2.5 分块 Prefill：防止头部阻塞

虽然连续批处理大幅提升了吞吐，但当一个**超长 Prompt 请求**突然到达时，仍会造成 **Head-of-Line 阻塞**（HOLB）——该请求的 Prefill 可能耗时数秒，期间 Decode 请求被迫等待，导致 TPOT（每输出词元时间，即两个词元之间的间隔，正式定义见 [11.5.1 节](/llm_internals/di-san-bu-fen-tui-li-yu-bu-shu-pian/11_serving/11.5_best_practices.md)）飙升。

**分块 Prefill**（Chunked Prefill）的策略是将长 Prompt 分解为多个固定大小的块（通常 512-1024 token），与 Decode 请求交错处理：

1. 取一块 Prompt 进行 Prefill（计算量小、耗时短）
2. 处理该块后，余下的 Decode 请求立即执行一步
3. 继续下一块 Prompt 的 Prefill
4. 以此循环，直到整个 Prompt 处理完

这样长 Prompt 不再“一把抓”，而是分段处理。虽然 Prefill 总耗时不变，但**中间穿插的 Decode 步骤保证了 TPOT 的平稳性**，避免了尾部用户的卡顿。提出这一调度策略的 [Sarathi-Serve](https://arxiv.org/abs/2403.02310) 报告：在满足严格尾延迟（P99 TBT）SLO 的前提下，可服务容量较 vLLM 提升 2.6-5.6 倍；代价是分块引入的少量额外计算开销，具体幅度取决于负载特征与块大小的选择。

## 11.2.6 缓存感知路由：前缀亲和性调度

在多实例推理集群中，不是所有请求都应该均匀分散。如果两个请求共享相同的前缀（如同一文档的 QA），应该尽可能路由到同一个推理实例，以最大化前缀缓存命中。

**缓存感知路由**的做法：

* 对可复用前缀计算词元级哈希值；语义向量只适合做结果缓存或负载聚类，不能证明 KV 前缀可复用
* 网关根据精确前缀哈希的**亲和性**将请求路由到已缓存该前缀的实例
* 如果前缀未被任何实例缓存，选择 KV 缓存占用率最低的实例

这种“粘性”路由避免了前缀在多个实例间分散的浪费，同时减少了 Prefill 重复计算。对于 RAG、文档问答等前缀复用率高的应用，能显著减少重复的 Prefill 计算，幅度取决于前缀复用率与路由命中率。
