前缀缓存与语义缓存:KV 复用与重复计算消除

系统提示词、few-shot 示例、RAG 上下文在每次请求里重复出现,prefill 一遍遍重算。前缀缓存复用 KV、语义缓存按语义命中答案,两者把重复计算压到接近零。本文讲透 RadixAttention 的块哈希复用、语义缓存的相似度匹配与正确性风险,以及分层缓存架构与命中率优化。

生产环境里大量请求共享同一段前缀:系统提示词、few-shot 示例、RAG 检索到的公共上下文。这些 token 每次都重算 prefill,纯属浪费。前缀缓存(KV 复用)与语义缓存(按语义匹配历史答案)是消除重复计算的两把刀。本文讲透 RadixAttention 的块哈希复用、语义缓存的相似度匹配与风险,以及分层缓存架构与命中率调优。

前置:/ai-kv-cache-paged-attention/(KV Cache 与分页)、/ai-vllm-system/(vLLM 服务架构)、/ai-prefill-decode-continuous-batching/(prefill/decode 与批处理)。

目录

1. 重复计算从哪里来

先看清重复计算的规模,才知道缓存的价值。

典型重复来源:
□ 系统提示词(system prompt):每次请求都带,常常数百到数千 token
□ few-shot 示例:固定示例集,每请求重复
□ RAG 公共上下文:同一知识库检索出的相似片段
□ 多轮对话历史:后续轮次重复前面的上下文
□ Agent 循环:同一任务的多步共享大量上下文

重复的代价:
□ prefill 是 O(N²)(注意力)→ 前缀越长越贵
□ 前缀 2000 token × 每请求 → 每天百万请求 = 20 亿 token 白算
□ 直接体现为 TTFT(首 token 延迟)与 GPU 成本
缓存能省多少:
□ 场景:2000 token 系统提示 + 200 token 用户输入
□ 无缓存:prefill 2200 token
□ 前缀命中:prefill 200 token(前缀 KV 直接复用)
→ prefill 计算量降约 90%,TTFT 大幅下降

工程要点:重复计算主要来自系统提示词、few-shot、RAG 公共上下文、多轮历史和 Agent 循环。前缀命中能把 prefill 从「全量重算」降到「只算新增」,TTFT 与成本同步下降。缓存的第一性收益是「消除重复 prefill」,识别重复来源是设计缓存的第一步。

2. 前缀缓存原理:KV 复用与哈希匹配

前缀缓存的本质:把已算过的 KV 存下来,命中时直接复用。

核心机制:
□ 前缀相同 → 其 KV 完全相同(注意力是因果的)
□ 把前缀按块(block)切分,每块算一个哈希
□ 新请求逐块比对哈希 → 找到最长匹配前缀
□ 匹配部分的 KV 直接复用,只需算「不匹配的尾部」

为什么前缀相同 KV 就相同:
□ 因果注意力:第 i 个 token 的 KV 只依赖前 i 个 token
□ 前缀一致 → 每个位置的输入一致 → KV 一致
□ 这是前缀缓存成立的数学基础
块哈希示意:
prompt: [系统提示 512 token][RAG 256 token][用户问题 64 token]

按 16 token 一块切分:
block 0:  hash(a1b2...) ─┐
block 1:  hash(c3d4...)  ├─ 命中已有缓存
block 2:  hash(e5f6...) ─┘
block 3:  hash(...)      ← 未命中,需计算
→ 复用前 3 块 KV,只算 block 3 起的部分
前缀缓存的两种粒度:
□ 块级(block-level):按固定大小分块哈希,粒度粗但快
□ token 级:逐 token 匹配,更精确但开销大
□ 主流实现用块级(与 PagedAttention 的块对齐)

工程要点:前缀缓存成立的数学基础是「因果注意力下,前缀相同则 KV 相同」。实现上用块哈希(与分页块对齐)逐块匹配,找到最长公共前缀后复用其 KV,只计算尾部。块级粒度在精确性与开销间取得平衡,是主流做法。

3. 前缀缓存实现:RadixAttention 与块哈希

SGLang 的 RadixAttention 是前缀缓存的代表性实现。

