2349 字
约 7 分钟
5
S23-J09 · 自回归生成与上下文状态

适用读者:要做容量规划与并发上限计算、或想搞清"为什么长上下文既贵又慢"的人。
前置: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. 自测

  1. Qwen3-8B 一条 16K 上下文的请求占多少 KV?32 条并发呢?
  2. P=2048, G=2048 时,累计读取上下文多少次?
  3. 上下文从 8K 降到 2K,"KV 读取超过权重读取"的 batch 阈值怎么变?
  4. 为什么"KV 位宽减半"能同时减半容量与累计读取量,却不能减半注意力运算量?
答案
  1. 144 KiB × 16384 = 2.25 GiB;32 条 = 72 GiB(已超一张 80 GB 卡的可用余量)。
  2. n_d=2047,累计 = Σ(2048+j) = 6,286,337 次(脚本实测值)。
  3. 从 13 涨到约 51(B* = R_W/(c_KV·H))。
  4. 位宽只影响"字节数",运算量取决于"配对数量"(N_pair),与元素宽度无关。

10. 下一篇

S23-J10 · 条件计算、网络形状与模型矩阵 —— MoE 把"每个 token 都算全部参数"改成"只算被选中的专家":省了什么、又新增了什么(专家放置与负载不均);以及整模型的容量与算力怎么逐层汇总。

S23-J09 · 自回归生成与上下文状态
https://www.clxhxhhr.top/posts/5536/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。