TT Lab
博客

한국어 English 日本語 中文

开始
博客

博客

服务引擎中的 KV 缓存:PagedAttention、连续批处理与前缀缓存

状态:草案(2026-10-13),部分实测。 论文和官方文档已于 2026-10-13 直接打开并确认。实测值引用自同站点《托斯 ML/AI Infra 准备》第 14 篇,使用单张 8GB 级 GPU、Qwen3-1.7B、vLLM 0.31.0 进行测量的结果。大模型及多 GPU 下的数值未经实测。 编写规则:以实测数据为基础,未能测量的标注为“未实测”,未能确认的标注为“未确认”。

目录

1. 吞吐量归根结底是批处理大小的问题

重申第 1 篇的结论。解码时每生成一个 token,都需要从内存中完整读取一次权重,因此如果只运行单个请求,GPU 的计算单元大部分时间处于闲置状态。如果在读取一次权重的同时间并解码多个请求,就可以通过同一次读取生成多个 token。因此, 增大批处理大小可以提高吞吐量。

批处理中的每个请求都需要在 GPU 内存中为其 KV 留出空间。由于 KV 共享了加载权重后剩余的内存,因此, 在相同内存中能将 KV 塞得多么紧凑,决定了批处理大小,进而决定了吞吐量。 服务引擎的大多数 KV 管理技术,本质上都是为了解决这一句话的问题。

技术 节省的内容 通往吞吐量的途径
PagedAttention 因预留和碎片化而浪费的 KV 内存 在相同内存中容纳更多请求 → 更大的批处理
连续批处理 提早结束的请求所留下的空位 每个步骤都将批处理填满
分块 Prefill 长 Prefill 暂停解码的时间 在遵守 token 间隔约束的同时扩大批处理
前缀缓存 相同前半部分的 KV 计算与内存 跳过 Prefill 计算并共享区块

2. PagedAttention:将 KV 划分为区块

KV 메모리를 나누는 두 방법 옛 방식 — 요청마다 최대 길이만큼 이어 붙여 예약 A B A 예약 B 예약 틈 틈 아직 안 쓴 예약과 사이의 틈은 다른 요청이 못 쓴다. vLLM 논문: 기존 시스템은 KV 메모리의 20.4~38.2% 만 실제 토큰에 썼다. PagedAttention — 16토큰 블록과 블록 표 요청 A 요청 B A0A1B0 A2 B1 블록 표 A → 3, 7, 1 B → 3, 5 GPU 메모리 0 1 2 3 공유 4 5 6 7 8 9 10 11 · 블록은 빈 곳 어디에나 놓인다 → 틈이 생기지 않는다. · 낭비는 요청마다 마지막 블록의 빈칸뿐이다. · A0 과 B0 은 같은 프롬프트라 3번 블록 하나를 함께 본다. 블록 열쇠 = (앞 블록 해시, 이 블록의 토큰, LoRA 등 추가 정보) 토큰이 찬 칸 예약만 한 칸 빈 블록
图:连续预留与区块划分的对比

旧方式的浪费

在 vLLM 出现之前,服务系统会针对每个请求 按最大输出长度预先分配一块完整的 KV 空间。 由于不知道输出会有多少个 token,因此只能预留充裕的空间。vLLM 论文(Kwon 等人,SOSP 2023)将由此产生的浪费分为三种:

论文测量显示,传统系统 仅将 KV 内存的 20.4%~38.2% 用于实际存储 token。 根据同一篇论文的图表,在 A100 上服务 13B 模型时,大约 65% 的内存用于权重,近 30% 用于 KV。KV 的份额原本就很小,其中却有三分之二以上被白白浪费了。

区块与区块表

PagedAttention 的解决方法类似于操作系统的虚拟内存。它将 KV 划分为 固定大小的区块(默认 16 个 token),并且为每个请求维护一个从“逻辑区块”到“物理区块”的 区块表。区块可以存放在 GPU 内存的任意空闲位置,当一个区块填满 token 时,系统才会为其分配一个新区块。

注意力算子会顺着区块表去读取分散在各处的 K 和 V。即使它们不是连续的数组,计算结果也是相同的。

共享区块

采用区块化管理后, 多个请求可以指向同一个区块。 在从单个提示词生成多个回答的并行采样或束搜索(Beam Search)中,提示词部分的 KV 是完全相同的,因此系统会为区块记录引用计数并进行共享。只有当某个请求需要向该区块追加写入时才会进行复制(写时复制,copy-on-write)。论文测量显示,由此节省的 KV 内存如下:[原文表格无具体数据]

