DeepSeek v4.1 Flash模型架构解读
SGLang and Miles Add Day‑0 Support for DeepSeek‑V4.1
KV Cache 降低 75%:DeepSeek-V4.1-Flash 全新架构解读
DeepSeek v4.1 Flash cache特点
可以参考源码:
https://huggingface.co/deepseek-ai/DeepSeek-V4.1-Flash-1017/tree/main/inference
kv cache生成和attention计算的整体逻辑

deepseek v4.1 flash跟过去的模型有很大的不同
首先,cache分为了3类:
第一部分:swa kv cache,这个类似于以前的full attention的cache,但是只存储尾部的128个token,这个每一层都计算和存储。主模型40层,还有dspark 3层。
第二个部分:compressed kv cache,由Compressor计算,这个的kv是独立于swa的Kv计算逻辑的。global KV指的就是这个压缩kv部分。
压缩率配置:
python
"compress_ratios": [
0, 0, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
0, 0, 0
],
也就是前2层不压缩,然后前20层除了前2层都采用2倍压缩。后20层全部采用1倍压缩,也就是不压缩。
过去的模型,不管是full attntion还是full+mamba/swa,每一层都是需要独立计算和存储kv cache的。
但对于compressor部分,deepseek v4.1 flash只需要生成4个层的kv cache,后续相邻的层直接复用前面的kv cache,当前层只算query state。生成compressor kv cache的模型层如下:
python
"kv_source_layer_ids": [
2, 8, 14, 20
],
第三部分:indexer k cache。indexer通过一个旁路attention来计算当前的token于过去token的相似度,但是不需要进行v加权求和,因此只有k cache。
python
"index_source_layer_ids": [
2, 8, 14, 20, 24, 28, 32, 36
],
虽然index_source_layer_ids的定义比kv_source_layer_ids多,但是,实际上20层之后的这几个层并不会重新生成indexer k cache,而只是重新生成indexer q state进行重新筛选token,k cache仍然复用第20层的部分。
总体共享逻辑如下:
| 层 | 角色 | 注意力和缓存结构 |
|---|---|---|
| 0~1 | Causal Encoder | 只有 SWA |
| 2~7 | Causal Encoder | SWA + 第 2 层产生的共享 Global KV,压缩比 2 |
| 8~13 | Causal Encoder | SWA + 第 8 层产生的共享 Global KV,压缩比 2 |
| 14~19 | Causal Encoder | SWA + 第 14 层产生的共享 Global KV,压缩比 2 |
| 20~39 | Decoder | SWA + 从 Encoder 最终输出投影得到的共享 Global KV,压缩比 1 |
每个token大小的计算
SGLang启动日志参考
注意这里是fp8 kv cache,没启用FP4 kv cache。
python
Use dsv4 attention backend for DeepseekV4ForCausalLM, setting page_size to 256.
Load weight begin. avail mem=176.24 GB
Load weight end. elapsed=588.76 s, type=DeepseekV4ForCausalLM, quant=fp8, avail mem=103.37 GB, mem usage=72.87 GB.
Load weight begin. avail mem=103.37 GB
Load weight end. elapsed=22.09 s, type=DeepseekV4ForCausalLMDSpark, quant=fp8, avail mem=101.38 GB, mem usage=1.98 GB.
Initialized DSpark draft runner. attention_backend=dsv4, model=DeepseekV4ForCausalLMDSpark, gamma=5, verify_num_draft_tokens=6, query_token_num=5, sample_from_anchor=True, mask_token_id=128799, markov_head=DSparkV4MarkovHead
Reserving 0.10 GB of the KV budget for post-sizing multimodal allocations (feature-transport pools + embedding cache).
DSV4 SWA sizing: mode=cap, swa_tokens=411392, request_cap+headroom=411392, prefix_tails=512
DSV4 memory calculation: bytes_per_full_token=1670.75, available_bytes=57.18 GB, c128_state_fixed=0.00 GB, swa_fixed=9.17 GB, full_token=30853632
DSV4 pool sizes: full=30853632, swa=411392, c4=7713408, c128=241044, c4_state=51424, c128_state=0
Initialize DeepSeekV4TokenToKVPool with max_num_reqs=128 swa_size=411392 c4_size=7713408 c4_logical_size=7713408 c128_size=241044 c4_state_pool_size=51424 c128_state_pool_size=33024
DSV4 SWA storage: worker=target, storage=paged, paged_allocator=True
Memory pool end. avail mem=44.74 GB
Initialize DeepSeekV4TokenToKVPool with max_num_reqs=128 swa_size=411392 c4_size=0 c4_logical_size=0 c128_size=0 c4_state_pool_size=0 c128_state_pool_size=0
DSV4 SWA storage: worker=draft, storage=paged, paged_allocator=True
Memory pool end. avail mem=44.07 GB
Using DeepseekV4AttnBackend for dsv4 attention backend (CUDA).
Overriding draft attention backend to dsv4.
Using DeepseekV4AttnBackend for dsv4 attention backend (CUDA).
Running FlashInfer autotune with cache: /ksp/preset/model/Deepseek-v41-Flash-cache/flashinfer/autotune/0.6.18/sm100/f67c93f6df4e56c9/rank_tp0_pp0_dp0.json
Disable CP decode attention TP
Disable prefill CUDA graph because cuda_graph_config resolved prefill.backend='disabled' (e.g. via --cuda-graph-backend-prefill=disabled or auto-disable rules).
Capture target verify CUDA graph begin. backend=full, num_tokens_per_req=6, bs=[1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 120, 128], avail mem=40.71 GB
multimem all-gather disabled (no multicast for world_size=4)
Capture target verify CUDA graph end. elapsed=48.85 s, mem usage=11.84 GB, avail mem=28.87 GB.
DSpark draft proposal (greedy + sampling) folded into the draft cuda graph.
Disable prefill CUDA graph because cuda_graph_config resolved prefill.backend='disabled' (e.g. via --cuda-graph-backend-prefill=disabled or auto-disable rules).
Capture draft verify CUDA graph begin. backend=full, num_tokens_per_req=5, bs=[1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 120, 128], avail mem=28.65 GB
Running FlashInfer autotune with cache: /ksp/preset/model/Deepseek-v41-Flash-cache/flashinfer/autotune/0.6.18/sm100/d4509a99da01b53f/rank_tp0_pp0_dp0.json
Capture draft verify CUDA graph end. elapsed=18.96 s, mem usage=0.61 GB, avail mem=28.04 GB.
max_total_num_tokens=30853632, chunked_prefill_size=32768, max_prefill_tokens=16384, max_running_requests=128, context_len=1048576, available_gpu_mem=28.04 GB
Init Unified Radix Cache. Components: (<ComponentType.FULL: 0>, <ComponentType.SWA: 1>). Tree Core: UnifiedTreeCore
Allocating 13.45 GB host memory for V4 paged pool 'swa' (layers=43, pages=2089, item_bytes=149760, layout=page_first_direct).
Allocating 23.46 GB host memory for V4 paged pool 'deepseek_v4_c1' (layers=1, pages=156678, item_bytes=149760, layout=page_first_direct).
Allocating 2.73 GB host memory for V4 paged pool 'deepseek_v4_c1_indexer' (layers=1, pages=156678, item_bytes=17408, layout=page_first_direct).
Allocating 35.20 GB host memory for V4 paged pool 'deepseek_v4_c2' (layers=3, pages=156678, item_bytes=74880, layout=page_first_direct).
Allocating 4.09 GB host memory for V4 paged pool 'deepseek_v4_c2_indexer' (layers=3, pages=156678, item_bytes=8704, layout=page_first_direct).
存储的key类型
page size 256, 输入长度925,共存储3个page,mooncake 存储的key样例如下:
python
DeepSeek-V4.1-Flash_b876a268540564795c02737__swa
DeepSeek-V4.1-Flash_6a5e7a91d994818cebed9b4__deepseek_v4_c1
DeepSeek-V4.1-Flash_7ba2e09b588d5450b839215__deepseek_v4_c1
DeepSeek-V4.1-Flash_b876a268540564795c02737__deepseek_v4_c1
DeepSeek-V4.1-Flash_6a5e7a91d994818cebed9b4__deepseek_v4_c1_indexer
DeepSeek-V4.1-Flash_7ba2e09b588d5450b839215__deepseek_v4_c1_indexer
DeepSeek-V4.1-Flash_b876a268540564795c02737__deepseek_v4_c1_indexer
DeepSeek-V4.1-Flash_7ba2e09b588d5450b839215__deepseek_v4_c2
DeepSeek-V4.1-Flash_6a5e7a91d994818cebed9b4__deepseek_v4_c2
DeepSeek-V4.1-Flash_b876a268540564795c02737__deepseek_v4_c2
DeepSeek-V4.1-Flash_7ba2e09b588d5450b839215__deepseek_v4_c2_indexer
DeepSeek-V4.1-Flash_6a5e7a91d994818cebed9b4__deepseek_v4_c2_indexer
DeepSeek-V4.1-Flash_b876a268540564795c02737__deepseek_v4_c2_indexer
可见存储了5种类型的key,分别后缀为
python
_deepseek_v4_c1,
_deepseek_v4_c1_indexer,
_deepseek_v4_c2,
_deepseek_v4_c2_indexer,
_swa
kv cache大小计算
一个请求样例,输入请求长度8197,page size为256,存32个page,_deepseek_v4_c1, _v4_c1_indexer, _deepseek_v4_c2, __deepseek_v4_c2_indexer后缀结束的各32个,然后_swa结束的key 1个。
根据你的启动日志,这次部署使用 V4 KV 缓存布局,并启用了 DSpark。每个 key 对应的 value 大小如下,已包含量化 scale 和页内 padding:
| key 后缀 | 每层包含的缓存 token 数 | 打包层数 | 每个 key 的字节数 |
|---|---|---|---|
_deepseek_v4_c1 |
256 | 1 (第20层) | 149,760 B |
_deepseek_v4_c1_indexer |
256 | 1 (第20层) | 17,408 B |
_deepseek_v4_c2 |
128 | 3 (前20层的3个) | 224,640 B |
_deepseek_v4_c2_indexer |
128 | 3 (前20层的3个) | 26,112 B |
_swa |
256 | 43 (full+dspark) | 6,439,680 B |
计算逻辑如下
配置文件相关信息
python
"sliding_window": 128,
"compress_ratios": [
0,0,2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,2,2,
1,1,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,
0,0,0
],
"compress_rope_theta": 160000,
"kv_source_layer_ids": [
2, 8, 14, 20
],
"index_source_layer_ids": [
2, 8, 14, 20, 24, 28, 32, 36
],
"index_n_heads": 32,
"index_head_dim": 128,
"index_topk": 512,
"candidate_source_layer_id": 20,
"candidate_topk_blocks": 2048,
"candidate_block_size": 8,
1. C1/C2 存储的层数和 token 数
模型配置 (line 106)(DeepSeek-V4.1-Flash/config.json:106)的
kv_source_layer_ids = [2, 8, 14, 20],结合各层的 compress_ratios:
- C1 的 source layer 是
[20],因此存 1 层。 - C2 的 source layer 是
[2, 8, 14],因此存 3 层。 - 一个逻辑 page 对应 256 个原始 token,压缩后分别存
256 / 1 = 256、256 / 2 = 128个缓存 token。
KV 和 indexer 池都按这些 source layer 分配,代码使用 layer_num=len(sources),见缓存池初始化(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:1591)。因此,indexer 的存储层数也不是 index_source_layer_ids 的列表长度。
2. C1/C2 KV:每 token 584 B,再对每层的页做 576 B 对齐
这次实际使用的 V4 布局是:
NoPE:448 维 × FP8 1 B = 448 B
RoPE: 64 维 × BF16 2 B = 128 B
scale:448 / 64 个 × 1 B,加 1 B padding = 8 B
合计 = 584 B/token
源码还要求每层的物理页大小向上对齐到 576 B,见字节数与对齐计算 (line 295)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:295)。定义:
align_up(x, a) = ceil(x / a) × a
于是:
每个 C1 key = 1 × align_up(256 × 584, 576)
= 1 × 149,760
= 149,760 B
每个 C2 key = 3 × align_up(128 × 584, 576)
= 3 × 74,880
= 224,640 B
注意,每层分别对齐后,再乘层数。
3. C1/C2 indexer:每缓存 token 68 B
虽然日志里的 enable_deepseek_v4_fp4_indexer=False,但低压缩率 C1/C2 的 indexer 在初始化代码 (line 1631)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:1631)中明确设置了 force_fp4=True。
index_head_dim=128,其字节公式 (line 53)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:53)为:
FP4 数据:128 / 2 = 64 B
scale: 128 / 32 = 4 B
合计 = 68 B/token
因此:
每个 C1 indexer key = 1 × 256 × 68 = 17,408 B
每个 C2 indexer key = 3 × 128 × 68 = 26,112 B
indexer 内部可能使用更小的物理页,但写入 HiCache/Mooncake 前会按 256 / ratio 个缓存 token 重新组合成一个逻辑页,见逻辑页组装代码 (line 829)(sglang/srt/mem_cache/hybrid_cache/hybrid_pool_assembler.py:829)。
4. SWA:一个完整的 256-token 页,包含 43 层
你的 sliding_window=128,保存时仍以完整的 256-token 页为单位。日志明确记录:
pool 'swa': layers=43, item_bytes=149760
这 43 层来自主模型 40 层,加上 DSpark draft 的 3 层。源码会把 draft SWA buffers 追加到主模型 SWA buffers 中,一起存入 SWA 池,见SWA 池组装 (line 975)(sglang/srt/mem_cache/hybrid_cache/hybrid_pool_assembler.py:975)。
每个 SWA key = (40 + 3) × align_up(256 × 584, 576)
= 43 × 149,760
= 6,439,680 B
最终,Mooncake 使用的页大小 (line 739)(sglang/srt/mem_cache/memory_pool_host.py:739)就是 layer_num × item_bytes,所以日志值和上述计算完全一致。
你观察到的 129 个 key,总 value 数据量为:
python
32 * (149760 + 17408 + 224640 + 26112) + 6439680
= 19813120 B
≈ 18.8953 MiB
这里无需再乘 TP=4;以上是每个 key 的数据长度,不包含 Mooncake 元数据、分配器开销或额外副本。
不考虑swa的部分,那么每个token的存储大小为
python
(149760 + 17408 + 224640 + 26112)/256
= 1632.5 B
跨层kv cache共享
kv_source_layer_ids 的含义是什么
kv_source_layer_ids 表示:哪些 Transformer 层负责生成共享的压缩 KV,供本层及后续若干层的 attention 复用 。层编号从 0 开始。
配置是:
kv_source_layer_ids = [2, 8, 14, 20]
结合 compress_ratios,实际共享关系如下:
| 层编号 | 压缩比 | 使用哪一层生成的压缩 KV |
|---|---|---|
| 0~1 | 0 | 仅使用 SWA |
| 2~7 | 2 | 第 2 层 |
| 8~13 | 2 | 第 8 层 |
| 14~19 | 2 | 第 14 层 |
| 20~39 | 1 | 第 20 层 |
例如,第 2 层根据自己的输入 hidden states 生成 C2 KV,写入缓存;第 3~7 层计算各自的 Query,但 attention 读取的是第 2 层生成的同一份 C2 KV。到第 8 层,再生成另一份共享 KV。
源码有两个直接对应的实现:
- 模型初始化 (line 1472)(sglang/srt/models/deepseek_v4.py:1472):对于压缩比为 1/2 的层,只有出现在
kv_source_layer_ids中,才创建负责生成 KV 的DeepseekV41Compressor。 - 缓存层映射 (line 1819)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:1819):为当前层选择压缩比相同、编号不大于当前层的最近一个 source layer,并映射到它的缓存。
所以,虽然模型有 18 个 C2 attention 层和 20 个 C1 attention 层,实际只需保存:
C2:3 份共享 KV,对应 source layer 2、8、14
C1:1 份共享 KV,对应 source layer 20
这就是前面计算 Mooncake 大小时,C2 乘 3 、C1 乘 1 的原因。
这里共享的是长上下文分支的 KV;每层自己的 SWA KV 仍然独立保存。另外,C1 的压缩比虽然是 1,没有减少 token 数,仍通过跨层共享减少了 KV 存储。
index_source_layer_ids的含义是什么,为什么比kv_source_layer_ids 多一些
index_source_layer_ids 表示:哪些层重新运行 indexer,为当前 Query 计算要访问的 Top-K 压缩 KV 位置;后续若干层复用这次选出的索引。
它与 kv_source_layer_ids 控制的是两件事:
| 配置 | 决定什么 |
|---|---|
kv_source_layer_ids |
在哪些层生成共享的压缩 KV,以及配套的 index K |
index_source_layer_ids |
在哪些层重新打分、选择 Top-K 位置 |
因此,可以复用同一份 KV 和 index K,但在更深的层重新选择访问哪些位置。
你的配置是:
kv_source_layer_ids = [2, 8, 14, 20]
index_source_layer_ids = [2, 8, 14, 20, 24, 28, 32, 36]
对于 C2,二者都是 [2, 8, 14]。对于 C1,所有层共享第 20 层生成的 KV,但每隔 4 层重新检索一次:
| attention 层 | KV / index K 来源 | Top-K 位置由哪层计算 |
|---|---|---|
| 20~23 | 20 | 20 |
| 24~27 | 20 | 24 |
| 28~31 | 20 | 28 |
| 32~35 | 20 | 32 |
| 36~39 | 20 | 36 |
以第 24 层为例:
- 读取第 20 层保存的 index K。
- 根据第 24 层自己的
q_lora生成 index Query,根据当前 hidden states 生成打分权重。 - 重新打分,选出最多
index_topk=512个位置。 - 第 24~27 层的主 attention 按这些位置访问第 20 层生成的压缩 KV。
源码中,Indexer 初始化 (line 203)(/root/luchangli/sglang_github/python/sglang/srt/layers/attention/dsv4/dsv41_sparse.py:203)明确区分了两种角色:
self.owns_k = layer_id in config.kv_source_layer_ids
每个 indexer 都有自己的 Query 投影和打分权重,但只有 owns_k=True 时,才创建用于生成 index K 的 wk 和 k_norm。官方参考实现的 Top-K 共享逻辑 (line 723)(/root/luchangli/deepseek/DeepSeek-V4.1-Flash/inference/model.py:723)也明确规定:index source 层运行 indexer,中间层复用它发布的结果。
这样设计允许模型在保持 KV 跨层共享、节省存储 的同时,随着深层特征变化,更频繁地更新关注的位置。新增的 [24, 28, 32, 36] 带来新的检索计算,不会新增四份 index K 缓存 ,所以前面计算的 C1 indexer 存储仍然只乘 1 层。
FP4 KV cache
采用环境变量开启FP4 kv cache:
SGLANG_DSV4_KV_LAYOUT=v41 \
SGLANG_DSV4_COMPRESSED_KV_LAYOUT=fp4 \
python -m sglang.launch_server \
--model-path <DeepSeek-V4.1-Flash路径> \
...原有参数
注意:
SGLANG_DSV4_COMPRESSED_KV_LAYOUT=fp4将所有压缩层的 KV cache 设为 FP4;SWA cache 仍然是 FP8。- 默认值是
auto:使用 V4.1 layout 时,压缩比 1/2 用 FP4,4/128 用 FP8。 - NVIDIA 上需要 SM100/SM103 + FlashMLA ;
trtllm后端仅支持 FP8 pool。 --kv-cache-dtype保持fp8_e4m3,这里通过上述环境变量控制压缩缓存格式。
日志参考
python
Load weight begin. avail mem=176.26 GB
Load weight end. elapsed=398.05 s, type=DeepseekV4ForCausalLM, quant=fp8, avail mem=102.85 GB, mem usage=73.42 GB.
Load weight begin. avail mem=102.84 GB
Load weight end. elapsed=23.65 s, type=DeepseekV4ForCausalLMDSpark, quant=fp8, avail mem=100.82 GB, mem usage=2.02 GB.
Initialized DSpark draft runner. attention_backend=dsv4, model=DeepseekV4ForCausalLMDSpark, gamma=5, verify_num_draft_tokens=6, query_token_num=5, sample_from_anchor=True, mask_token_id=128799, markov_head=DSparkV4MarkovHead
Reserving 0.10 GB of the KV budget for post-sizing multimodal allocations (feature-transport pools + embedding cache).
DSV4 SWA sizing: mode=cap, swa_tokens=411392, request_cap+headroom=411392, prefix_tails=512
DSV4 memory calculation: unified=False, unified_fp8=False, dspark_draft_bf16=False, bytes_per_full_token=912.25, available_bytes=56.63 GB, c128_state_fixed=0.00 GB, c2_state_fixed=0.01 GB, swa_fixed=8.29 GB, swa_ring_fixed=0.00 GB, c4_state_fixed=0.00 GB, full_token=56874240
DSV4 pool sizes: full=56874240, swa=411392, c4=14218560, c128=444330, c4_state=0, c128_state=0
Initialize DeepSeekV4TokenToKVPool with max_num_reqs=128 swa_size=411392 c4_size=14218560 c4_logical_size=14218560 c128_size=444330 c4_state_pool_size=0 c128_state_pool_size=0 unified=False unified_fp8=False
DSV4 SWA storage: worker=target, storage=paged, paged_allocator=True
Memory pool end. avail mem=44.62 GB
Initialize DeepSeekV4TokenToKVPool with max_num_reqs=128 swa_size=411392 c4_size=0 c4_logical_size=0 c128_size=0 c4_state_pool_size=0 c128_state_pool_size=0 unified=False unified_fp8=False
DSV4 SWA storage: worker=draft, storage=paged, paged_allocator=True
Memory pool end. avail mem=44.01 GB
Using DeepseekV4AttnBackend for dsv4 attention backend (CUDA).
Overriding draft attention backend to dsv4.
Using DeepseekV4AttnBackend for dsv4 attention backend (CUDA).
Disable prefill CUDA graph because cuda_graph_config resolved prefill.backend='disabled' (e.g. via --cuda-graph-backend-prefill=disabled or auto-disable rules).
Capture target verify CUDA graph begin. backend=full, num_tokens_per_req=6, avail mem=40.76 GB
Candidate indexer graph limits: [('candidate_unfiltered', 16384)]; use full filtering above 16384.
multimem all-gather disabled (no multicast for world_size=4)
Capture target verify CUDA graph end. bs=[1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 120, 128], elapsed=135.65 s, mem usage=7.56 GB, avail mem=33.20 GB.
DSpark draft proposal (sampling) folded into the draft cuda graph.
Disable prefill CUDA graph because cuda_graph_config resolved prefill.backend='disabled' (e.g. via --cuda-graph-backend-prefill=disabled or auto-disable rules).
Capture draft verify CUDA graph begin. backend=full, num_tokens_per_req=5, avail mem=33.20 GB
Draft metadata prep through the glue graph, one per replay key.
Capture draft verify CUDA graph end. bs=[1, 2, 3, 4, 5, 6, 7, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 32, 40, 44, 48, 52, 56, 60, 64, 72, 80, 88, 96, 104, 112, 120, 128], elapsed=10.89 s, mem usage=0.50 GB, avail mem=32.71 GB.
max_total_num_tokens=56874240, chunked_prefill_size=32768, max_prefill_tokens=16384, max_running_requests=128, context_len=1048576, available_gpu_mem=32.71 GB
Init Unified Radix Cache. Components: (<ComponentType.FULL: 0>, <ComponentType.SWA: 1>). Tree Core: RustUnifiedTreeCore
HiCache memory headroom: host 545.7 GiB, cgroup 572.4 GiB
Allocating 12.14 GB host memory for V4 paged pool 'swa' (layers=43, pages=2089, item_bytes=135168, layout=page_first_direct).
HiCache memory headroom: host 500.5 GiB, cgroup 527.1 GiB
Allocating 21.29 GB host memory for V4 paged pool 'deepseek_v4_c1' (layers=1, pages=288814, item_bytes=73728, layout=page_first_direct).
HiCache memory headroom: host 418.6 GiB, cgroup 445.1 GiB
Allocating 5.03 GB host memory for V4 paged pool 'deepseek_v4_c1_indexer' (layers=1, pages=288814, item_bytes=17408, layout=page_first_direct).
HiCache memory headroom: host 397.6 GiB, cgroup 424.1 GiB
Allocating 31.94 GB host memory for V4 paged pool 'deepseek_v4_c2' (layers=3, pages=288814, item_bytes=36864, layout=page_first_direct).
HiCache memory headroom: host 274.4 GiB, cgroup 302.7 GiB
Allocating 7.54 GB host memory for V4 paged pool 'deepseek_v4_c2_indexer' (layers=3, pages=288814, item_bytes=8704, layout=page_first_direct).
按当前源码和 DeepSeek-V4.1-Flash 配置,FP4 压缩 KV、V4.1 layout、page size=256、没有 PP 切分,每个 key 的 value 大小如下。这里计算的是写入 Mooncake 的数据字节数,包含 scale 和页内 padding,不含 Mooncake 元数据或副本开销。
| key 后缀 | 每个 key 的计算公式 | 每个 key 大小 | 数量 |
|---|---|---|---|
_deepseek_v4_c1 |
1 × 256 × 288 |
73,728 B(72 KiB) | 32 |
_deepseek_v4_c1_indexer |
1 × 256 × 68 |
17,408 B(17 KiB) | 32 |
_deepseek_v4_c2 |
3 × 128 × 288 |
110,592 B(108 KiB) | 32 |
_deepseek_v4_c2_indexer |
3 × 128 × 68 |
26,112 B(25.5 KiB) | 32 |
_swa |
43 × 256 × 528 |
5812224 B | 1 |
1. 为什么 C1 乘 1,C2 乘 3?
模型的 kv_source_layer_ids=[2,8,14,20],其中:
- 第 2、8、14 层的压缩比为 2,对应 3 个 C2 源层。
- 第 20 层的压缩比为 1,对应 1 个 C1 源层。
源码只为这些共享源层分配低压缩比 KV 和 indexer 存储,其他层复用。因此不能按所有使用 C1/C2 的层数相乘,也不能直接用 index_source_layer_ids 的数量计算 indexer cache。见 源层筛选 (line 94)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:94)、低压缩比 pool 创建 (line 1629)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:1629)。配置见 官方 config.json。
2. FP4 KV 为什么是 288 B/slot?
每个压缩 KV slot 有 512 个值,采用 FP4,并且每 16 个值附带一个 1 B 的 E4M3 scale:
FP4 数据:512 × 4 / 8 = 256 B
scale: 512 / 16 × 1 = 32 B
合计: 288 B/slot
C1 每个逻辑页保存 256/1=256 个 slot;C2 保存 256/2=128 个 slot。
v41 的 FP4 物理页还要按 256 B 对齐:
C1 每层:align_up(256 × 288, 256) = 73,728 B
C2 每层:align_up(128 × 288, 256) = 36,864 B
这两个大小本来就对齐,所以没有额外 padding;v41_new 在这里得到相同大小。见 KVLayout 定义和 page_bytes() (line 18)(sglang/kernels/ops/attention/dsv4/kv_layout.py:18)。
3. FP4 indexer 为什么是 68 B/slot?
Indexer 的维度是 128,每 32 个值对应一个 1 B scale:
FP4 数据:128 × 4 / 8 = 64 B
scale: 128 / 32 × 1 = 4 B
合计: 68 B/slot
所以:
C1 indexer:1 个源层 × 256 slot × 68 B = 17,408 B
C2 indexer:3 个源层 × 128 slot × 68 B = 26,112 B
CUDA 实现把 FP4 payload 和 scale 放在同一个 buffer,所以你看到每种 indexer 只有一类 key,没有单独的 _indexer_scale key。见 字节数公式 (line 53)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:53)、buffer 分配 (line 594)(sglang/srt/mem_cache/deepseek_v4_memory_pool.py:594)。
4. SWA 为什么一个 key 就有约 5 MiB?
SWA 仍然采用 FP8,每个 token 保存:
数据:512 × 1 B = 512 B
scale:512 / 32 = 16 B
合计: 528 B/token/层
模型有 40 个主模型层,以及3层的dspark,SWA pool 为每层保存一页:
每层一页:256 × 528 = 135,168 B
一个 SWA key:43 * 135168 = 5812224 B
虽然模型的滑动窗口是 128 token,存储仍然按完整的 256-token 页进行,所以不能用 128 计算这个 key 的大小。见 SWA host pool 组装 (line 1031)(sglang/srt/mem_cache/hybrid_cache/hybrid_pool_assembler.py:1031)。
5. 一个 key 包含所有对应层,总量是多少?
get_page_buffer_meta() 在 page_first 布局下直接返回 layer_num × item_bytes;在 layer_first 下返回各层 buffer,Mooncake 再把这些 buffer 打包到同一个 key。见 页大小计算 (line 739)(sglang/srt/mem_cache/memory_pool_host.py:739)、多 buffer 打包 (line 1295)(sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py:1295)。
因此,你观察到的 129 个 key,合计 value 数据量为:
32 * (73728 + 17408 + 110592 + 26112) + 5812224
= 13103104 B
= 12.49609375 MiB
KV cache存储逻辑
按当前仓库 c43abd6d9e 的实现,SWA 会随 chunked prefill 的缓存 checkpoint 发布;普通 decode 没有类似 mamba_track_interval 的 SWA 定期快照参数。SWA 与 Full 命中不一致时,也有分叉点补建机制。
这里要区分:GPU 上更新 KV、插入 radix cache、写入 Mooncake,是三个不同动作。
- 为什么只有一个
_swakey?
模型配置(DeepSeek-V4.1-Flash/config.json) 中,sliding_window=128,而 page size 为 256。
四类 C1/C2 KV 和 indexer 按完整前缀保存:
floor(8197 / 256) = 32 pages
其中 C2 的每个对象对应一个逻辑上的 256-token page,内部保存压缩后的数据。
SWA 则只需要保存某个可恢复边界之前的尾部窗口 。128-token 窗口按 page 保存,只需一个 page,因此一个恢复边界可以对应一个 _swa key。实现中,SWA 使用尾部 page 的 hash,并采用 TRAILING_PAGES 匹配策略,见 SWA 存储实现 (line 1207)(sglang/srt/mem_cache/unified_cache/components/swa.py:1207)。
一个 _swa key 不代表整个请求只执行过一次 checkpoint。 例如,如果 prefill 分成:
第一段完成:8192 → 缓存边界 8192
第二段完成:8197 → 缓存边界仍为 8192
两次对应同一个 page hash,key 数量不会增加。这个情况可以解释你的观察,但没有实际 chunk 大小、输出长度和写入策略,还不能据此确定原因。
- 是不是每个 chunked prefill 都保存一次?
每个 chunk 完成后,会走缓存 checkpoint 路径。 中间 chunk 通过 stash_chunked_request() 调用 checkpoint_kv_cache();prefill 完成且请求继续生成时,也会 checkpoint。见 中间 chunk 入口 (line 3772)(sglang/srt/managers/scheduler.py:3772) 和 prefill 完成入口 (line 394)(sglang/srt/managers/scheduler_components/batch_result_processor.py:394)。
但是否最终写入 Mooncake,还取决于:
| 参数 | 作用 |
|---|---|
--chunked-prefill-size |
影响 prefill checkpoint 出现在哪些位置 |
--page-size |
决定可缓存前缀的对齐粒度 |
--hicache-write-policy write_through |
插入后触发备份,GPU→CPU 完成后异步写 Mooncake |
write_through_selective |
普通节点达到命中阈值后再备份;当前阈值为 2 |
write_back |
主要在驱逐时触发备份 |
因此,checkpoint 次数、写入次数、最终不同 key 的数量,不能直接画等号。 写入入口见 HiCache 备份完成处理 (line 1796)(sglang/srt/mem_cache/unified_radix_cache.py:1796)。
- decode 的存储逻辑是什么?有没有 SWA track interval?
在普通、非 PD 专用 offload 的生成路径中:
- 每个 decode step 更新 GPU KV,并按滑窗规则回收旧 SWA。
- 不会每隔一个 page 或固定数量 token,自动向 radix cache/Mooncake 发布一次 SWA checkpoint。
- 请求正常结束时,再将已计算 KV 的可缓存部分插入树,随后按 HiCache 写入策略备份。见 decode 结束处理 (line 1435)(sglang/srt/managers/scheduler_components/batch_result_processor.py:1435) 和 结束时 checkpoint (line 416)(sglang/srt/mem_cache/common.py:416)。
当前没有 swa_track_interval 这样的参数。
容易混淆的两个参数是:
--mamba-track-interval:控制 decode 时 Mamba 状态快照的跟踪间隔;它本身也不等于每个间隔都写一次 Mooncake。SGLANG_SWA_EVICTION_INTERVAL:当前默认 128,控制旧 SWA 的回收节奏,不控制持久化频率。见 SWA 回收路径 (line 4072)(sglang/srt/managers/schedule_batch.py:4072)。
- SWA 与 Full 命中不一致时,会在分叉点补 checkpoint 吗?
如果你最后一句指的是 SWA 与 Full ,答案是会。当前代码显式计算:
usable_boundary = device_prefix_len + host_hit_length
branch_boundary = floor(full_kv_hit_length / page_size) * page_size
若 branch_boundary > usable_boundary:
swa_branching_seqlen = branch_boundary
例如,Full 命中到 6144,但完整 SWA 窗口只支持恢复到 4096,就会记录 swa_branching_seqlen=6144。请求从可用位置继续计算,覆盖该边界后,在 checkpoint 时补建 6144 边界所需的 SWA 窗口,供后续请求复用。匹配时只是记录目标,实际补建发生在后续计算和插入阶段。见 分叉点计算 (line 379)(sglang/srt/mem_cache/unified_cache/components/swa.py:379) 和 分叉点插入处理 (line 999)(sglang/srt/mem_cache/unified_cache/components/swa.py:999)。
另外,Mooncake 的冷加载会检查该边界的 SWA 是否完整 。如果只保存了 8192 边界的 _swa,即使四类 C1/C2 对象覆盖了前 32 pages,也不能据此直接恢复到任意较短边界,例如 4096;那个边界需要自己的 SWA 窗口。见 Mooncake 可恢复边界检查 (line 1039)(sglang/srt/mem_cache/storage/mooncake_store/mooncake_store.py:1039)。
这些行为来自本地实现;上游对应的 UnifiedRadixCache 也采用 Full、SWA、Mamba 多组件缓存结构。
控制SWA cache不写入L3
当前仓库的 DeepSeek V4.1 Flash 可以加:
--enable-encoder-swa-bounded-replay
它让 SWA 保存在请求自己的窗口里,prefix cache 命中时重新计算 SWA,因此 SWA 不进入 HiCache 的 L2/L3,仍缓存 Main KV 和 Indexer keys。见参数定义 (line 168)(/root/luchangli/sglang_github/python/sglang/srt/arg_groups/fields/exec_.py:168)和HiCache 实现 (line 966)(/root/luchangli/sglang_github/python/sglang/srt/mem_cache/hybrid_cache/hybrid_pool_assembler.py:966)。
注意:
- 这是实验功能;需要设置正数的
--max-running-requests,chunked prefill 至少为128 + page_size。 - 不支持 CP、PD decode 等配置。
--enable-decoder-swa-bounded-replay是另一个参数,不能代替它。