复杂模型与 KV 存储:MLA、滑窗、混合状态、MoE 与量化
KV 管理主题 · KV 原理与容量推导 · KV 存储详解 · 互动容量计算器
前面的容量公式 2 × 层数 × KV head 数 × head dim × 字节数 只适用于普通 MHA/GQA。现在很多主流模型不是这样:DeepSeek 系列用 MLA,部分模型混用滑窗层和全局层,还有模型把注意力和 Mamba 类状态层混在一起。模型一变,“存什么、存多大、怎样才算命中”都会跟着变。这一页把这些差异讲清楚,KV 存储岗位面试经常追问。
一句话结论:先问清楚“这个模型每层到底缓存了什么状态”,再谈容量、切分、复用和存储格式。不要把普通 GQA 的公式套到所有模型上。
1. 总览:六类差异一张表
| 模型特征 | 每层缓存什么 | 容量怎么算 | TP 切分 | 前缀复用条件 | 对存储的影响 |
|---|---|---|---|---|---|
| MHA / GQA / MQA | 每个 KV head 的 K 和 V | 2 × KV head × head dim × 字节 × token | KV head 能整除 TP 时均分;不够分时常见实现会复制 | 完整连续前缀 | 标准情况,前面各章的算例都基于它 |
| MLA(如 DeepSeek-V2/V3) | 一个压缩后的 latent 向量 + 一小段位置相关的 key | (latent 维度 + RoPE 维度) × 字节 × token,没有单独的 V | latent 被所有 head 共用,常见实现在每个 TP rank 各存一份完整副本 | 完整连续前缀 | 单 token 小很多;各 rank 的对象可能内容相同 |
| 滑窗注意力(SWA) | 只需最近 w 个 token 的 K、V | min(token 数, w) × 每 token 大小 | 同 GQA | 只需前缀末尾的 w 个 token | 长前缀下这些层几乎不占空间;命中规则与全局层不同 |
| 混合状态(Mamba/SSM、线性注意力) | 每条序列一个固定大小的状态,每步覆盖更新 | 状态大小,与 token 数无关 | 按实现切分 | 必须有恰好在前缀边界的状态快照 | 不能用分块拼接;只能存“检查点” |
| MoE | 注意力部分与对应的稠密模型相同 | 按注意力结构计算,专家数量不影响 KV | 常与注意力 DP、专家 EP 组合 | 同注意力结构 | KV 公式不变;部署方式改变 KV 落在哪个 rank |
| KV 量化(FP8 等) | 低精度 K/V + 缩放因子 | 元素字节减半(或更少)+ 缩放元数据 | 同原结构 | 同原结构,但格式必须一致 | dtype 与缩放方案必须进格式身份 |
多模态、LoRA、投机解码等不改变“每层存什么”,但会改变“什么情况下两段 KV 相同”,放在第 7 节单独讲。
2. MLA:缓存一个压缩向量
2.1 白话原理
普通注意力为每个 head 分别缓存 K 和 V。MLA(Multi-head Latent Attention)的做法是:每个 token 只缓存一个低维的 latent 向量,计算时再从它“展开”出各个 head 需要的 K 和 V。为了让位置编码(RoPE)还能正常工作,另外单独缓存一小段带位置信息的 key。
所以 MLA 的缓存是“一个共享 latent + 一段 RoPE key”,不再是“很多个 head 的 K 和 V”。
2.2 算一笔账
以 DeepSeek-V3 的公开配置为例:61 层,latent 维度(kv_lora_rank)512,RoPE key 维度 64,BF16 每个元素 2 字节。
每层每 token = (512 + 64) × 2 B = 1,152 B
每 token(61 层)= 1,152 × 61 = 70,272 B ≈ 68.6 KiB
8K token 前缀 ≈ 68.6 KiB × 8192 ≈ 549 MiB
作为对比,如果同样的 128 个注意力头用普通 MHA 缓存(K 每头 192 维、V 每头 128 维),每层每 token 是 128 × (192 + 128) × 2 B = 81,920 B,约为 MLA 的 71 倍。这正是 MLA 能支持长上下文的关键原因之一。
注意:以上按 BF16 未量化计算;实际部署还可能用 FP8 KV,或有对齐填充。精确数字以所用引擎的 KV 规格为准。
2.3 TP 下的特殊之处
latent 被所有 head 共用,无法像普通 KV head 那样按 head 切给不同 rank。以 vLLM 学习版本为例,使用 MLA 时每个 GPU 的“KV head 数”按 1 处理,也就是每个 TP rank 都保存完整的 latent。
- 容量:增加 TP 不会降低每个 rank 的 KV 占用,这和 GQA 不同。
- 存储:各 rank 的对象内容可能完全相同。理论上可以只存一份、多个 rank 共享读取,节省容量和写流量;是否这样做取决于具体缓存系统的 key 与布局设计,不能默认已经去重。
- 正确性:即使内容相同,布局、dtype 和填充也必须一致才能共享。
3. 滑窗注意力:只看最近一段
3.1 白话原理
滑窗层的每个 token 只关注最近 w 个 token。更早的 KV 对这一层已经没用,可以丢掉。不少模型把滑窗层和全局层交替排列:滑窗层省空间,少量全局层保留长距离信息。
3.2 算一笔账
假设一个 48 层模型中 8 层是全局层、40 层是滑窗层,窗口 w = 1024,前缀长 32K token。以“层 × token”作为 KV 占用的单位:
全部按全局层算:48 × 32,768 = 1,572,864
实际:8 × 32,768 + 40 × 1,024 = 262,144 + 40,960 = 303,104
约为全局版本的 19%
3.3 复用规则不同
要从长度为 p 的缓存前缀继续计算:全局层需要 [0, p) 全部 token 的 KV;滑窗层只需要 [p − w, p) 这一段。因此:
- 滑窗层的早期 chunk 缺失不一定影响复用,只要窗口内的 chunk 都在。
- 全局层仍要求整段连续前缀。
- 最终可跳过的范围,是各类层分别满足条件后的共同部分。LMCache 学习版本在分组 key 中为每个 group 记录了
sliding_window_size,按各自窗口折叠命中结果,正是为了处理这种情况。
4. 混合状态模型:只能存检查点
4.1 白话原理
Mamba/SSM、线性注意力这类层不保存每个 token 的 K、V,而是维护一个固定大小的“状态”,每处理一个 token 就把它更新一次。好处是长度再长,状态大小也不变。
4.2 为什么不能分块拼接
普通 KV 可以按 chunk 存:chunk 0、chunk 1、chunk 2 拼起来就是前缀的 KV。状态层不行:位置 p 的状态已经“吸收”了之前所有 token,无法由几个 chunk 的部分结果拼出来。要从位置 p 继续,必须有恰好在 p 处保存的完整状态快照。
4.3 存储上怎么做
- 在 chunk 边界保存状态快照(检查点),命中只能发生在这些边界上。
- 快照越密,命中越精细,但存储和写入量越大;快照大小和 token 数无关,适合与注意力层的 KV 分开计算预算。
- 混合模型的命中判断要同时满足:注意力层的 KV 前缀完整,并且状态层在同一边界有快照。
5. MoE:专家不改变 KV
MoE 的“专家”在前馈网络(FFN)里,注意力部分不变,所以每 token 的 KV 大小只取决于注意力结构,与专家数量无关。例如 DeepSeek-V3 是 MoE 模型,它的 KV 大小由 MLA 决定。
MoE 改变的是部署方式:常见做法是注意力用数据并行(DP)、专家用专家并行(EP)。注意力 DP 时,每个 DP rank 保存自己负责的请求的完整 KV,不再按 TP 切分。对缓存系统的影响是:
- 请求被路由到哪个 DP rank,决定 KV 在哪里产生、在哪里能命中,缓存感知路由更重要。
- 专家通信(all-to-all)与 KV 传输可能共用网络,需要一起做流量规划。
6. KV 量化:省一半,也多一层格式
把 KV 从 BF16 换成 FP8,元素大小减半,容量、带宽和存储都近似减半。代价有两点:
- 精度:需要用评测集验证输出质量,不能只看速度。
- 格式:FP8 KV 通常伴随缩放因子(按张量、按 head 等不同粒度)。量化方式、缩放粒度和 dtype 都必须进入格式身份;BF16 的缓存不能给 FP8 实例用,反之亦然。
7. 还会改变“相同”的因素
| 因素 | 为什么影响复用 | key 或格式里要体现什么 |
|---|---|---|
| 多模态输入 | 图片、音频常用相同的占位 token 表示,但不同图片产生的 KV 完全不同 | 媒体内容的 hash 必须进入前缀身份。例如 vLLM 学习版本在计算 block hash 时,把多模态内容 hash 作为额外 key 加入;自建缓存必须同样处理 |
| LoRA 适配器 | 同一基座、不同 LoRA,相同 token 的 KV 不同 | 适配器 ID 与版本(vLLM 学习版本同样把 LoRA 名称写进 block hash 的额外 key) |
| 投机解码 | 草稿模型有自己的 KV,与目标模型不能混用 | 按模型分开命名空间 |
| 位置编码配置 | RoPE 缩放等参数变化会改变 K | 模型配置版本 |
| 并行与布局 | TP/PP 改变每个 rank 保存的内容与形状 | rank、layout 描述 |
8. 面试怎么答
问:给你一个新模型,怎样估算 KV 容量并设计缓存?
- 先读模型配置和引擎的 KV 规格,按层列出每层缓存的是 KV、latent、窗口 KV 还是状态。
- 按层分别算容量再求和,标出哪些层随 token 数增长、哪些固定。
- 确认 TP/DP 下每个 rank 保存什么,是否存在复制。
- 分别写出每类层的命中条件,取共同可用范围。
- 把模型版本、dtype、量化方式、布局、适配器和多模态内容纳入身份与格式。
- 用一次正确性实验(对比开关缓存的输出)验证。
常见失分:把 GQA 公式套到 MLA;认为 TP 一定能降低每 rank KV;认为滑窗层缺早期 chunk 就不能复用;认为状态层也能按 chunk 拼接;认为 MoE 的专家数影响 KV 大小。
动手验证:在互动容量计算器里选择“MLA”,层数填 61、维度填 576,可以得到每 token 约 68.6 KiB,并看到 TP 增大时每 rank 占用不变。