如何根据 config.json 估算大模型的注意力缓存

从 config.json 中的 hidden_size、head_dim、KV 头数、sliding_window、kv_lora_rank、Indexer 和 KDA 状态出发,估算常见模型的注意力缓存大小。

Code walkthroughsLLM serving internalsAttentionKV cacheLLM servingconfig-json

大模型推理里经常出现一种简化说法:KV 缓存大小等于“层数 × token 数 × KV 头数 × 单头维度 × 2”。

这只对逐 token 保存键和值的 MHA、GQA、MQA 成立。进入 MLA、DSA、KDA 和 DeepSeek V4 以后,缓存内容已经不一定是传统的 K/V:

text
MHA / GQA / MQA
  每个历史 token 保存 K 和 V

SWA
  仍保存 K 和 V,但只保留最近 W 个 token

MLA
  每个历史 token 保存低维表示 + RoPE 键

DSA
  MLA 缓存 + 用于稀疏选择的索引键缓存

KDA
  每层维护固定大小的递推矩阵状态
  复用前缀时再额外保存检查点

DeepSeek V4
  滑动窗口共享 K=V
  + 4 倍压缩的 CSA 记录
  + 128 倍压缩的 HCA 记录
  + CSA 索引缓存

所以更稳妥的做法不是死记一个 KV 缓存公式,而是先回答:每一层究竟持久化什么状态、状态按多少 token 产生一次、状态使用什么数据类型。

object mapping

不同注意力机制如何表示历史状态

token 历史

同一段已处理前缀

逐 token 缓存

MHA / GQA:K + VMLA:低维表示 + RoPEDSA:MLA + 索引键

有界缓存

SWA:最近 W 个 tokenDeepSeek V4:局部窗口 + 压缩池

递归状态

KDA 递推矩阵ShortConv 状态前缀检查点

先从 config.json 判断缓存布局

拿到一个陌生模型时,先不要直接套“层数 × token 数 × 头数 × 单头维度 × 2”。更可靠的顺序是:

text
config.json
  → 判断层数和注意力类型
  → 确定每个历史位置保存什么对象
  → 确定对象维度、产生频率和保留长度
  → 确认缓存数据类型
  → 再检查模型代码和推理引擎的实际内存布局

最常见的缓存相关字段

字段主要含义对缓存的作用
architectures / model_type选择具体模型实现决定应该阅读哪份 modeling_*.py
num_hidden_layersTransformer 层数同构模型通常作为层数乘数
layer_types / attention_type每层的注意力类型混合模型必须按层型分别计算
hidden_size残差流的宽度是 Q/K/V 投影输入,不一定等于缓存宽度
num_attention_heads查询头数MHA 中相关;GQA/MQA 不能拿它代替 KV 头数
num_key_value_headsKV 头数直接决定传统 MHA/GQA/MQA 的缓存宽度
head_dim单个头的向量维度与 KV 头数一起决定每 token 的 K/V 元素数
max_position_embeddings模型支持的位置范围不是当前请求已经分配的缓存长度
sliding_window局部注意力窗口将有效历史限制为 min(T,W)
q_lora_rankMLA 查询路径的低秩维度通常不进入持久化缓存
kv_lora_rankMLA 的 KV 低维表示维度通常直接进入 MLA 缓存
qk_rope_head_dimMLA 单独保存的 RoPE 键维度通常与 kv_lora_rank 一起缓存
index_head_dim稀疏索引键与查询的维度可能决定索引缓存中每个位置的宽度
index_topk当前查询读取的历史位置数影响稀疏读取,不等于持久化历史长度
indexer_typesIndexer 是独立还是共享决定独立索引缓存或选择器的数量
short_conv_kernelKDA 等层的短卷积核大小通常需要保留 kernel_size - 1 个卷积状态
torch_dtype / dtype模型默认计算或权重类型不一定等于推理引擎的 KV 缓存类型
use_cache是否返回或复用历史状态不是缓存容量参数

hidden_size、头数与 head_dim

token 嵌入和每层残差流的状态通常具有如下形状:

htRdmodel,

其中 dmodel 对应 hidden_size

注意力层会把它投影成 Q、K、V。假设配置为:

