跳到内容

自动前缀缓存

PagedAttention 的核心思想是将每个请求的 KV 缓存划分为 KV 块。每个块包含固定数量标记的注意力键和值。PagedAttention 算法允许这些块存储在非连续的物理内存中,从而通过按需分配内存来消除内存碎片。

为了自动缓存 KV 缓存,我们利用以下关键观察:每个 KV 块可以通过块内的标记以及块前缀中的标记唯一标识。

                    Block 1                  Block 2                  Block 3
         [A gentle breeze stirred] [the leaves as children] [laughed in the distance]
Block 1: |<--- block tokens ---->|
Block 2: |<------- prefix ------>| |<--- block tokens --->|
Block 3: |<------------------ prefix -------------------->| |<--- block tokens ---->|

在上面的示例中,第一个块中的 KV 缓存可以通过标记“A gentle breeze stirred”唯一标识。第三个块可以通过块中的标记“laughed in the distance”,以及前缀标记“A gentle breeze stirred the leaves as children”唯一标识。因此,我们可以建立以下一对一映射

hash(prefix tokens + block tokens) <--> KV Block

通过此映射,我们可以在 vLLM 的 KV 缓存管理中增加另一层间接性。以前,vLLM 中的每个序列都维护着从其逻辑 KV 块到物理块的映射。为了实现 KV 块的自动缓存,我们将逻辑 KV 块映射到它们的哈希值,并维护一个包含所有物理块的全局哈希表。通过这种方式,所有共享相同哈希值的 KV 块(例如,两个请求之间共享的前缀块)都可以映射到相同的物理块并共享内存空间。

此设计实现了自动前缀缓存,而无需在 KV 块之间维护树结构。更具体地说,所有块都是相互独立的,可以自行分配和释放,这使得我们能够像操作系统中的普通缓存一样管理 KV 缓存。

通用缓存策略

将所有 KV 块保存在哈希表中使 vLLM 能够缓存早期请求的 KV 块,从而节省内存并加速未来请求的计算。例如,如果一个新请求与前一个请求共享系统提示,则共享提示的 KV 缓存可以直接用于新请求而无需重新计算。然而,KV 缓存的总空间是有限的,当缓存已满时,我们必须决定保留或驱逐哪些 KV 块。

通过哈希表管理 KV 缓存允许我们实现灵活的缓存策略。例如,在当前的 vLLM 中,我们实现了以下驱逐策略

  • 当没有空闲块时,我们将驱逐引用计数(即,当前使用该块的请求数量)等于 0 的 KV 块。
  • 如果有多个引用计数等于 0 的块,我们优先驱逐最近最少使用的块(LRU)。
  • 如果有多个块的最后访问时间相同,我们优先驱逐位于最长前缀末尾的块(即,其前面有最大数量块的块)。

请注意,当应用于全注意力模型时,此驱逐策略有效实现了与 RadixAttention 中完全相同的策略,该策略优先驱逐前缀树中引用计数为零和最近最少使用的叶节点。

然而,基于哈希的 KV 缓存管理为我们提供了处理更复杂服务场景和实现超越上述策略的更复杂驱逐策略的灵活性

  • 多 LoRA 服务。当为多个 LoRA 适配器提供服务时,我们可以简单地让每个 KV 块的哈希值也包含请求查询的 LoRA ID,以启用所有适配器的缓存。通过这种方式,我们可以联合管理不同适配器的 KV 块,这简化了系统实现并提高了全局缓存命中率和效率。
  • 多模态模型。当用户输入不仅仅包含离散标记时,我们可以使用不同的哈希方法来处理不同模态输入的缓存。例如,对图像使用感知哈希来缓存相似的输入图像。