Skip to content

Commit 83d9823

Browse files
committed
dsv4
Signed-off-by: Zhi Yiliu <2584074296@qq.com>
1 parent 350a4e8 commit 83d9823

11 files changed

Lines changed: 653 additions & 105 deletions

File tree

85 KB
Loading
44.3 KB
Loading
946 KB
Loading
949 KB
Loading
1.06 MB
Loading
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
# DeepseekV4
2+
## 模型架构
3+
![](../img/dsv4_overview.png)
4+
### Attention
5+
这里所有的 Attention 实际上都是 MLA 的方式计算,只是 KV 的压缩程度不同以及是否有 DSA 参与
6+
7+
#### CSA(Compressed Sparse Attention)
8+
- **Compressed KV Entries**:每 m 个 token 生成 1 个压缩 KV,但这个压缩 KV 实际会参考当前 block 的 m 个 token,以及前一个 block 的 m 个 token,一共 2m 个候选 token,然后用 learned softmax 权重加权求和(**Overlap**)
9+
- **DSA Strategy**:对于每个 query,选择 top-k 的压缩 KV entry 进行 attention 计算,k 是一个超参数
10+
- **MQA**:compressed kv entry 既是 K 又是 V,与 query 进行 MQA 计算
11+
- **Grouped Output Projection**:先投影到c * n_h 太大,直接 $R^{c n_h} -> R^d$ 的输出投影太贵,所以先按 head 分组,把每组从 $R^{c n_h/g}$ 压到 $R^{d_g}$,再把 g 个小向量拼起来投影回 $R^d$
12+
13+
![](../img/dsv4_csa.png)
14+
15+
16+
#### HCA(Heavily Compressed Attention)
17+
- **Compressed KV Entries**:每 m′ 个 token 压缩成一个 compressed KV,m′ 远大于 m,压缩程度更高,适用于更长的上下文
18+
> 第 i 个 compressed KV 是第 i 个 block 内 m′ 个 token 的加权和,不在考虑前一个 block 的 token
19+
20+
- **Shared KV MQA**:所有 query 共享同一组 compressed KV 进行 MQAttention 计算
21+
22+
- **Grouped Output Projection**:同 CSA
23+
24+
![](../img/dsv4_hca.png)
25+
26+
### Other Details
27+
#### Partial RoPE
28+
- 与 MLA 相同,只对最后 64 维度进行 RoPE
29+
- Attention Output 施加 position -i 的 RoPE
30+
> [!IMPORTANT]
31+
> 因为 compressed KV Entries 同时作为 K 和 V,实际上 V 不应该携带位置信息,所以这里对 attention output 施加 position -i 的 RoPE 来抵消掉 V 中的位置信息
32+
33+
#### Additional Branch of SWA
34+
- **Question**:CSA 和 HCA 的 compressed KV 是按 block 生成的,可能会导致 query token 看不到同一个 compressed block 里已经过去的 token,这会损失非常重要的近邻信息
35+
- **Solution**:增加一个 SWA 分支,合并 compressed KV + 最近 n_win 个未压缩 KV
36+
37+
#### Attention Sink
38+
- **Motivation**:对于一些 query 来说,所有的 compressed KV 都不相关,如果强行让它们参与 attention 计算,可能会引入噪声,反而降低性能
39+
- **Attention Sink 允许**:如果当前 head 觉得这些 KV 都没用,就把大部分概率质量给 sink。
40+
> 因为 sink 的 value 近似为 0,所以该 head 输出可以接近 0。
41+
42+
43+
## 推理流程
44+
1. Embedding:embed_tokens -> hidden_states * hc_mult(4)(通过 repeat 扩展)
45+
2. HC-Head:hc_head 把 4 个 sub-state 合并回 1 个 -> norm -> lm_head -> logits
46+
3. *每层 (DeepseekV4DecoderLayer)*
47+
- *HC-pre (attn)*:Sinkhorn 归一化把 hidden 拆成 4 个 sub-state
48+
- Input LayerNorm
49+
- *Attention (MQALayer)*
50+
- Q/KV 计算(支持 fused wqkv_a)
51+
- RoPE(Triton 融合 kernel)
52+
- (可选) C4Indexer:计算 indexer Q,量化,top-k 选择
53+
- (可选) Compressor:压缩 KV
54+
- FlashMLA:SWA cache + C4/C128 cache 注意力
55+
- O 投影
56+
- *HC-post (attn)*:合并残差
57+
- *HC-pre (FFN)*:再次 Sinkhorn 拆分
58+
- Post-attention LayerNorm
59+
- MoE:DeepseekV2MoE(hash-based / biased top-k 路由 + shared expert 融合)
60+
- *HC-post (FFN)*:合并残差
61+
62+
![](../img/dsv4_inference.png)
63+
64+
### KV Cache Pool
65+
#### SWA Pool
66+
每个 token 的 完整 KV(未压缩)存入此池。所有层共享同一个 SWA 池(每个 layer_id 对应 kv_buffer[layer_id])。FlashMLA kernel 读取 SWA cache 时只看最近 window_size=128 个 token。
67+
68+
```python
69+
swa_kv_pool = DeepSeekV4SingleKVPool(
70+
swa_size, swa_page_size=256, qk_nope_head_dim=448, qk_rope_head_dim=64,
71+
layer_num=layer_num, # 所有层都在这里存一份
72+
is_swa_pool=True, # 标记为 SWA 池
73+
)
74+
```
75+
#### C4 Compressed Pool — 4:1 压缩 KV Cache
76+
- **成员变量**:与 SWA Pool 同构,区别是 page_size=64 且 layer_num 不同。
77+
- **作用**:只存 compress_ratio=4 的层的压缩后的 KV。每 4 个相邻 token 压缩为 1 个 token,然后量化存为相同的 NopeFp8RopeBf16Pack 格式。
78+
- **FlashMLA 读取**:做 top-512 稀疏注意力,只取最近的 512 个压缩 token。
79+
80+
```python
81+
c4_kv_pool = DeepSeekV4SingleKVPool(
82+
c4_size, c4_page_size=64, # page_size = 256/4 = 64
83+
qk_nope_head_dim=448, qk_rope_head_dim=64,
84+
layer_num=c4_layer_num, # 只给 compress_ratio==4 的层
85+
)
86+
```
87+
88+
#### C128 Compressed Pool — 128:1 压缩 KV Cache
89+
- **成员变量**:与 C4 池同构,page_size=2。
90+
- **作用**:存 compress_ratio=128 的层的压缩 KV。每 128 个 token 压缩为 1 个。
91+
- **FlashMLA 读取**:做密集注意力(读所有压缩 token,不做稀疏选择)。
92+
- **与 C4 的关键区别**
93+
- C4 用重叠压缩(相邻 m 个 token 重叠(即 2m 个 token 计算),overlap=True, coff=2),取 top-512
94+
- C128 用非重叠压缩(overlap=False, coff=1),全量 dense attention
95+
```python
96+
c128_kv_pool = DeepSeekV4SingleKVPool(
97+
c128_size, c128_page_size=2, # page_size = 256/128 = 2
98+
...
99+
layer_num=c128_layer_num, # 只给 compress_ratio==128 的层
100+
)
101+
```
102+
103+
#### C4 Indexer Pool — 索引器 KV Cache
104+
- **存储格式**:每 page 存 `page_size * index_head_dim + page_size * num_scales_per_token * 4` 字节。与主 KV cache 不同,这里存的是 FP8 量化的 indexer K + float32 scale(通过 index_buf_accessor.SetKAndS 写入)。
105+
- **作用**:C4 层需要两步稀疏选择:
106+
1. 先通过 C4Indexer 模块计算 indexer Q(index_head_dim=128),与 indexer pool 中的 K 做 fp8_paged_mqa_logits(top-k logits 计算)
107+
2. 选出 top-k 的索引,再在 FlashMLA 中对 C4 压缩池做稀疏注意力
108+
109+
```python
110+
c4_indexer_kv_pool = DeepSeekV4IndexerPool(
111+
c4_size, c4_page_size=64, index_head_dim=128,
112+
layer_num=c4_layer_num,
113+
)
114+
```
115+
116+
#### Compress State Pool — 压缩中间状态 Ring Buffer
117+
118+
- **KVAndScore**: 是一个将 tensor 后半部分视为 score、前半部分视为 kv 的包装类。
119+
- **作用**: 压缩不是凭空从 hidden_state 直接算出最终 KV,而是分两步:
120+
1. 第一步:linear_bf16_fp32(x, wkv_gate) 算出原始 kv_score(compress_forward 的输入)
121+
2. 第二步:compress_forward 取 ring buffer 中累积的历史 kv_score + 当前 kv_score,配合 APE 做压缩
122+
- APE: Attention Position Embedding,控制每个 token 对 compressed output 的贡献
123+
1. Load 8 kv + 8 score entries from ring buffer (last ratio tokens)
124+
2. Add corresponding APE bias to each score: score[j][i] += bias[j][i]
125+
3. Safe softmax over 8 scores → attention weights
126+
4. Weighted sum Σ(kv[j] * weight[j]) → compressed output
127+
> ring buffer 存储的是未完成的压缩中间结果(kv + score),待到累积满 ratio 个 token 后才执行一次压缩输出。
128+
```python
129+
# 每层两个 compress state pool:一个给主压缩,一个给 indexer 压缩
130+
for ratio in compression_ratios:
131+
compress_state_pool = CompressStatePool(
132+
size, swa_page_size=256, ring_size=ring_size,
133+
overlap=(ratio==4), head_dim=512, ratio=ratio,
134+
)
135+
if ratio == 4:
136+
indexer_compress_state_pool = CompressStatePool(...) # head_dim=128
137+
```
138+
139+
## KV Cache 量化
140+
整体来说,DeepSeek V4 有 三类 KV cache 存储,每类有不同的量化策略:
141+
142+
| 存储类型 | 压缩率 | 量化方式 | 存储格式 |
143+
| ------------------------------ | ------------------------- | ------------------------------------- | --------------------------- |
144+
| SWA KV Cache (dense attention) | ratio=0, 不压缩 | FP8 nope + BF16 rope + UE8M0 scale | NopeFp8RopeBf16Pack |
145+
| C4 压缩 KV Cache | ratio=4, 重叠滑动窗口 | FP8 nope + BF16 rope + UE8M0 scale | NopeFp8RopeBf16Pack |
146+
| C128 压缩 KV Cache | ratio=128, 非重叠滑动窗口 | FP8 nope + BF16 rope + UE8M0 scale | NopeFp8RopeBf16Pack |
147+
| C4 Indexer K Cache | ratio=4 only | FP8 + per-tile FP32 scale (act_quant) | index_k_fp8 + index_k_scale |
148+
149+
![](../img/dsv4_quant.png)
150+
151+
### 存储格式(NopeFp8RopeBf16Pack)
152+
这是所有压缩模式共享的 KV 存储格式。
153+
- Nope 部分:FP8 量化的 K/V,448 维(7 个 head,每个 head 64 维)
154+
- RoPE 部分:BF16 量化的 RoPE 位置编码,64 维
155+
- Scale 部分:UE8M0 量化的 scale,用于从 Nope 的 uint8 还原到 float32,7 个 head 每个 head 1 个 scale
156+
```python
157+
@dataclass
158+
class NopeFp8RopeBf16Pack:
159+
k_nope_fp8: torch.Tensor # (N, 448) fp8
160+
k_rope_bf16: torch.Tensor # (N, 64) bf16
161+
scale_k_nope_ue8m0: torch.Tensor # (N, 7) uint8
162+
```
163+
每token存储: 448×1 + 64×2 + 7×1 + 1(pad) = 584 bytes
164+
165+

