摘要
长上下文服务的容量不能只看模型权重。请求读入文档后,注意力层会为历史 token 保存键和值;生成继续,这部分状态也继续增长。本文围绕 KV Cache 建立一个可核算的容量模型,再分别讨论分页和前缀复用解决什么问题。PagedAttention 的分块管理来自 Kwon 等人的研究,本文的贡献是以独立算例和调度草图串联这些机制,便于评估智能研究助手等长文本场景。
01 / 缓存为什么随对话增长
在标准因果注意力中,预填充(prefill)处理输入序列,为每层建立历史 K、V 状态,并获得首个输出 token。随后解码(decode)逐步处理新 token:当前查询向量读取已有缓存,新的 K、V 再追加进去。历史位置无法看到后来的 token,所以它们已经算好的状态可以保留。
这节省了对历史状态的重复计算,却没有消除读取历史的成本。较长上下文通常意味着更大的缓存和更多注意力访存。缓存长度按已处理的 token 计算;用于容量规划时,可取输入长度加计划生成上限,覆盖整个请求的增长空间。
02 / 先算清每个 token 的开销
设模型有 L 层,每层 Hkv 个 KV 头,每个头维度为 D,每个元素占 b 字节,缓存长度为 T。若各层配置相同,完整模型的张量容量为:
Mkv = 2 × L × Hkv × D × T × b
bytes_per_token = 2 × L × Hkv × D × b
系数 2 对应 K 与 V。分组查询注意力(GQA)允许多个查询头共享一组 K、V,因此应代入 KV 头数,不能直接使用查询头数。这里按未量化、无滑动窗口的缓存计算;分片部署时,还要按实际层和 KV 头的分布换算每张设备的占用。
构造一个 32 层、8 个 KV 头、头维度 128、每元素 2 字节的教学模型。每个 token 需要 131,072 字节,即 128 KiB。以下三条独立请求的长度均指缓存中的 token 数:
| 请求 | 缓存长度 | 张量容量 |
|---|---|---|
| A | 8,192 tokens | 1 GiB |
| B | 4,096 tokens | 0.5 GiB |
| C | 2,048 tokens | 0.25 GiB |
| 合计 | 14,336 tokens | 1.75 GiB |
若每条请求一开始就按 8,192 token 预留连续空间,总计会占 3 GiB。当前有效状态只有 1.75 GiB,差额来自尚未使用的预留区,而非张量数值本身。这里使用二进制单位:1 GiB = 1,024 MiB。该配置用于算术推导,不代表任何线上模型的实测容量。
03 / 分页改变分配方式
PagedAttention 原论文将逻辑上连续的 token 分成固定大小的块,借助块表映射到不必连续的物理空间。请求增长时申请新块,结束后释放其不再共享的块。这样减少了大段预留和连续空间分配带来的浪费。
以每块 16 token 为例,上述模型每块占 2 MiB。长度为 37 的请求需要 ceil(37 / 16) = 3 块,共 6 MiB;其中最后一块有 11 个空位,对应 1.375 MiB。分页仍有尾块空位,也需要块表,但不必为整条最大序列一次性预留。它没有压缩每个 K、V 元素,更不是把完整注意力变成稀疏注意力。
页更小可以细化分配,页更多也会增加管理和寻址工作。实际块大小需要结合使用的注意力内核选择。vLLM 的历史内核说明展示了块组织与访存布局之间的关系,适合理解这一约束,不应当作当前版本所有后端的统一实现。
04 / 哪些前缀可以复用
多个请求先阅读同一份报告,再提出不同问题时,可以共享公共前缀对应的状态。vLLM 自动前缀缓存文档说明了复用已计算前缀以减少预填充工作的方式;后续输出仍需逐步生成。
复用的依据是相同的计算上下文:模型权重与 adapter 相同,实际 token 前缀、位置编码设置和注意力规则一致。文本看起来相似不足以成立;提示模板或前面的 token 一旦改变,后续位置的状态也可能改变。工程上还需使用兼容的缓存数据类型与布局,才能直接读取同一物理块。
继续使用前面的模型,两条各有 8,192 token 的序列若共享最前面的 4,096 token,可将总状态由 2 GiB 降为 0.5 + 0.5 + 0.5 = 1.5 GiB:一份公共前缀,两份独立后缀。只有复用相同计算,才有这项收益;两段相同文字出现在不同前文之后,不能直接套用这个算例。
05 / 将容量放进调度决策
一次迭代不仅受计算 token 预算约束,也要有足够物理块接收新状态。以下伪代码以块为单位预留本轮增量;extra_blocks 包括追加 token 所需的新块以及共享尾块转为独立写入的开销。
def plan_iteration(candidates, free_blocks, token_budget, block_size):
plan = []
for request in candidates:
tokens = min(request.next_chunk, token_budget)
if tokens == 0:
break
needed = extra_blocks(request, tokens, block_size)
if needed > free_blocks:
continue
reserve_blocks(request, needed)
plan.append((request, tokens))
free_blocks -= needed
token_budget -= tokens
return plan
请求完成后释放引用,仍被其他请求使用的前缀继续保留。候选顺序决定谁先获得空间,页面管理本身不决定服务优先级。该片段表达一次预算过程,调度轮次与排队策略见配套的连续批处理文章。
06 / 从可容纳到可服务
先从设备容量中扣除权重、运行工作区及预留空间,再得到缓存池。例如 24 GiB 中留出 17 GiB 给这些部分,剩余 7 GiB 在上述张量模型下对应七条 8,192 token 序列。块表等管理开销还需计入最终容量,七条也不代表已经满足响应时间要求。
增加并发可摊薄部分权重读取成本,也会扩大历史状态的总读取量。适合长文档复用的配置,未必适合大量互不相同的短请求。评估应同时观察缓存占用、可复用 token 比例、首 token 等待和逐 token 间隔,并保留输入与输出长度分布。容量公式解释“能放多少”,实际负载实验才能回答“能以什么速度服务”。
参考文献
- Kwon et al. — Efficient Memory Management for Large Language Model Serving with PagedAttention,SOSP 2023:分页缓存与块级共享的原始研究。
- vLLM — Automatic Prefix Caching:前缀复用的功能与适用场景。
- vLLM — Paged Attention:基于早期实现的缓存布局与内核说明。
← 返回研究论文