为什么 Prefix Caching 成为大模型推理的必备优化?
在大模型推理服务的生产环境中,一个经常被忽视但影响巨大的现象是:大量请求共享相同的前缀。无论是系统提示词(System Prompt)、Few-shot 示例,还是多轮对话中的历史上下文,这些重复出现的 Token 序列在每一次请求中都会被完整地重新计算一遍 Prefill,生成完全相同的 KV Cache。
这种重复计算带来的浪费是惊人的。以一个 8K Token 的 System Prompt 为例,在 A100 上其 Prefill 阶段大约需要 150ms。如果你的服务每秒处理 100 个请求,其中 80% 共享这个 System Prompt,那么每秒有 80 次完全相同的 Prefill 计算,累计浪费的 GPU 算力高达 12 秒/秒——也就是说,你需要额外 12 张 A100 才能弥补这个浪费。
Prefix Caching 的核心思想很简单:如果多个请求共享相同的前缀,那么只需要为第一个请求计算 Prefill 并缓存其 KV Cache,后续请求可以直接复用这部分缓存,跳过重复计算。这个看似简单的优化,在实际生产环境中可以将 Prefill 的平均延迟降低 40%-70%,同时将 GPU 利用率提升 30% 以上。

KV Cache 复用的理论基础:从 Block 粒度到哈希索引
要理解 Prefix Caching 的实现,首先需要理解现代推理引擎中 KV Cache 的管理方式。以 vLLM 为代表的引擎采用了 PagedAttention 机制,将 KV Cache 按固定大小的 Block(通常为 16 或 64 个 Token)进行管理,类似于操作系统中的虚拟内存分页机制。
在 PagedAttention 下,Prefix Caching 的实现变得自然且高效:
- Block 粒度的缓存:KV Cache 以 Block 为单位存储和复用,不需要整个前缀完全匹配,只要前缀的某个 Block 段匹配即可复用
- 引用计数管理:每个 Block 维护一个引用计数,当所有引用该 Block 的请求完成后,Block 才可以被释放或驱逐
- Copy-on-Write 语义:当新请求的前缀与缓存匹配时,直接引用已有的 Block;当请求进入非前缀部分时,分配新的 Block 写入 KV Cache
Block 哈希:如何快速判断前缀是否可复用?
Prefix Caching 的关键技术挑战是如何高效判断一个新请求的前缀是否已有缓存。朴素的做法是对每个 Block 计算其内容的哈希值,然后查表。但这里有一个微妙的问题:KV Cache 的 Block 不是独立存在的,它们构成了一条链——每个 Block 的 KV 值依赖于之前所有 Block 的计算结果。
因此,正确的做法是计算增量哈希(Incremental Hash)。具体来说,Block
1 | i |
的哈希值不仅取决于 Block
1 | i |
本身的 Token 内容,还取决于 Block
1 | 0 |
到 Block
1 | i-1 |
的哈希链。这样,两个请求只有在从第一个 Token 开始完全一致的前缀上才能共享 Block:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 # 简化的增量哈希计算逻辑
def compute_block_hash(tokens, block_size, prev_hash=0):
"""计算一个 Block 的增量哈希"""
block_tokens = tokens # 当前 Block 的 Token 列表
block_content = tuple(block_tokens)
# 将前一个 Block 的哈希作为输入,保证链式依赖
return hash((prev_hash, block_content))
def compute_prefix_hashes(token_ids, block_size=16):
"""为整个前缀计算所有 Block 的哈希"""
hashes = {}
prev_hash = 0 # 初始哈希
for i in range(0, len(token_ids), block_size):
block = token_ids[i:i+block_size]
prev_hash = compute_block_hash(block, block_size, prev_hash)
block_index = i // block_size
hashes[block_index] = prev_hash
return hashes
这种增量哈希方案确保了:只有真正相同的前缀才能命中缓存。即使两个请求中间有 1 个 Token 的差异,差异点之后的所有 Block 都无法复用。

