vLLM - 设计 - 自动前缀缓存(Automatic Prefix Caching)
vLLM采用自动前缀缓存优化大模型推理,通过哈希机制缓存已处理的KV-cache块,当新请求前缀匹配时直接复用,减少重复计算。该方法支持多模态输入(如图片)和缓存隔离,通过请求级salt增强多租户安全性。vLLM v1采用块池和双向链表管理KV cache,实现高效分配、追加和回收操作。前缀缓存被OpenAI等广泛采用,vLLM通过SHA256哈希降低碰撞风险,每token处理仅增加100-200
自动前缀缓存(Automatic Prefix Caching)
前缀缓存 KV-cache 块是大模型推理中常见的优化方法,可以避免重复的 prompt 计算。核心思想很简单——我们缓存已处理请求的 KV-cache 块,当新请求前缀与已有请求一致时,重用缓存块。由于前缀缓存几乎无副作用且不会改变模型输出,已被许多公有端点(如 OpenAI、Anthropic 等)和大多数开源 LLM 推理框架(如 SGLang)广泛采用。
实现前缀缓存有多种方法,vLLM 采用了基于哈希的方式。具体来说,我们为每个 KV-cache 块根据块内 token 和块前的前缀 token 进行哈希:
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 cache 可唯一用 token “A gentle breeze stirred” 标识。第三块可用块内 token “laughed in the distance” 和前缀 token “A gentle breeze stirred the leaves as children” 唯一标识。因此,块哈希可构造为 hash(tuple[components])
,其中 components 包括:
- 父块哈希值:父块的哈希值。
- 块 token:此块内的 token 元组。包含具体 token 可减小哈希冲突概率。
- 额外哈希:如 LoRA ID、多模态输入哈希(见下例)、cache salt 等,确保块唯一性,支持多租户环境下缓存隔离。
!!! note “注意 1”
只缓存完整块。
!!! note “注意 2”
上述哈希结构理论上并非 100% 无冲突。为避免多租户下哈希碰撞,建议用 SHA256 替代默认内置 hash。
vLLM v0.8.3 起支持 SHA256,可通过命令行参数启用。性能开销约每 token 100-200ns(如 5 万 token 约 6ms)。
多模态输入的哈希示例
以下示例说明多模态输入(如图片)如何支持前缀缓存。假设请求如下:
messages = [
{"role": "user",
"content": [
{"type": "text",
"text": "What's in this image?"
},
{"type": "image_url",
"image_url": {"url": image_url},
},
]},
]
处理后 prompt 为:
Prompt:
<s>[INST]What's in this image?\n[IMG][/INST]
Tokenized prompt:
[1, 3, 7493, 1681, 1294, 1593, 3937, 9551, 10, 4]
带占位符的 prompt (<P>):
[1, 3, 7493, 1681, 1294, 1593, 3937, 9551, <P>, <P>, ..., <P>, 4]
如上,分词后 [IMG]
被替换为一系列占位符 token,预填充时由图片 embedding 替换。前缀缓存支持此场景的难点在于需区分占位符与图片。解决方法是编码前端图片处理器生成的图片哈希。例如,假设块大小16且有41个占位符 token,各块哈希如下:
Block 0
父哈希: None
Token ID: 1, 3, 7493, 1681, 1294, 1593, 3937, 9551, <p>, ..., <p>
额外哈希: <image hash>
Block 1
父哈希: Block 0 hash
Token ID: <p>, ..., <p>
额外哈希: <image hash>
Block 2
父哈希: Block 1 hash
Token ID: <p>, ..., <p>
额外哈希: <image hash>
Block 3
父哈希: Block 2 hash
Token ID: <p>, ..., <p>, 4
额外哈希: <image hash>
下文将介绍 vLLM v1 的前缀缓存数据结构、主要 KV cache 操作(如分配、追加、释放、驱逐)流程,并以实例说明端到端流程。
安全隔离(Cache Isolation)
为增强多租户环境下隐私,vLLM 支持通过请求级 salt 隔离前缀缓存复用。请求中加 cache_salt
,该值注入到首块哈希,确保只有相同 salt 的请求可复用缓存 KV 块,防止通过时延差推断缓存内容的攻击。此举提升安全性且无性能损失。
{
"messages": [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Here is a document with details about the world series: ..."},
{"role": "user", "content": "Who won the world series in 2020?"}
],
"cache_salt": "your-cache-salt"
}
如此,只有同 salt 的用户或请求可共享缓存,实现“信任组”内复用,对外隔离。
!!! note
V0 引擎不支持缓存隔离。
数据结构
vLLM v1 的前缀缓存由 KV cache manager 管理,核心为“Block”数据类(简化版):
class KVCacheBlock:
# 块 ID(不可变)
block_id: int
# 块哈希(块满时赋值,被驱逐时重置)
block_hash: BlockHash
# 当前使用该块的请求数
ref_cnt: int
# 用于空闲队列的双向链表指针
prev_free_block: Optional["KVCacheBlock"] = None
next_free_block: Optional["KVCacheBlock"] = None
两点设计说明:
- KVCacheBlock 块初始化时全部分配为块池,避免 Python 对象创建开销,便于全程追踪所有块。
- 在 KVCacheBlock 中直接引入双向链表指针,可直接构造空闲队列,带来两大好处:
- O(1)时间将任意位置元素移到队尾。
- 避免引入额外 Python 队列(如
deque
),减少包装层。
KV cache manager 初始化时包含如下组件:
- 块池:KVCacheBlock 列表。
- 空闲块队列:仅存头尾块指针以便操作。
- 缓存块:哈希键到块 ID 映射。
- 请求块:请求 ID 到分配块 ID 映射。
操作流程
块分配
新请求: 调度器为新请求分配 KV cache 块流程:
- 调度器调用
kv_cache_manager.get_computed_blocks()
,根据请求的 prompt token 做哈希查找缓存块。 - 调度器调用
kv_cache_manager.allocate_slots()
,步骤如下:- 计算所需新块数,无足够块则返回失败。
- “Touch”已命中的缓存块,增加块引用计数,若块未被其他请求使用则从空闲队列移除,避免被驱逐(见下文例子)。
- 从空闲队列头部分配新块,若头块已缓存,则“驱逐”该块,使其不再被其他请求复用。
- 若新分配块已满,则立即加入缓存块,供同批次其他请求复用。
运行中请求: 调度器为运行中请求分配 KV cache 块流程:
- 调度器调用
kv_cache_manager.allocate_slots()
,步骤如下:- 计算所需新块数,无足够块则返回失败。
- 从空闲队列头部分配新块,若头块已缓存,则“驱逐”该块。
- 向已有块和新块追加 token ID,若块满则加入缓存块。
重复块
假设块大小为4,请求1(Request 1)prompt为 ABCDEF,解码长3:
Prompt: [A, B, C, D, E, F]
Output: [G, H, I]
Time 0:
Tokens: [A, B, C, D, E, F, G]
Block Table: [0 (ABCD), 1 (EFG)]
Cache Blocks: 0
Time 1:
Tokens: [A, B, C, D, E, F, G, H]
Block Table: [0 (ABCD), 1 (EFGH)]
Cache Blocks: 0, 1
Time 2:
Tokens: [A, B, C, D, E, F, G, H, I]
Block Table: [0 (ABCD), 1 (EFGH), 2 (I)]
Cache Blocks: 0, 1
现在块0和块1已缓存,重复发送同样请求(Request 2),若贪婪采样输出一致:
Prompt: [A, B, C, D, E, F]
Output: [G, H, I]
Time 0:
Tokens: [A, B, C, D, E, F, G]
Block Table: [0 (ABCD), 3 (EFG)]
Cache Blocks: 0, 1
Time 1:
Tokens: [A, B, C, D, E, F, G, H]
Block Table: [0 (ABCD), 3 (EFGH)]
Cache Blocks: 0, 1, 3
可见,块3为新满块并缓存,但与块1内容冗余。v0 检测到块3重复时,会释放块3并让 Request 2 用块1,块表变为 [0, 1]
。但 vLLM v1 的块表只能追加,不能从 [0, 3]
改为 [0, 1]
,因此会产生重复块,待请求释放时消除。
释放
请求结束后,若其块未被其他请求使用(引用计数为0),则释放所有块。例如释放请求1及相关块2、3、4、8,释放块按逆序加入空闲队列。原因是请求最后一块哈希更多 token,被复用概率低,应优先驱逐。
驱逐(LRU)
当空闲队列头(最近最少使用块)已缓存时,须驱逐,防止被其他请求使用。驱逐步骤:
- 从空闲队列头弹出块(LRU块)。
- 从缓存块移除该块ID。
- 移除块哈希。
示例
假设块大小为4,KV-cache manager总共10块。
时刻1:缓存为空,新请求到来。 分配4块,其中3块已满并缓存,第4块只填了3个token。
时刻3:请求0使块3满,需新块继续解码。 缓存块3,分配块4。
时刻4:请求1带来14个 prompt token,前10个与请求0一致。 仅前2块(8个token)命中缓存,第3块只匹配2个token。
时刻5:请求0完成释放。 块2、3、4逆序加入空闲队列(但块2、3仍缓存)。块0、1因被请求1用未加入空闲队列。
时刻6:请求1完成释放。
时刻7:请求2带来29个 prompt token,前12个与请求0一致。 即使空闲队列顺序为 7 - 8 - 9 - 4 - 3 - 2 - 6 - 5 - 1 - 0
,命中缓存块(0、1、2)会在分配前被 touch 并移出队列,分配后队列为 7 - 8 - 9 - 4 - 3 - 6 - 5
,分配块为0(缓存)、1(缓存)、2(缓存)、7、8、9、4、3(驱逐)。
原文地址:https://docs.vllm.ai/en/latest/design/prefix_caching.html
当前更新时间:2025-10-08
更多推荐
所有评论(0)