AI 技术
#LLM推理#KV Cache#前缀缓存#RadixAttention#vLLM#SGLang

前缀缓存在大模型推理中的实现与命中率优化

多轮对话和批量请求场景中,大模型推理常因重复计算相同前缀而浪费算力。本文聚焦前缀缓存如何通过树状结构与哈希匹配避免冗余 KV 计算,对比 vLLM 与 SGLang 的实现,分析 LRU 驱逐、碎片化对命中率的影响,并讨论缓存命中如何转化为延迟与吞吐收益。

在线推理服务中,一个常见场景是:用户在多轮对话里反复提及相同的系统提示,或者批量请求共享同一段长文档作为上下文。直觉上,模型每次都应该重新计算整个输入序列的注意力键值(KV),但这样做会把大量算力浪费在重复计算相同的前缀上。基线方案是每个请求独立计算完整的 KV Cache,这在单请求时没有问题;当并发量上升,冗余计算会迅速占满 GPU 显存和计算单元,导致可同时服务的请求数(batch size)下降,延迟上升。

前缀缓存正是针对这一瓶颈。它的核心思想是:一旦某个前缀的 KV 状态被计算出来,就将其缓存起来;后续请求如果带有相同前缀,可以直接复用缓存结果,只计算新增部分。工程上,这需要解决三个问题:如何高效匹配前缀、如何管理缓存生命周期、以及如何在多请求并发时维持高命中率。本文将围绕多轮对话和批量请求场景,对比 vLLM 和 SGLang 的实现,分析缓存命中率对延迟和吞吐的影响,并讨论碎片化等实际问题。

为什么 KV Cache 复用是瓶颈

在自回归生成中,每生成一个新 token,模型都要对之前所有 token 计算注意力。KV Cache 将已计算 token 的键(Key)和值(Value)张量存储下来,后续步骤只需为新 token 计算注意力,并将新的 KV 追加到缓存。这避免了序列长度平方级别的重复计算,是单请求推理的基础优化。

然而,KV Cache 的内存占用巨大。以 Llama-2-70B 为例,每个 token 的 KV Cache 约占用 2.5 MB(FP16),一条 4096 token 的序列就需要约 10 GB 显存。当多个请求并发时,如果每个请求都独立分配 KV Cache,显存很快耗尽。更糟糕的是,许多请求的提示具有公共前缀——例如多轮对话中,每一轮都包含完整的对话历史;或者批量请求使用相同的系统提示。传统系统中,这些公共前缀的 KV 状态会被重复计算和存储,造成显存和计算的双重浪费。

vLLM 的 PagedAttention 论文指出,这种冗余和碎片化会严重限制 batch size。PagedAttention 通过将 KV Cache 划分为固定大小的块(block),并允许块的非连续存储,解决了显存碎片问题,但它本身并不自动复用跨请求的相同前缀。前缀缓存正是在分块管理的基础上,进一步识别和共享相同的块序列。

前缀匹配:从哈希到基数树

前缀缓存的关键是快速判断新请求的提示是否命中已缓存的前缀。vLLM 和 SGLang 采用了不同的数据结构来实现这一匹配。

vLLM 的哈希匹配

vLLM 采用基于哈希的块匹配。每个 KV 缓存块都有一个哈希值,计算方式为:hash(父块哈希, 本块 token 序列, 额外信息)。其中父块哈希保证了前缀的连续性,额外信息可以包含 LoRA ID 或多模态输入的图像哈希。当新请求到来时,vLLM 按顺序计算每个块的哈希,并查询全局缓存表(哈希到块 ID 的映射)。如果命中,则直接复用该块的 KV 数据;一旦某个块未命中,后续所有块都需要重新计算。

这种设计简单直接,但存在两个局限。第一,哈希冲突。虽然概率极低,但理论上不同序列可能产生相同哈希。vLLM 从 0.8.3 版本开始支持 SHA256 来避免冲突,代价是每个 token 约 100~200 ns 的额外开销。第二,匹配粒度固定为块大小(通常 16 个 token)。如果新请求的前缀长度不是块大小的整数倍,最后一个部分匹配的块无法命中,只能重新计算。

SGLang 的 RadixAttention

SGLang 提出了 RadixAttention,使用基数树(Radix Tree)来管理前缀缓存。基数树是一种压缩前缀树,每个边可以标记一个 token 序列,而非单个 token。在 SGLang 运行时,所有已完成请求的 KV 缓存都保留在基数树中,节点代表一个 token 序列,并存储对应的 KV 缓存张量(以分页布局在 GPU 上)。新请求到来时,运行时在树中执行最长前缀匹配:从根节点开始,沿着边匹配 token,直到无法继续。匹配到的路径即为可复用的前缀。

与哈希匹配相比,基数树有两个优势。一是匹配粒度灵活,可以精确到 token 级别,不会因为块边界而浪费部分匹配的前缀。二是树结构天然支持前缀共享,不同请求的公共前缀在树中只存储一份,节省内存。但树结构本身需要 CPU 端维护,并在匹配时遍历,当并发请求极多时可能引入 CPU 开销。SGLang 声称树结构维护开销极小,因为操作主要涉及指针更新。

