在线大模型服务中,每个请求都会在显存中留下一个随解码过程动态增长的 KV Cache。当并发请求增多时,KV Cache 的显存占用往往超过模型权重本身,成为限制吞吐的主要瓶颈。一个 70B 模型在 FP8 精度下加载到 H100 80GB 显存后,留给 KV Cache 的空间通常只有 8~10GB。如果按最大序列长度预分配连续显存,典型工作负载下会浪费 60%~80% 的分配空间,导致同一块显存只能容纳少量并发请求。
vLLM 和 SGLang 是目前两个主流的高性能推理服务框架,分别提出 PagedAttention 与 RadixAttention 来管理 KV Cache。两者都采用块式分配,但核心思路不同:PagedAttention 借鉴操作系统的虚拟内存分页,解决显存碎片与浪费;RadixAttention 在前者基础上引入前缀树,让多个请求共享相同的 KV 计算。理解两者的差异,能帮助你在部署在线服务时选择正确的引擎,并配置合适的缓存策略。
问题背景:KV Cache 为何成为瓶颈
KV Cache 是 Transformer 解码过程中为每个 token 计算的 Key 和 Value 向量。生成第 n 个 token 时,注意力层需要读取前 n-1 个 token 的 KV,因此 KV Cache 随序列长度线性增长。在批处理场景中,不同请求的序列长度不同,KV Cache 的占用也动态变化。
传统推理系统通常为每个请求预分配一块连续显存,大小按模型最大序列长度计算。这种静态分配有两个问题:一是内部碎片,请求实际序列远短于上限,预分配的空间被浪费;二是外部碎片,不同请求的分配与释放导致显存空洞,无法被新请求利用。结果就是显存利用率低,可容纳的并发请求数受限。
vLLM 论文(SOSP 2023)指出,现有系统因 KV Cache 管理低效而浪费大量显存,限制了批处理规模。PagedAttention 通过块式分页实现近零浪费,并支持请求内与请求间的 KV 共享。SGLang 论文(arXiv 2312.07104)进一步观察到,多轮对话、少样本学习、检索增强生成等场景中,多个请求共享相同的前缀,重复计算这些前缀的 KV 是巨大浪费,因此提出 RadixAttention 自动复用前缀的 KV Cache。
PagedAttention:块式分页与按需分配
PagedAttention 将 KV Cache 划分为固定大小的块,默认每块容纳 16 个 token 的 KV。每个请求的逻辑 KV Cache 由一组块组成,物理块从全局块池中按需分配。当请求生成新 token 时,若当前块已满,则从池中取一个新块;请求结束后,其占用的块立即释放回池中。
这种机制消除了内部碎片:请求只占用实际需要的块数,而不是按最大长度预留。同时,物理块不要求连续,外部碎片也不再存在。块池中的空闲块可以被任意请求使用,显存利用率接近 100%。
PagedAttention 还支持 KV 共享。在并行采样或束搜索中,多个请求共享同一个前缀,它们的 KV 块可以指向相同的物理块,避免重复存储。vLLM 论文报告,相比 FasterTransformer 和 Orca 等系统,vLLM 在相同延迟下将吞吐提升 2~4 倍,且序列越长、模型越大、解码算法越复杂,提升越明显。
块分配与释放的流程
以下流程图展示 PagedAttention 下单个请求的 KV Cache 生命周期:
flowchart TD
A[请求到达] --> B[初始化逻辑块列表]
B --> C[解码生成 token]
C --> D{当前块是否已满?}
D -- 否 --> E[写入当前块]
D -- 是 --> F[从块池分配新物理块]
F --> G[映射到逻辑块]
E --> H[继续解码]
G --> H
H --> C
C --> I[请求结束]
I --> J[释放所有物理块回池]
在 vLLM 中,块分配由块管理器(BlockManager)负责。它维护一个物理块空闲列表,当请求需要新块时,从列表中取出一个并记录映射。请求结束时,块管理器回收这些块,加入空闲列表。这个过程是同步的,调度器在每步解码后检查请求状态,及时释放完成请求的块。
自动前缀缓存(APC)
vLLM 还提供了自动前缀缓存(Automatic Prefix Caching, APC),通过 --enable-prefix-caching 开启。APC 将系统提示词等固定前缀的 KV 块缓存下来,后续请求若前缀完全一致,则直接复用这些块,跳过前缀部分的前向计算。
但 APC 只对完全相同的扁平前缀有效。如果请求的前缀有分支(例如多轮对话中用户输入不同),或者前缀长度可变,APC 无法高效处理,因为缓存键是完整前缀的哈希,稍有不同就无法命中。这导致多轮对话场景中,每轮都需要重新计算历史对话的 KV。
RadixAttention:前缀树与自动复用
RadixAttention 是 SGLang 的核心优化,它将 KV Cache 组织成一棵基数树(radix tree)。树的每个节点代表一个 token 序列前缀,子节点继承父节点的前缀并追加新的 token。当新请求到达时,SGLang 在树中查找最长的匹配前缀,然后只从未匹配处开始计算,共享前缀的 KV 直接复用。
例如,一个包含 512 token 系统提示词的请求,如果 1000 个并发请求都使用相同系统提示词,那么该系统提示词的 KV 只需计算一次,其余 999 个请求直接复用。同样,一个 10 轮对话的请求,在计算第 10 轮时,前 9 轮的 KV 已经缓存,只需计算新增部分。
RadixAttention 默认开启,无需额外配置。它支持任意边界的前缀复用,不仅限于固定系统提示词,还能处理多轮对话、工具调用、RAG 文档等场景中常见的可变前缀。
前缀树的查找与更新
以下流程图展示 RadixAttention 处理一个新请求的过程:
flowchart TD
A[新请求到达] --> B[解析请求 token 序列]
B --> C[在基数树中查找最长匹配前缀]
C --> D{找到匹配?}
D -- 是 --> E[从匹配点开始计算剩余 token]
D -- 否 --> F[从第一个 token 开始计算]
E --> G[生成新 token 并更新树]
F --> G
G --> H[请求结束,保留缓存]
SGLang 的运行时维护这棵基数树,每个节点存储 token 序列和对应的 KV 块。查找过程从根节点开始,逐 token 匹配,直到无法匹配为止。匹配结束后,新生成的 token 会作为子节点插入树中,并分配新的 KV 块。
缓存命中率与显存利用
RadixAttention 的缓存命中率与请求间的前缀重叠比例直接相关。Spheron 的基准测试显示,在 80% 前缀重叠的情况下,512 token 前缀的缓存命中率约为 82%,首 token 延迟(TTFT)降低约 26%;1024 token 前缀命中率约 88%,TTFT 降低约 35%;2048 token 前缀命中率约 92%,TTFT 降低约 42%。这些数据表明,前缀越长、重叠比例越高,RadixAttention 的收益越明显。
对于完全不同的请求,RadixAttention 不会带来额外开销,性能与 PagedAttention 相当。这是因为查找匹配前缀的代价很小,且未命中时计算路径与普通解码一致。
驱逐策略与缓存管理
KV Cache 的显存是有限的,当缓存占用超过预算时,需要驱逐旧缓存。两种框架的驱逐策略不同,直接影响缓存命中率和显存利用率。
vLLM 的 APC 采用简单的 LRU(最近最少使用)策略,当显存不足时,优先驱逐最久未使用的缓存块。这种策略实现简单,但无法感知前缀结构,可能驱逐仍在被多个请求共享的块。
SGLang 的 RadixAttention 使用更精细的驱逐策略。它维护每个节点的引用计数和最近访问时间,驱逐时优先选择引用计数低且最近未使用的节点。由于树结构记录了前缀的共享关系,SGLang 可以避免驱逐仍被大量请求引用的前缀。此外,SGLang 还支持设置缓存大小上限,超过上限时触发驱逐。
在实际部署中,驱逐策略的选择会影响长尾请求的延迟。如果缓存被频繁驱逐,前缀命中率下降,TTFT 会回升。因此,对于前缀重叠高的场景,应分配足够的显存给 KV Cache,并调整驱逐阈值。
调度与批处理差异
除了 KV Cache 管理,vLLM 和 SGLang 在调度策略上也有差异,影响整体吞吐和延迟。
vLLM 采用连续批处理(continuous batching),每步解码后动态调整批大小,新请求可以插入当前批次,完成的请求立即退出。这种调度方式与 PagedAttention 的块分配紧密配合,因为块按需分配,新请求可以随时加入。
SGLang 的调度器则更强调前缀感知。它维护一个等待队列,当 GPU 空闲时,优先调度那些能复用缓存前缀的请求,以提高缓存命中率。SGLang 还支持 prefill-decode 分离,将 prefill 和 decode 阶段分开调度,避免长 prefill 阻塞 decode。
在批处理中,SGLang 的 RadixAttention 可以跨请求共享 KV 块,而 vLLM 的 APC 只支持完全相同的扁平前缀。因此,在多轮对话或 RAG 场景中,SGLang 的批处理效率更高,因为共享前缀的请求可以合并计算。
对比表格与选型建议
下表从多个维度对比 vLLM 与 SGLang 的 KV Cache 管理机制:
| 维度 | vLLM(PagedAttention) | SGLang(RadixAttention) |
|---|---|---|
| 核心机制 | 固定大小块,按需分配 | 基数树,前缀共享 |
| 前缀复用 | APC(可选,扁平前缀) | 默认开启,任意前缀边界 |
| 多轮对话效率 | 无 APC 时每轮重算历史 | 跨轮复用历史 KV |
| 显存碎片 | 最小(块级) | 最小(块级) |
| 驱逐策略 | LRU(简单) | 引用计数 + LRU(精细) |
| 调度策略 | 连续批处理 | 前缀感知 + prefill-decode 分离 |
| 适用场景 | 通用、前缀重叠低 | 前缀重叠高、多轮、RAG |
| 默认配置 | PagedAttention 始终开启 | RadixAttention 始终开启 |
| 监控指标 | /metrics | /metrics 含 cache hit rate |
选型建议:如果工作负载中超过 60% 的请求共享前缀(如固定系统提示词、RAG 文档、工具定义),SGLang 的 RadixAttention 能显著降低 TTFT,实测可降低 20%~40%。如果请求前缀大多不同,两者吞吐差距在 5% 以内,此时应优先考虑模型支持和部署便利性。vLLM 对新型模型的支持更广,社区集成更快,且部署简单(单 pip install)。如果使用 Blackwell GPU 或需要 Eagle3 投机解码,vLLM 更成熟。
生产环境中的观察与调优
在生产环境中,需要监控以下指标来判断 KV Cache 管理是否高效:
- 缓存命中率:SGLang 的
/metrics暴露sglang_cache_hit_rate,反映前缀复用比例。命中率低说明前缀重叠少或缓存被频繁驱逐。 - TTFT 和 TPOT:首 token 延迟和每 token 延迟。TTFT 受前缀命中影响大,TPOT 受批大小和显存带宽影响。
- KV Cache 利用率:已分配块与总块数的比例。利用率低说明块池过大或请求序列短。
- 显存占用:监控 GPU 显存使用,避免 OOM。
调优时,可以调整块大小(vLLM 默认 16 token)和缓存大小上限(SGLang 可配置)。块大小影响分配粒度和内部碎片,块越小碎片越少,但块表开销增大。缓存上限影响命中率和显存占用,需要根据工作负载平衡。
一个常见问题是缓存命中率波动大。这通常是因为请求前缀变化频繁,或驱逐策略过于激进。此时应增加缓存大小,或调整驱逐阈值。另一个问题是长尾请求的 TTFT 高,可能是由于缓存被短请求占满,长前缀被驱逐。可以设置最小保留空间给长前缀,或使用 SGLang 的引用计数策略。
局限性与未来方向
两种机制都有其适用边界。PagedAttention 的 APC 无法处理分支前缀,多轮对话场景效率低。RadixAttention 虽然能处理任意前缀,但基数树的内存开销和查找复杂度随树规模增长,极端情况下可能成为瓶颈。此外,前缀复用要求请求的前缀完全一致,如果前缀有微小差异(如时间戳、随机数),则无法复用。
未来方向包括:更智能的驱逐策略,基于请求频率和前缀长度的加权;跨请求的 KV 压缩,减少重复存储;以及结合投机解码的缓存优化。SGLang 的 RadixArk 项目正在将 RadixAttention 扩展到 TPU,vLLM 也在改进 APC 以支持更复杂的前缀。
对于在线服务团队,理解这些机制的差异,能够根据实际请求模式选择引擎,并配置合适的缓存参数,从而在有限的显存下获得更高的吞吐和更低的延迟。