Prefix Caching学习笔记

在部署大模型推理服务时,Prefix Caching(前缀缓存) 是近年工程上非常实用的一类优化:它把「多条请求里相同的前缀」对应的中间结果缓存起来,避免重复算一遍。


一、基础概念与背景

1.1 什么是 Prefix Caching?

定义 :在自回归大语言模型推理中,对输入序列前缀 部分的 Key/Value(KV)表示进行跨请求复用的技术。新请求若与历史请求(或同一会话内前文)共享同一段 token 前缀,可直接加载已算好的 KV,而不必对该前缀再做一遍完整的前向计算。

核心目标可以概括为三点:

  1. 减少重复计算:相同前缀只算一次(或少量增量)。
  2. 降低首 token 延迟(TTFT):前缀越长、命中率越高,收益越明显。
  3. 提升吞吐:同样算力下能服务更多 QPS,或在固定 SLA 下承载更大并发。

1.2 为什么需要 Prefix Caching?

LLM 推理的成本结构

推理大致分为两阶段(后面第二部分会细讲):

  • Prefill :一次性处理整段 prompt,计算量大(近似与 prompt 长度成超线性关系,取决于实现与并行策略),主要吃 算力
  • Decode :逐 token 生成,每步算子规模小但步数多,往往更受 内存带宽KV 显存占用 制约。

因此,长 prompt、多轮对话、RAG、带长 system 的 Agent 等场景里,Prefill 与 KV 存储都是明显瓶颈;若能跨请求复用前缀 KV,相当于把「已经付过账」的那一段直接从缓存里「取出来」。

典型场景里的「重复前缀」
场景 重复内容通常是什么
多轮对话 system、工具说明、历史前文(若拼接方式固定)
RAG 检索到的文档块、模板化的 query 包装
Few-shot 固定的若干示例 + 格式说明
批量评测 / 数据标注 同一指令模板 + 不同样本后缀

只要多条请求的 token 序列从开头起有一段完全一致,这段就适合作为「可缓存前缀」的候选。

传统 KV Cache 的局限性

单请求推理里,KV Cache 只在本条请求内有效:Prefill 时按层、按位置写入 KV;Decode 时继续追加。请求结束后,显存中的 KV 通常被释放。

因此,另一条新请求 若带着相同的前缀再来一遍,系统会重新做 Prefill ,无法自动「继承」上一条的 KV。Prefix Caching 要做的,就是在多请求 / 多会话 维度上,把「前缀 → KV 块」做成可查找、可复用、可淘汰的共享资源


二、技术原理解析

2.1 KV Cache 机制回顾

Transformer 注意力里的 K/V

在每一层、每个注意力头中,对位置 (i) 的 hidden state 会线性映射得到 Query Q i Q_i Qi、Key K i K_i Ki、Value V i V_i Vi。自注意力需要当前步的 (Q) 与此前所有位置的 (K,V) 做点积与加权。若每步都把历史再算一遍,复杂度会爆炸。

KV Cache 的做法是:对已经处理过的位置,把各层的 (K,V) 存下来 ;后续步只算当前 token 的 (Q,K,V),再与历史 (K,V) 拼接参与注意力。这样 Decode 每步的计算量与上下文长度相关,而不是每步重算整段历史。

Prefill 与 Decode 中的 KV
flowchart LR subgraph Prefill["Prefill(整段 prompt)"] P1["逐 token / 分块前向"] P2["写入各层 KV"] end subgraph Decode["Decode(逐 token 生成)"] D1["只算新 token 的 QKV"] D2["KV 追加到 cache"] end Prefill --> Decode
  • Prefill :输入整段 prompt,生成整段前缀的 KV(可并行度高,算力密集)。
  • Decode:每生成一个新 token,KV 长度 +1;计算形态以「读大量历史 KV + 写当前步」为主。
KV 的内存占用(直觉量级)