缓存生命周期管理:LRU 与驱逐

前缀缓存需要占用显存,而显存是有限的。当缓存池满时,必须驱逐一些旧缓存来腾出空间。驱逐策略直接影响命中率。

vLLM 和 SGLang 都采用最近最少使用(LRU)作为基础驱逐策略,但实现细节不同。vLLM 维护一个空闲块队列,所有未使用的块按 LRU 顺序排列。当需要新块时,从队列头部弹出块;如果该块是缓存块(即被哈希表引用),则先将其从缓存中驱逐(移除哈希映射),再分配给新请求。释放块时,按照逆序(最后使用的块最先放回)加入队列尾部,这样最近使用的块更晚被驱逐。

SGLang 的 RadixAttention 在基数树上实现 LRU。每个节点记录最近访问时间,当显存不足时,递归地驱逐叶节点(即不再被任何请求引用的末端节点)。优先驱逐叶节点是因为它们代表更长的后缀,被复用的概率通常低于靠近根的前缀节点。这种策略与 TensorRT-LLM 的“驱逐叶节点而非分支节点”思路一致,旨在保护共享度高的前缀。

一个容易被忽视的问题是重复块。在 vLLM 的示例中,如果两个请求生成完全相同的序列,由于块表是追加式的,第二个请求可能会分配新块并缓存,导致同一哈希对应两个物理块。vLLM 在请求结束时通过引用计数检测重复,并释放冗余块,但运行期间会短暂浪费显存。SGLang 的树结构则天然避免了重复,因为相同序列在树中只会有一个节点。

命中率如何影响延迟与吞吐

前缀缓存的收益直接取决于命中率。命中率越高,节省的计算和显存越多,从而允许更大的 batch size 和更低的延迟。

以多轮对话为例,假设一个会话有 4 轮交互,每轮都包含完整的对话历史。如果没有前缀缓存,每一轮都要重新计算所有历史 token 的 KV,总计算量约为 O(4×N²)(N 为平均序列长度)。启用前缀缓存后,第一轮计算完整前缀,后续三轮只需计算新增部分,总计算量降至 O(N² + 3×ΔN²),其中 ΔN 远小于 N。AWS 博客中的实验显示,在 8 台 g5.2xlarge 实例上运行 Llama-3.1-8B 模型,使用 SGLang 的有状态路由(确保同一会话请求到达同一节点)后,多轮对话的首 token 延迟(TTFT)相比随机路由降低约 30%~50%(不同轮次有差异)。

批量请求场景中,如果多个请求共享同一系统提示或文档上下文,前缀缓存可以显著提升吞吐。vLLM 论文评估显示,在相同延迟下,PagedAttention 结合前缀共享可将吞吐提升 2~4 倍,尤其当序列较长、模型较大时效果更明显。SGLang 论文则报告,在包含少样本示例、自洽性采样等任务上,RadixAttention 可带来最高 6.4 倍的吞吐提升。

然而,命中率并非恒定。它受请求分布、缓存容量和驱逐策略共同影响。如果请求的前缀差异大(例如不同用户使用完全不同的系统提示),命中率会急剧下降。缓存容量有限时,频繁的驱逐会导致“缓存抖动”:刚被驱逐的前缀又被新请求需要,不得不重新计算。此外,碎片化也会降低有效缓存容量——即使总空闲显存足够,如果它们不是连续的块,可能无法分配一个大块的 KV Cache,迫使系统驱逐更多缓存。

集群环境下的命中率挑战

单节点上的前缀缓存已经有效,但生产环境通常使用多节点集群来承载更大流量。这时,负载均衡策略会显著影响缓存命中率。

随机负载均衡会将请求均匀分发到所有节点,这在资源调度上公平,但破坏了前缀的局部性。同一个会话的多轮请求可能被分发到不同节点,导致每个节点都无法复用之前计算的 KV Cache。AWS 博客指出,这会使缓存复用概率“随着集群规模的增大而降低”。

解决思路是“有状态路由”或“会话亲和性”(Sticky Session)。Amazon SageMaker 提供了有状态会话路由功能:客户端在创建会话时获得一个会话 ID,后续请求携带该 ID,负载均衡器会将它们路由到同一节点。这样,同一会话的所有请求都能复用该节点上的前缀缓存。实验表明,这种策略在多轮对话中能显著降低 TTFT。

但会话亲和性也有代价。它可能导致节点负载不均,因为某些会话可能特别长或请求频繁,造成热点。此外,如果节点故障,会话缓存会丢失,需要客户端重试并重建缓存。因此,工程上需要在命中率和负载均衡之间权衡,例如结合一致性哈希,既保证相同前缀路由到相同节点,又允许节点动态增减。

实现对比:vLLM vs SGLang

下表从多个维度对比 vLLM 和 SGLang 在前缀缓存实现上的差异。

