KV Cache
用会话内存换取 Decode 阶段的历史投影复用
建议阅读:约 19 分钟
学习目标
本章关键词
| 关键词 | 解释 | ESP32 工程类比 |
|---|---|---|
| Prefill | 批量处理已有 prompt 并为每层建立初始 K/V 状态的阶段。 | 像连接建立时一次性解析完整握手报文。 |
| Decode | 根据上一步输出逐 token 追加位置并生成下一 token 的阶段。 | 像事件循环每轮等待上一步结果后再推进状态机。 |
| KV cache | 保存历史 token 在每层产生的 Key 与 Value 投影的会话状态。 | 像按层保存已核验字段的索引与数据描述符。 |
| GQA / MQA | 让多组 Query head 共享更少 K/V head,从结构上降低每 token cache 宽度。 | 像多个消费者共享较少的只读索引表。 |
承上:回顾与定位
上一章把 Attention 的 Q/K/V、causal mask 与多头聚合拆成显式数据流。本章只研究生成阶段如何复用历史 K/V,以及这种复用产生的会话内存、带宽与回收责任。
历史发展脉络
KV cache 的历史是自回归生成从“数学上可行”走向“系统上可承载”的缩影。Transformer 定义了注意力,随后 MQA/GQA 减少每个 token 的状态,分页、复用与量化又开始解决动态会话带来的内存管理问题。
Transformer 建立缩放点积与多头注意力
《Attention Is All You Need》把序列建模的瓶颈拆成计算量、串行步骤和远距离路径长度,用 encoder–decoder、缩放点积、多头注意力、位置编码与 causal mask 替代循环结构。输入处理因此更易并行,但自回归输出仍然按前缀逐 token 生成。
Attention Is All You Need 原始论文 ↗GQA 在质量与 cache 规模间增加档位
Grouped-Query Attention 让若干 query head 共享一个 K/V head,在传统多头注意力与单组 MQA 之间提供可调折中。
GQA 原始论文 ↗PagedAttention 处理动态会话碎片
PagedAttention 借鉴虚拟内存分页,把每个请求增长不定的 KV cache 映射到非连续块,并支持更灵活的共享。
PagedAttention 原始论文 ↗本地 runtime 暴露 cache 类型与 offload 策略
llama.cpp 等执行器把 K/V 数据类型、offload、上下文与缓存复用变成可配置项,cache 已是部署配置而非隐藏实现细节。
llama.cpp 官方 CLI 文档 ↗类比图解
侦探桌上不断增长的案卷索引卡
侦探读完每页证词后,不再每次从第一页重读,而是为每层分析保存两类索引卡:一类写“将来怎样找到这条线索”,另一类写“找到后取出什么内容”。新问题拿着查询卡扫过历史索引,形成判断,再把新证词的索引追加进去。
类比的边界: KV cache 不是可读摘要、事实数据库,也不是原始 token 的无损副本;它是特定模型权重、位置编码和精度下的中间张量。换模型、改前缀位置或任意编辑历史后,旧卡片通常不能直接沿用,缓存也不会让注意力本身变成常数成本。
本章讲解
从一层因果注意力看懂为什么要存 K 和 V
每个 token 的隐藏状态在线性投影后得到 Q、K、V。当前 Q 与所有可见历史 K 计算相似度,经 causal mask 和 softmax 加权历史 V;模型权重固定时,旧 token 在该层的 K/V 不会因新 token 到来而改变,因此可以缓存。Q 只服务当前计算,通常无需长期保存。缓存省掉的是对历史 token 重做各层 K/V 投影,却仍要读取历史 cache 并完成随上下文增长的注意力计算。
先用模型结构计算 cache 斜率
常见近似为 batch×layers×2×tokens×kv_heads×head_dim×每元素字节数,其中 2 代表 K 和 V;实现还含对齐和分页元数据。不要用 attention heads 代替 kv_heads,GQA/MQA 模型二者不同。把公式化成“每新增一个 token、一个会话增加多少字节”,再叠加权重、临时 workspace 与 runtime 开销。若实测斜率不符,检查 cache dtype、滑窗和预分配。
用增长曲线、容量算例与状态测试验证 cache
MQA/GQA 从模型结构上减少 KV heads,低比特 cache 减少容量和带宽,滑动窗口限制可访问的远端历史,而分页主要减少碎片;这些节省不能混为一谈。固定模型和采样参数,逐级增加 prompt 长度并记录 prefill、每 token 延迟和 KV 字节斜率;以 32 层、8 个 KV head、head_dim 128、FP16 为例,每 token 约 128 KiB,4096 token 约 512 MiB。再随机启动、增长和取消分页会话,核对空闲块回到基线,并用关闭 cache 的参考路径检查 logits 与位置没有被破坏。
Prefill 与 Decode 必须分开记账
Prefill 批量处理 prompt 的全部 token,通常有更高并行度和计算密度;Decode 每轮只产生一个新位置,却反复读取逐渐增长的历史 K/V,常更受内存带宽和调度开销影响。TTFT 包含排队、tokenize、prefill 与首轮采样,ITL 则关注后续 token 间隔。把两者平均成一个 tokens/s,会掩盖长 prompt、短回答和多会话场景中完全不同的瓶颈。
Cache 正确性依赖模型、前缀、位置和精度
K/V 是特定层权重、特定 token 前缀、位置编码和 cache dtype 下产生的中间张量。模型或 adapter 改变、前缀中间被编辑、position 偏移或旋转位置参数不一致时,旧 cache 通常不可直接复用。Prompt cache 的键必须覆盖这些身份信息;取消、超时和会话淘汰还要把页面或槽位完整归还,否则系统会在长时间运行后出现隐性容量泄漏。
动态过程演示
一个新 token 如何读取并扩展 KV Cache
播放器区分只发生一次的 prefill 与重复发生的 decode,并让缓存随步数增长。
Prefill 提示 · N prompt tokens
并行处理已有序列,在每层生成 K/V
首轮集中计算并建立历史状态
循环 / 返回条件: “追加并循环”回到“产生新 Query”,直到 EOS、长度上限或取消;取消必须释放该序列占用的页或槽位。
查看静态全景图
Prompt tokens ──Prefill──► per-layer K/V cache
New token ──Q,K,V──► Q × cached Kᵀ → weighted V → logits → sample
└──── append new K/V and repeat ────┘代码或命令示例
kv_bytes = batch * layers * 2 * tokens * kv_heads * head_dim * bytes_per_value
bytes_per_new_token = batch * layers * 2 * kv_heads * head_dim * bytes_per_value
# 实现还需计入对齐、分页元数据与预分配策略动手实验
实验记录与导出
工程陷阱
核心测试
完成 3 道单选题后提交;提交前不会显示答案。
在 GitHub 上讨论本章
登录 GitHub 后提问、补充实测或分享你的实现;评论会保存在本课程的 GitHub Discussions 中。
评论区需要 JavaScript;也可以直接打开 GitHub 讨论区: 打开 GitHub 讨论区 ↗
延伸阅读
启下:下一章如何使用本章能力
下一章进入 LLM 量化与 GGUF:把权重、张量类型、block 开销、metadata 与输出质量连成可验证的模型包。