粗略地,单层单 token 的 KV 体量与 hidden_size × num_heads(或 head 维拆分方式) 相关;总显存约随 2 × 层数 × 序列长度 × 每 token KV 大小 增长(系数依张量并行、量化等变化)。因此长上下文 + 高并发 下,KV 既是算力问题也是显存预算问题;前缀缓存还要在「复用收益」与「缓存占用」之间做权衡。

Prefix Caching 核心原理

前缀匹配:为何常用「块 + 哈希」

工程实现里很少按「单个 token」做细粒度一致性校验(开销大),多采用 PagedAttention 风格的固定大小块(block/page)

  • 将序列按块切分,例如每块 16 个 token。
  • 对每个块的内容(或块内 token id 序列)算 hash,在全局表里查找是否已有相同块的 KV。

命中条件 :从序列开头起,连续多个块 的 hash 均命中,则这些块对应的 GPU 上 KV 可被复用;第一个未命中块之后的内容仍需正常 Prefill。

flowchart LR R["新请求 token 序列"] R --> M{"块级 hash 前缀匹配"} M -->|连续命中| H["加载已缓存 KV 块"] M -->|某块未命中| N["从该块起做 Prefill"] H --> N N --> G["进入 Decode 并更新缓存元数据"]

缓存粒度:Token 级 vs Block 级

粒度 优点 缺点
Token 级 理论上复用最细 元数据与查找开销大,碎片严重
Block 级 与分页 KV 一致,易与 allocator 结合 仅块内全同才能命中;块边界可能浪费少量 token

主流推理框架(如 vLLM)与 Prefix Caching 结合时,块级是自然选择。

命中后的执行路径(概念上)

  1. 请求到达,调度器拿到 token id 序列。
  2. 自前缀起按块查表:命中则记录「物理块 → 逻辑前缀」映射;未命中处开始算子执行 Prefill。
  3. GPU 上为未命中后缀分配新块,必要时把命中块的 KV 引用到本请求的 block table(见下文引用计数)。
  4. TTFT 主要受益于跳过的 Prefill 计算量;后续 Decode 与无缓存时一致(仍要读整段 KV)。

2.2 数据结构与算法

Radix Tree(基数树)

当需要支持动态插入、按前缀查找、合并公共前缀 时,基数树 很合适:每条从根到叶的路径对应一段 token(或块)序列;共享前缀对应共享路径。插入新序列时,可能在某节点分裂 ;淘汰或过期时可能剪枝

与「纯 hash 块链」相比,Radix 更利于表达层次化的前缀关系可变长度的共享边界(具体实现因框架而异)。

flowchart LR root["根"] root --> A["块: You are..."] A --> B["块: 工具定义..."] A --> C["块: 另一分支..."] B --> L1["...会话 1 后缀"] C --> L2["...会话 2 后缀"]
LRU(或近似 LRU)淘汰

缓存容量有限时,需要驱逐策略。常见做法:

  • 维护块的最后访问时间或访问频次;
  • LRU:最近最少使用的块先被淘汰;
  • 实际系统可能还有引用计数:正在被某进行中的请求引用的块不可释放。
块分配与引用计数

一致性要点:

  • 多个请求共享 同一物理 KV 块时,用 ref_count 表示引用数;请求结束 ref_count 减一。
  • refcount 归零且 LRU 需要空间时,块可回收到空闲池。
  • 新写入的块在 Prefill/Decode 过程中逐步填充,并加到全局「块 hash → 物理块」的映射上。

这样可以在安全复用显存回收之间取得平衡。


三、主流框架实现对比

vLLM:Automatic Prefix Caching

vLLM 的 KV 以 PagedAttention 的块为单位管理。开启 Automatic Prefix Caching 后,会在块粒度上对前缀做 hash 与全局缓存映射。

实现要点(与上文概念对应)

  • 块池 / 空闲队列:物理块在空闲与已缓存之间流转。
  • 缓存块映射:内容 hash → 物理块(命中时把该块链入当前请求的 block table)。
  • 与连续批处理、抢占等机制叠加时,需要保证同一物理块在并发请求间生命周期正确。

