适用读者:要做容量规划与并发上限计算、或想搞清"为什么长上下文既贵又慢"的人。
前置:S23-J07(单步估算)、J08(计算图与矩阵尺寸)。
覆盖旧文:A-4335 §2.2–§2.3(自回归生成的资源消耗 + 上下文状态表示与访问,174 点)→ 174 点。
扩展点:E-S23-020(每 token KV 成本公式)、E-S23-021(累计读取量 vs 存储量:为什么差三个数量级)。
上一篇:S23-J08 · 计算图与前向执行
下一篇:S23-J10 · 条件计算、网络形状与模型矩阵。
1. 一句话结论
权重是"读很多次、存一份",KV 是"存一份、读很多次"。 一条 8K 输入 + 1K 输出的请求,最终只存约 1.27 GiB KV,但生成期间累计读取 1224 GiB——差近千倍。容量规划看前者,带宽/延迟看后者。
出处:AI Infra 教材 §2.2–§2.3(
id 4335),模型 Qwen3-8B。
权重: 存一次,读 N 次(每步读一遍)→ 容量小、访问大
KV: 存一份,读 G 次(每步读全部历史)→ 容量随上下文线性、访问随生成长度平方
临时数据:用完即释放 → 只看"峰值"和"每次访问量"
2. prefill 与 decode:同一模型,两种负载
| 阶段 | 输入 | 每条请求新增 token | 计算形状 |
|---|---|---|---|
| prefill | 已知的 P 个 token | P 个 | 大矩阵乘(m=P) |
| decode | 上一步的输出 + 已有上下文 | 1 个 | 矩阵×向量(m=1) |
为什么要缓存 K、V(原文推理):因果模型里旧 token 看不到后来加入的 token;只要权重、位置索引与计算条件不变,旧 token 产生的 K、V 就可以保留。后续查询读取这些上下文 K、V,再补入当前位置的 K、V,就避免了对旧 token 的大量重算。
不缓存:每生成 1 个 token,把整个前缀重算一遍 → 计算量随上下文长度爆炸
缓存后:把"重算旧 token 的表示"换成"保存并访问旧 token 的状态"
判据:长期保存的是 K 和 V,不是 Q。 旧查询已经完成自己的任务,下一步使用新的查询——这也是"KV cache"这个名字的由来。
3. 每 token 的 KV 成本(E-S23-020)
c_KV = 2 · L · n_KV · d_h · b_KV
第一个 2 = 键 + 值两份
L = 层数
n_KV = KV 头数(GQA 后的头数,不是查询头数)
d_h = 每头维度
b_KV = 每元素字节数(BF16 = 2)
Qwen3-8B 代入(L=36, n_KV=8, d_h=128, b=2):
c_KV = 2 × 36 × 8 × 128 × 2 = 147,456 bytes = 144 KiB / token
| 上下文长度 H | KV 容量 |
|---|---|
| 2,048 | 288 MiB |
| 8,192 | 1.125 GiB |
| 32,768 | 4.5 GiB |
| 131,072 | 18 GiB |
判据:H 翻倍 → KV 翻倍;L 或 n_KV 减半 → 增速减半;batch 增加 → 每条请求各加一份(不共享)。
这就是"长上下文为什么贵"的全部算式:不是模型变大了,是每条请求的状态变大了。
4. 三类数据的账要分开记
原文把数据按用途和生命周期分成三类——这是容量表与带宽表必须分开的原因:
| 数据类别 | 存储与复用 | 要算的主要项目 |
|---|---|---|
| 权重 | 多次调用间不变 | 常驻字节、被访问的权重集合、批内复用 |
| 上下文状态 | 随请求保留、持续追加 | 实际容量、每步读写量、更新所需计算 |
| 临时数据 | 算子内产生、用完释放 | 张量尺寸、经过接口的数据量、同时保存的峰值 |
Qwen3-8B 全部 BF16 权重约 16.38 GB(15.26 GiB);一次前向只访问其中"本层用到的矩阵",嵌入按 token ID 查行。
判据:存储量看"某一时刻要保留哪些数据",访问量看"执行过程中读写了多少次"。 两者相差几个数量级是常态。
5. 累计读取量:为什么比存储量大近千倍(E-S23-021)
设定:B=1,已有前缀 S,本次输入 P,返回 G 个 token,H = S + P,后续 decode 次数 n_d = G − 1。
第 j 次 decode 执行前,上下文长度为 H + j。每次把已有上下文读一遍:
R_old = c_KV · Σ_{j=0}^{n_d−1} (H + j)
= c_KV · [ n_d·H + n_d(n_d−1)/2 ]
最终保存:c_KV · (H + n_d) (含 prefill 写入的 c_KV·P)
生成新增:c_KV · n_d
例(原文例题 2-3):S=0, P=8192, G=1025 → n_d=1024
| 量 | 数值 |
|---|---|
| 累计读取上下文 token 次数 | 8,912,384 |
| 累计读取字节 | 约 1224 GiB |
| 生成期间新增 KV | 144 MiB |
| 结束时保存 | 约 1.27 GiB |
比值:1224 / 1.27 ≈ 963 倍。
判据:"KV 缓存占多少显存"和"KV 要读多少字节"是两个问题。 前者决定能开多少并发,后者决定 decode 的带宽压力。
5.1 运算量也是同样的形状
F_decode,total = n_d·F_0 + a·[ n_d·H + n_d(n_d−1)/2 ]
F_0:每步固定矩阵运算量(与上下文无关);a:与每个上下文 token 交互所需的运算量(注意力)。
输出变长时,固定部分线性增长;过程中的上下文增长使一部分工作量按平方增长。判据:长对话的算力成本不是线性的——这也是第 8 章调度(J22)要处理的核心矛盾。
6. batch 的两个转折点
原文例题 2-2:上下文读取量何时超过权重读取量?
R_W(每批共享权重读取)≈ 15,136,811,008 bytes
每请求上下文读取 = c_KV · H = 147,456 × H
令 B·c_KV·H = R_W
⇒ B* = R_W / (c_KV · H)
| 上下文长度 H | 上下文读取超过权重的 batch |
|---|---|
| 8,192 | 13 |
| 2,048 | 51 |
判据:上下文越短,权重复用越划算。 上下文长度缩到 1/4,转折点 batch 涨到约 4 倍——这解释了为什么"短请求高并发"和"长上下文低并发"是完全不同的服务形态。
与 J07 的
B*=147.7对照:那个转折点是"计算 vs 权重读取",这里是"KV 读取 vs 权重读取"。两个转折点都在,取更早到的那个才是实际约束。
6.1 可复算(实测)
$ cd /root/博客运营官/blog-v2 && python3 tools/s23_book_calc.py | tail -9
Qwen3-8B c_KV = 2*36*8*128*2 = 147456 bytes = 144 KiB/token
H= 2048: KV 0.281 GiB
H= 8192: KV 1.125 GiB
H= 32768: KV 4.500 GiB
H=131072: KV 18.000 GiB
P=8192 G=1025: n_d=1024, 累计读取 token 次 = 8,912,384, 字节 = 1224 GiB, 最终保存 = 1.27 GiB
H=8192: B* = R_W/(c*H) = 12.5 -> 最小整数 13
H=2048: B* = R_W/(c*H) = 50.1 -> 最小整数 51
P=2048 G=2048: 累计读取 token 次 = 6,286,337
口径:
R_W = 15,136,811,008字节(原文值),c_KV = 147,456字节;本块是按公式复算,不是线上测量。
7. 前提与边界
| 结论 | 成立条件 | 不成立时 |
|---|---|---|
c_KV 线性于 H 与 batch |
每请求独立、无前缀共享 | 前缀缓存/共享 KV → 容量下降(J22) |
| 累计读取按"每步读全部历史" | 无分块/无压缩 | 稀疏注意力、滑窗 → 读取量下降 |
| 只缓存 K、V | 因果(decoder-only)模型 | 编码器模型只需缓存一次表示 |
| 权重 16.38 GB | BF16 全参数 | 量化/切分 → 按比例下降 |
8. 常见误区
| 误区 | 纠正 |
|---|---|
| 用"KV 容量"说明带宽压力 | 容量与访问量差近千倍(1224 GiB vs 1.27 GiB) |
| 认为 batch 能分摊 KV | KV 是每条请求独占,只有权重能共享 |
把 n_KV 当成查询头数 |
GQA:8 个 KV 头 vs 32 个查询头,差 4 倍 |
| 忽略 prefill 的 KV 写入 | 它占最终容量的大头(例中 8192 个输入 token) |
| 长对话只按 token 数线性估成本 | 注意力部分随长度呈平方增长 |
9. 自测
- Qwen3-8B 一条 16K 上下文的请求占多少 KV?32 条并发呢?
P=2048, G=2048时,累计读取上下文多少次?- 上下文从 8K 降到 2K,"KV 读取超过权重读取"的 batch 阈值怎么变?
- 为什么"KV 位宽减半"能同时减半容量与累计读取量,却不能减半注意力运算量?
答案
144 KiB × 16384 = 2.25 GiB;32 条= 72 GiB(已超一张 80 GB 卡的可用余量)。n_d=2047,累计= Σ(2048+j) = 6,286,337次(脚本实测值)。- 从 13 涨到约 51(
B* = R_W/(c_KV·H))。 - 位宽只影响"字节数",运算量取决于"配对数量"(
N_pair),与元素宽度无关。
10. 下一篇
S23-J10 · 条件计算、网络形状与模型矩阵 —— MoE 把"每个 token 都算全部参数"改成"只算被选中的专家":省了什么、又新增了什么(专家放置与负载不均);以及整模型的容量与算力怎么逐层汇总。