---
title: 推理阶段、KV cache 与显存
url: https://doc.liz6.com/ai/01-model-foundations/03-inference-and-kv-cache
locale: zh
area: ai
tags:
- 大模型与 Agent
- 模型基础
date: 2026-06-30
modified: 2026-09-09
description: 同一个模型，提示变长或同时服务更多请求后，显存与延迟会怎样变化？先区分 prefill 与 decode，再分别核算权重和历史 K/V。本文给出可复算的空间模型，并把平均计算量、内存占用和实际吞吐分开。
---

# 推理阶段、KV cache 与显存

同一个模型，提示变长或同时服务更多请求后，显存与延迟会怎样变化？先区分 prefill 与 decode，再分别核算权重和历史 K/V。本文给出可复算的空间模型，并把平均计算量、内存占用和实际吞吐分开。

## 从完整前缀到一个新位置

prefill 处理已经给出的提示。因果注意力下，位置 i 只使用前 i 个位置，但已知输入的多个位置可以在一次前向处理中一起计算。decode 则在已有前缀上继续生成：得到新 token 后，把它作为下一次前向输入，计算它的 K/V 并与历史拼接。

已有位置的表示不会因未来 token 而改变，因此可以复用历史 K/V，避免每一步重算全部前缀。缓存没有替代对历史的访问：新 query 仍要与可访问的 key 计算权重并聚合 value。下方以单层、单头的允许注意力项数计量，帮助区分“省去旧位置重算”与“新位置不需要历史”。真实内核可能按块计算，也不一定物化完整分数矩阵。[KV cache 原理](https://huggingface.co/docs/transformers/en/cache_explanation)

对 n 个提示位置，允许的 query-key 对数为 n(n+1)/2；随后生成 m 个新位置时，带缓存的新 query 对数为 mn+m(m+1)/2。这里把已经采样的 token 作为后续前向输入计数，提示末尾 logits 产生的第一个输出不另算一轮提示处理。这是依赖计数，不是 FLOPs、总时间或某个引擎的速度预测。

### 有缓存之后，还要计算哪些位置？

每增加一个位置，未缓存方案重新处理整个增长的前缀；缓存方案只新增一个 query 行。比较 n=6、m=2：一次初始处理有 21 项，后两行分别为 7、8 项，总计 36；重复处理完整前缀则为 21+28+36=85。

**有缓存之后，还要计算哪些位置？**

缓存省去旧 query 的重复处理，新 query 仍需访问历史 K/V。


## 权重与 KV cache 的空间账

### 权重存储先按总量估算

假设共有 35×10⁹ 个参数，全部以理想 4 bit 保存，那么裸权重为 17.5×10⁹ 字节，约 **16.30 GiB**。FP16 裸权重则为 70×10⁹ 字节，约 65.19 GiB。实际量化还包含尺度、分组元数据，且某些张量保留更高精度；量化格式名中的位数不能直接当作最终文件平均位数。

如果全部权重都驻留同一设备，这部分设备内存就要按总量预算。也可以做跨设备切分、CPU 卸载或分层缓存，因而“所有专家必须常驻同一 GPU”并不成立。代价是不同位置选择专家时，可能要支付传输或等待成本。能加载与能以目标延迟服务是两道门槛。

### KV cache 看结构与序列，而不是总参数标签

对每层保存完整 K/V、每条序列等长且采用标准布局的简化模型：

**KV 字节数 ≈ 2 × L × B × S × Hkv × D × b**

| 符号 | 含义 |
|---|---|
| 2 | K 与 V 两份缓存 |
| L | 采用该缓存结构的层数 |
| B、S | 同时缓存的序列数、每条序列长度 |
| Hkv、D | KV 头数、每头维度 |
| b | 每个缓存元素的字节数 |

例如 L=32、B=1、S=8192、Hkv=8、D=128、b=2，结果是 **1 GiB**；长度扩到 65536 后为 **8 GiB**。若其他参数不变而 KV 头数改成 32，则分别是 4 GiB 和 32 GiB。这也解释了为何仅说“64K 上下文”还无法估显存。

GQA 让多组 query 共享较少的 KV 头，缓存量按 KV 头数计算，不是按 query 头数计算。[GQA 论文](https://arxiv.org/abs/2305.13245) 对滑动窗口、压缩缓存、混合循环层或不同层结构，应逐层按实际布局核算，不能机械套这个乘积。

```python
from fractions import Fraction

def kv_bytes(layers, sequences, length, kv_heads, head_dim, item_bytes):
    return 2 * layers * sequences * length * kv_heads * head_dim * item_bytes

one = kv_bytes(32, 1, 8192, 8, 128, 2)
long = kv_bytes(32, 1, 65536, 8, 128, 2)
assert one == 1024**3
assert long == 8 * 1024**3
weights = Fraction(35_000_000_000 * 4, 8)
print("理想 4-bit 权重 GiB:", round(float(weights / 1024**3), 2))
print("示例 KV GiB:", one // 1024**3, long // 1024**3)
# 理想 4-bit 权重 GiB: 16.3
# 示例 KV GiB: 1 8
```

最后还要加激活、算子工作区、分配器开销及其他进程占用，并在目标输入长度与并发下测峰值。上述算例没有测量某一真实模型；具体部署与量化记录见 [本地 LLM 部署](/homelab/local-llm.md)。

### 哪一个因子让 KV 变大？

先固定其他项，只改一个因子，再尝试同时增大长度和并发。乘积揭示的是缓存空间关系：头数指 KV heads，位置数包含已保留的历史，GiB 以 2³⁰ 字节换算。模型没有把这些字节直接换成实际吞吐。

$$
M_{KV}=2LBSH_{KV}Db
$$

**哪一个因子让 KV 变大？**

KV 随缓存位置数、序列数和 KV 头数相乘增长；总参数标签不能替代这些结构信息。


## 从计算量到实际吞吐

每 token 激活参数能帮助粗估某些矩阵计算，却不能直接换算成 token/s。处理长提示的 prefill、逐步生成的 decode 和高并发批处理，其瓶颈可能不同。

| 观察到的现象 | 可能的限制 | 应比较的测量 |
|---|---|---|
| 首个 token 等很久 | 长输入处理、排队、前缀复用情况 | 排队时间、prefill 时间、缓存命中 |
| 单请求生成较慢 | 权重或 KV 读取、算子效率 | 每步延迟、上下文长度、内存带宽 |
| 批量增大后收益小 | 专家热点、通信、工作区或调度 | 总吞吐与各请求尾延迟 |
| 卸载后能运行但卡顿 | 专家或层的数据搬运 | 设备间传输量与等待时间 |

MoE 的专家矩阵可能较小，分发和合并也有开销；多设备专家并行还可能需要交换 token 表示。即使两个模型的激活参数接近，层数、KV 结构、量化内核、批量和路由分布都可能让速度明显不同。因此“35B-A3B 就跑得像 3B dense”只能是待验证的性能假设。

继续阅读：[推理计算、候选与验证](/ai/01-model-foundations/04-reasoning-and-verification)。
