小白学Transformer架构(四) - KV Cache原理

历史文章

小白学Transformer架构- Input Embedding
小白学Transformer架构-Attention-位置感知嵌入变量
小白学Transformer架构-Self-Attention

摘要

在自注意力模块。每一个token会与wq。wk。wv 矩阵进行矩阵乘法生成qkv矩阵。每一个q矩阵会与全部token的k进行点积、缩放、softmax计算归一注意力权重。而后和V矩阵进行加权求和。因为在计算新token时,要重新计算所有历史token的k,v,所以可以对历史token的kv做了缓存。

每次请求进来。默认对固定大小的token块做hash。判断是否在kv cache中。直到无法命中时结束。这个就是prompt缓存的前缀匹配处理流程。

公有云 API 调用一般在缓存索引层的 key 上加账号 ID,实现账号粒度的 KV Cache 隔离。本质是共享同一批物理实例和显存,隔离的目的是防止账号间计费冲突与提示词泄漏风险。

qwen场景下token长度大于有大小限制才会命中cache原因

  1. 是低于阈值是事实计算开销更小。
  2. 防止小token请求导致cache池被打满。

qwen支持显式缓存和隐式缓存。显式缓存未命中cache需要支付125%费用,命中cache会支付10%费用。隐式缓存未命中cache支付100%费用。命中cache会20%费用。他们的差异是开启显式缓存后(请求维度)服务端承诺5分钟内不淘汰block块

Qwen官网-上下文缓存

image.png

优化技巧

智能路由

问题:由于模型多实例部署,请求会存在随机路由到不同实例,会破坏KV Cache的命中率;
方案:根据System Prompt前缀计算hash,根据hash动态路由到对应模型实例或者实例组;

开启显示缓存

在一些场景(超长固定提示词+少量动态用户输入)下,开启显示缓存,命中单价仅10%;

模型选型

自部署模型

是否开启跨请求前缀的KV缓存,是由推理框架(vLLM、SGLang决定的)。这一层绝大多数自建框架默认不开启,需要手动显式打开

推理引擎 是否开启
vLLM 旧版本:默认关闭,必须启动参数加--enable-prefix-caching ,新版 vLLM 0.7+ 部分发行版默认打开,但官方文档依然建议手动指定确认开启。
SGLang 新版 RadixAttention 前缀树缓存默认开启,开箱即用跨请求前缀复用,对标隐式缓存。
llama.cpp / llama-server 默认不开启跨请求缓存,启动加参数才生效:--cache-reuse

公有云模型

公有云模型部分模型可能没开启Cache缓存,需要确认:

Qwen官网-上下文缓存

image.png

提示词优化

可以将占位符变量放在提示词的末尾(将固定的放在System Prompt,将变化的放在User Prompt上)。

注意:占位符位置不影响效果!!!

  • Transformer 的 self-attention :让每个 token 都能看到上下文中所有其他 token,规则在前、数据在后,模型生成时照样能对上。
  • 如果将占位符传入到中间更容易出问题:规则中间插入动态数据,模型读到一半被 userInfo 打断,后面继续读规则时注意力已经被数据段落分散了。把规则完整放前面、数据统一放后面,模型反而能更完整地理解规则全貌,再带着完整的规则理解去看数据。

工程链路优化

离线场景按层级/行业分批跑,相同前缀的请求连续处理,最大化缓存命中率。

原理

为什么叫做自注意力(Self-Attention)?

为什么叫做self-attention模块,其实就是token的Q矩阵需要和包括自己,所有token的K矩阵计算注意力权重,计算完权重,还要和V矩阵加权求和,提取V信息,融合成新token的向量。

为什么KV只需要算一次

只要前缀token序列相同,位置相同,Wk、Wv相同,那么K、V就完全相同,可以存下来直接用。

  1. token 经过 embedding层向量化 +PE 计算 得到向量 X(携带位置编码)
  2. K=X * Wk , V=X * Wv,是线性变换
  3. 对于同一个token在同一位置,X是确认的,Wk、Wv是模型参数(预训练阶段生成),所以K、V是确认的。
  4. 前缀token的位置在自回归生成过程(模型一个 token 一个 token 往后吐的生成方式)中不会变。
  5. 因此前缀的K、V算一次就够了,后面每一个新token只需要算自己的Q和缓存的K,V做attention。

KV Cache的数据结构

KV Cache = 前缀索引层(hash 或前缀树,负责跨请求匹配可复用 block)+ block_table(负责单请求运行时的逻辑到物理 block 映射)+ 物理 block 池(负责实际存储 K/V 矩阵)

维度 vLLM SGLang
key token block 的 hash 值 token id 序列本身
key 逐 block 算 hash 查表 在树上按 token 逐步走
需要 hash 需要 不需要
公共前缀共享 各自独立查,不感知关联 天然共享同一段树路径
索引层 hash(逐 block 独立) 前缀树(感知路径关系)
复用粒度 单个 block 级别 公共前缀路径级别
共享效率 多请求相同前缀需各自查 hash 多请求走同一树路径,天然共享
实现复杂度 简单 较复杂