RadixAttention 的数据结构:
□ 用基数树(Radix Tree)组织已缓存的 token 序列
□ 树的每条边是一段 token,节点指向对应的 KV 块
□ 新请求在树上做最长前缀匹配
□ 匹配到的节点 → 复用其 KV;未匹配 → 新分支

为什么用基数树:
□ 天然支持「共享前缀」(公共祖先即公共前缀)
□ 插入/查询高效(按 token 段跳跃)
□ LRU 淘汰只需标记叶子节点
RadixAttention 匹配示意:
root
 ├─ "You are a helpful assistant..." (系统提示 A)
 │    ├─ "Example 1: ..." (few-shot)
 │    │    └─ "Q: 用户问题 1"
 │    └─ "Example 2: ..."
 └─ "You are a coding expert..." (系统提示 B)
      └─ "Q: 用户问题 2"

新请求 "You are a helpful assistant... Example 1: ... Q: 新问题"
→ 沿树走到 "Example 1" 节点,复用其 KV
→ 从 "Q: 新问题" 处新开分支,只算这段
# 块哈希的简化实现思路
def block_hash(tokens, block_size, prefix_hash=None):
    h = prefix_hash or 0
    for i in range(0, len(tokens), block_size):
        blk = tuple(tokens[i:i + block_size])
        h = hash((h, blk))     # 链式哈希:包含所有前序块
    return h
# 链式哈希保证:哈希相同 → 前缀完全相同(含顺序)

工程要点:RadixAttention 用基数树组织缓存,把「共享前缀」表达为树的公共祖先,最长前缀匹配即沿树下行。块哈希必须用链式哈希(把前序块的哈希也纳入),保证「哈希相同即前缀相同」。这是前缀缓存既高效又正确的前提。

4. 前缀缓存调度:命中、淘汰与多租户

缓存命中率决定收益,淘汰策略决定命中率。

调度要点:
□ 命中判定:请求到达即做前缀匹配(在调度前)
□ 缓存复用:命中块的 KV 引用计数 +1
□ 引用计数归零 → 可被淘汰
□ 淘汰:LRU(最近最少使用)为主,按块粒度
多租户下的隔离:
□ 跨租户共享前缀:公共系统提示可共享(省显存)
□ 但需注意信息泄露:A 租户的前缀不应被 B 看到
□ 做法:缓存键加入租户/模型标识 → 逻辑隔离
□ 或:仅在明确安全的公共前缀上跨租户共享
命中率的影响因素:
□ 前缀稳定性:系统提示是否常变(变了就全失效)
□ 请求模式:是否大量共享前缀(RAG、Agent 场景高)
□ 缓存容量:显存能存多少块
□ 淘汰策略:LRU 对局部性强的负载有效
缓存容量 vs 命中率:
□ 容量小 → 频繁淘汰 → 命中率低
□ 容量大 → 挤占 KV 空间 → 并发下降
□ 平衡点:把「缓存块」与「活跃 KV」统一分页管理

工程要点:前缀缓存调度分「命中判定、引用计数、LRU 淘汰」三步,与 PagedAttention 的块管理天然统一。多租户下要防信息泄露——缓存键须含租户/模型标识,仅公共前缀可跨租户共享。命中率由前缀稳定性、请求模式、容量与淘汰策略共同决定。

5. 语义缓存:embedding 相似度匹配

前缀缓存只能命中「完全相同的前缀」,语义缓存则按「意思相近」命中。

语义缓存的思路:
□ 把历史「请求 → 回答」存入缓存
□ 新请求先算 embedding
□ 在向量库中检索最相似的缓存项
□ 相似度超过阈值 → 直接返回缓存答案(跳过推理)
→ 命中时零推理成本(连 prefill 都省了)
# 语义缓存的最小流程
def semantic_cache_lookup(query, embed, vstore, threshold=0.95):
    q_vec = embed(query)
    hits = vstore.search(q_vec, top_k=1)
    if hits and hits[0].score >= threshold:
        return hits[0].answer          # 命中:零推理
    return None                        # 未命中:走正常推理