RadixAttention:从线性前缀到树状结构的质变
传统的 Prefix Caching 只能处理线性前缀匹配——即两个请求的前缀必须从第一个 Token 开始完全一致。但在实际场景中,前缀的共享关系远比线性结构复杂:
- 多轮对话:同一个用户的多轮对话共享历史,但不同用户的历史各不相同,形成树状分支
- 不同 System Prompt 变体:多个 System Prompt 共享相同的基础部分,但在末尾有不同的任务指令
- 并行采样:同一个 Prompt 生成多个回复时,所有回复共享相同的前缀 KV Cache
SGLang 提出的 RadixAttention 机制正是为了解决这一问题。它将 KV Cache 的复用从线性结构推广到了基数树(Radix Tree)结构,能够高效地发现和利用树状前缀中的共享模式。
RadixAttention 的核心数据结构
RadixAttention 维护一棵基数树,其中:
- 边(Edge):表示一段 Token 序列(可能跨越多个 Block)
- 节点(Node):存储对应位置的 KV Cache Block 指针和引用计数
- 路径(Path):从根节点到某个叶子节点的路径代表一个完整的前缀
当新请求到来时,RadixAttention 会在基数树中执行最长前缀匹配(Longest Prefix Match),找到已有缓存中与该请求共享的最长前缀,然后直接复用这部分 KV Cache,只对不匹配的部分执行 Prefill 计算。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41 class RadixTreeNode:
"""基数树节点"""
def __init__(self):
self.children = {} # token_sequence -> RadixTreeNode
self.kv_blocks = [] # 该节点对应的 KV Cache Block 列表
self.ref_count = 0 # 引用计数
class RadixAttentionTree:
"""RadixAttention 基数树"""
def __init__(self):
self.root = RadixTreeNode()
def match(self, token_ids):
"""最长前缀匹配,返回匹配的节点路径和剩余未匹配的 Token"""
node = self.root
matched_blocks = []
remaining = token_ids
while remaining and node.children:
matched = False
for edge_tokens, child in node.children.items():
# 检查剩余 Token 是否与某条边匹配
prefix_len = self._common_prefix_len(edge_tokens, remaining)
if prefix_len == len(edge_tokens):
# 完全匹配这条边,继续向下
matched_blocks.extend(child.kv_blocks)
node = child
remaining = remaining[prefix_len:]
matched = True
break
elif prefix_len > 0:
# 部分匹配,需要分裂节点(Radix Tree 标准操作)
matched_blocks.extend(child.kv_blocks[:prefix_len // BLOCK_SIZE])
remaining = remaining[prefix_len:]
node = child # 简化,实际需要分裂
matched = True
break
if not matched:
break
return matched_blocks, remaining
RadixAttention 的驱逐策略
当 GPU 显存不足时,需要驱逐部分 KV Cache Block 以腾出空间。传统 LRU 策略在基数树结构下并不适用,因为树中的节点有父子依赖关系——你不能驱逐一个父节点而保留其子节点。
RadixAttention 采用了引用感知的 LRU 驱逐策略:
- 优先驱逐引用计数为 0 的叶子节点(没有任何活跃请求正在使用)
- 当所有叶子节点的引用计数都大于 0 时,选择最近最少访问的叶子节点
- 驱逐叶子节点后,如果其父节点也变为无引用的叶子节点,则递归驱逐
- 保证基数树始终是压缩的(没有不必要的中间节点)
这种策略确保了:被驱逐的总是「最冷」且「无引用」的 KV Cache,最大限度地减少对活跃请求的影响。

vLLM 的 Automatic Prefix Caching(APC)实现详解
vLLM 从 v0.4.0 开始引入了 Automatic Prefix Caching(APC),其实现基于 PagedAttention 的 Block 管理机制。与 SGLang 的 RadixAttention 不同,vLLM 的 APC 采用的是基于哈希的 Block 级缓存,而非树状结构。
APC 的核心组件
| 组件 | 功能 | 关键设计 |
|---|---|---|
| BlockSpaceManager | 管理 Block 分配与释放 | 区分可驱逐 Block(evictable)与不可驱逐 Block(non-evictable) |
| BlockPool | 物理 Block 池 | 维护 free list 和 cached list |
| Evictor | 缓存驱逐策略 | 支持 LRU 和 Clock 算法 |
| PrefixHashTable | 前缀哈希索引 | 增量哈希 + Block 内容哈希 |
APC 的工作流程
当 APC 启用时,vLLM 的请求处理流程如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39 # APC 启用后的请求处理伪代码
def process_request_with_apc(request, scheduler, block_manager):
# 1. 计算请求前缀的 Block 哈希
prefix_hashes = compute_prefix_hashes(
request.token_ids,
block_size=block_manager.block_size
)
# 2. 在哈希表中查找可复用的 Block
matched_blocks = []
for block_idx, block_hash in prefix_hashes.items():
cached_block = block_manager.prefix_table.get(block_hash)
if cached_block is not None:
matched_blocks.append(cached_block)
else:
break # 哈希不连续,后续无法复用
# 3. 复用已缓存的 Block,只 Prefill 剩余部分
if matched_blocks:
num_cached_tokens = len(matched_blocks) * block_manager.block_size
request.cached_prefix_blocks = matched_blocks
request.remaining_tokens = request.token_ids[num_cached_tokens:]
# 引用计数 +1
for block in matched_blocks:
block.ref_count += 1
# 4. 对剩余 Token 执行 Prefill
scheduler.schedule_prefill(
request,
num_cached_tokens=len(matched_blocks) * block_manager.block_size
)
# 5. 新计算出的 Block 加入哈希表
for new_block in request.new_blocks:
block_hash = compute_block_hash(
request.token_ids[new_block.token_range],
prev_hash=matched_blocks[-1].hash if matched_blocks else 0
)
block_manager.prefix_table[block_hash] = new_block
值得注意的是,vLLM 的 APC 在 Prefill 调度层面也做了优化:当请求命中缓存时,PagedAttention 的 Attention 计算会自动跳过已缓存的 Token,因为这部分 Token 的 KV 值已经存在于 Block 中,不需要重新计算。这个跳过是通过在 Attention Kernel 中设置
1 | cache_offset |
参数实现的。
Prefix Caching 对请求调度策略的影响
Prefix Caching 的引入不仅仅是 KV Cache 管理层的优化,它还深刻影响了推理引擎的请求调度策略。在没有 Prefix Caching 时,调度器只需考虑 GPU 显存容量和请求优先级;而引入 Prefix Caching 后,调度器还需要考虑请求之间的前缀共享关系,以最大化缓存命中率。
前缀感知调度(Prefix-Aware Scheduling)
前缀感知调度的核心原则是:优先调度与当前缓存中已有前缀匹配的请求。这样可以最大化缓存复用,减少重复 Prefill 计算。
具体来说,调度器在从等待队列中选择下一个要处理的请求时,会执行以下逻辑:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21 def prefix_aware_schedule(waiting_queue, cache_state):
"""前缀感知调度器"""
best_request = None
best_cache_hit = -1
for request in waiting_queue:
# 计算该请求能命中多少缓存
cache_hit_tokens = estimate_prefix_match(
request.token_ids,
cache_state
)
# 综合考虑缓存命中率和请求等待时间
score = cache_hit_tokens * CACHE_WEIGHT + \
request.waiting_time * WAIT_WEIGHT
if score > best_cache_hit:
best_cache_hit = score
best_request = request
return best_request
这种策略面临一个经典的权衡:缓存命中率 vs 请求公平性。如果过度优先调度缓存命中的请求,可能导致那些没有缓存命中的新请求长期等待(饥饿问题)。实践中的解决方案是设置一个最大等待时间阈值,超过阈值的请求无论缓存命中率如何都会被优先调度。
分组 Prefill(Chunked Prefill)与 Prefix Caching 的协同
Chunked Prefill 将长 Prompt 的 Prefill 拆分为多个 Chunk 逐步执行,与 Prefix Caching 天然互补:
- 缓存命中时:Chunked Prefill 可以直接跳过已缓存的前缀 Chunk,只计算未缓存的 Chunk
- 缓存未命中时:Chunked Prefill 可以将 Prefill 计算与其他请求的 Decode 交错执行,避免长 Prompt 占满 GPU 导致其他请求的 Decode 延迟飙升
- 部分命中时:Chunked Prefill 可以精确地在缓存命中的边界处切分,确保每个 Chunk 要么完全命中缓存,要么完全需要重新计算
在 vLLM 和 SGLang 的最新版本中,Chunked Prefill 与 Prefix Caching 的协同已经默认启用,通常不需要额外配置。

生产环境部署:配置调优与性能基准
在生产环境中启用 Prefix Caching 并非一键开启那么简单,需要根据业务场景和硬件配置进行细致的调优。以下是基于 A100/H100 集群的实际部署经验总结。
vLLM APC 配置
1
2
3
4
5
6
7 # 启动 vLLM 时启用 APC
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.1-70B-Instruct \
--enable-prefix-caching \
--block-size 16 \
--max-num-seqs 256 \
--gpu-memory-utilization 0.90
关键参数说明:
-
1--enable-prefix-caching
:启用 Automatic Prefix Caching
-
1--block-size 16
:Block 大小,较小的 Block 大小提供更细粒度的缓存复用,但会增加管理开销。推荐值:16(默认)或 64
-
1--gpu-memory-utilization 0.90
:GPU 显存利用率,为 KV Cache 预留足够空间。Prefix Caching 会额外占用显存,建议不低于 0.85
SGLang RadixAttention 配置
1
2
3
4
5
6
7
8
9
10 # SGLang 默认启用 RadixAttention,无需额外参数
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.1-70B-Instruct \
--tp 8 \
--mem-fraction-static 0.88 \
--chunked-prefill-size 8192
# RadixAttention 相关环境变量
export SGLANG_RADIX_CACHE_THRESHOLD=0 # 缓存阈值:匹配多少 Token 以上才复用
export SGLANG_RADIX_CACHE_EVICTION=1 # 启用驱逐
性能基准测试结果
以下是在 8×A100-80GB 集群上,使用 Llama-3.1-70B 模型的性能基准测试结果。测试场景模拟了典型的聊天服务负载,其中 70% 的请求共享一个 4K Token 的 System Prompt:
| 指标 | 无 Prefix Caching | vLLM APC | SGLang RadixAttention |
|---|---|---|---|
| 平均 TTFT (ms) | 320 | 145 | 128 |
| P99 TTFT (ms) | 890 | 380 | 310 |
| 吞吐量 (tokens/s) | 12,400 | 17,800 | 19,200 |
| GPU 利用率 (%) | 62 | 78 | 82 |
| KV Cache 显存占用 (GB) | 45 | 58 | 56 |
| 缓存命中率 (%) | — | 68 | 74 |
从数据可以看出:
- TTFT 降低 55%-60%:Prefix Caching 的最大收益在于首 Token 延迟的显著降低,因为跳过了重复前缀的 Prefill 计算
- 吞吐量提升 44%-55%:GPU 算力不再浪费在重复 Prefill 上,可以服务更多请求
- SGLang 略优于 vLLM:RadixAttention 的树状结构在多分支场景下缓存命中率更高,但差距不大
- 显存占用增加 25%-30%:这是缓存带来的代价,需要预留足够的显存空间
进阶话题:Prefix Caching 的局限性与未来方向
当前局限性
Prefix Caching 虽然效果显著,但并非万能药,以下场景下其收益有限甚至无法使用:
- 前缀完全无共享:如果每个请求的 Prompt 都完全不同(如摘要任务,每条输入是不同的文章),Prefix Caching 几乎没有收益
- 前缀频繁变化:如果 System Prompt 包含动态内容(如当前时间、用户信息),前缀的共享部分会变短,缓存命中率下降
- 并行采样(Beam Search / Best-of-N):虽然多个采样共享 Prompt 前缀,但采样后的 Divergence 部分无法共享,且并行采样期间的 Block 引用管理复杂度较高
- 跨请求的 Sliding Window Attention:当使用 SWA 时,旧 Token 的 KV Cache 会被自动丢弃,导致前缀缓存的有效窗口受限
未来方向
Prefix Caching 的研究仍在快速发展,以下是一些值得关注的方向:
1. 跨模型缓存复用:当同一架构的不同量化版本(如 FP16 和 INT8)部署在同一 GPU 上时,理论上可以共享 Token Embedding 和部分 Attention 层的 KV Cache,避免重复计算。这需要在 Block 存储层增加格式转换逻辑。
2. 分布式 Prefix Cache:在多卡、多节点的部署中,将 Prefix Cache 分布在多张 GPU 上,通过 RDMA 或 NVLink 实现跨卡缓存查找与复用。这类似于 CDN 的思路——将热点前缀的 KV Cache 缓存在离计算单元最近的位置。
3. 语义级缓存:当前的 Prefix Caching 是精确匹配(Token-level),语义级缓存则尝试在语义相似但不完全相同的 Prompt 之间复用 KV Cache。这需要引入近似匹配算法和容错机制,是更前沿的研究方向。
4. 与 Speculative Decoding 的结合:当 Prefix Caching 与 Speculative Decoding 同时启用时,Draft Model 和 Target Model 可以共享前缀缓存,而 Speculative Token 的验证过程也可以复用已缓存的 KV 值,进一步减少验证阶段的计算量。

实战建议:如何最大化 Prefix Caching 的收益
基于以上分析,以下是生产环境中最大化 Prefix Caching 收益的实践建议:
- 统一 System Prompt:尽量让同一服务内的所有请求使用相同的 System Prompt,这是最直接有效的缓存命中率提升手段
- 将动态内容放在 Prompt 末尾:将变化的指令、用户输入等放在 Prompt 的最后部分,确保前面的静态内容可以被缓存
- 合理设置缓存容量:监控缓存命中率和驱逐率,如果驱逐率过高(>20%),说明缓存容量不足,需要增加 GPU 显存或减少并发请求数
- 启用 Chunked Prefill:Prefix Caching 与 Chunked Prefill 协同效果最佳,建议同时启用
- 监控缓存命中率:vLLM 和 SGLang 都提供了缓存命中率的监控指标,通过 Prometheus 采集并设置告警,命中率低于 30% 时需要排查原因
- 注意显存预算:Prefix Cache 会额外占用显存,建议
1gpu_memory_utilization
设为 0.88-0.92 之间,为缓存预留足够空间
Prefix Caching 已经成为现代大模型推理服务的标配优化。无论是 vLLM 的 Block 级 APC 还是 SGLang 的 RadixAttention,其核心思想都是消除重复计算,最大化 KV Cache 的复用率。对于任何有共享前缀场景的推理服务来说,启用 Prefix Caching 都是最具性价比的优化之一——投入为零(只需开启配置),收益却是 TTFT 减半、吞吐量提升近 50%。
汤不热吧