推理阶段、KV cache 与显存

本页目录

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

从完整前缀到一个新位置

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

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

对 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

符号含义
2K 与 V 两份缓存
L采用该缓存结构的层数
B、S同时缓存的序列数、每条序列长度
Hkv、DKV 头数、每头维度
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 论文 对滑动窗口、压缩缓存、混合循环层或不同层结构,应逐层按实际布局核算,不能机械套这个乘积。

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 部署。

哪一个因子让 KV 变大?

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

正在呈现知识画面
哪一个因子让 KV 变大?

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

从计算量到实际吞吐

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

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

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

继续阅读:推理计算、候选与验证。