配置 :命令行或 API 中启用前缀缓存(具体参数名随版本演进,常见为 --enable-prefix-caching 或配置项 enable_prefix_caching)。升级版本时建议查阅对应 release 的说明,确认默认值与已知限制(例如与某些 speculative 特性、部分并行模式的组合)。

适合理解的重点 :vLLM 的路线是 「PagedAttention 块 + hash 匹配」,与 allocator 强绑定,工程上落地快、与现有 KV 分页一致。

SGLang:RadixAttention

SGLang 在前缀复用上强调 RadixAttention :用 Radix Tree 组织已出现过的前缀,节点与 KV 块对应,随请求动态插入、分裂、合并。

特点

  • 多会话共享:适合大量短请求、长 system、树状分叉的对话与 Agent 图。
  • 节点演化:新 prompt 插入时可能分裂已有节点以区分后缀;淘汰时合并或删除子树以省显存。
  • 自洽性采样 / 多路复用:同一 prompt 多分支采样时,公共前缀在树中只存一份,各分支共享根到分叉点的 KV,降低重复 Prefill。

与 vLLM 的对比直觉

维度 vLLM Automatic Prefix Caching SGLang RadixAttention
组织方式 块 hash + 映射表为主 Radix 树 + 块
强项 与 PagedAttention 一体、易集成批调度 分叉多、多路采样、前缀树状演化
学习曲线 相对贴近「分页 + 哈希表」 需理解树操作与节点生命周期

四、实验

4.1 基准测试

  • 实验设置:不同提示词长度、并发请求数。
  • 性能指标:TTFT、吞吐量、缓存命中率。
  • 结果分析:Prefix Caching的收益边界。

小结

  • Prefix Caching 解决的是:跨请求复用相同前缀的 KV,从而省 Prefill、降 TTFT、提吞吐。
  • 工程上多为 块级 hash 匹配 + 全局块池与淘汰(LRU + refcount) ;高阶实现用 Radix Tree 管理分叉与前缀共享。
  • vLLMSGLang 分别代表了「分页块缓存」与「基数树注意力」两条主流落地路径,选型时可结合业务前缀重复形态(线性长前缀 vs 多分叉)与框架生态评估。

若你正在调某一版 vLLM/SGLang,建议把 块大小、缓存容量、并发下命中率TTFT/P99 放在同一 dashboard 上看;Prefix Caching 的收益高度依赖负载是否真有稳定前缀,需要先有数据再决定是否默认开启。

相关推荐
TechEdu20260621 小时前
[人工智能]Claude、GPT、Gemini、Copilot与Llama:概念、架构、应用与评估
人工智能·ai·llm
长谷深风11121 小时前
为什么你的 Tool 总被模型选错?
java·大数据·ai·llm·ai agent·工具设计·agent设计
boooooooom1 天前
尝尝咸淡:从朴素 RAG 到 Graph RAG——一个烹饪问答系统的三层检索升级之路
前端·后端·llm
武子康1 天前
33B 音画模型塞进 Apple Silicon,h3.c 重写了哪些 Runtime 职责
人工智能·llm·agent
知几蜗牛1 天前
一张图看懂AI、机器学习、深度学习和生成式AI之间的关系
llm
XLYcmy1 天前
小红书 算法一面 十一
google·meta·llm·负载均衡·token·moe·glam
谢白羽1 天前
Mooncake在LLM的kv cache offload
人工智能·llm·论文·agent·mooncake
牧艺2 天前
DeepSeek Harness 上手:一切皆插件的 Agent 运行时,核心流程怎么走
llm·agent·deepseek
魔术师Grace2 天前
模型“开源”了,就能直接拿来做 Agent 吗?
llm·aigc·agent
夫子3962 天前
【第三部分:第一个 Agent 应用】10. 不使用框架,手写一个最小 Agent
llm·agent·ai编程