KV Cache 与推理内存
第 15 课:KV Cache 与推理内存
上一部分我们一直在"造模型"——预训练、对齐、切分、省显存。这一课换个视角:模型训好了,怎么把它服务出去。一次请求在引擎里被切成两段,其中一段几乎不费算力却要吃光显存;这个矛盾决定了对话越长越贵、并发上不去、以及为什么"分页"和"调度"这两个词会成为推理引擎的核心。讲清 KV Cache 的公式与体积、它如何卡住并发、为什么每个 token 都要把模型读一遍,最后看分页 KV 与连续批处理凭什么把吞吐翻十倍。
② 写出 KV Cache 的字节公式,算出 70B 在 8K 上下文下的实际占用
③ 用"显存预算 ÷ 每请求 KV"算出并发上限,并说清 KV 量化与 GQA(即多个查询头分组、共享少数几组 K/V)各能换回多少并发
上一部分我们把模型造了出来;从这一课开始,我们把它服务出去。先把两个阶段说白:prefill(预填充)是把整段 prompt 一次性喂进去,为每个输入 token 算出它在这一层的 K/V;decode(解码)是拿着最后一个 token 一路算下去,一次只产出 1 个新 token,并把它自己的 K/V 追加进缓存。
为什么要缓存?因为自回归生成要求第 t 个 token 看到前面所有 token 的 K/V。如果不缓存,每生成一个新 token 都要把前面所有 token 重新算一遍——可以类比成"每写一个新字,就把整篇文章从头誊写一遍"。缓存下来之后,每个新 token 只需算自己那一份,之前算过的直接取用。
至于"同一个开头问第二遍会快得多":引擎把已算过的前缀对应的 KV 块留在显存里,第二个请求命中同一批块,prefill 几乎不用重算——这就是前缀缓存。
| 要生成第几个 token | 必须完整前向的 token 数 | 原因 |
|---|---|---|
| 第 1 个 | n(整段 prompt) | 每个输入 token 都要算 K/V |
| 第 2 个 | n + 1 | 前面 n 个要重算,加上刚生成的 1 个 |
| 第 t 个 | n + t − 1 | 靠前的 token 被反复重算 |
| 生成 m 个合计 | m·n + m(m−1)/2 | 随生成长度呈平方增长 |
| 方案 | 每个新 token 要算的 K/V | 合计(n = m = 1000) | 相对 |
|---|---|---|---|
| 不缓存 | n + t − 1 个 token | 1,499,500 | 1.0×(基准) |
| KV Cache | 只算自己那 1 个 token | 2,000 | 1/750 |
| 阶段 | 输入形状 | 算术强度(FLOP/byte) | 瓶颈 |
|---|---|---|---|
| prefill | 整段 prompt 一起算(矩阵×矩阵) | 约 1,000 | 算力(compute-bound) |
| decode | 一次 1 个 token(矩阵×向量) | 约 1 | 显存带宽(memory-bound) |
KV Cache 不是"一个优化技巧",而是自回归解码能成立的前提:它把生成 n 个 token 的 K/V 计算量从平方级压回线性级(本例 750 倍)。但它换来的代价是一块随序列长度与并发数常驻的显存——这块显存有多大、怎么分配、怎么共享,就是接下来 5 张卡片的全部内容。
上一张卡只说了"KV 要常驻",没说多大。这一张把它算准——因为后面所有关于并发、吞吐、成本的判断,都建立在这个数上。
先看公式里每一项能不能动:层数 L 由模型结构定死;KV 头数 Hkv 是 GQA 的产物(70B 是 8);每头维度 dh 通常 128;序列长度 s 由用户决定;字节数 b 由精度决定(bf16 是 2、fp8 是 1)。所以一句话:KV Cache ∝ 序列长度 × 批量 × 层数 × KV 头数 × 每头维度 × 精度,而与参数量无关。
这个"与参数量无关"很反直觉,可以类比成「会议纪要」:会议开得越久(s)、参会人越多(b),纪要就越厚;而纪要的厚度跟"这家公司有多少员工"(参数量)毫无关系。
| 符号 | 含义 | Llama 2 70B 取值 | 能不能动 |
|---|---|---|---|
| 2 | Key 一份 + Value 一份 | 2 | 不能 |
| L | 层数 | 80 | 不能 |
| Hkv | KV 头数(非 Q 头数) | 8(GQA,Q 头是 64) | 能(Card 5) |
| dh | 每头维度 | 128 | 不能 |
| s | 序列长度(prompt + 已生成) | 4096 / 8192 / … | 由用户决定 |
| b | 每个元素字节数 | bf16 = 2 | 能(Card 5) |
| 模型 | 2 × L × Hkv × dh × b | 每 token | 每 token(MiB) |
|---|---|---|---|
| Llama 2 70B(GQA,8 头) | 2 × 80 × 8 × 128 × 2 B | 327,680 B | 0.3125 MiB |
| 同名模型若用 MHA(64 头) | 2 × 80 × 64 × 128 × 2 B | 2,621,440 B | 2.5 MiB |
| LLaMA-13B(MHA,40 层 40 头) | 2 × 40 × 40 × 128 × 2 B | 819,200 B | 0.78125 MiB |
| 序列长度 s | b = 1(单请求) | b = 4(4 个并发) | b = 16 |
|---|---|---|---|
| 2,048 | 0.625 GiB | 2.5 GiB | 10 GiB |
| 4,096 | 1.25 GiB | 5 GiB | 20 GiB |
| 8,192 | 2.5 GiB | 10 GiB | 40 GiB |
| 32,768 | 10 GiB | 40 GiB | 160 GiB |
KV Cache 的大小只由"序列长度 × 批量 × 结构 × 精度"决定,与模型参数量无关。这一条推翻了直觉:大家以为"模型越大越费显存",但在 serving 场景里,真正随业务量增长的是 KV——权重是买机器时定下的常数,KV 是每个用户、每轮对话都在长的变量。所以能装多少并发,不取决于模型多大,而取决于 KV 多小(下一张卡把这句话算成钱)。
上一张卡算出了"每个请求要多少 KV"。这一张把它放进一张卡的真实预算里——因为并发数是除出来的,不是调出来的。
先把预算列清楚。一张 80 GB 卡上跑 70B,要同时放三样东西:权重、这一批请求的 KV、以及框架自身的开销(激活、临时缓冲、通信缓冲、显存碎片)。第一项是常数,第三项通常是几个 GiB 的固定开销,于是能装几个请求,就完全由第二项也就是 KV 决定——可以类比成一个固定面积的车库:先要腾出位置停那辆"货车"(权重),剩下的空间能停几辆"私家车"(并发请求),取决于每辆车占多大(上下文长度)。
| 项 | 70B 在一张 80 GB 卡上 | 说明 |
|---|---|---|
| 权重(bf16) | 140 GB | 单卡放不下(呼应第 14 课) |
| 权重(bf16,TP = 2) | 70 GB / 卡 | 张量并行把权重切一半 |
| 框架开销(激活 / 缓冲 / 碎片) | 约 2 ~ 5 GB | 经验区间(估算,随实现与序列长度浮动;本卡按保守下界 2 GB 估) |
| 留给 KV 的余量 | 约 8 GB | 80 − 70 − 约 2 = 约 8 GB |
| 上下文长度 | 每卡每请求 KV(TP = 2 → 4 个 KV 头) | 8 GB 能装几个 | 说明 |
|---|---|---|---|
| 2,048 | 0.3125 GiB | 25 个 | 8 ÷ 0.3125 = 25.6 |
| 4,096 | 0.625 GiB | 12 个 | 8 ÷ 0.625 = 12.8 |
| 8,192 | 1.25 GiB | 6 个 | 8 ÷ 1.25 = 6.4 |
| 32,768 | 5 GiB | 1 个 | 8 ÷ 5 = 1.6 |
| 杠杆 | 做法 | 每卡权重 | 每卡每请求 KV(8K) | 可装并发 |
|---|---|---|---|---|
| 基准 | TP = 2,GQA,bf16 KV | 70 GB | 1.25 GiB | 6 |
| ① KV 量化到 fp8 | KV 的 b 从 2 降到 1(权重不变) | 70 GB | 0.625 GiB | 12 |
| ② 提高张量并行到 TP = 8 | 每卡只留 1 个 KV 头(权重也随之切到 1/8) | 17.5 GB | 0.3125 GiB | 约 194 |
| ③ 前缀缓存命中 | 共享相同 system prompt 的块 | 70 GB | 按命中比例减免 | 最多再翻数倍 |
| ④ 换 MLA 结构 | 每层只存一个潜向量 | 由基座决定 | 再降(见 Card 5) | 再升 |
并发上限 = (单卡显存 − 权重 − 框架开销)÷ 每请求 KV。这个式子里,只有分母是你业务上真正在消耗的东西。所以一个反直觉的结论是:推理成本的主变量不是模型参数量,而是"上下文长度 × 并发数"——同一个 70B,做 2K 上下文的客服和做 32K 上下文的文档分析,单卡能服务的并发差 25 倍(25 个 vs 1 个)。
上一张卡说明了"并发被 KV 卡住"。但还有一半没解释:为什么单请求的每个 token 也这么慢、这么贵?答案在"搬运"这两个字上。
先看 decode 一步到底做了什么:它把当前 token 的向量,依次乘过每一层的权重矩阵。每一层的权重都必须从显存里读出来——不是读一次就够,而是每生成一个 token 都要重读一遍全部权重。GPU 的算力单元很快,但显存带宽是有限的;当"要搬的数据"远多于"要算的活",耗时就被搬运时间决定。这就是 memory-bound 的含义——可以类比成"每写一个字都要把整座图书馆的书重新搬出来翻一遍":书本身没变,但你每次都付出了搬运的代价。
| 量 | 取值 | 说明 |
|---|---|---|
| 要搬的权重 | 70 GB / 卡(TP = 2) | bf16,每步都要重读 |
| H100 HBM3 带宽 | 3.35 TB/s | 单卡 |
| 一步耗时下限 | 约 20.9 ms | 70e9 ÷ 3.35e12 |
| 单请求吞吐上限 | 约 48 token/s | 1 ÷ 0.0209 |
| 量 | 单请求(b = 1) | 怎么来的 |
|---|---|---|
| 这一步的浮点运算 | 2 × 70e9 = 1.4e11 FLOPs | 每参数 2 次乘加 |
| 这一步搬运的字节 | 约 1.4e11 B | 权重本体 |
| 算术强度 | 约 1 FLOP/byte | 1.4e11 ÷ 1.4e11 |
| H100 的"算力/带宽拐点" | 约 295 FLOP/byte | 989e12 ÷ 3.35e12 |
| 差距 | 约 295 倍 | 295 ÷ 1 |
| 批量 b | 算术强度 | 相对拐点 | 结果 |
|---|---|---|---|
| 1 | 1 FLOP/byte | 差 295 倍 | 严重带宽受限 |
| 8 | 8 FLOP/byte | 差 36.9 倍 | 仍带宽受限 |
| 32 | 32 FLOP/byte | 差 9.2 倍 | 仍带宽受限,但吞吐已接近 32 倍 |
| 约 295 | 约 295 FLOP/byte | ≈ 拐点 | 才开始转向算力受限 |
decode 每一步的耗时由"搬运多少权重"决定,而不是由"上下文多长"决定——这解释了两件事:① 为什么对话变长时,每 token 的耗时基本平稳(只有 KV 那一小部分在涨),总时间大致线性;② 为什么批量几乎免费——因为所有请求共用同一份权重,搬运一次就能服务一批。所以推理引擎的核心工作只有一句话:让每一步都在"有足够多 token 可用"的状态下跑——这正是下一张卡(把 KV 做小)与第 6 张卡(把调度做好)的分工。
上一张卡片说明了 decode 为什么受带宽限制;这一张把问题倒过来问:既然 KV 的公式这么清楚,能不能直接把它变小?
回到公式 2 × L × Hkv × dh × s × b,逐个因子审一遍:L 由模型定死、s 由用户定、dh 基本固定、2 是定义。真正能动的是两个:Hkv(少存几个头) 和 b(每个数用更少字节);MLA 走的是第三条路——不再存 K/V 本身,改存一个更窄的替代品。
这个取舍可以类比成存照片:与其每张都保留 RAW 原图(bf16、全部 KV 头),不如存压缩图(fp8、更少的头)——省得多,但要接受细节上的损失。
三条路线省的量级差得很远,先记住三个数:MQA 把所有 Q 头压到一组 K/V,相对 MHA(每个 Q 头各配一组 K/V)省 64 倍;GQA(70B 的 8 个 KV 头)省 8 倍;而 MLA 换的是另一条路——不存完整的 K/V,每层每个 token 只缓存一个低秩潜向量。DeepSeek-V2 论文报告:相对其前代 DeepSeek 67B,MLA 把 KV Cache 压掉 93.3%(降到约 1/15);由论文 Table 1 的公式 (d_c+d_h^R)l ≈ 4.5·d_h·l 可推得一个更可比的读数——MLA 的每 token 缓存只相当于 2.25 组 GQA(此换算为本课按该公式推导,非论文原话),对照 70B 的 8 组 GQA,就是再小约 3.6 倍(8 ÷ 2.25 ≈ 3.6)。这就是为什么「换结构」虽然要重训,却是把 KV 砍得最狠的一刀。
| 因子 | 能不能动 | 手段 | 代价 |
|---|---|---|---|
| 2(K 与 V) | 不能 | — | — |
| L(层数) | 不能 | 删层会毁掉模型 | — |
| Hkv(KV 头数) | 能 | MQA / GQA(要重训或 uptrain) | 质量略降,需验证 |
| dh(每头维度) | 基本不能 | — | — |
| s(序列长度) | 业务决定 | 滑窗 / 驱逐 / 摘要压缩 | 丢信息 |
| b(每元素字节) | 能 | KV 量化到 fp8 / int8 | 长程任务上更敏感 |
| 方案 | KV 头数 | 每 token | 8K 上下文 | 相对 |
|---|---|---|---|---|
| MHA(64 个 Q 头各配一组 K/V) | 64 | 2.5 MiB | 20 GiB | 1.0× |
| GQA(Llama 2 70B 实际采用) | 8 | 0.3125 MiB | 2.5 GiB | 1/8 |
| MQA(所有 Q 头共享一组) | 1 | 0.0390625 MiB | 0.31 GiB | 1/64 |
| KV 精度 | 每元素字节 | 每 token | 8K 上下文 | 相对 bf16 |
|---|---|---|---|---|
| fp32 | 4 | 0.625 MiB | 5 GiB | 2× |
| bf16(基准) | 2 | 0.3125 MiB | 2.5 GiB | 1.0× |
| fp8 / int8 | 1 | 0.15625 MiB | 1.25 GiB | 1/2 |
三个杠杆的性价比排序很清楚:KV 量化(部署期可做、省一半、无需重训)> GQA(结构级、省 8 倍、需训模型时决定)> MLA(省得更多但要新架构)。而它们共同的代价提醒我们一件事:KV Cache 不是纯工程问题,它同时是"质量 vs 成本"的交换点——压得越狠,长程推理越容易掉分,所以任何压缩都必须配回归评测(这是第 19 课的主题)。
上一张卡片从公式的因子上把 KV 压小了;这一张处理一个更"冤枉"的浪费:显存并没有真的用满,但引擎说没地方了。
原因是"预留"。为了不让 KV 边生成边重新分配,早期系统会按模型支持的最大长度给每个请求预留一块连续显存——比如给每个请求先占 8K 的位置。真实对话平均只用到一两千 token,剩下的就白占着。更麻烦的是碎片:每块大小不一,释放后留下的空隙往往拼不出下一个请求要的形状。于是出现"总空闲很多、但就是放不下"——这可以类比成停车场按"最大车型"给每辆车划线:车位总量明明够,但短车占着长车位、剩下的零碎空隙又停不下下一辆。
| 项 | 数值 | 说明 |
|---|---|---|
| 按最大长度预留 | 8,192 token | 每个请求都先占满 |
| 实际平均使用 | 约 1,024 token | 短对话居多的典型分布 |
| 利用率 | 12.5% | 1024 ÷ 8192 |
| 浪费 | 87.5% | 1 − 12.5% |
| 业界论文口径 | 60% ~ 80% | vLLM 论文实测(含碎片) |
| 方案 | 分配单位 | 浪费来源 | 浪费率 |
|---|---|---|---|
| 预留式连续分配 | 整条请求(按最大长度) | 预留未用 + 内部碎片 + 外部碎片 | 60% ~ 80% |
| 分页(PagedAttention) | 固定块(如 16 token) | 只有每个序列的最后一块 | < 4% |
| 块大小取 16 时的一块 | 16 token | 最坏情况浪费 15 token | — |
| 调度单位 | 一条请求结束后 | GPU 在等谁 | 实测 |
|---|---|---|---|
| 整条请求(早期) | 整个批次一起结束才能换 | 等最慢的那条 | 基准 |
| 一次迭代(Orca 起) | 任何时刻都能插入新请求、移出已结束的 | 不等任何人 | 36.9×(GPT-3 175B) |
| + chunked prefill(Sarathi-Serve) | 把长 prefill 切块混进 decode | 不再被打断 | 容量再 2.6× ~ 5.6× |
"吞吐翻十倍"不是一个技巧做出来的,而是三件事叠起来:分页(把浪费从 60~80% 压到 <4%)→ 连续批处理(让每一步都能重新组批)→ 分块 prefill(让长请求不再打断短请求)。它们共同的立足点是这一课反复出现的那句话:KV 才是推理显存的主角。而反向证据同样重要——分页内核本身要付出 12% ~ 37% 的单步开销,所以"分页一定更快"是错的,正确的说法是"分页让更多请求同时在跑,因此整体吞吐更高"。这套调度机制的内部细节(调度策略、抢占、前缀缓存淘汰)是第 18 课"推理引擎"的主题。