json
{
  "hidden_size": 4096,
  "num_attention_heads": 32,
  "num_key_value_heads": 8,
  "head_dim": 128
}

逻辑形状大致是:

text
隐藏状态:[T, 4096]
Query:        [T, 32, 128]
Key:          [T,  8, 128]
Value:        [T,  8, 128]

这是 GQA:32 个查询头共享 8 组 KV 头。每层、每 token 的缓存元素数是:

Hkv(Dk+Dv)=8×(128+128)=2048.

BF16 每元素占 2 bytes,因此是:

2048×2=4096 bytes/layer/token.

经典 MHA 常满足:

dhead=hidden\_sizenum\_attention\_heads,

但这不是现代模型的强制约束。配置若显式提供 head_dimqk_nope_head_dimqk_rope_head_dimv_head_dim,应优先使用显式值;仍不确定时再查看投影矩阵和注意力层前向计算中的张量形状。

MLA 中不要把 q_lora_rank 算进 KV 缓存

例如:

json
{
  "q_lora_rank": 1536,
  "kv_lora_rank": 512,
  "qk_nope_head_dim": 128,
  "qk_rope_head_dim": 64,
  "v_head_dim": 128
}

q_lora_rank=1536 是查询路径的中间低秩表示,通常不会跨解码步持久化。

优化后的 MLA 推理实现通常保存:

text
512 维 KV 低维表示
+ 64 维 RoPE 键

所以每 token、每层缓存的是:

512+64=576

个元素,而不是所有注意力头展开后的完整 K/V。

max_position_embeddingssliding_windowindex_topk

这三个字段描述的是不同概念:

text
max_position_embeddings
  模型支持的位置范围上限

sliding_window
  某类注意力层实际保留的局部历史长度

index_topk
  当前稀疏注意力从完整历史中读取多少位置

因此:

max positionallocated cache length,

并且:

top-k sparse readtop-k persistent history.

配置中出现 sliding_window 也不代表所有层都是 SWA;仍需检查 layer_types 或模型实现中的层型分布。

config.json 给出的是架构意图,不一定是最终物理布局

同一份 MLA 配置可能被两种推理引擎以不同方式实现:

text
优化路径
  保存低维缓存

通用路径
  先展开完整 K/V
  再保存标准 KV 缓存

因此完整阅读顺序是:

text
config.json
→ modeling_*.py 中的 Attention 前向计算
→ 缓存更新调用
→ 运行时缓存张量形状
→ kv_cache_dtype 与量化 scale
→ 分页/分块分配器、分片、复制和 offload

后面的 MHA、SWA、MLA、DSA、KDA、GLM-5.2 与 DeepSeek V4 章节,都是这套读取方法的具体案例。

用一个统一公式覆盖所有模型

设:

  • B:并发序列数;
  • T:每条序列已缓存的 token 数;
  • L:模型层数;
  • S:每个缓存元素占用的字节数;
  • El:第 l 层每个缓存位置保存的元素数;
  • Tl:第 l 层实际保留的缓存位置数;
  • Rl:第 l 层与序列长度无关的递推状态元素数。

总缓存可以写成:

Mtotal=Bl=1LTlElSl+Bl=1LRlSl+Mscale+Mmetadata.

第一项是随 token 数增长的缓存,例如 MHA K/V、MLA 低维表示、DSA 索引键和 DeepSeek V4 压缩记录。

第二项是固定状态,例如 KDA 递推矩阵和 ShortConv 窗口。

最后两项来自量化尺度、块表、页元数据、引用计数、对齐和分配碎片。

这个公式的价值在于:它没有假设所有层都使用同一种 Attention,也没有假设每层一定保存两份 K/V。

数据类型不只决定权重,也决定缓存大小

理论元素占用如下:

数据类型每元素字节数额外成本
FP324通常不需要量化 scale
FP162
BF162
FP8 / INT81scale、可能的 zero point 和对齐
FP4 / INT40.5打包、group scale 和对齐

模型仓库里的 torch_dtypedtype 或权重量化配置,不能单独决定 KV Cache 类型。例如:

text
weights = FP8
KV 缓存 = BF16

以及:

text
weights = BF16
KV 缓存 = FP8 + FP32 scale