与前缀缓存的差异:
维度         前缀缓存              语义缓存
匹配依据     token 完全相同          embedding 相似
命中收益     省 prefill            省全部推理
正确性       100%(KV 等价)       有风险(相似≠等价)
适用场景     固定前缀、多轮、Agent  FAQ、客服、高频重复问题
典型组合:
□ 请求先查语义缓存(最省)
□ 未命中 → 查前缀缓存(省 prefill)
□ 都未命中 → 正常推理,结果回填两层缓存

工程要点:语义缓存用 embedding 相似度匹配历史问答,命中时连推理都省掉,收益最大但有正确性风险。它与前缀缓存互补——前缀缓存保证 100% 正确只省 prefill,语义缓存省全量推理但需阈值控制。生产上常见「语义 → 前缀 → 正常推理」的三级漏斗。

6. 语义缓存的正确性与风险

语义缓存最危险的地方:相似不等于等价。

正确性风险:
□ 相似问句答案不同:「今天天气如何」vs「明天天气如何」
□ 阈值太低 → 误命中 → 返回错误答案
□ 阈值太高 → 命中率低 → 收益消失
□ 数值/实体敏感:日期、金额、ID 差一点答案完全不同
□ 时效性:历史答案可能已过期
缓解手段:
□ 高阈值(如 0.95+)+ 人工标注验证
□ 实体校验:命中后比对关键实体(日期、数字、ID)是否一致
□ 时效标记:缓存项带 TTL,过期作废
□ 白名单场景:只在「答案稳定」的场景开语义缓存
□ 降级:命中后仍走一次轻量校验(如小模型判别)
不该用语义缓存的场景:
□ 强时效(实时数据、新闻)
□ 强个性化(用户私有数据,答案因人而异)
□ 数值敏感(计算、财务、代码生成)
□ 高风险决策(医疗、法律建议)
安全考量:
□ 跨用户缓存 → 隐私泄露(A 的问题被 B 命中)
□ 必须按用户/租户隔离缓存命名空间
□ 缓存键要含:用户 ID、模型版本、参数(temperature 等)

工程要点:语义缓存的核心风险是「相似不等于等价」——阈值、实体、时效、个性化任一失控都会返回错误答案。缓解靠高阈值 + 实体校验 + TTL + 场景白名单 + 用户隔离。强时效、强个性化、数值敏感、高风险场景不应使用语义缓存。

7. 分层缓存架构:L1、L2 与分布式

单机缓存不够时,缓存要分层。

三层缓存架构:
□ L1(GPU 显存):前缀 KV 块 → 最快,容量最小
□ L2(CPU 内存):KV 块溢出存放 → 中等速度,容量大
□ L3(分布式/磁盘):跨实例共享 → 最慢,容量最大
KV 缓存的层级迁移:
□ 命中 L1:直接复用(微秒级)
□ 命中 L2:H2D 拷贝回 GPU(毫秒级)
□ 命中 L3:网络传输 + H2D(数十毫秒)
□ 未命中:重算 prefill(最慢)
分布式前缀缓存:
□ 多实例共享公共前缀(如统一系统提示)
□ 一台实例算好 → 广播/共享给其他实例
□ 适合「所有请求共享同一超长系统提示」的场景
□ 挑战:一致性、失效同步、网络带宽
LMCache 类方案的思路:
□ 把 KV 块抽出来独立管理(不绑定单个推理实例)
□ 支持 CPU/磁盘/远端多级存储
□ 实例重启不丢缓存
□ 多实例共享同一缓存池

工程要点:缓存分层是「速度与容量」的权衡——L1 GPU 最快最小,L2 CPU 中等,L3 分布式最大最慢。分布式前缀缓存让多实例共享公共前缀,适合统一超长系统提示的场景,但要解决一致性、失效同步与带宽。LMCache 类方案把 KV 块独立管理,支持多级存储与跨实例复用。

8. 生产实践与命中率优化

把缓存落到生产,命中率是第一指标。

