DeepSeek v4.1 Flash SGLang引擎KV cache存储逻辑

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 层为例:

  1. 读取第 20 层保存的 index K。
  2. 根据第 24 层自己的 q_lora 生成 index Query,根据当前 hidden states 生成打分权重。
  3. 重新打分,选出最多 index_topk=512 个位置。
  4. 第 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,是三个不同动作。

  1. 为什么只有一个 _swa key?

模型配置(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 大小、输出长度和写入策略,还不能据此确定原因。

  1. 是不是每个 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)。

  1. 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)。
  1. 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 是另一个参数,不能代替它。
相关推荐
这是谁的博客?1 小时前
CUDA 内存一致性模型与同步原语深度解析:从弱内存序、作用域可见性到异步代理栅栏的 GPU 并发正确性底层原理
人工智能
u1301302 小时前
GitHub 热榜项目:周榜(2026-11-01)
人工智能·github
lifallen2 小时前
ub-mode skill:软件工程原则
人工智能·学习·ai·软件工程·ai编程
Geek-Chow2 小时前
05.4 · DeepInfer 推理框架总览:源码结构、开源与未开源部分对照
人工智能
Flynn Liang2 小时前
Spring AI 多模态实战(下):文生图 + TTS 语音合成(含 2.0 流式音频分片)
人工智能·spring·文生图·音视频·语音合成·tts·springai
长不大的杨瑞杰2 小时前
OpenCode v2 Custom Tools 实战:开发数据库、工单与发布工具
人工智能·typescript·开发工具·opencode·custom tools
羊仔AI探索2 小时前
离世仅仅半个月,张雪峰被蒸馏了
人工智能·aigc·蒸馏·张雪峰·skills
zhippt_10142 小时前
只剩截图没有源文件?图转PPT把图片还原成可编辑幻灯片
前端·人工智能·powerpoint
小oo呆2 小时前
【自然语言处理与大模型】自然语言处理的漫漫长路之深度学习时代
人工智能·深度学习·自然语言处理