都很常见。计算部署显存时要同时确认:

text
model weight dtype
运行时的 kv_cache_dtype
quantization group size
scale dtype
哪些缓存张量被量化

MHA:每个查询头都保存自己的 K/V

MHA 中:

Hkv=Hq.

如果键和值的单头维度都是 Dh

MMHA=BTL×2HqDhS.

因为 HqDh=Dmodel,也可以写成:

MMHA=BTL×2DmodelS.

Llama 2 7B:MHA BF16

使用下面这组常见参数:

text
layers              = 32
query heads         = 32
kv heads            = 32
head dim            = 128
缓存数据类型        = BF16

每层、每 token 保存:

2×32×128=8192

个 BF16 元素,也就是 16 KiB。

全部 32 层:

32×16 KiB=512 KiB/token.

因此 batch size 为 1 时:

上下文长度MHA BF16 Cache
4K2 GiB
32K16 GiB
128K64 GiB
1M512 GiB

这里 32K、128K 和 1M 只是展示缩放规律,不代表 Llama 2 7B 原生支持这些长度。

MHA 最关键的增长关系是:

MMHAT×L×Dmodel.

GQA 与 MQA:真正要看的是 KV 头数

GQA 让多个 Query Heads 共享一组 KV Heads:

Hkv<Hq.

公式仍然是:

MGQA=BTL×2HkvDhS.

相对于相同 Query Head 数的 MHA:

MGQAMMHA=HkvHq.

MQA 是极端情况:

Hkv=1.

因此读取模型配置时,计算缓存应优先找 num_key_value_heads,而不是只看 num_attention_heads

SWA:缓存表示不变,只限制保留长度

Sliding-Window Attention 通常仍使用 MHA 或 GQA 格式保存 K/V,但只保留最近 W 个 token:

Teffective=min(T,W).

所以:

MSWA=Bmin(T,W)L×2HkvDhS.

如果还保留 attention sink 或全局 token,可把它们额外加到有效长度里。

Mistral 7B:GQA + 4096-token SWA

公开配置中的核心参数是:

text
layers              = 32
query heads         = 32
kv heads            = 8
head dim            = 128
sliding window      = 4096
缓存数据类型        = BF16

每 token、全部 32 层:

32×2×8×128×2=131072 bytes=128 KiB.

窗口填满后:

4096×128 KiB=512 MiB.

因此:

实际上下文长度有效缓存长度Cache
2K2K0.25 GiB
4K4K0.50 GiB
32K4K0.50 GiB
128K4K0.50 GiB

SWA 用固定的可见历史换取有界缓存:

MSWAmin(T,W).

如果模型是 Full Attention 与 SWA 混合层,必须分层相加,不能把总层数统一乘同一个窗口。

MLA:缓存的是低维表示,而不是展开后的所有注意力头

MLA 将每个 token 的 K/V 历史压缩为:

ctKVRdc,

并在 decoupled RoPE 路径中额外保存:

ktRRdr.

优化后的 latent 缓存为:

MMLA=BTL(dc+dr)S.

其中:

text
compression dimension d_c  -> kv_lora_rank
RoPE key dimension d_r     -> qk_rope_head_dim

q_lora_rank 属于当前查询向量的计算路径,不是需要跨解码步骤保存的 KV 缓存。

DeepSeek V3:512 维低维表示 + 64 维 RoPE

DeepSeek V3 的相关参数为:

text
layers                 = 61
attention heads        = 128
kv_lora_rank           = 512
qk_nope_head_dim       = 128
qk_rope_head_dim       = 64
v_head_dim             = 128
缓存数据类型           = BF16(本节假设)

每层、每 token 保存:

512+64=576

个元素。

全部 61 层:

61×576×2=70272 bytes/token=68.625 KiB/token.

对应:

上下文长度BF16 MLA CacheFP8 数据主体
32K2.14 GiB约 1.07 GiB
128K8.58 GiB约 4.29 GiB
1M68.63 GiB约 34.31 GiB

FP8 一列没有加入 scale 和对齐成本。

为什么不能直接按 128 个注意力头计算

如果把 DeepSeek V3 展开成传统 K/V,每层、每 token 的元素数将近似为:

128×[(128+64)+128]=40960.

