文章

PagedAttention 实战:为什么大模型服务的瓶颈不是算力,而是 KV Cache 调度

从 KV Cache 内存碎片、PagedAttention、连续批处理到前缀缓存,系统讲清大模型推理服务如何提升吞吐、降低延迟,并给出工程落地检查清单。

背景:推理服务的瓶颈,常常不是”模型会不会算”

很多人第一次做大模型服务优化,会先盯着 GPU 算力、量化、FlashAttention 或模型大小。但真实线上服务里,一个更容易被低估的问题是:请求之间长度不一样、结束时间不一样、KV Cache 会动态增长。只要内存管理做得粗,GPU 上能同时容纳的请求数就会下降,吞吐上不去,排队时间也会被拉长。

这也是 vLLM 和 PagedAttention 能快速成为主流推理引擎核心技术的原因。PagedAttention 论文指出,LLM 服务需要把足够多请求合批才能获得高吞吐,但每个请求的 KV Cache 很大,而且会随生成过程增长、结束后释放;如果管理不当,内存碎片和重复存储会限制 batch size。论文进一步给出结论:vLLM 通过 PagedAttention 在测试场景中相较 FasterTransformer、Orca 等系统获得了 2-4 倍吞吐提升,同时保持相似延迟水平。

换句话说,大模型推理服务的关键问题不是”单个请求怎么最快算完”,而是”很多长度不同、生命周期不同的请求,如何在有限 GPU 内存中稳定共存”。

先理解 KV Cache:它为什么会吃掉推理服务的吞吐

自回归模型每生成一个 token,都需要关注前面已经出现过的 token。为了避免每一步都重新计算历史 token 的 Key 和 Value,推理引擎会把它们缓存起来,这就是 KV Cache

一个请求进入模型后,通常分为两个阶段:

Prefill:一次性处理输入上下文

用户传入 prompt、文档、历史对话或工具结果后,模型需要先把这些输入 token 过一遍 Transformer,生成第一批 KV Cache。这个阶段计算量较大,通常更像”大矩阵计算”。

Decode:逐 token 生成输出

第一个 token 之后,模型每次只生成一个或少量 token,但每一步都需要读取历史 KV Cache。此时任务更容易变成 memory-bound:不是 GPU 不会算,而是不断读写 KV Cache、调度请求、搬运数据。

问题在于,不同请求的输入长度、输出长度、结束时间都不同。如果系统为每个请求提前分配一大块连续显存,短请求会浪费空间,长请求又容易触发扩容和碎片。PagedAttention 论文把这个问题类比为操作系统里的虚拟内存管理:请求像进程,token 像字节,KV Cache block 像 page。

PagedAttention 的核心:把 KV Cache 从”连续大块”改成”分页块”

传统做法容易把一个请求的 KV Cache 当成连续张量管理。这样实现简单,但对动态请求很不友好:

  • 请求最大长度不确定,预留太多会浪费。
  • 请求实际输出长度不同,短请求释放后容易产生碎片。
  • beam search、parallel sampling、共享系统提示词等场景下,相同前缀难以复用。

PagedAttention 的思路是把每个请求的 KV Cache 切成固定大小的 block。逻辑上,一个请求仍然看到连续的 token 序列;物理上,这些 block 可以分散在不同显存位置,由 block table 管理映射关系。

这带来三个直接收益:

1. 按需分配,减少内部浪费

请求生成到哪里,就分配到哪里。不必一开始就按最大长度预留完整空间。对输出长度不可预测的聊天、Agent、RAG 场景,这一点尤其重要。

2. 固定块大小,缓解外部碎片

所有 block 大小一致,释放后可以回收到池子里继续给别的请求用。相比”大块连续显存”,固定块管理更接近内存池,调度器更容易判断还能放多少请求。

3. 块级共享,支持前缀复用

如果多个请求共享相同 prompt、系统提示词、文档开头或多轮会话历史,底层可以让它们引用同一批 KV block。在需要修改时再做 copy-on-write,避免重复存储。

这也是为什么 PagedAttention 不只是 attention kernel 优化,而是一个 服务端内存管理问题

连续批处理:为什么不能只做普通 batch

很多业务一开始会把多个请求拼成 batch,希望提升吞吐。但普通 batch 有一个典型问题:一批请求里只要有慢请求,其他请求就可能被拖住;新请求也要等下一个批次。

TensorRT-LLM 文档把 in-flight batching 描述为连续批处理iteration-level batching:上下文阶段的序列可以和生成阶段的序列一起处理,以更好交错请求、降低延迟并提高 GPU 利用率。它还强调,为了效率,输入张量需要 packed,避免在 generation 阶段为单 token 请求 padding 到最大输入长度。

这对线上系统很关键。因为真实请求通常长短混杂:

  • 有人只问一句话。
  • 有人带几万字文档做 RAG。
  • 有人生成 20 个 token 就结束。
  • 有人要求长文案,输出几千 token。