数据集 并行采样 束搜索
Alpaca 6.1~9.8% 37.6~55.2%
ShareGPT 16.2~30.5% 44.3~66.3%

结果

论文报告称,在相同的延迟水平下,其吞吐量比 FasterTransformer 和 Orca 高出 2~4 倍 (测试环境涵盖 OPT-13B/66B/175B 与 LLaMA-13B,单张 A100 40GB 至 8 张 A100 80GB)。在 ShareGPT 负载下,系统支持的请求吞吐量最高达到了 FasterTransformer 的 22 倍。由于倍数会随对比对象和负载的不同而产生很大差异,将“2~4 倍”作为代表性数值来理解即可。

3. 连续批处理:在每个步骤中填补空缺

GPU 한 걸음마다 배치에 무엇이 들었나 가로 = 시간, 줄 = 배치의 자리, 칸 하나 = 한 걸음 (가) 정적 배치 — 모두 끝나야 다음 배치 자리 1자리 2자리 3 빈 자리, 새 요청 대기 (나) 연속 배칭 — 빈 자리에 바로 새 요청 자리 1자리 2자리 3 긴 걸음 자리 1·3 의 토큰이 늦다 (다) 청크 프리필 — 긴 프리필을 잘라 디코드와 섞기 자리 1자리 2자리 3 고르게 나온다 프리필 디코드 토큰 하나 빈 자리 긴 칸일수록 그 걸음이 오래 걸린다. 모양은 설명용이고 길이는 실측이 아니다.
图:静态批处理、连续批处理与分块 Prefill

即便节省了内存,如果批处理的组织方式不佳,内存空间依然会闲置。这是因为每个请求的输出长度各不相同。 静态批处理会将若干个请求打包开始执行,并且 在全部结束之前 不接收新请求。如果一个 10 个 token 的回答提前完成了,该位置直到 500 个 token 的回答结束前都会处于闲置状态。

Orca(Yu 等人,OSDI 2022)将调度单位改为了 模型的单步(iteration)粒度。每一步结束时,已完成的请求会立即返回,等待中的请求则从下一步开始加入。这就是目前大多数引擎采用的 连续批处理。论文报告称,在 GPT-3 175B 级模型(16 张 A100 40GB)上,当把每个 token 的延迟中位数控制在 190ms 时,FasterTransformer 每秒处理 0.185 个请求,而 Orca 处理了 6.81 个请求(达到 36.9 倍)。该对比的基准是静态批处理,且这一差距不仅归功于连续批处理,还包含 Orca 的其他设计。

连续批处理与 PagedAttention 互为前提。若要让请求在每个步骤中自由进出,就必须能够随时分配和释放 KV 空间,而大块预留只会让空隙越来越多。基于区块的分配机制使这成为了可能。

新问题:长 Prefill 会暂停解码

连续批处理依然会面临图(b)所示的问题。当新请求进入时,必须在单步内完成该请求的 Prefill,如果提示词很长,该步骤就会耗时很久, 而在此期间,批处理中的其他请求将无法获得下一个 token。 Sarathi-Serve 论文(Agrawal 等人,OSDI 2024)将此称为生成停滞(generation stall),并报告称在 Yi-34B、双卡 A100 的配置下,这种暂停可持续长达数秒。

解决方法如图(c)所示:将 长 Prefill 切割成多个分块,分摊到多个步骤中,并在每一步与解码 token 混排(分块 Prefill)。在同一篇论文的表 4 中(Yi-34B,双卡 A100,单步 token 预算 1,024),仅进行混排时的 P99 token 间隔为 0.68 秒,而同时采用分块与“优先填充解码”策略时为 0.14 秒。在尾部延迟约束下,Mistral-7B(单卡 A100)和 Yi-34B(双卡 A100)所能承载的负载分别达到了 vLLM 的 2.6 倍和 3.7 倍(论文摘要)。

我们的实测。 在 8GB 级 GPU 上使用 Qwen3-1.7B,在单个请求解码 3,000 个 token 的过程中插入 3 个 3,500 token 的输入,并改变单步输入的 token 数量上限(--max-num-batched-tokens),测量了插入区间的 token 间隔。

单步输入 token 上限 插入区间最大 token 间隔 长输入本身的首个 token 耗时
512 69ms 481~494ms
1,024 129ms 468~493ms
2,048(此版本默认值) 227ms(3 次测量分别为 227、226、226) 466~480ms
8,192(实效 3,500) 445ms 461~474ms