优化后的 latent 缓存只有:

512+64=576.

两者的元素数量比约为:

576409601.41%.

但只有推理引擎真正保存 latent 缓存时,这个压缩才会兑现。通用实现如果先恢复完整 K/V,再把展开后的张量放进标准 KV Cache,实际显存会重新接近传统 Attention。

DSA:Top-k 减少计算,不会自动删除历史缓存

DeepSeek Sparse Attention 可以理解为:

text
MLA 历史缓存
  + 轻量级 Indexer key 缓存
  + top-k gather / sparse attention workspace

持久化部分近似为:

MDSA=BTL(dc+dr)SMLA+BTLIdISI.

其中:

  • LI:拥有独立 Indexer 缓存的层数;
  • dI:每个历史 token 的索引键维度;
  • SI:索引键的数据类型字节数。

index_topk=2048 影响的是每次主 Attention 实际 gather 多少个位置,不代表持久化缓存只保留 2048 个 token。下一条 Query 可能选中任何历史位置,因此完整 MLA history 和 index history 通常仍随 T 线性增长。

DeepSeek V3.2:MLA + 每层索引键

用下面的公开参数做 BF16 上界估算:

text
layers                 = 61
kv_lora_rank           = 512
qk_rope_head_dim       = 64
index_head_dim         = 128
index_topk             = 2048
MLA 缓存数据类型       = BF16
索引缓存数据类型       = BF16

每 token、全部层:

61×(512+64+128)×2=85888 bytes=83.875 KiB.

对应:

上下文长度BF16 MLA + Index Cache
128K10.48 GiB
1M83.88 GiB

如果索引键使用 FP8,数据主体会再减少约 0.95 GiB(128K)或 7.63 GiB(1M),但还要计入量化因子。

GLM-5.2:IndexShare 共享的是稀疏选择,不是 MLA 主缓存

GLM-5.2 的公开配置给出了:

text
layers                    = 78
kv_lora_rank              = 512
qk_rope_head_dim          = 64
index_head_dim            = 128
index_n_heads             = 32
index_topk                = 2048
max_position_embeddings   = 1,048,576
缓存/模型数据类型         = BF16(公开配置)
indexer_types             = 21 full + 57 shared

GLM-5.2 的 IndexShare 让每四个稀疏 Attention 层共用一次 Indexer 的 top-k 结果。公开 indexer_types 中只有 21 层为 full,另外 57 层复用前一个 full 层的结果。

MLA 主缓存

无论 Indexer 是否共享,78 层主 Attention 仍各自拥有 MLA 历史:

MGLM-MLA=BT×78×(512+64)×2.

每 token:

78×576×2=89856 bytes=87.75 KiB.

因此:

上下文长度GLM-5.2 MLA 主缓存
128K10.97 GiB
1M87.75 GiB

IndexShare 对索引缓存的影响

如果 78 层各自保存 128 维 BF16 索引键:

MI,independent=BT×78×128×2.

1M 上下文是:

19.5 GiB.

按公开配置中的 21 个 full Indexer 计算共享 Index Cache:

MI,shared=BT×21×128×2.

1M 上下文是:

5.25 GiB.

于是 BF16 理论主缓存为:

上下文长度MLAShared Index合计
128K10.97 GiB0.66 GiB11.63 GiB
1M87.75 GiB5.25 GiB93.00 GiB

这个计算揭示了 IndexShare 的边界:

  • 它把 Indexer dot-product 与 top-k 工作显著减少,官方给出的 1M 场景 per-token FLOPs 降幅为 2.9 倍;
  • 它也允许共享 Indexer 对应的 index history;
  • 但每个 Transformer 层自己的 MLA latent 并没有因此共享,所以 87.75 GiB 的主缓存仍然占大头。

实际的 vLLM、SGLang 或专用内核可能使用 FP8 缓存、上下文并行、卸载或不同的索引缓存布局,因此 93 GiB 只是一份单序列、单副本的 BF16 主张量估算,并不代表某个生产环境的固定显存占用。

layer view

GLM-5.2 的 IndexShare 不等于 KV Share

Layer 2: full indexer

生成 top-k,并拥有这一组 index history