连续批处理的目标,是让调度器每一轮都重新评估:哪些请求还在 decode,哪些请求可以进入 prefill,哪些请求已经结束可以释放 KV Cache,哪些新请求可以趁空隙插进来。

Chunked Prefill:长 prompt 不要一次吃完全部预算

长上下文场景下,prefill 很容易一次占满 token budget,导致短请求排队。TensorRT-LLM 文档提到 chunked context,也就是把上下文切成若干 chunk,使上下文 chunk 可以和 generation 阶段的 token 一起 batch,从而提升整体吞吐,并移除一些输入长度约束。

工程上可以这样理解:

策略行为影响
不切分长文档 prefill 一次占满预算 → 短请求等待TTFT 抖动
切分后长文档 chunk 1 + 短请求 decode → 长文档 chunk 2 + 新请求 prefill → 长文档 chunk 3 + 其他请求 decodeTTFT 更稳定

它的收益不只是吞吐,还包括 尾延迟稳定性。如果你的服务有 SLA,比如 TTFT 必须小于某个阈值,chunked prefill 往往比单纯加大 max_num_tokens 更可控。

Prefix Caching:让相同前缀不要反复算

vLLM 文档中的 Automatic Prefix Caching(APC)会缓存已有查询的 KV Cache;如果新查询与已有查询共享相同前缀,就可以直接复用对应 KV Cache,跳过共享部分的计算。文档列出的典型收益场景包括:用户反复查询同一长文档,以及多轮会话反复携带相同聊天历史。

这类优化特别适合以下业务:

长文档问答

用户对同一份合同、年报、接口文档、需求文档连续提问。文档内容相同,问题不同。没有前缀缓存时,每次都要重新 prefill 长文档;有前缀缓存后,只需要复用文档部分的 KV Cache。

Agent 固定系统提示词

很多 Agent 都有很长的 system prompt、工具说明、输出协议和安全规则。如果这些内容每次都一样,可以被视为稳定前缀。

多轮对话

多轮会话会不断把历史消息放回上下文。前面几轮内容稳定,后面追加新消息。只要请求组织方式稳定,就有机会复用历史前缀。

不过,前缀缓存不是免费午餐。它需要缓存空间、命中率统计、驱逐策略和多租户隔离。请求前缀如果经常被时间戳、随机 ID、动态排序字段打乱,命中率会很差。

SGLang 的 RadixAttention:从”相同前缀”扩展到”前缀树复用”

SGLang 论文提出的 RadixAttention,也是围绕 KV Cache 复用展开。它将多次生成调用之间可复用的 KV Cache 组织成 radix tree,适合复杂 LLM 程序、多轮交互、few-shot、RAG、JSON 解码等任务。论文摘要中提到,SGLang 运行时包含 RadixAttention、压缩 FSM 等优化,在多个语言与多模态任务中相对当时系统取得了最高 6.4 倍吞吐提升

与简单的”同一个完整前缀命中”相比,radix tree 更像做了结构化前缀索引:

公共系统提示词
├── 工具说明 A
│   ├── 用户问题 1
│   └── 用户问题 2
└── 工具说明 B
    ├── 用户问题 3
    └── 用户问题 4

如果你的应用是固定模板 + 动态变量,或者多条任务共享大量开头内容,RadixAttention 这种思路会比”只看整段 prompt 是否完全相同”更有想象空间。

2026 年的新变化:PagedAttention 开始向更多硬件和调度策略扩散

PagedAttention 最初主要围绕 GPU 服务引擎被讨论,但近两年相关工作已经扩展到 TPU、FlexAttention、commodity GPU 调度等方向。

  • 2026 年 4 月的 Ragged Paged Attention 论文面向 TPU 场景,提出基于 Pallas 和 Mosaic 的高性能 TPU attention kernel,并提到它被集成为 vLLM 和 SGLang 的 TPU 后端基础之一。
  • 2026 年 6 月的 PersistentKV 则关注 page-aware decode scheduling,强调长上下文 decode 中,决定吞吐的不只是单个 attention kernel,也包括怎样把 page、KV-head group、sequence split 和 workqueue 组织起来。

这说明一个趋势:LLM Serving 的竞争点正在从”有没有 PagedAttention”转向”KV Cache、调度器、硬件后端、前缀复用如何协同设计”。

工程落地:不要只打开参数,要建立观测闭环

如果准备在自己的系统里落地 PagedAttention、连续批处理和前缀缓存,建议按下面顺序推进。

第一步:先量化请求形态

至少统计这些指标:

指标类别关键指标
Token 分布prompt_tokens p50 / p90 / p99,output_tokens p50 / p90 / p99
延迟TTFT p50 / p90 / p99,TPOT p50 / p90 / p99
并发与取消并发请求数,请求取消率
缓存prefix cache hit rate
内存KV cache memory usage,GPU memory fragmentation 或 block pool 使用率