提升命中率的手段:
□ 固定系统提示:把可变内容(时间戳、用户 ID)移到后面
□ 提示词模板化:few-shot 示例顺序稳定
□ 前缀对齐:公共前缀尽量放最前面
□ 块大小调优:与分页块对齐,减少碎片
□ 容量规划:按请求模式估算所需缓存块数
关键反模式:
□ 系统提示里带时间戳/随机 ID → 每次都不同 → 永不命中
□ few-shot 示例随机打乱顺序 → 前缀变化 → 命中率崩
□ 把用户输入放在最前 → 公共部分在后面 → 无法复用
□ 缓存键不含模型版本 → 换模型后命中脏缓存
监控指标:
□ 前缀命中率(命中 token / 总 prefill token)
□ 语义缓存命中率与误命中率
□ TTFT 的 P50/P99(缓存生效的直接体现)
□ 缓存显存占用与淘汰频率
□ 缓存收益:省下的 prefill FLOPs 与成本

工程要点:命中率优化的核心是「让前缀稳定且靠前」——固定系统提示、模板化 few-shot、把可变内容后置。最常见反模式是「前缀里塞时间戳/随机 ID」,直接让命中率归零。监控要看前缀命中率、语义命中率与 TTFT 尾延迟。

9. 常见坑与排查

缓存的坑集中在「失效」与「正确性」。

高频踩坑:
□ 前缀含时间戳/随机数 → 命中率恒为 0
□ 缓存未按模型版本隔离 → 换模型后命中错误 KV
□ 语义缓存阈值过低 → 返回错误答案
□ 缓存块未随会话结束释放 → 显存泄漏
□ 多租户共享缓存未隔离 → 信息泄露
□ 缓存容量过大挤占 KV → 并发下降
□ 前缀缓存与 chunked prefill 配合不当 → 命中却仍重算
□ 缓存键未含 temperature 等参数 → 采样配置不同却复用
□ 缓存失效未广播(分布式)→ 各实例结果不一致
排查清单:
□ 打日志:每个请求的「命中前缀长度 / 总长度」
□ 检查系统提示是否含动态内容
□ 检查缓存键构成(模型、租户、参数)
□ 语义缓存抽样人工核验(误命中率)
□ 显存曲线:缓存占用是否稳定(泄漏检测)
□ 对比「开/关缓存」的 TTFT 与成本

工程要点:缓存的坑分两类——失效类(前缀含动态内容、键不含模型/参数版本)与正确性类(语义阈值、租户隔离、失效广播)。排查先打「命中前缀长度」日志,再查缓存键构成,语义缓存必须抽样人工核验误命中率。显存曲线用于检测缓存泄漏。

10. 速查表与一句话记忆

问题一句话答案
重复从哪来系统提示、few-shot、RAG 上下文、多轮、Agent 循环
前缀缓存原理因果注意力下前缀相同则 KV 相同,块哈希匹配复用
RadixAttention基数树组织缓存,公共祖先即公共前缀
块哈希要点链式哈希,保证哈希相同即前缀相同
多租户风险缓存键须含租户/模型标识,防信息泄露
语义缓存原理embedding 相似度匹配历史问答,命中省全量推理
语义缓存风险相似不等于等价,靠高阈值 + 实体校验 + TTL
三级缓存L1 GPU、L2 CPU、L3 分布式
命中率怎么提前缀稳定且靠前,动态内容后置
头号反模式系统提示里塞时间戳/随机 ID

一句话记忆:前缀缓存 = 因果注意力下的 KV 等价复用(块哈希 + 基数树),语义缓存 = embedding 相似度命中(省全量推理但有正确性风险)——命中率靠「前缀稳定且靠前」,头号反模式是「提示里塞动态内容」,监控看前缀命中率与 TTFT 尾延迟。

延伸阅读

  • /ai-kv-cache-paged-attention/ — KV Cache 分页与块管理
  • /ai-vllm-system/ — vLLM 服务与自动前缀缓存
  • /ai-prefill-decode-continuous-batching/ — prefill/decode 与 chunked prefill
  • /ai-llm-serving-observability/ — 服务指标与可观测性
  • /ai-inference-benchmark/ — TTFT 与吞吐基准
  • /ai-serverless-inference/ — 无服务器推理与冷启动

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「ai」更多文章

  1. GPU 共享与调度:MPS、MIG 与多租户隔离
  2. 异构推理硬件:ROCm、Intel 与国产 NPU 适配实践
  3. 长上下文推理:RoPE 扩展、位置插值与 Ring Attention