公共概念:推理、KV Cache、内存和性能指标
本篇定位:第 2 篇,必读
把后面反复用到的概念一次讲清:KV 缓存什么、Prefill/Decode、容量公式、复用条件、四个“完成”时刻、指标口径。
需要数学推导读推理基础;需要 KV 收益模型读KV 容量与性能模型。
这一章介绍推理系统的共同基础。第一次先读本章;需要更完整的数学推导,再进入 Attention 与推理教材或 KV 容量与复用。
1. KV Cache 缓存的是什么
在一层 attention 中,输入隐藏状态经过投影,得到 Q、K、V。可以粗略理解为:Q 用来提出“现在要关注什么”,K 用来匹配历史位置,V 提供匹配后要汇总的信息。
Q = X Wq,K = X Wk,V = X Wv
Attention(Q, K, V) = softmax(Q Kᵀ / √d + causal mask) V
自回归生成一次向后推进一个或一组 token。历史 token 在固定 causal 前缀下的 K/V 可以保留,新 token 只需生成自己的 Q/K/V,再读取历史 K/V 做 attention,避免重新跑一遍完整历史前向。
为什么通常不缓存 Q?未来 token 用的是自己的 Q;历史 Q 已经完成当时的 attention,普通 Decode 不需要再用历史 Q 查询一次。KV Cache 仍然没有消除每轮读取历史状态的成本,长上下文 Decode 的 attention 成本仍会增长。
它与另外两种东西不同:
| 名字 | 存的内容 | 命中意味着什么 |
|---|---|---|
| 模型权重 | 学到的参数 | 模型可以执行,不表示请求已经算过 |
| KV Cache | 各层的中间状态 | 可以省掉适用前缀的一部分重复前向 |
| 输出/答案缓存 | 完整回答文本 | 在业务允许时直接返回答案 |
2. Prefill 与 Decode 为什么要分开看
Prefill 一次处理输入中的多个 token,通常能形成更大的矩阵运算。长输入时计算量很大,attention 的访存与实现也会影响瓶颈。
Decode 在常规自回归路径中,每个活跃请求每轮处理少量新 token。低 batch 时,读取权重和 KV 的带宽、kernel 启动、通信常常更突出。batch 增大、模型改变或使用投机解码后,瓶颈也会改变。
因此“Prefill 计算受限、Decode 带宽受限”是常见观察,不是所有设备和负载都成立的定律。
如果一个长 Prefill 独占 GPU 太久,正在 Decode 的用户会感觉输出卡顿。Chunked Prefill 把长输入分几轮算,让调度器在轮次之间服务其他请求。块太小则可能增加开销、降低计算效率。
3. 容量公式:面试中要真的会算
对普通、每层形状一致的 full-attention KV:
每 token KV 字节数 = 2 × L × Hkv × d × b
单请求 KV 字节数 = 每 token KV 字节数 × 已保留 token 数
2 表示 K 和 V,L 是层数,Hkv 是 KV heads 数,d 是 head dimension,b 是每元素字节数。不要把 Query heads 数 Hq 直接代入 Hkv。
教学模型取 L=32, Hkv=8, d=128, BF16 b=2:
每 token = 2 × 32 × 8 × 128 × 2 = 131,072 B = 128 KiB
8,192 token = 1 GiB
6,144 token = 0.75 GiB
再增加并缓存 256 token,额外 KV = 32 MiB
生成出的最后一个 token 通常要到下一次 forward 才会建立自己的 KV;请求若直接结束,这一步可能不再执行。因此容量预算可以按完整计划长度保守估算,精确测量要看实际已计算位置。
16 条互不共享、各保留 8,192 token 的活跃请求,需要约 16 GiB KV payload。这还没算权重、页尾浪费、工作空间、CUDA Graph 相关内存和在途副本。
变体与并行边界
| 情况 | 该怎么调整 |
|---|---|
| MHA | Hkv = Hq |
| GQA / MQA | 多个 Q head 共享较少的 KV head,KV 容量通常更小 |
| MLA | 看实际缓存的是压缩 latent、位置部分还是展开形式,不能直接套 GQA 公式 |
| 滑窗 attention | 按实际保留窗口及缓存实现算,不是所有层都保留完整历史 |
| 混合 attention / 递归状态 | 分层或按 group 算;部分状态按序列保存快照,而不是逐 token KV |
| TP | 只有 KV heads 等实际均匀切分时,单 rank 才近似除以 TP;head 不足可能复制 |
| PP | 单 stage 按本 stage 的层数计算 |
| DP / 存储副本 | 是多份资源占用,不能拿来抵消单份 KV 大小 |
4. 页、前缀与 chunk:三个粒度不要混淆
Page / block:解决内存怎么分
给每个请求预留最大长度的连续显存,会浪费很多空间,也难以扩容。分页把 KV 放进固定大小物理块,通过 block table 把逻辑 token 位置映射到物理地址。
假设每块放 16 个 token,一个 33-token 请求需要 3 块,最后一块只用 1 个位置。分页减少了连续大块分配的困难,但仍有尾部浪费和元数据开销。
Prefix cache:解决哪些计算可以复用
两个请求的前缀完全一致时,可以共享对应的只读 KV。共享块要有引用管理;分叉后新增 token 写各自尾部。若要修改共享尾块,必须按实现采用 copy-on-write 或其他隔离方案,不能直接覆盖。
PagedAttention 讨论地址与访存,前缀哈希或 Radix 树讨论匹配和共享;它们可以配合。
Chunk / object:解决存储怎么组织
外部缓存通常把一段 token 的 KV 组织成 chunk,再映射成一个或多个对象。若 chunk=256、page=16,在理想对齐条件下,一个 chunk 覆盖 16 个 page。遇到多 rank、多 group、按层存储时,一个 chunk 可能有多个对象。
小 chunk 复用更细,但产生更多 key、RPC、锁和完成事件;大 chunk 更容易跑满带宽,但部分前缀更难复用,也可能多搬无用数据。
5. 什么情况下才可以复用
“同样一段文字”不够。普通精确前缀复用至少要确认:
- 模型及实际计算语义兼容,包括权重 revision、adapter、位置编码等。
- 完整前缀的 token IDs 和位置关系相同。
- KV dtype、布局、层/group、rank 分片与目标一致,或者存在明确且正确的转换。
- 命中对象完整、仍有效、已成功加载到执行位置。
- 租户与访问策略允许共享。
例如“合同 A”放在不同 system prompt 后面,深层隐藏状态会受到不同前文影响,不能只按合同正文 hash 认定 KV 一样。位置变化、文档顺序变化也需要相应算法处理。
链式前缀 key 可以概念化为:
namespace = 模型与计算配置身份 + 允许共享的范围
chunk_hash[i] = H(namespace, chunk_hash[i-1], token_ids_of_chunk[i])
object_key = chunk_hash[i] + 必要的 rank/group/layout 标识
这是正确性设计示意,不是四个项目通用的序列化协议。某些字段在实际实现中由实例配置固定,某些需要显式编码;应检查数据从哪里传来。salt/namespace 能避免错误共享,但不等于鉴权。
普通精确前缀缓存不能跨过缺口:如果只有第 1、3 段 KV,第 2 段缺失,不能把第 3 段随意拼进去宣称整段免算。CacheBlend 等特殊机制另有位置处理、选择性重算和质量约束。
6. 多级缓存为什么可能更慢
常见存储层级是 GPU HBM、主机 DRAM、远端 DRAM、SSD。容量和单位成本通常更有利的层,访问延迟与数据路径也更长。
重算路径 = 排队 + 完整 Prefill + 首 token 其他开销
复用路径 = 排队 + 查询 + 加载关键路径 + 剩余 Prefill + 首 token 其他开销
只有当减少的计算和排队收益,超过查询、加载及其引入的额外排队时,缓存才改善该请求的延迟。加载关键路径指无法被计算掩盖的那部分时间。
假设读取 1 GiB,网络有效带宽 20 GiB/s:
网络部分至少约 50 ms
另假设查询 2 ms,H2D 40 ms,布局/同步 8 ms
串行加载约 100 ms
如果原本能省下 700 ms Prefill,值得尝试;如果只能省 60 ms,串行加载反而更慢。流水线能重叠部分阶段,但有启动、尾部和依赖成本,要用 trace 验证。
单位也要说清:200 Gb/s = 25 GB/s ≈ 23.28 GiB/s 的物理线速;应用 payload 带宽更低。不能把 200G 当 200 GB/s。
7. 四个“完成”时刻
知道对象存在
→ 存储对象可以读取
→ 数据已到目标 buffer,相关传输完成
→ GPU 已满足布局和同步依赖,可以消费
前一个时刻不能替代后一个。异步调用返回往往只是“任务已提交”。同理,请求取消或上层超时,也不代表 NIC/GPU 已停止访问旧内存。
安全回收要回答:谁拥有内存、谁还在读写、哪个事件证明访问结束、失败时如何停止或排空在途任务。lease、对象引用、锁、CUDA event、传输完成状态分别覆盖不同边界。
8. 指标怎么说才不含糊
| 指标 | 白话解释 | 常见误用 |
|---|---|---|
| TTFT | 从发送/接收请求到第一个输出 token 的等待 | 不说明起点,漏掉排队或网络 |
| ITL | 相邻输出 token 的时间间隔分布 | 只报均值,掩盖某些轮次卡顿 |
| TPOT | 常用口径为首 token 后的平均每 token 时间 | 把平均 TPOT 当最坏 ITL;单输出 token 无此分母 |
| E2E | 完整请求耗时 | 不固定输出长度就比较 |
| Throughput | 每秒完成的请求/token | 不区分输入与输出,不报请求失败 |
| Goodput | 满足指定成功和延迟标准的有效吞吐 | 不说明 SLO、统计窗口和通过条件 |
| 命中率 | 在指定分母下,有多少数据可复用 | 混用请求、token、字节命中率 |
报性能至少说明模型、输入/输出分布、到达率、并发、冷热状态、硬件、版本及 P50/P95/P99。相同并发的闭环测试和固定到达率的开环测试不是一回事:前者可能因为服务变慢而自动降低请求压力。
9. 后面经常遇到的术语
| 术语 | 白话解释 | 与 KV 的关系 |
|---|---|---|
| HBM / DRAM | 前者通常指 GPU 高带宽显存,后者在本文通常指主机内存 | 存在 host 的 KV 需要经适用路径送入 GPU 才能用于普通 GPU attention |
| H2D / D2H | 主机到设备 / 设备到主机的拷贝 | offload 和 restore 不能只算网络时间 |
| Pinned memory | 被锁定、不能随意换出的主机内存,常用于异步设备拷贝 | 锁页本身不等于完成 RDMA 注册,也不延长对象所有者寿命 |
| TP,张量并行 | 把一层中的部分张量计算分给多张卡协作 | KV 是否分片,要看实际 head 和布局 |
| PP,流水并行 | 把不同模型层放在不同设备/stage | 每个 stage 通常只持有相应层的状态 |
| DP,数据并行副本 | 多份模型处理不同请求;MoE 的组织还可能与 EP 耦合 | 普通副本的本地 KV 不会自动共享 |
| EP,专家并行 | MoE 的专家分布在不同设备,token 需要路由过去再汇总 | 专家 dispatch/combine 通信与 attention KV 存储是两条问题线 |
| IPC / RPC | 进程间通信 / 远程调用 | 控制消息走某条通路,不意味着大块 KV 也走同一条 |
| SLO / P99 | 约定的服务目标 / 99% 样本不超过的延迟值 | 需说明请求范围、统计窗口、失败和超时如何计入 |
| Trace / profiler | 记录各阶段或设备工作的时间线 | 用来判断瓶颈和是否真的发生计算/传输重叠 |
10. 这章需要达到的程度
闭卷算出一个模型的 KV 容量和传输下界;画出共享块与私有尾部;解释一个错误命中案例;给出一个远端命中不值得加载的例子;分清查询、加载、计算和回收的边界。
这些掌握以后,再读各项目的具体类和函数,才不会把实现细节记成互不相干的名词。