docs/sglang/hicache.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ HiCache 在同一个 TreeNode 上增加分层状态:
6868
本地已经没有可用数据,通常节点会被删除;L3 是否存在要实时查询 storage。
6969
```
7070
这就是分层 radix cache 的本质:tree 结构仍然按 token prefix 组织,但每个节点标记 KV 数据在哪一层。
71+
7172
---
7273

7374
### HiCacheController

mkdocs.yml

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,19 @@ extra_css:
185185
# - css/ai-summary.css
186186

187187
extra:
188+
giscus:
189+
repo: tom-jerr/tom-jerr.github.io
190+
repo_id: R_kgDONZrjcA
191+
category: General
192+
# Replace this with the real General category ID from https://giscus.app
193+
category_id: REPLACE_WITH_GENERAL_CATEGORY_ID
194+
mapping: pathname
195+
strict: 0
196+
reactions_enabled: 1
197+
emit_metadata: 1
198+
input_position: top
199+
theme: preferred_color_scheme
200+
lang: en
188201
social:
189202
- icon: /fontawesome/brands/github
190203
link: https://github.com/tom-jerr/
@@ -345,6 +358,7 @@ nav:
345358
- EAGLE: paperreadings/llm/speculative decoding/eagle.md
346359
- EAGLE2: paperreadings/llm/speculative decoding/eagle2.md
347360
- EAGLE3: paperreadings/llm/speculative decoding/eagle3.md
361+
- DFLASH: paperreadings/llm/speculative decoding/DFLASH.md
348362
- Database Systems:
349363
- Do not use MMAP in DBMS: paperreadings/db/storage/NoMMAP.md
350364
- Vector Search:
@@ -358,10 +372,13 @@ nav:
358372
# - Exploration with Global Consistency Using Real-Time Re-integration and Active Loop Closure: paperreadings/activeslam/Exploration with Global Consistency Using Real-Time Re-integration and Active Loop Closure.md
359373
- Blogs:
360374
- index: blogs/index.md
375+
- Models:
376+
- DeepSeek v4: paperreadings/llm/models/deepseekv4.md
361377
# - Diffusion Model:
362378
# - DiT Generate Model in SGLang: notes/diffusion/DiT Video Generate.md
363379
# - Masked Diffusion Model: notes/diffusion/Masked Diffusion LLM.md
364380
- SGLang 专题:
381+
- HiCache: sglang/hicahce.md
365382
- PD Disaggregation in SGLang: sglang/PD Disaggregation in SGLang.md
366383
- EAGLE2 in SGLang: sglang/Eagle2 in SGLang.md
367384
- 一条 Request 在 SGLang 的前世今生: sglang/一条 Request 在 SGLang 的前世今生.md

overrides/partials/comments.html

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,29 @@
11
{% if page.meta.comments is not defined or page.meta.comments != false %}
2-
<!-- Insert generated snippet here -->
3-
<script src="https://giscus.app/client.js" data-repo="tom-jerr/tom-jerr.github.io"
4-
data-repo-id="R_kgDONZrjcA" data-category="Announcements" data-category-id="DIC_kwDONZrjcM4Ck-PT"
5-
data-mapping="title" data-strict="0" data-reactions-enabled="1" data-emit-metadata="1" data-input-position="top"
6-
data-theme="preferred_color_scheme" data-lang="en" data-loading="lazy" crossorigin="anonymous" async>
7-
</script>
8-
<!-- Synchronize Giscus theme with palette -->
2+
{% set giscus = config.extra.giscus %}
3+
{% if giscus and giscus.repo and giscus.repo_id and giscus.category and giscus.category_id and not giscus.category_id.startswith("REPLACE_WITH_") %}
4+
<script
5+
src="https://giscus.app/client.js"
6+
data-repo="{{ giscus.repo }}"
7+
data-repo-id="{{ giscus.repo_id }}"
8+
data-category="{{ giscus.category }}"
9+
data-category-id="{{ giscus.category_id }}"
10+
data-mapping="{{ giscus.mapping | default('pathname') }}"
11+
data-strict="{{ giscus.strict | default('0') }}"
12+
data-reactions-enabled="{{ giscus.reactions_enabled | default('1') }}"
13+
data-emit-metadata="{{ giscus.emit_metadata | default('1') }}"
14+
data-input-position="{{ giscus.input_position | default('top') }}"
15+
data-theme="{{ giscus.theme | default('preferred_color_scheme') }}"
16+
data-lang="{{ giscus.lang | default('en') }}"
17+
data-loading="lazy"
18+
crossorigin="anonymous"
19+
async>
20+
</script>
921
<script>
1022
var giscus = document.querySelector("script[src*=giscus]")
1123

1224
// Set palette on initial load
1325
var palette = __md_get("__palette")
14-
if (palette && typeof palette.color === "object") {
26+
if (giscus && palette && typeof palette.color === "object") {
1527
var theme = palette.color.scheme === "slate"
1628
? "transparent_dark"
1729
: "light"
@@ -23,6 +35,10 @@
2335
// Register event handlers after documented loaded
2436
document.addEventListener("DOMContentLoaded", function () {
2537
var ref = document.querySelector("[data-md-component=palette]")
38+
if (!ref) {
39+
return
40+
}
41+
2642
ref.addEventListener("change", function () {
2743
var palette = __md_get("__palette")
2844
if (palette && typeof palette.color === "object") {
@@ -32,12 +48,17 @@
3248

3349
// Instruct Giscus to change theme
3450
var frame = document.querySelector(".giscus-frame")
35-
frame.contentWindow.postMessage(
36-
{giscus: {setConfig: {theme}}},
37-
"https://giscus.app"
38-
)
51+
if (frame && frame.contentWindow) {
52+
frame.contentWindow.postMessage(
53+
{giscus: {setConfig: {theme}}},
54+
"https://giscus.app"
55+
)
56+
}
3957
}
4058
})
4159
})
4260
</script>
61+
{% else %}
62+
<!-- Giscus is disabled until extra.giscus.category_id is updated with a real GitHub Discussions category ID. -->
63+
{% endif %}
4364
{% endif %}

tmp.md

Lines changed: 0 additions & 93 deletions
This file was deleted.

0 commit comments

Comments
 (0)