第 2 层 MLA 缓存A 组索引键A 组 top-k

Layers 3-5: shared

复用 top-k A,但各层仍读取自己的 MLA history

第 3 层 MLA 缓存第 4 层 MLA 缓存第 5 层 MLA 缓存

Layer 6: next full indexer

建立下一组共享选择

第 6 层 MLA 缓存B 组索引键B 组 top-k

KDA:在线状态不随上下文长度线性增长

Kimi Delta Attention 为每层、每个 Head 维护递推状态矩阵:

StRdk×dv.

如果每个 KDA Head 都有独立状态:

MKDA-live=BLKDAHKDAdkdvSstate.

这个公式没有 T,因此活跃递推状态相对于历史长度是 O(1)

不过 KDA 还可能需要:

  • Q/K/V ShortConv 的最近 kernel_size - 1 个输入;
  • 归一化状态或门控状态;
  • 分块预填充过程中的临时 WY/UT 工作区;
  • 前缀缓存需要保留的历史检查点。

Kimi Linear 48B-A3B:20 层 KDA + 7 层 MLA

公开配置可以概括为:

text
model layers          = 27
KDA layers            = 20
full MLA layers       = 7
KDA heads             = 32
KDA key dim           = 128
KDA value dim         = 128
ShortConv kernel      = 4
MLA kv_lora_rank      = 512
MLA RoPE              = NoPE

每个 KDA 层的矩阵状态:

32×128×128=524288

个元素。

20 层合计:

状态数据类型20 层递推状态
BF1620 MiB
FP3240 MiB

Q/K/V 三路 ShortConv 若保留此前 3 个位置,BF16 估算为:

20×3×32×128×3×21.41 MiB.

七层 NoPE MLA 每 token 保存:

7×512×2=7168 bytes=7 KiB/token.

于是:

上下文长度7 层 MLABF16 KDA + Conv合计
128K0.875 GiB约 0.021 GiB约 0.90 GiB
1M7.00 GiB约 0.021 GiB约 7.02 GiB

Kimi Linear 不是“整个模型都变成常数缓存”。准确说法是:20 个 KDA 层变成固定状态,只有 7 个全局 MLA 层继续按 token 线性增长。

前缀缓存会重新引入检查点数量

在线请求只需要最新的 St。但为了在任意前缀边界恢复,系统要保存:

St1,St2,,Stn.

如果每隔 C 个 token 保存一份:

NcheckpointTC.

持久化检查点的大小为:

Mcheckpoint=NcheckpointLKDAHKDAdkdvSstate.

因此要区分:

text
活跃递推状态
  一条运行中请求只有最新状态

前缀检查点存储
  为多个可复用边界保存多份完整状态

KDA 的活跃状态是 O(1),不代表支持细粒度前缀复用的持久化存储也是 O(1)

DeepSeek V4:它已经不再使用 MLA 缓存布局

DeepSeek V4 用新的 hybrid attention 替代 V3 的 MLA。根据 DeepSeek V4 论文Transformers 架构文档和公开配置,每个 Attention 层可能属于:

text
SWA
  只保留最近 128 条原始记录

CSA: Compressed Sparse Attention
  每 4 个 token 产生一条压缩记录
  Lightning Indexer 从压缩历史中选择 top-k
  同时保留 128-token 的局部分支

HCA: Heavily Compressed Attention
  每 128 个 token 产生一条压缩记录
  对全部压缩记录执行稠密注意力计算
  同时保留 128-token 的局部分支

它还有一个容易算错的关键点:

DeepSeek V4 使用共享 K=V 的 MQA。kv_proj 只生成一条共享向量,同一个张量同时作为键和值使用,因此不能再机械地乘传统 K/V 的 2。

一条压缩记录占多少字节

DeepSeek V4-Pro 的公开参数包括:

text
layers                 = 61
attention heads        = 128
kv heads               = 1
head dim               = 512
RoPE dim               = 64
sliding window         = 128
CSA compression        = 4
HCA compression        = 128
CSA index dim          = 128
CSA index top-k        = 1024

论文与参考实现使用混合精度 KV 存储:

  • 64 维 RoPE 部分使用 BF16;
  • 其余 448 维使用 FP8;
  • CSA 索引键使用 FP4;
  • scale 和压缩器状态另算。

