0%

算一算 DeepSeek Flash V4.1 的 KVCache 大小

DS Flash V4.1 虽然只加了一个小版本号,但是模型架构调整也不小…

每 token 的全局 KVCache 大小,压缩到 890 byte,相比 V3 的 34k(fp8)又压缩了四十倍 …

今儿来详细算一算这个 KVCache 的大小

本来计划算一下,PD 分离、KVCache 池化,对于 KVCache 传输带宽的诉求,但是,赶上 KVCache 又压缩了这么多,索性先从 KVCache 大小算起

其实,搞大模型,就得会搞各种算术题,真的庖丁解牛般算清楚了,可以减少很多的无效的尝试(当然,算 KVCache 大小只是个初级的门槛了

Global KVCache:890 byte

最终计算公式:

KV_global = 3 × (356 / 2) + 1 × 356 = 890 Bytes/token

接下来,我们逐个拆解

kv_source_layer_ids

1
"kv_source_layer_ids": [2, 8, 14, 20]

虽然模型有 40 层,但是只有这四层会产生 Global KVCache,890 byte 就是这个部分,对应上面公式的 3 和 1,表示的是这四层。

其他层用的滑动窗口注意力,128 的窗口,当然也是需要占 HBM 的。但是,按照 DS 的说法(SWA Bounded Replay),这部分可以不用持久化到 KVCache 池里,因为只需要回放最近 128 个 token 就能重新算出来。

再扯远一点,这次 V4.1 是 Causal Encoder-Decoder(CED)架构,Decoder 的全局 KVCache 是由 Encoder 最终 hidden state 投影出来的,不走逐层计算。所以在 PD 分离架构中,Prefill 节点都不用跑后面 20 层 Decoder(Prefill 每 token 只激活 8B 参数,Decode 是 16B),Decoder 的 SWA 窗口 KV 再由 Decode 节点自己 replay 出来,这样既减少 P 节点的计算量,也减少 HBM 用量。

Global KV entry:356 byte

356 byte 是一个 Global KV entry 的大小,又分为两个部分

  1. Main KV = 512 * 0.5 + 512 / 16 = 288

hidden_size: 5120

head_dim: 512

5120 维的 hidden state,经过 512 维的投影,压缩了 10 倍,只有 512 维了

0.5 是 KVCache 使用的 FP4 E2M1 格式,每个只有 0.5 byte

但是,FP4 E2M1 范围有限,每 16 个 channel 会共享一个缩放因子,FP8 E4M3 格式的

也就是每个 KVCache 的值,实际上是 FP4 * FP8 的乘积

  1. Indexer K = 128 * 0.5 + 128 / 32 = 68

index_topk = 512

每次只选 top 512 token 的 KVCache 参与计算,那么选哪 512 个呢,这里需要 Indexer 来计算

index_head_dim = 128

而 Indexer 选择也不会直接去读 KVCache 原数据,而是再投影到 128 维的 Index Key,减少 Index 的开销。

Key 是用 FP4 存储,每 32 channel 共享一个 FP8 缩放因子

最终两者汇总加起来:288 + 68 = 356

compress_ratios

V4.1 主打一个复用,没有 V4 那种 1/4、1/128 的复杂压缩了。

前面 3 层有 KVCache 会每 2 token 压缩,还是 512 维度,最后 1 层就不做 token 维度的压缩了

对应这个公式:KV_global = 3 × (356 / 2) + 1 × 356 = 890 Bytes/token

也就是前面 3 个 356 byte,需要 1/2,最后一个不需要了

SWA:2.58 MB

40 层中,只有 4 层会产生 Global KVCache,但滑动窗口注意力是每层都有的,也要占用 HBM,不过这个就小很多了

40 * 128 * (512 + 512/32) = 2.58 MB

head_dim 还是 512 维的压缩,FP8 格式存储,每 32 channel 共享一个 FP8 的 scale

一共 40 层,每层都有自己独立的滑动窗口,窗口大小是 128,因此,不管 context 有多长,滑动窗口的容量就是固定的了

作为一个对比,1M 上下文,Global KVCache 是 890 MB,SWA 始终都是 2.58 MB,差了 300 多倍,还是小很多的。

replay

值得一提的是,SWA 如果需要存储在 KVCache 池里,也是挺麻烦的事情(KDA 的 recurrent state 也类似)

虽然 HBM 容量占用是不大了,但是为了可能的复用,需要较频繁的往 KVCache 池里写入,淘汰,给 KVCache 存储带来了不少的麻烦。

本质上这种更新很频繁,复用概率较低的数据,存入 KVCache 池的性价比就不高。

DS Flash V4.1 干脆让 SWA 可以 replay 重新计算(近似)恢复,属于计算换存储/IO 了。理论上精确重建需要回放 层数 × 窗口 = 20 × 128 = 2560 个 token(每多一层,窗口就往前移一格),Bounded Replay 只回放最后 128 个,所以是近似恢复;但也正因为窗口只有 128,计算成本是可以接受的。

40 层架构

梳理下最终 40 层的架构

Encoder(L00 ~ L19),三层产生 Global KV,每层 178 B:

注意力 CR Global KV Index KV 增量
L00 ~ L01 SWA-128 only 0 0 B
L02 SWA+CSA2 2 ★ NEW KV02 INDEX → TopK02 178 B
L03 ~ L07 SWA+CSA2 2 KV←02 REUSE TopK02 0 B
L08 SWA+CSA2 2 ★ NEW KV08 INDEX → TopK08 178 B
L09 ~ L13 SWA+CSA2 2 KV←08 REUSE TopK08 0 B
L14 SWA+CSA2 2 ★ NEW KV14 INDEX → TopK14 178 B
L15 ~ L19 SWA+CSA2 2 KV←14 REUSE TopK14 0 B

Decoder(L20 ~ L39),全局 KV 全部由 L20 从 Encoder 最终 hidden state 投影产生,后续层只复用 KV、按需重建索引:

注意力 CR Global KV Index KV 增量
L20 SWA+CSA2 1 ★ NEW KV20 INDEX → Candidates + TopK20 356 B
L21 ~ L23 SWA+CSA2 1 KV←20 REUSE TopK20 0 B
L24 SWA+CSA2 1 KV←20 ★ REINDEX → TopK24 0 B
L25 ~ L27 SWA+CSA2 1 KV←20 REUSE TopK24 0 B
L28 SWA+CSA2 1 KV←20 ★ REINDEX → TopK28 0 B
L29 ~ L31 SWA+CSA2 1 KV←20 REUSE TopK28 0 B
L32 SWA+CSA2 1 KV←20 ★ REINDEX → TopK32 0 B
L33 ~ L35 SWA+CSA2 1 KV←20 REUSE TopK32 0 B
L36 SWA+CSA2 1 KV←20 ★ REINDEX → TopK36 0 B
L37 ~ L39 SWA+CSA2 1 KV←20 REUSE TopK36 0 B

合计:3 × 178 + 1 × 356 = 890 B/token

最后

DeepSeek 为了降低成本,确实玩得很花

仔细拆解下来,这次 V4.1 在 Attention 机制上,主要是跨层的 KVCache 复用,以及由此延伸出的 Encoder 和 Decoder 的区分

至于 Index 跨层共享,在 GLM 5.2 也已经有了

还有 Engram 终于也是用起来了,这又是一个新的落地玩法

看看 DeepSeek Pro scale 之后能达到什么效果吧,期待 …