前面聊大模型生成文字时,我们已经知道:输入 token 变成向量,经过多层 Attention 和 FFN,最后给词表里的候选 token 打分,再选出一个新 token。
接着遇到一个很容易理解错的东西:KV Cache。
看到“缓存”,很自然会想到:是不是问题问过一次,模型就把答案存起来,下次直接返回?
不是。KV Cache 缓存的是前文在各层计算出来的 K、V 向量,不是最终答案。
那它究竟怎么加速?又为什么会占用那么多显存?这篇沿着一个小例子,把执行过程和硬件连起来。
本文以常规、从左到右生成文本的 Transformer 为例。分词和输出均为教学假设;硬件配置核验于 2026-09-09,本轮仅核对配置,未运行模型或测速。容量算例是数学估算,不是本机实测。
一、先明确缓存的是什么
先把 Q、K、V 压成一句话:
当前 token 的 Q 与可见位置的 K 算匹配程度,再按算出的比例混合 V。
生成下一个 token 时,新的位置会带来新的 Q,但前面位置的 K、V 还可以继续用。缓存保存的就是这部分。
这里要区分三件事:
| 东西 | 从哪里来 | 是否属于 KV Cache |
|---|---|---|
| 计算 Q、K、V 的权重矩阵 | 训练得到 | 不是 |
| 本次输入算出的各层 K、V | 推理时计算 | 是 |
| 最终隐藏向量、候选分数和答案 | 后续计算或选择得到 | 不是 |
KV Cache 通常也不保存历史 Q 和整张注意力比例表。新的 token 到来后,仍要计算它自己的注意力比例。缓存原理
二、第一轮:处理“我喜欢”,建立缓存
假设我们输入:
“我” “喜欢”
为方便讲解,约定每个文字块都是一个 token;真实分词器不一定这样切。
开始时缓存为空。模型把两个 ID 转成向量,依次经过各层计算。每到一层,就会得到这一层的 Q、K、V,并把 K、V 留下来。
第 1 层缓存:“我”的 K、V + “喜欢”的 K、V
第 2 层缓存:“我”的 K、V + “喜欢”的 K、V
……
第 N 层缓存:“我”的 K、V + “喜欢”的 K、V
每层都有自己的缓存,不能把第 1 层的 K、V 拿到第 2 层直接代用。
这一步通常叫 Prefill,可以理解成“把输入先处理一遍”。同一层里的多个输入位置可以并行计算,但层与层之间仍有先后依赖;并行也不代表允许看到未来位置。
处理完所有层后,用“喜欢”位置的最终表示,经过最终归一化、LM Head 打分和选择,假设选出了“猫”。
请留意这个时间点:刚选出“猫”时,缓存里还没有“猫”自己的 K、V。 它刚被选出来,还没作为输入走过模型。
三、第二轮:输入“猫”,读取旧缓存
这一轮通常叫 Decode,也就是逐步生成阶段。
“猫”的 ID 不必先转文字再重新分词,直接作为新增输入即可。下面只看它在某一层怎么走。
1. 算出“猫”自己的 Q、K、V
这一步仍要正常计算,不能跳过。该层使用自己的权重,把“猫”的本层输入向量变成 Q、K、V。实际实现还会包含归一化、位置信息处理等操作。
2. 读取本层旧缓存,加入“猫”的 K、V
原来的 K:[K我,K喜欢]
更新后的 K:[K我,K喜欢,K猫]
原来的 V:[V我,V喜欢]
更新后的 V:[V我,V喜欢,V猫]
这里说的“加入”是逻辑上的追加,不代表底层每生成一个 token,都必须重新复制整份缓存。实现可以预留空间、分块扩容,或者用分页方式管理。MLX 的官方教程就演示了预分配与切片写入的缓存做法。MLX 缓存实现说明
3. 用新的 Q 计算注意力
“猫”的 Q,会与旧的 K 和自己的 K 一起计算匹配程度:
Q猫 与 K我 → 参考“我”的比例
Q猫 与 K喜欢 → 参考“喜欢”的比例
Q猫 与 K猫 → 参考自己的比例
再按比例混合对应的 V,得到本层注意力的结果。
旧 K、V 可以复用,但“猫”这次参考谁、参考多少,必须重新计算。
接着完成输出投影、残差、FFN 等计算,把更新后的向量交给下一层。下一层重复上述过程,但读取的是下一层自己的缓存。
走完全部层,才再次打分、选择,假设得到“。”。此时各层缓存包含“我”“喜欢”“猫”的 KV,而“。”的 KV 要等继续处理它时才产生。按层更新与模型执行代码
四、为什么旧 K、V 不会过期?
关键是这里采用因果注意力:一个位置只能看自己和前文,不能看后面。
“我” 能看:我
“喜欢” 能看:我、喜欢
“猫” 能看:我、喜欢、猫
后来出现“猫”,不会反过来改变前面“我”“喜欢”在这些层里的表示。因此,只要模型、已有前缀、位置及相关计算条件保持兼容,前面的 K、V 就可以继续使用。因果注意力原理
这也解释了为什么通常不缓存 Q:当前要计算的是“猫”参考谁,只需要“猫”的 Q;前文的 Q 已经完成了自己的任务。
不过,这不是“给每个词永久存一份 KV”。同一个“喜欢”出现在不同上下文里,后续层的 K、V 可能不同。跨请求复用前缀缓存,也需要推理引擎支持,并核对前缀及运行条件,不能因为文字意思相近就直接复用。
五、这些计算和缓存,分别用什么硬件?
先说一个容易混淆的名字:KV Cache 是软件维护的中间数据,不是 CPU 的 L1/L2 缓存,也不是一种专门的硬件。
它需要硬件提供计算和存储,但两者不是同一个角色。
| 硬件 | 在 GPU 推理中的主要作用 |
|---|---|
| CPU | 分词、准备输入、调度任务、运行框架控制逻辑等 |
| GPU | 执行 Q/K/V 投影、注意力、FFN、LM Head 等大量并行数值计算 |
| GPU 显存 | 存放模型权重、活跃 KV Cache,以及计算工作区和中间数据 |
| 主机内存 | 承载程序、输入、加载缓冲;某些方案也把部分权重或缓存卸载到这里 |
| SSD | 保存模型文件、数据文件等;支持持久化缓存的系统也可能用它保存缓存文件 |
这里是常见分工,不是硬性限制。CPU 也能推理,采样等操作也可能放在 GPU 执行。
独立显卡:显存和主机内存是两块存储
传统独显机器通常有主机内存和显卡自带的显存。活跃数据放在显存中,GPU 读取并计算;跨 CPU/GPU 存储搬运则受到互连带宽等条件限制。
所以,“显存主要给 KV Cache 用”并不准确。更完整的关系是:
推理占用的显存
≈ 模型权重 + KV Cache + 中间数据/工作区 + 框架等开销
模型能成功加载,不代表剩余空间足够支撑很长上下文或很多并发请求。
Apple Silicon:CPU、GPU 共享统一内存
Apple Silicon 的 CPU 和 GPU 可以访问同一个统一内存池。以 MLX 为例,数组可以由 CPU 或 GPU 运算使用,不必像独显的两块物理存储那样,为切换计算设备而搬到另一份内存里。MLX 统一内存说明
这不代表容量和带宽无限,也不意味着所有张量复制、同步和临时内存开销都消失了。
六、本文使用的本地硬件配置
这次核对的本机配置如下。CPU、GPU 和内存来自 macOS 本机硬件信息;内存带宽来自对应档位的 Apple 官方规格。
| 项目 | 本机配置 |
|---|---|
| 机器 | MacBook Pro |
| 芯片 | Apple M3 Max |
| CPU | 14 核:10 个性能核心、4 个能效核心 |
| GPU | 30 核 |
| 内存 | 96 GB 统一内存 |
| 内存带宽 | 官方规格 300 GB/s,非实测吞吐 |
14 核 CPU、30 核 GPU 这一档 M3 Max 对应 300 GB/s;不要套用另一档 16 核 CPU、40 核 GPU 的 400 GB/s。Apple 官方规格
有三个注意点:
- 96 GB 是整机共享的统一内存,不是 96 GB 独立显存,也不是都能给 KV Cache 用。 macOS、其他应用、权重和计算工作区都会占空间,框架也可能有资源限制。
- 30 核 GPU 不等于模型有 30 个注意力头。 一个是硬件计算资源,一个是模型结构,两者不是一一对应关系。
- 这里没有跑速度测试。 因此不能据此承诺某个模型能跑多少 token/s;后面的缓存算例,也不是本机正在运行的模型。
七、KV Cache 会占多少空间?自己算一次
对于各层 KV 头数与维度一致、K 和 V 头维度相同、每层都保留完整历史的常规缓存,可以先估算单个请求、单条序列的纯 KV 数据量:
KV 字节数
= 2 × 层数 × 已缓存 token 数 × KV 头数 × 每头维度 × 每个数的字节数
开头的 2,表示 K、V 各一份。“已缓存 token 数”包括已经处理过的输入和生成内容;刚选出、还没送进模型的新 token,此时还不计入。
这里填的是 KV 头数,不一定是 Q 头数。例如 GQA 会让多组 Q 头共享较少的 KV 头。常规缓存的单层张量,通常按“批次、KV 头、token 位置、头内维度”组织;上式就是把各部分的数量乘起来,再累加所有层。缓存张量结构
现在人为设定一个教学模型:
- 32 层。
- 每层 8 个 KV 头。
- 每个 KV 头 128 维。
- K、V 都使用 FP16 或 BF16,每个数占 2 字节。
每增加一个已缓存 token,所有层合计增加:
2 × 32 × 8 × 128 × 2
= 131072 字节
= 128 KiB
于是得到:
| 单请求已缓存 token 数 | 纯 KV 数据量 |
|---|---|
| 1 | 128 KiB |
| 4096 | 512 MiB |
| 8192 | 1 GiB |
| 32768 | 4 GiB |
这里 1 KiB = 1024 字节,1 MiB = 1024 KiB,1 GiB = 1024 MiB。
若有 8 条相互独立、各缓存 32768 个 token、没有共享前缀存储的请求,纯 KV 合计就是 32 GiB。不是说 8 条请求就一定需要 8 份模型权重;权重可以共享,而各请求的上下文状态需要单独维护。
这些只是 KV 本体,不包括模型权重、预留空位、分页元数据、量化附加数据、计算工作区和框架开销。 实际分配的内存可能更大;滑动窗口、MLA 等不同缓存结构也不能照搬这个公式。
还要特别注意:模型权重是 4 bit,不代表 KV Cache 自动也是 4 bit。 两者是不同的数据,KV 是否量化需要看框架和单独配置。缓存量化能减少占用,但也可能带来精度变化和额外处理成本。缓存量化与策略
八、缓存能加速,为什么还会慢?
因为它省掉的是重复计算,不是所有计算。
1. 新 token 仍要经过全部层
即使用缓存,“猫”仍要做本层 Q/K/V、注意力、FFN 等计算,一直走到输出打分。
2. 历史 KV 仍然需要读取
完整注意力下,上下文越长,每个新位置需要处理的历史 K、V 通常越多。旧数据不重算,不等于旧数据不再读取。
3. 算得快,还要喂得上数据
看硬件时可以分开三个问题:
| 想知道什么 | 主要关注什么 |
|---|---|
| 能否装下模型和上下文 | 可用显存或统一内存容量 |
| 矩阵运算能有多快 | 对应数值格式的有效算力、运算实现和利用率 |
| 权重、KV 能否及时读到 | 内存带宽与实际访存方式 |
大批量矩阵计算和逐 token、小批量生成的瓶颈未必相同。后者常常需要警惕数据读取成为限制,不能只看宣传的峰值 TFLOPS。NVIDIA 的性能说明也区分了算力受限、内存带宽受限和并行度不足等情况。GPU 性能原理
本文列出的 300 GB/s 是内存带宽规格,不是 SSD 速度,更不是每秒能生成多少文字。没有具体模型、精度、上下文和实测,不能直接换算成可靠的 token/s。
如果显存不够,有些框架支持把 KV 卸载到主机内存,用到对应层时再搬回来。这能缓解容量压力,但可能增加搬运延迟。SSD 可以装下很多模型文件,也可以被某些缓存系统用于持久化,却不能当作“加了一块同速度的显存”。缓存卸载说明
总结:把计算、缓存和硬件分开看
KV Cache 的执行过程,可以压成四步:
- 处理输入时,保存各层算出的 K、V。
- 新 token 进来,只为新位置计算 Q、K、V,并复用本层历史缓存。
- 新的 Q 重新计算注意力比例,当前和历史的 V 一起参与混合。
- 继续走完所有层,再打分、选择下一个 token。
GPU 负责大量计算,显存或统一内存承载权重、KV 和工作区,SSD 主要负责文件存储。
KV Cache 不是把答案记下来,而是让前文已经做过的计算不必每轮重来。代价是占用更多内存,并在后续计算中持续读取这些数据。