因为 K=V 共享,一条 compressed KV entry 的数据主体为:

EV4-entry=64×2+448×1=576 bytes.

一条 128 维 FP4 索引键的数据主体为:

128×0.5=64 bytes.

V4-Pro 的 61 层怎么分布

公开 compress_ratios 与论文给出的层型分布对应:

text
CSA layers        = 30   compression ratio 4
HCA layers        = 31   compression ratio 128
本地窗口          = 全部 61 层,每层 128 个 token

暂不考虑压缩器重叠缓冲区、门控状态、量化因子、MTP 缓存和分配元数据,主持久化张量可以近似为:

MV4-Pro61×128×576+30T4×576+31T128×576+30T4×64.

四项分别是:

  1. 所有层的本地 SWA 共享 K=V;
  2. 30 层 CSA compressed K=V;
  3. 31 层 HCA compressed K=V;
  4. 30 层 CSA FP4 索引键。

100 万 token 上下文的分项计算

T=1,048,576

缓存对象估算大小
61 层、每层 128-token 本地窗口0.004 GiB
30 层 4x CSA 池4.219 GiB
31 层 128x HCA 池0.136 GiB
30 层 FP4 CSA 索引池0.469 GiB
主 Tensor 合计约 4.83 GiB

128K 时同一公式约为 0.61 GiB。

这个结果是主持久化张量的下界估算,不是某个推理引擎的缓存池预留量。实际还要加入:

  • FP8/FP4 scale;
  • CSA 重叠压缩缓冲区;
  • HCA 压缩缓冲区;
  • 压缩器的 gate/score 状态;
  • entry count 和索引;
  • 页对齐;
  • MTP 层状态;
  • 推理引擎为 CUDA Graph 或最大批次预留的空间。

DeepSeek 官方报告给出的整体结果是:在 1M context 下,V4-Pro 的 KV Cache 约为 V3.2 的 10%,单 token 推理 FLOPs 约为 V3.2 的 27%。这里的 4.83 GiB 估算解释了压缩来自哪里,但不应替代官方生产布局测量。

V4-Flash

V4-Flash 的公开参数为:

text
layers                 = 43
pure SWA bootstrap     = 2 layers
CSA                    = 21 layers
HCA                    = 20 layers
CSA top-k              = 512
head dim / RoPE dim    = 512 / 64

使用同一混合精度和 shared K=V 假设,1M context 的主 Tensor 下界约为:

3.37 GiB.

V4 的重点不是“把 MLA 再压小一点”,而是把一个统一的线性增长历史缓存拆成多种生成频率和生命周期不同的对象。

layer view

DeepSeek V4 的异构 Cache

本地 SWA

所有相关层只保留最近 128 条共享 K=V 记录

按 token 生成固定保留窗口BF16 RoPE + FP8 内容

CSA 池

每 4 个 token 产生一条压缩记录

top-k 1024(Pro)FP4 索引键长距离稀疏读取

HCA 池

每 128 个 token 产生一条压缩记录

无需 Indexer对粗粒度历史执行稠密注意力生成频率很低

把常见模型放到同一张表里

下面统一假设:

text
批大小 = 1
单个逻辑缓存副本
无张量并行或上下文并行分片
无前缀共享
无块大小取整
无分配器预留
模型 / 架构主要缓存表示数据类型假设128K1M
Llama 2 7B MHA32 层完整 K/VBF1664 GiB512 GiB
Mistral 7B GQA + SWA8 KV Heads,窗口 4096BF160.50 GiB0.50 GiB
DeepSeek V3 MLA512 latent + 64 RoPEBF168.58 GiB68.63 GiB
DeepSeek V3.2 DSAMLA + 每层 128-d indexBF1610.48 GiB83.88 GiB
GLM-5.2 DSA + IndexShare78 层 MLA + 21 组 indexBF1611.63 GiB93.00 GiB
Kimi Linear7 层 MLA + 20 层 KDABF16约 0.90 GiB约 7.02 GiB
DeepSeek V4-ProSWA + CSA + HCA + indexBF16/FP8/FP4≥0.61 GiB≥4.83 GiB