维度vLLMSGLang
匹配结构哈希表,按块匹配基数树,按 token 序列匹配
匹配粒度块大小(通常 16 token)token 级别
缓存管理空闲块队列 + LRU树节点 LRU,递归驱逐叶节点
重复块处理运行时可能产生重复,请求结束时回收树结构天然避免重复
多模态支持通过额外哈希(图像哈希)区分支持图像 token,树边可标记
CPU 开销哈希计算(可升级 SHA256)树遍历与维护(声称极小)
适用场景高吞吐批量推理,与 PagedAttention 深度集成复杂语言模型程序,如多轮对话、树搜索

两者并非互斥,而是针对不同设计目标。vLLM 的哈希匹配实现更轻量,与 PagedAttention 的分页机制无缝结合,适合通用推理服务。SGLang 的 RadixAttention 则更灵活,能高效处理结构化提示和动态前缀(如思维树搜索),适合复杂生成任务。

碎片化:隐藏的命中率杀手

即使有完善的缓存和驱逐策略,显存碎片化仍可能侵蚀前缀缓存的收益。碎片化来自两方面:一是分块管理本身,块大小固定,请求长度不一定是块大小的整数倍,导致每个请求的最后一个块部分填充,浪费显存;二是缓存块的分配与释放顺序,可能将连续显存分割成小块,使得大块连续请求无法满足,即使总空闲显存足够。

vLLM 的 PagedAttention 通过虚拟内存方式解决了碎片化:KV Cache 块可以不连续存储,块表维护逻辑到物理块的映射。这消除了外部碎片,但内部碎片(块内未使用的槽位)依然存在。前缀缓存进一步加剧了内部碎片,因为复用的前缀块可能被多个请求共享,而每个请求的最后一个块仍然部分空置。

SGLang 的 RadixAttention 同样使用分页布局,面临类似问题。不过,由于树结构共享前缀,它可能减少整体块分配量,间接缓解碎片化。

工程上,缓解碎片化的手段包括:选择合适的块大小(越小碎片越少,但管理开销越大);使用内存池预分配所有块,避免动态分配造成的碎片;以及定期整理或重启服务。但这些方法都有代价,需要根据负载特征调整。

缓存命中流程:一个多轮对话示例

下面通过一个具体场景展示前缀缓存的完整工作流。假设一个多轮对话服务,用户连续发送两条消息,系统提示相同。

flowchart TD
    A[请求1: 系统提示+用户消息1] --> B[计算KV Cache并缓存前缀]
    B --> C[生成回复1]
    C --> D[请求2: 系统提示+用户消息1+回复1+用户消息2]
    D --> E{前缀匹配}
    E -- 命中 --> F[复用缓存KV]
    E -- 未命中 --> G[重新计算未命中部分]
    F --> H[仅计算新增token的KV]
    G --> H
    H --> I[生成回复2]
    I --> J[释放请求2,更新缓存]

在 vLLM 中,请求 1 到来时,调度器调用 get_computed_blocks() 检查缓存,未命中,于是分配新块并计算 KV。当块填满时,计算哈希并加入缓存表。请求 2 到来时,调度器按顺序计算每个块的哈希并查表,命中前几个块(对应系统提示和对话历史),直接复用;未命中的块分配新块计算。请求结束时,如果某块引用计数为零,则释放回空闲队列,并根据 LRU 顺序决定是否驱逐缓存。

在 SGLang 中,请求 1 的 KV 缓存被插入基数树,形成从根到叶的路径。请求 2 到来时,运行时遍历树,匹配到最长公共前缀(对应系统提示和第一轮对话),然后只计算剩余部分,并将新节点插入树中。当显存不足时,树中最近最少访问的叶节点被驱逐,释放其 KV 缓存。

未解决的问题与权衡

前缀缓存虽然有效,但仍有一些开放问题。首先,缓存共享的粒度与安全隔离之间的平衡。在多租户环境中,不同用户的请求可能共享系统提示,但必须确保用户数据不会通过缓存侧信道泄露。vLLM 的哈希匹配可以通过为不同租户使用不同哈希种子来隔离,但会降低共享度。SGLang 的树结构也可以按租户分树,但同样面临权衡。

其次,动态前缀场景下的缓存失效。例如,如果系统提示包含时间戳或随机数,每次请求的前缀都不同,缓存完全失效。工程上需要识别并剔除这种易变部分,或者使用模板参数化,将可变部分与固定部分分离。

最后,缓存与调度策略的协同。前缀缓存增加了调度复杂度:调度器不仅需要考虑请求优先级和资源可用性,还要考虑缓存局部性,尽量将共享前缀的请求批量调度到一起。vLLM 和 SGLang 都在这方面做了初步探索,但尚未完全解决。

前缀缓存的收益取决于工作负载特征。如果请求之间几乎没有公共前缀,缓存不仅无益,还会占用显存并增加管理开销。因此,部署前应分析实际请求日志,评估前缀重复率,再决定是否启用以及如何配置缓存容量。

资料来源

  1. vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention
  2. SGLang: Efficient Execution of Structured Language Model Programs
  3. 基于 Amazon SageMaker 有状态路由优化大规模推理集群下的 KV Cache 复用方案 | 亚马逊AWS官方博客
  4. 自动前缀缓存 | vLLM 中文站