分块 Prefill 只能 缩短 间隔,而无法将其消除。只要 Prefill 和解码共享同一张 GPU,它们就会互相干扰。第 4 篇的分离式服务将用另一种方式解决这个问题。

4. 前缀缓存:相同的前半部分只需计算一次

请求之间往往存在许多相同的前半部分。例如附加在所有对话中的系统提示词、针对同一文档多次提问的 RAG,以及原封不动带上上一轮对话的多轮对话。正如我们在第 1 篇中所看到的,前半部分相同,其 KV 也相同。因此, 如果在 GPU 中找到了现成的区块并直接使用,就能跳过相应部分的 Prefill。

vLLM 的自动前缀缓存(设计文档,2026-10-13 查阅)会为每个区块生成一把密钥。

我们的实测。 我们向 3,000 个 token 的公共前缀附加了每个请求各不相同的 32 个 token 尾部,并依次发送。

设置 首个请求首个 token 耗时 后续请求首个 token 耗时(中位数)
开启前缀缓存(默认) 394ms 28ms
关闭 395ms 393ms

命中时跳过了 3,000 个 token 的 prefill,首 token 速度提升了 14 倍。相反,在前半部分完全不重叠的负载(输入 2,048,并发 32)下,开启它的代价是吞吐量降低约 1.2%,首 token 中位数增加约 8%(推测为哈希计算开销,未作分解)。

测量时的陷阱。 如果重复相同的句子来测量性能,前缀缓存会命中,从而产生 GPU 无法达到的速度。性能测试的输入必须每次都不同,只有在想要观察缓存效果时,才刻意制造公共的前半部分。

5. 当 KV 空间不足时

批处理大小的上限取决于 KV,这一点从数字上也看得出来。在相同的实际测试环境中,KV 空间为 22,048 个 token,如果是输入 512 + 输出 128 = 640 个 token 的请求,大约可以容纳 34 个。

并发请求 吞吐量 首 token 中位数
1 66 token/秒 73ms
8 383 token/秒 351ms
32 831 token/秒 666ms
64 787 token/秒 5.7秒

将并发从 32 增加到 64 时,吞吐量反而下降,首 token 延迟增加了 8.6 倍。空间不足的请求会在队列中等待。如果更加短缺, 已经在运行的请求会被迫交出 KV 并被驱逐(抢占),然后从头开始重新 prefill。 在输入 2,048、并发 32 的情况下发生了 7 次抢占,在输入 1,500、输出 256、并发 64 的情况下发生了 22 次,首 token 中位数分别达到了 12.5 秒和 35.4 秒。由于需要对相同的 KV 进行两次计算,吞吐量不会增加,只有延迟会增加。

每次启动服务器时,KV 空间的大小都有所不同,在 22,048 到 27,504 个 token 之间,而在并发 64 这样的接近极限状态下,这 20% 的差距让首 token 延迟改变了 2.7 倍。在运维中,最好记录服务器日志中的 GPU KV cache size ,并将并发上限调整到接近「由平均请求长度决定的 KV 可容纳请求数」附近。

6. 总结:以及 GPU 内存的高墙

技术 依据 验证的效果
PagedAttention 论文 将浪费限制在单个块内,在相同延迟下吞吐量提升 2~4 倍
连续批处理 论文(Orca) 相比静态批处理,在相同延迟下提升 36.9 倍(175B 级别,包含其他设计)
分块 prefill 论文(Sarathi-Serve)·实测 P99 token 间隔 0.68→0.14秒(论文),最大间隔 445→69ms(实测)
前缀缓存 实测 3,000 token 前缀的首 token 延迟从 394 降至 28ms

这些技术全都在 GPU 内存内部 运行。如果 GPU 空间不足,前缀缓存会从最早的块开始清除,当被清除的前半部分再次到来时,会从头开始计算。如果有多个引擎实例,它们各自独立缓存,邻近的实例无法使用已经计算好的 KV。第 3 篇将介绍把 KV 放在 GPU 外部(CPU 内存、磁盘、其他服务器)的 LMCache。

Titi 的碎碎念。 如果把乒乓球桌「由一个团队包场一整天」,就会有很多空桌子。如果按 30 分钟为一个单位切分,在前一组刚结束的位置立刻安排下一组,并让同一个球友会共用一张桌子,那么在同一个体育馆里就能让更多的人打球。PagedAttention 和连续批处理做的就是这件事。

参考

登录后即可点赞

评论

还没有评论。

登录后即可发表评论