表里的数字不能直接用来比较模型优劣,因为模型宽度、层数、训练目标和能力不同。它们展示的是历史状态表示的缩放规律:

架构Cache 随历史长度的增长
MHA / GQA / MLAO(T)
DSA持久化 history 通常仍是 O(T)
SWAO(min(T,W))
纯 KDA 活跃状态O(1)
KDA + Full Attention HybridO(TLfull)+O(LKDA)
DeepSeek V4 CSA/HCAO(T/4)+O(T/128)+O(W)
KDA 前缀检查点O(Ncheckpoint)

分页 KV 缓存要按实际分配位置取整

理论公式使用真实 token 数 T。vLLM、SGLang 等推理引擎通常按固定页或块分配空间。

设 block size 为 P

Tallocated=TPP.

例如:

text
block size = 64
request length = 6976

因为:

6976/64=109,

它恰好使用 109 个 block。

如果长度变成 6977:

Tallocated=110×64=7040.

最后一个 block 只写了一个 token,但物理空间通常已经按整块占用。

多请求时应分别取整:

Tallocated,total=iTiPP,

而不是先把所有请求 token 数加起来再取整。

对于 DeepSeek V4、KDA 等混合模型,不同缓存组还可能采用不同的物理块大小、匹配粒度和检查点边界,不能把它们强行塞进同一组传统 KV 块。

张量并行下不能总是简单除以 TP

如果 KV Heads 能在 TP 张卡间均匀切分:

Mper-GPUMglobalTP.

但下面这些情况可能导致复制:

  • MQA 只有一个 KV Head;
  • KV Head 数小于 TP size;
  • RoPE 缓存在各 rank 间复制;
  • MLA latent 选择 replicated 而不是 sharded;
  • KDA 递推状态按 Head、隐藏维度或 CP 分组采用不同布局;
  • DSA Indexer 缓存与主 MLA 缓存的并行策略不同;
  • DeepSeek V4 的本地缓存、CSA、HCA 和 Indexer 池分别采用不同的放置策略。

所以集群总 Cache、单 GPU Cache 和逻辑序列 Cache 是三个不同数字。

一个覆盖常见架构的 Python 计算器

python
from __future__ import annotations

from math import ceil


GIB = 1024**3


def gib(num_bytes: float) -> float:
    return num_bytes / GIB


def token_kv_cache_bytes(
    *,
    batch_size: int,
    tokens: int,
    layers: int,
    kv_heads: int,
    key_head_dim: int,
    value_head_dim: int,
    dtype_bytes: float,
    window_size: int | None = None,
) -> float:
    """MHA、GQA、MQA 与 SWA。"""
    effective_tokens = min(tokens, window_size) if window_size else tokens
    return (
        batch_size
        * effective_tokens
        * layers
        * kv_heads
        * (key_head_dim + value_head_dim)
        * dtype_bytes
    )


def mla_cache_bytes(
    *,
    batch_size: int,
    tokens: int,
    layers: int,
    kv_lora_rank: int,
    rope_key_dim: int,
    dtype_bytes: float,
) -> float:
    """优化 latent 缓存路径下的 MLA。"""
    return (
        batch_size
        * tokens
        * layers
        * (kv_lora_rank + rope_key_dim)
        * dtype_bytes
    )


def dsa_index_cache_bytes(
    *,
    batch_size: int,
    tokens: int,
    independent_indexers: int,
    index_key_dim: int,
    dtype_bytes: float,
) -> float:
    """DSA 的持久化 Index Key Cache。"""
    return (
        batch_size
        * tokens
        * independent_indexers
        * index_key_dim
        * dtype_bytes
    )


def kda_state_bytes(
    *,
    batch_size: int,
    layers: int,
    heads: int,
    key_dim: int,
    value_dim: int,
    state_dtype_bytes: float,
) -> float:
    """KDA 在线递推矩阵状态。"""
    return (
        batch_size
        * layers
        * heads
        * key_dim
        * value_dim
        * state_dtype_bytes
    )