SGLang推理架构

SGLang 前缀索引层是前缀树结构,请求输入时,token id 序列从根节点开始逐步在树中匹配,能匹配多深就复用多少物理 block,匹配断了即停止,后续 block 新分配计算。

  • RadixAttention(前缀树):在 PagedAttention 的基础上加的跨请求复用能力。前缀树的叶子指向物理 block,新请求匹配到前缀后直接把对应的物理 block id 填进自己的 block_table,跳过重新计算。

  • block_table(二维数组):PagedAttention 的索引,记录单个请求的逻辑 block 到物理 block 的映射。每个请求有自己的 block_table。

  • PagedAttention:最底层,解决显存碎片问题。把 K/V 显存切成固定大小的 block 池,按需分配,序列不需要占连续显存。block_table 是它的核心数据结构。

┌─────────────────────────────────────┐
│         RadixAttention              │  ← 跨请求复用层(SGLang)
│  前缀树:找到可复用的物理 block        │
└────────────────┬────────────────────┘
                 │ 查到可复用 block 后
                 ↓ 填入
┌─────────────────────────────────────┐
│           block_table               │  ← 索引层
│  记录:逻辑 slot → 物理 block id     │
└────────────────┬────────────────────┘
                 │ 按 block_table 读写
                 ↓
┌─────────────────────────────────────┐
│         PagedAttention              │  ← 内存管理层(vLLM 提出)
│  物理 block 池:实际存 K/V 矩阵的显存 │
└─────────────────────────────────────┘

vLLM推理架构

vLLM的前缀索引层是hash结构。请求输入时,需要对token进行切块,并计算block hash值,在前缀索引层中匹配,第一个未命中时停止,后续block不再查找,全部新分配计算。

hash冲突的解决方案:hash命中后还会校验token序列本身是否一致。

┌─────────────────────────────────────┐
│         prefix hash 索引             │  ← 跨请求复用层(vLLM)
│  hash(token_block) → physical_block │
│  逐 block 匹配,未命中即停止          │
└────────────────┬────────────────────┘
                 │ 命中后填入
                 ↓
┌─────────────────────────────────────┐
│           block_table               │  ← 索引层
│  [seq_id][logical_slot]             │
│       → physical_block_id           │
└────────────────┬────────────────────┘
                 │ 按 block_table 读写
                 ↓
┌─────────────────────────────────────┐
│         PagedAttention              │  ← 内存管理层
│  物理 block 池(预分配显存)           │
│  free_list 管理空闲 block            │
│  每个 block 存 N 个 token 的 K/V     │
└─────────────────────────────────────┘

模型和推理框架的关系

  • 模型本身只是一堆参数矩阵 + 架构描述,它不知道怎么运行自己。
Qwen2.5-7B/
├── config.json          # 模型架构描述(多少层、多少头、hidden size...)
├── tokenizer.json       # 分词器
├── model.safetensors    # 模型权重(WQ、WK、WV、FFN 等参数的数值)
└── tokenizer_config.json

你需要一个推理框架来"跑"它:

┌──────────────────┐
│   模型权重文件    │  ← 你下载的东西,静态的参数
│  WK、WV、WQ...   │
└────────┬─────────┘
         │ 加载进来
         ↓
┌──────────────────┐
│    推理框架       │  ← vLLM / SGLang / Ollama...
│  负责:           │
│  · 把权重加载进GPU│
│  · 执行前向计算   │
│  · 管理KV Cache  │
│  · 接收HTTP请求   │
│  · 返回生成结果   │
└──────────────────┘

KV cache是推理框架概念,不是模型的

模型(Qwen/Llama/DeepSeek 等)

  • 定义了架构:多少层、多少头、hidden size、WK/WV 矩阵的参数值;
  • 定义计算规则:K=X * Wk、V=X*Wv,Attention怎么算;
  • 模型本身不知道KV Cache,

推理框架(vLLM、SGLang、TGI等):

  • 负责执行模型的前向计算(把输入数据按模型定义的结构和参数,从头到尾算一遍,得到输出。推理框架负责调度这个计算过程,把数据和参数放到 GPU 上跑。)
  • 引入了 KV Cache 作为工程优化:发现前缀 token 的 K、V 不变,就缓存下来
  • 定义了 KV Cache 的数据结构:block 大小、block table、内存分配策略
  • 定义了缓存复用策略:prompt cache、PagedAttention 等
  • 管理 GPU 显存分配:多少显存给 KV Cache,多少给模型参数

模型部署方式

通常组合是张量并行+多机多卡两种方式组合

维度 单机多卡 多机多卡
目的 单次推理更快(降延迟) 同时处理更多请求(提 QPS)
适合场景 模型太大单卡放不下 模型单卡能跑,QPS 不够
原理说明 张量并行(Tensor Parallelism)把模型的权重矩阵切分到多张卡上,每张卡只算一部分,结果汇总,4 张卡跑一个模型,单次推理更快,显存压力更小 每个实例独立跑一个模型副本,负载均衡把请求分发到不同实例。QPS 线性扩展。
资源利用 多卡协作跑一个请求 多卡各自独立跑请求