没有这些指标,调优很容易变成猜参数。

第二步:区分 prefill 和 decode 压力

  • 如果 TTFT 高,可能是 prefill 排队、长 prompt 阻塞或 batch token budget 设置不合理。
  • 如果 TPOT 高,可能是 decode 阶段带宽、KV Cache 读取、batch active sequence 太少或 attention kernel 不合适。

把 prefill 和 decode 拆开看,比只看”平均响应时间”有效得多。

第三步:为不同业务设置不同路由

不要把所有请求扔到同一个服务池。可以按请求形态拆:

池类型特点
短问答池低延迟、小上下文
长文档池高 token budget、chunked prefill
Agent 池高前缀复用、启用 prefix caching
批处理池吞吐优先,延迟要求低

这样做通常比全局调一个 max_batch_size 更稳定。

第四步:固定 prompt 模板,提升缓存命中

如果要吃到 prefix caching 的收益,应用层 prompt 也要配合:

  • 固定 system prompt 顺序。
  • 避免把时间戳、traceId、随机 nonce 放在 prompt 前缀。
  • RAG 文档排序保持稳定。
  • 工具说明使用固定版本号,不要每次拼接不同描述。
  • 把动态字段尽量放到后面。

前缀缓存不是模型层单方面能解决的问题,它要求应用层也做 prompt layout engineering

第五步:给驱逐策略和隔离策略留空间

缓存越多,不代表效果越好。必须考虑:

  • 多租户之间是否允许共享缓存。
  • 敏感文档的 KV Cache 是否需要隔离。
  • cache eviction 是 LRU、基于成本,还是基于命中预测。
  • 长文档缓存是否会挤掉高频短前缀。
  • 模型版本、tokenizer 版本变更后缓存是否全部失效。

这些问题如果上线后再补,会很麻烦。

常见误区

误区一:只要用了 vLLM,吞吐自然就高

vLLM 提供了优秀的底层能力,但实际吞吐取决于模型大小、请求长度分布、batch token budget、显存大小、并发、是否启用 prefix caching、是否有 chunked prefill、以及应用层 prompt 是否稳定。

误区二:max_batch_size 越大越好

过大的 batch 参数可能提高吞吐上限,但也可能增加排队和首 token 延迟。TensorRT-LLM 文档也强调,max_num_tokens 需要在吞吐、GPU 利用率、TTFT、TPOT 之间平衡。

误区三:长上下文问题只能靠更大显存

更大显存能缓解问题,但不能替代调度。长上下文真正需要的是:分页 KV Cache、chunked prefill、prefix caching、合理的队列隔离和监控。

误区四:前缀缓存可以自动解决所有重复计算

如果 prompt 每次都有动态字段插在开头,缓存命中会被破坏。应用层必须稳定 prompt 结构,才能让底层缓存发挥作用。

上线检查清单

  • 已统计 prompt/output token 分布
  • 已拆分 TTFT 与 TPOT 指标
  • 已监控 KV Cache block 使用率
  • 已启用或评估 PagedAttention / paged KV cache
  • 长上下文场景已评估 chunked prefill
  • 多轮会话与长文档场景已评估 prefix caching
  • 已检查 prompt 前缀是否稳定
  • 已设置不同业务池或路由策略
  • 已压测 p50/p90/p99,而不是只看平均值
  • 已制定缓存驱逐、租户隔离和模型升级失效策略

结论:大模型服务优化,本质是”算力 + 内存 + 调度”的系统工程

PagedAttention 的价值不在于某个单点技巧,而在于它把 LLM 服务里的 KV Cache 管理问题显性化了。连续批处理解决请求动态进出的问题,chunked prefill 解决长 prompt 独占预算的问题,prefix caching 和 RadixAttention 解决重复上下文计算的问题。

当业务进入多轮会话、长文档问答、Agent 工具调用和高并发服务阶段,真正决定成本和体验的,往往不是”模型再小一点”或”GPU 再强一点”,而是推理引擎能否把 KV Cache、请求调度、缓存复用和硬件后端组织成一个稳定系统。

如果只把它当成一个参数开关,收益会有限;如果把它当成服务架构的一部分,PagedAttention 这一类技术才会真正发挥价值。

主要参考资料

常见问题

PagedAttention 主要解决什么问题?
它主要解决大模型推理时 KV Cache 动态增长带来的内存碎片和预分配浪费问题,让更多请求可以同时留在 GPU 上。
连续批处理和普通 batching 有什么区别?
普通 batching 往往按一批请求整体推进,连续批处理则可以在每轮解码中动态加入新请求,把 prefill 和 decode 更细粒度地交错调度。
前缀缓存适合所有业务吗?
不适合。它更适合系统提示词、长文档问答、多轮会话等共享前缀明显的场景;如果请求之间几乎没有公共前缀,收益会有限。