def deepseek_v4_cache_bytes(
    *,
    batch_size: int,
    tokens: int,
    total_layers: int,
    csa_layers: int,
    hca_layers: int,
    window_size: int,
    head_dim: int,
    rope_dim: int,
    csa_ratio: int,
    hca_ratio: int,
    index_dim: int,
    content_dtype_bytes: float = 1,
    rope_dtype_bytes: float = 2,
    index_dtype_bytes: float = 0.5,
) -> float:
    """DeepSeek V4 主持久化 Tensor 下界,不含 scale/buffer/metadata。"""
    # Shared K=V: 只保存一条向量,不乘 2。
    entry_bytes = (
        rope_dim * rope_dtype_bytes
        + (head_dim - rope_dim) * content_dtype_bytes
    )

    local = (
        batch_size
        * total_layers
        * min(tokens, window_size)
        * entry_bytes
    )
    csa = (
        batch_size
        * csa_layers
        * ceil(tokens / csa_ratio)
        * entry_bytes
    )
    hca = (
        batch_size
        * hca_layers
        * ceil(tokens / hca_ratio)
        * entry_bytes
    )
    index = (
        batch_size
        * csa_layers
        * ceil(tokens / csa_ratio)
        * index_dim
        * index_dtype_bytes
    )
    return local + csa + hca + index


TOKENS_1M = 1_048_576

# GLM-5.2: BF16 MLA + 21 shared Indexer groups
# 21 来自公开 config 中 21 个 "full" + 57 个 "shared"。
glm_mla = mla_cache_bytes(
    batch_size=1,
    tokens=TOKENS_1M,
    layers=78,
    kv_lora_rank=512,
    rope_key_dim=64,
    dtype_bytes=2,
)
glm_index = dsa_index_cache_bytes(
    batch_size=1,
    tokens=TOKENS_1M,
    independent_indexers=21,
    index_key_dim=128,
    dtype_bytes=2,
)
print(f"GLM-5.2 1M: {gib(glm_mla + glm_index):.2f} GiB")

# DeepSeek V4-Pro: shared K=V + BF16 RoPE + FP8 content + FP4 index
dsv4 = deepseek_v4_cache_bytes(
    batch_size=1,
    tokens=TOKENS_1M,
    total_layers=61,
    csa_layers=30,
    hca_layers=31,
    window_size=128,
    head_dim=512,
    rope_dim=64,
    csa_ratio=4,
    hca_ratio=128,
    index_dim=128,
)
print(f"DeepSeek V4-Pro 1M lower bound: {gib(dsv4):.2f} GiB")

真正决定缓存大小的是内存布局,不是模型名

计算一个新模型时,可以按下面的顺序阅读配置和实现:

text
1. layer_types
   每层是 full、sliding、MLA、DSA、KDA、CSA 还是 HCA?

2. 持久化对象
   保存的是 K/V、latent、RoPE key、Indexer key、压缩记录还是递推状态?

3. 生成频率
   每个 token 一条、每 4 个 token 一条、每 128 个 token 一条,还是只保留最新状态?

4. 保留范围
   完整历史、滑动窗口、top-k 聚合,还是一组检查点?

5. 各对象的数据类型
   BF16、FP8、FP4,以及 scale 的粒度分别是什么?

6. 运行时布局
   latent 是否展开、K/V 是否共享、哪些对象需要分片、复制或 offload?

7. 内存分配器
   块大小、页面取整、前缀共享、CoW 和预留池分别如何设置?

最容易犯的错误也正好对应这些步骤:

  • 用 Query Head 数代替 KV Head 数;
  • q_lora_rank 算进 MLA Cache;
  • 认为 DSA 的 top-k 会让持久化历史只剩 top-k;
  • 认为 KDA 的 O(1) 活跃状态意味着前缀检查点也是 O(1)
  • 对 shared K=V 的 DeepSeek V4 仍然乘传统 K/V 的 2;
  • 从权重 FP8 推断 Cache 一定是 FP8;
  • 忽略推理引擎把 latent 展开成标准 K/V 的实现差异。

最后可以把任何 hybrid model 写回最开始的统一形式:

Mtotal=BlTlElSl+BlRlSl+Mscale+Mmetadata.

只要能从模型配置和推理引擎代码中确定每层的 TlElRl 与实际数据类型,就能计算 MHA、SWA、MLA、DSA、KDA、DeepSeek V4,以及未来更复杂的混合注意力缓存。