随机分发破坏KV Cahce命中

公有云背后大概率做了前缀感知路由,相同 system prompt 的请求被稳定路由到同一个实例或实例组上。

  • 路由层不需要完整匹配所有前缀,通常只取前 N 个 token 的 hash 作为路由 key,比如前 128 或前 256 个 token。

  • 路由层是工程侧实现

┌─────────────────────────────────────┐
│         工程侧(调用方自建)           │  ← 前缀感知路由在这层
│  · 计算请求前缀 hash                  │
│  · 根据 hash 路由到对应实例            │
│  · 负载均衡 + 实例健康检查             │
└────────────────┬────────────────────┘
                 ↓
┌─────────────────────────────────────┐
│    推理框架(vLLM / SGLang)          │  ← 只管单实例内部的 KV Cache
│  · prefix cache 命中判断              │
│  · block_table 管理                  │
│  · 实际执行前向计算                   │
└─────────────────────────────────────┘

KV Cache如何做到账号隔离

vLLM 和 SGLang 原生都没有租户隔离的功能,需要你自己改或者在外层封装。

账号 ID 不是加在 system prompt 里做前缀,而是在路由层缓存索引层做隔离:

请求进来,携带 account_id
    ↓
路由层:根据 account_id + 前缀 hash → 路由到对应实例
    ↓
缓存索引层:block hash 的 key 加上 account_id 做命名空间隔离
    hash(account_id + token_block) → physical_block_id

显式缓存开启

维度 隐式缓存(默认自带) 显式缓存(create_cache)
开启方式 自动无需改代码 手动加 cache_control 标记
命中可靠性 不确定,系统按需清理 前缀完全一致则 100% 命中
创建计费 无额外溢价,全量原价 新建部分 125%(+25%)
命中计费 原价 20% 原价 10%(更省)
有效期 不固定,后台回收 固定 5 分钟,命中自动续期
适用 临时调用、短 prompt、低频复用 长文本、高频复用、生产稳定服务

1. 超长固定 System 提示词 + 少量动态用户输入(最常用)
例子:上千 Token 角色设定、翻译规则、输出格式约束、Few-shot 示例、行业固定指令。
调用结构:【固定大段System】+【每次不一样的用户短句】
收益:System 前缀永久锁定缓存,后续每轮只给动态内容计费,命中仅原价 10%,比隐式缓存 20% 更便宜,且不会出现系统没识别到缓存导致全额扣费。

2. 单份长文档反复问答(知识库 / 合同 / 小说 / 代码库解读)
比如一份几万字合同、整本小说、项目源码,针对这份文件连续发起 N 个问题。
首次上传全文创建缓存(+25% 溢价)
后续所有提问,文档部分直接命中缓存按 10% 计费,只对问题本身计费
隐式缓存容易因调度、请求间隔导致缓存丢失,显式能保证 5 分钟内必命中。

3. 长上下文多轮连续对话(客服、会话助手、代码结对编程)
每一轮都会带上完整历史对话,越往后上下文越长,重复前缀极多。
显式缓存可以增量缓存历史上下文:旧对话命中 10%,新增本轮对话增量按 125% 创建,大幅降低长上下文 token 开销与推理耗时。
隐式缓存无法稳定继承上一轮会话缓存,经常从头计算 KV,延迟更高、费用更贵。

4. Agent 复杂编排(工具调用、思考链、记忆常驻)
Agent 自带固定思考框架、工具描述、记忆上下文,会循环多轮调用模型。
这类场景上下文结构复杂,隐式缓存很难抓取公共前缀;显式可以手动标记必须常驻的上下文片段,哪怕中间插入工具返回、临时文本,标记部分依旧稳定命中缓存,防止上下文膨胀后成本爆炸。

5. 对接口延迟有硬性 SLA 要求(线上高并发服务)
显式缓存命中后推理速度显著优于未缓存;
隐式缓存命中不可控,高峰期可能大量请求不命中缓存,RT 抖动严重。
如果业务不能接受接口耗时忽快忽慢,必须用显式锁定缓存保证稳定性。

6. 批量批量任务(同一份基准 Prompt 批量跑多条数据)
例如:统一审核规则 + 逐条审核文本、统一抽取模板 + 批量抽取多条数据。
基准 Prompt 只创建一次缓存,后面几十上百条批量请求全部命中,单次溢价分摊后成本极低。

绝对不建议开启显式缓存的场景

  1. 该段文本只会调用 1 次:额外多 25% 计费,纯亏本。
  2. 每次请求前缀完全不一样,没有任何公共重复内容:无法命中缓存,创建毫无意义。
  3. 单次前缀 Token 少于1024:显式缓存有最小 1024token 门槛,太短无法生效;短文本复用直接用默认隐式缓存即可(隐式最低 256token)。
  4. 请求间隔经常超过 5 分钟:缓存过期会强制重新创建,频繁重建会持续叠加 125% 费用,收益大打折扣。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容