历史文章
小白学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原因
- 是低于阈值是事实计算开销更小。
- 防止小token请求导致cache池被打满。
qwen支持显式缓存和隐式缓存。显式缓存未命中cache需要支付125%费用,命中cache会支付10%费用。隐式缓存未命中cache支付100%费用。命中cache会20%费用。他们的差异是开启显式缓存后(请求维度)服务端承诺5分钟内不淘汰block块

优化技巧
智能路由
问题:由于模型多实例部署,请求会存在随机路由到不同实例,会破坏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缓存,需要确认:

提示词优化
可以将占位符变量放在提示词的末尾(将固定的放在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就完全相同,可以存下来直接用。
- token 经过 embedding层向量化 +PE 计算 得到向量 X(携带位置编码)
- K=X * Wk , V=X * Wv,是线性变换
- 对于同一个token在同一位置,X是确认的,Wk、Wv是模型参数(预训练阶段生成),所以K、V是确认的。
- 前缀token的位置在自回归生成过程
(模型一个 token 一个 token 往后吐的生成方式)中不会变。 - 因此前缀的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 次:额外多 25% 计费,纯亏本。
- 每次请求前缀完全不一样,没有任何公共重复内容:无法命中缓存,创建毫无意义。
- 单次前缀 Token 少于1024:显式缓存有最小 1024token 门槛,太短无法生效;短文本复用直接用默认隐式缓存即可(隐式最低 256token)。
- 请求间隔经常超过 5 分钟:缓存过期会强制重新创建,频繁重建会持续叠加 125% 费用,收益大打折扣。