如何根据 config.json 估算大模型的注意力缓存
从 config.json 中的 hidden_size、head_dim、KV 头数、sliding_window、kv_lora_rank、Indexer 和 KDA 状态出发,估算常见模型的注意力缓存大小。
大模型推理里经常出现一种简化说法:KV 缓存大小等于“层数 × token 数 × KV 头数 × 单头维度 × 2”。
这只对逐 token 保存键和值的 MHA、GQA、MQA 成立。进入 MLA、DSA、KDA 和 DeepSeek V4 以后,缓存内容已经不一定是传统的 K/V:
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 缓存
有界缓存
递归状态
先从 config.json 判断缓存布局
拿到一个陌生模型时,先不要直接套“层数 × token 数 × 头数 × 单头维度 × 2”。更可靠的顺序是:
config.json
→ 判断层数和注意力类型
→ 确定每个历史位置保存什么对象
→ 确定对象维度、产生频率和保留长度
→ 确认缓存数据类型
→ 再检查模型代码和推理引擎的实际内存布局
最常见的缓存相关字段
| 字段 | 主要含义 | 对缓存的作用 |
|---|---|---|
architectures / model_type | 选择具体模型实现 | 决定应该阅读哪份 modeling_*.py |
num_hidden_layers | Transformer 层数 | 同构模型通常作为层数乘数 |
layer_types / attention_type | 每层的注意力类型 | 混合模型必须按层型分别计算 |
hidden_size | 残差流的宽度 | 是 Q/K/V 投影输入,不一定等于缓存宽度 |
num_attention_heads | 查询头数 | MHA 中相关;GQA/MQA 不能拿它代替 KV 头数 |
num_key_value_heads | KV 头数 | 直接决定传统 MHA/GQA/MQA 的缓存宽度 |
head_dim | 单个头的向量维度 | 与 KV 头数一起决定每 token 的 K/V 元素数 |
max_position_embeddings | 模型支持的位置范围 | 不是当前请求已经分配的缓存长度 |
sliding_window | 局部注意力窗口 | 将有效历史限制为 |
q_lora_rank | MLA 查询路径的低秩维度 | 通常不进入持久化缓存 |
kv_lora_rank | MLA 的 KV 低维表示维度 | 通常直接进入 MLA 缓存 |
qk_rope_head_dim | MLA 单独保存的 RoPE 键维度 | 通常与 kv_lora_rank 一起缓存 |
index_head_dim | 稀疏索引键与查询的维度 | 可能决定索引缓存中每个位置的宽度 |
index_topk | 当前查询读取的历史位置数 | 影响稀疏读取,不等于持久化历史长度 |
indexer_types | Indexer 是独立还是共享 | 决定独立索引缓存或选择器的数量 |
short_conv_kernel | KDA 等层的短卷积核大小 | 通常需要保留 kernel_size - 1 个卷积状态 |
torch_dtype / dtype | 模型默认计算或权重类型 | 不一定等于推理引擎的 KV 缓存类型 |
use_cache | 是否返回或复用历史状态 | 不是缓存容量参数 |
hidden_size、头数与 head_dim
token 嵌入和每层残差流的状态通常具有如下形状:
其中 hidden_size。
注意力层会把它投影成 Q、K、V。假设配置为:
{
"hidden_size": 4096,
"num_attention_heads": 32,
"num_key_value_heads": 8,
"head_dim": 128
}
逻辑形状大致是:
隐藏状态:[T, 4096]
Query: [T, 32, 128]
Key: [T, 8, 128]
Value: [T, 8, 128]
这是 GQA:32 个查询头共享 8 组 KV 头。每层、每 token 的缓存元素数是:
BF16 每元素占 2 bytes,因此是:
经典 MHA 常满足:
但这不是现代模型的强制约束。配置若显式提供 head_dim、qk_nope_head_dim、qk_rope_head_dim 或 v_head_dim,应优先使用显式值;仍不确定时再查看投影矩阵和注意力层前向计算中的张量形状。
MLA 中不要把 q_lora_rank 算进 KV 缓存
例如:
{
"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 推理实现通常保存:
512 维 KV 低维表示
+ 64 维 RoPE 键
所以每 token、每层缓存的是:
个元素,而不是所有注意力头展开后的完整 K/V。
max_position_embeddings、sliding_window 与 index_topk
这三个字段描述的是不同概念:
max_position_embeddings
模型支持的位置范围上限
sliding_window
某类注意力层实际保留的局部历史长度
index_topk
当前稀疏注意力从完整历史中读取多少位置
因此:
并且:
配置中出现 sliding_window 也不代表所有层都是 SWA;仍需检查 layer_types 或模型实现中的层型分布。
config.json 给出的是架构意图,不一定是最终物理布局
同一份 MLA 配置可能被两种推理引擎以不同方式实现:
优化路径
保存低维缓存
通用路径
先展开完整 K/V
再保存标准 KV 缓存
因此完整阅读顺序是:
config.json
→ modeling_*.py 中的 Attention 前向计算
→ 缓存更新调用
→ 运行时缓存张量形状
→ kv_cache_dtype 与量化 scale
→ 分页/分块分配器、分片、复制和 offload
后面的 MHA、SWA、MLA、DSA、KDA、GLM-5.2 与 DeepSeek V4 章节,都是这套读取方法的具体案例。
用一个统一公式覆盖所有模型
设:
:并发序列数; :每条序列已缓存的 token 数; :模型层数; :每个缓存元素占用的字节数; :第 层每个缓存位置保存的元素数; :第 层实际保留的缓存位置数; :第 层与序列长度无关的递推状态元素数。
总缓存可以写成:
第一项是随 token 数增长的缓存,例如 MHA K/V、MLA 低维表示、DSA 索引键和 DeepSeek V4 压缩记录。
第二项是固定状态,例如 KDA 递推矩阵和 ShortConv 窗口。
最后两项来自量化尺度、块表、页元数据、引用计数、对齐和分配碎片。
这个公式的价值在于:它没有假设所有层都使用同一种 Attention,也没有假设每层一定保存两份 K/V。
数据类型不只决定权重,也决定缓存大小
理论元素占用如下:
| 数据类型 | 每元素字节数 | 额外成本 |
|---|---|---|
| FP32 | 4 | 通常不需要量化 scale |
| FP16 | 2 | 无 |
| BF16 | 2 | 无 |
| FP8 / INT8 | 1 | scale、可能的 zero point 和对齐 |
| FP4 / INT4 | 0.5 | 打包、group scale 和对齐 |
模型仓库里的 torch_dtype、dtype 或权重量化配置,不能单独决定 KV Cache 类型。例如:
weights = FP8
KV 缓存 = BF16
以及:
weights = BF16
KV 缓存 = FP8 + FP32 scale
都很常见。计算部署显存时要同时确认:
model weight dtype
运行时的 kv_cache_dtype
quantization group size
scale dtype
哪些缓存张量被量化
MHA:每个查询头都保存自己的 K/V
MHA 中:
如果键和值的单头维度都是
因为
Llama 2 7B:MHA BF16
使用下面这组常见参数:
layers = 32
query heads = 32
kv heads = 32
head dim = 128
缓存数据类型 = BF16
每层、每 token 保存:
个 BF16 元素,也就是 16 KiB。
全部 32 层:
因此 batch size 为 1 时:
| 上下文长度 | MHA BF16 Cache |
|---|---|
| 4K | 2 GiB |
| 32K | 16 GiB |
| 128K | 64 GiB |
| 1M | 512 GiB |
这里 32K、128K 和 1M 只是展示缩放规律,不代表 Llama 2 7B 原生支持这些长度。
MHA 最关键的增长关系是:
GQA 与 MQA:真正要看的是 KV 头数
GQA 让多个 Query Heads 共享一组 KV Heads:
公式仍然是:
相对于相同 Query Head 数的 MHA:
MQA 是极端情况:
因此读取模型配置时,计算缓存应优先找 num_key_value_heads,而不是只看 num_attention_heads。
SWA:缓存表示不变,只限制保留长度
Sliding-Window Attention 通常仍使用 MHA 或 GQA 格式保存 K/V,但只保留最近
所以:
如果还保留 attention sink 或全局 token,可把它们额外加到有效长度里。
Mistral 7B:GQA + 4096-token SWA
公开配置中的核心参数是:
layers = 32
query heads = 32
kv heads = 8
head dim = 128
sliding window = 4096
缓存数据类型 = BF16
每 token、全部 32 层:
窗口填满后:
因此:
| 实际上下文长度 | 有效缓存长度 | Cache |
|---|---|---|
| 2K | 2K | 0.25 GiB |
| 4K | 4K | 0.50 GiB |
| 32K | 4K | 0.50 GiB |
| 128K | 4K | 0.50 GiB |
SWA 用固定的可见历史换取有界缓存:
如果模型是 Full Attention 与 SWA 混合层,必须分层相加,不能把总层数统一乘同一个窗口。
MLA:缓存的是低维表示,而不是展开后的所有注意力头
MLA 将每个 token 的 K/V 历史压缩为:
并在 decoupled RoPE 路径中额外保存:
优化后的 latent 缓存为:
其中:
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 的相关参数为:
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 保存:
个元素。
全部 61 层:
对应:
| 上下文长度 | BF16 MLA Cache | FP8 数据主体 |
|---|---|---|
| 32K | 2.14 GiB | 约 1.07 GiB |
| 128K | 8.58 GiB | 约 4.29 GiB |
| 1M | 68.63 GiB | 约 34.31 GiB |
FP8 一列没有加入 scale 和对齐成本。
为什么不能直接按 128 个注意力头计算
如果把 DeepSeek V3 展开成传统 K/V,每层、每 token 的元素数将近似为:
优化后的 latent 缓存只有:
两者的元素数量比约为:
但只有推理引擎真正保存 latent 缓存时,这个压缩才会兑现。通用实现如果先恢复完整 K/V,再把展开后的张量放进标准 KV Cache,实际显存会重新接近传统 Attention。
DSA:Top-k 减少计算,不会自动删除历史缓存
DeepSeek Sparse Attention 可以理解为:
MLA 历史缓存
+ 轻量级 Indexer key 缓存
+ top-k gather / sparse attention workspace
持久化部分近似为:
其中:
:拥有独立 Indexer 缓存的层数; :每个历史 token 的索引键维度; :索引键的数据类型字节数。
index_topk=2048 影响的是每次主 Attention 实际 gather 多少个位置,不代表持久化缓存只保留 2048 个 token。下一条 Query 可能选中任何历史位置,因此完整 MLA history 和 index history 通常仍随
DeepSeek V3.2:MLA + 每层索引键
用下面的公开参数做 BF16 上界估算:
layers = 61
kv_lora_rank = 512
qk_rope_head_dim = 64
index_head_dim = 128
index_topk = 2048
MLA 缓存数据类型 = BF16
索引缓存数据类型 = BF16
每 token、全部层:
对应:
| 上下文长度 | BF16 MLA + Index Cache |
|---|---|
| 128K | 10.48 GiB |
| 1M | 83.88 GiB |
如果索引键使用 FP8,数据主体会再减少约 0.95 GiB(128K)或 7.63 GiB(1M),但还要计入量化因子。
GLM-5.2:IndexShare 共享的是稀疏选择,不是 MLA 主缓存
GLM-5.2 的公开配置给出了:
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 历史:
每 token:
因此:
| 上下文长度 | GLM-5.2 MLA 主缓存 |
|---|---|
| 128K | 10.97 GiB |
| 1M | 87.75 GiB |
IndexShare 对索引缓存的影响
如果 78 层各自保存 128 维 BF16 索引键:
1M 上下文是:
按公开配置中的 21 个 full Indexer 计算共享 Index Cache:
1M 上下文是:
于是 BF16 理论主缓存为:
| 上下文长度 | MLA | Shared Index | 合计 |
|---|---|---|---|
| 128K | 10.97 GiB | 0.66 GiB | 11.63 GiB |
| 1M | 87.75 GiB | 5.25 GiB | 93.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
Layers 3-5: shared
复用 top-k A,但各层仍读取自己的 MLA history
Layer 6: next full indexer
建立下一组共享选择
KDA:在线状态不随上下文长度线性增长
Kimi Delta Attention 为每层、每个 Head 维护递推状态矩阵:
如果每个 KDA Head 都有独立状态:
这个公式没有
不过 KDA 还可能需要:
- Q/K/V ShortConv 的最近
kernel_size - 1个输入; - 归一化状态或门控状态;
- 分块预填充过程中的临时 WY/UT 工作区;
- 前缀缓存需要保留的历史检查点。
Kimi Linear 48B-A3B:20 层 KDA + 7 层 MLA
公开配置可以概括为:
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 层的矩阵状态:
个元素。
20 层合计:
| 状态数据类型 | 20 层递推状态 |
|---|---|
| BF16 | 20 MiB |
| FP32 | 40 MiB |
Q/K/V 三路 ShortConv 若保留此前 3 个位置,BF16 估算为:
七层 NoPE MLA 每 token 保存:
于是:
| 上下文长度 | 7 层 MLA | BF16 KDA + Conv | 合计 |
|---|---|---|---|
| 128K | 0.875 GiB | 约 0.021 GiB | 约 0.90 GiB |
| 1M | 7.00 GiB | 约 0.021 GiB | 约 7.02 GiB |
Kimi Linear 不是“整个模型都变成常数缓存”。准确说法是:20 个 KDA 层变成固定状态,只有 7 个全局 MLA 层继续按 token 线性增长。
前缀缓存会重新引入检查点数量
在线请求只需要最新的
如果每隔
持久化检查点的大小为:
因此要区分:
活跃递推状态
一条运行中请求只有最新状态
前缀检查点存储
为多个可复用边界保存多份完整状态
KDA 的活跃状态是
DeepSeek V4:它已经不再使用 MLA 缓存布局
DeepSeek V4 用新的 hybrid attention 替代 V3 的 MLA。根据 DeepSeek V4 论文、Transformers 架构文档和公开配置,每个 Attention 层可能属于:
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 的公开参数包括:
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 的数据主体为:
一条 128 维 FP4 索引键的数据主体为:
V4-Pro 的 61 层怎么分布
公开 compress_ratios 与论文给出的层型分布对应:
CSA layers = 30 compression ratio 4
HCA layers = 31 compression ratio 128
本地窗口 = 全部 61 层,每层 128 个 token
暂不考虑压缩器重叠缓冲区、门控状态、量化因子、MTP 缓存和分配元数据,主持久化张量可以近似为:
四项分别是:
- 所有层的本地 SWA 共享 K=V;
- 30 层 CSA compressed K=V;
- 31 层 HCA compressed K=V;
- 30 层 CSA FP4 索引键。
100 万 token 上下文的分项计算
当
| 缓存对象 | 估算大小 |
|---|---|
| 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 的公开参数为:
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 下界约为:
V4 的重点不是“把 MLA 再压小一点”,而是把一个统一的线性增长历史缓存拆成多种生成频率和生命周期不同的对象。
layer view
DeepSeek V4 的异构 Cache
本地 SWA
所有相关层只保留最近 128 条共享 K=V 记录
CSA 池
每 4 个 token 产生一条压缩记录
HCA 池
每 128 个 token 产生一条压缩记录
把常见模型放到同一张表里
下面统一假设:
批大小 = 1
单个逻辑缓存副本
无张量并行或上下文并行分片
无前缀共享
无块大小取整
无分配器预留
| 模型 / 架构 | 主要缓存表示 | 数据类型假设 | 128K | 1M |
|---|---|---|---|---|
| Llama 2 7B MHA | 32 层完整 K/V | BF16 | 64 GiB | 512 GiB |
| Mistral 7B GQA + SWA | 8 KV Heads,窗口 4096 | BF16 | 0.50 GiB | 0.50 GiB |
| DeepSeek V3 MLA | 512 latent + 64 RoPE | BF16 | 8.58 GiB | 68.63 GiB |
| DeepSeek V3.2 DSA | MLA + 每层 128-d index | BF16 | 10.48 GiB | 83.88 GiB |
| GLM-5.2 DSA + IndexShare | 78 层 MLA + 21 组 index | BF16 | 11.63 GiB | 93.00 GiB |
| Kimi Linear | 7 层 MLA + 20 层 KDA | BF16 | 约 0.90 GiB | 约 7.02 GiB |
| DeepSeek V4-Pro | SWA + CSA + HCA + index | BF16/FP8/FP4 | ≥0.61 GiB | ≥4.83 GiB |
表里的数字不能直接用来比较模型优劣,因为模型宽度、层数、训练目标和能力不同。它们展示的是历史状态表示的缩放规律:
| 架构 | Cache 随历史长度的增长 |
|---|---|
| MHA / GQA / MLA | |
| DSA | 持久化 history 通常仍是 |
| SWA | |
| 纯 KDA 活跃状态 | |
| KDA + Full Attention Hybrid | |
| DeepSeek V4 CSA/HCA | |
| KDA 前缀检查点 |
分页 KV 缓存要按实际分配位置取整
理论公式使用真实 token 数
设 block size 为
例如:
block size = 64
request length = 6976
因为:
它恰好使用 109 个 block。
如果长度变成 6977:
最后一个 block 只写了一个 token,但物理空间通常已经按整块占用。
多请求时应分别取整:
而不是先把所有请求 token 数加起来再取整。
对于 DeepSeek V4、KDA 等混合模型,不同缓存组还可能采用不同的物理块大小、匹配粒度和检查点边界,不能把它们强行塞进同一组传统 KV 块。
张量并行下不能总是简单除以 TP
如果 KV Heads 能在
但下面这些情况可能导致复制:
- 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 计算器
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")
真正决定缓存大小的是内存布局,不是模型名
计算一个新模型时,可以按下面的顺序阅读配置和实现:
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 的
活跃状态意味着前缀检查点也是 ; - 对 shared K=V 的 DeepSeek V4 仍然乘传统 K/V 的 2;
- 从权重 FP8 推断 Cache 一定是 FP8;
- 忽略推理引擎把 latent 展开成标准 K/V 的实现差异。
最后可以把任何 hybrid model 写回最开始的统一形式:
只要能从模型配置和推理引擎代码中确定每层的