专栏DeepSeek-V4 模型结构·系统8 / 8
7 min学习

DeepSeek-V4 模型结构(7):结构决定系统

连载收尾。前六篇的结构选择各自逼着系统做了一件事:两种压缩率和滑窗让 KV cache 分成「状态」和「分页」两类,块大小必须是 lcm(4, 128);压缩窗口会跨过上下文并行的边界,要一次点对点加一次 all-gather;磁盘前缀缓存里压缩条目好存、滑窗 KV 难存,三种策略换取不同的重算量;mHC 的 4 倍访存靠融合 kernel、重算和 DualPipe 压到 6.7%。

目录7 节

对应论文 §3.4.2、§3.4.3、§3.5,以及 §3.4.1 和 §3.1 的一句话。这一篇不讲新的结构,讲结构的后果:一个模块换掉之后,训练和推理系统里哪些东西跟着变。只挑和结构直接相关的部分,EP 通信重叠、TileLang、确定性内核、RL 基础设施不在范围内。

文本 token复制成 4 份,进入 4 条残差流(mHC 的 hc_mult = 4)残差流 ×4每条 d 维重复 29 次CSA 与 HCA 交错第 2 – 59 层,偶数层 CSA、奇数层 HCA每层注意力后接一个 MoEmHC:每个子层一次读、一次写 + 混:sigmoid,4 条流压成 1 条:2·sigmoid,输出分给 4 条流:Sinkhorn 20 轮,双随机三者都由当前 4 条流的内容动态生成logits最后一次只读:4 条流压成 1 条4 条流的最终状态 + 下一个 token 的 embeddingmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 读算子 A:对 4 条流做 sigmoid 加权求和,得到子层的 d 维输入读 AmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 BmHC 写算子 C 与混合矩阵 B:子层输出按 C 分到 4 条流,同时 4 条流按双随机矩阵 B 互相混合写 C · 混 B词表 129280Token Embedding129280 × 7168Heavily Compressed Attention:每 128 个 token 压成一个 KV 条目,不做稀疏选择HCA第 0、1 层 · m′ = 128前三层的 MoE 按 token id 查表决定 expert,权重仍由 router 打分Hash-MoE第 0–2 层的 FFN · 384 选 6Compressed Sparse Attention:每 4 个 token 压成一个条目,indexer 选 top-k 个条目,加 128 个滑窗条目CSA偶数层 · ×30 · m = 4sqrt(softplus) 打分,无辅助损失偏置路由,SwiGLU clamp,FP4 expertDeepSeekMoE384 选 6 + 1 shared每 128 个 token 压成一个条目,全部条目都看HCA奇数层 · ×29 · m′ = 128同上DeepSeekMoE384 选 6 + 1 shared最后一层是 CSACSA第 60 层 · 收尾同上DeepSeekMoE第 60 层最终归一化RMSNorm不与 embedding 共享权重LM Head→ 129280多 token 预测模块:一个完整的 decoder block,注意力只有滑窗分支,有自己的 4 流残差和输出头MTP 层 ×1滑窗注意力 + MoE · 自带 mHC
序列 · CSA序列 · HCA序列 · 滑窗深度 · mHC宽度 · DeepSeekMoE零件
这一篇覆盖注意力和 mHC 各自逼出来的系统工作。

混合注意力的 KV cache:两类条目,两种管理

PagedAttention 的假设是所有层的 KV cache 长得一样:每个 token 每层一条,按固定大小的页分配,页表随序列增长。V4 的注意力把这个假设破坏了三次:

  1. 不同层的条目粒度不同。 CSA 每 4 个 token 一条,HCA 每 128 个一条,滑窗层(Flash 的前两层、MTP)没有压缩条目。
  2. 同一层里有两种条目。 滑窗的 128 条未压缩 KV,和压缩条目。indexer 还有自己的 128 维索引键。
  3. 有些东西不是「缓存」而是「状态」。 还没凑满 mm 个的尾 token 和它们的压缩权重,要留在缓冲区里等下一批。滑窗 KV 是环形覆盖的,也有自己的淘汰规则。

论文的解法是把 KV cache 拆成两部分:

状态 cache:每个序列一块,大小固定,像 SSM 的状态滑窗段最近 128 个 token 的未压缩 KV,环形覆盖,每层一份CSA 尾段未满 4 个的尾 token(≤ 3 个)和它们的压缩权重,每 CSA 层一份HCA 尾段未满 128 个的尾 token(≤ 127 个)和压缩权重,每 HCA 层一份命中 / 淘汰策略和分页 cache 不同,所以单独管理经典 KV cache:每个序列多块,块内按层类型分区块 0原 token 0–127CSA:32 条 × 512+ indexer 32 条 × 128HCA:1 条 × 512每层各自的这一段块 1原 token 128–255CSA:32 条 × 512+ indexer 32 条 × 128HCA:1 条 × 512每层各自的这一段块 2原 token 256–383CSA:32 条 × 512+ indexer 32 条 × 128HCA:1 条 × 512每层各自的这一段块大小取个原 token 的整数倍,两种层的条目都对齐到块边界块满了才写入;每块内容只依赖它覆盖的 token,所以可以按块做前缀缓存和落盘
论文 Figure 6 的布局。左边是随序列走的「状态」,右边是随长度增长、可以分页和复用的压缩条目。

状态 cache。 滑窗段和未满块的尾 token 只依赖「当前位置」,大小固定,不随长度增长,行为像一个状态空间模型的状态。于是预分配一个固定大小的池,每个序列分一块,用完归还。它不进 PagedAttention 的页表。

经典 cache。 压缩条目随长度增长,按块分页。块大小的约束来自内核:高性能注意力内核假设每个块有固定数量的条目 BB,对应 CSA 的 BmB \cdot m 个原 token、HCA 的 BmB \cdot m' 个。要让两种层共用一套页表,一块覆盖的原 token 数必须同时是 4 和 128 的倍数,即 lcm(m,m)=128\operatorname{lcm}(m, m') = 128 的整数倍。一块 128 个原 token 在 CSA 层是 32 条、在 HCA 层是 1 条。稀疏注意力内核和 cache 布局一起设计,不同层每块条目数不同也不掉性能。

上下文并行:压缩窗口会跨过 rank 边界

训练时上下文并行(CP)把一条长序列切到多个 rank,每个 rank 拿连续的 ss 个 token。压缩注意力给它添了两个麻烦:

  • 压缩边界和 rank 边界不对齐。 一个压缩窗口的 mm 个 token 可能一半在 rank ii、一半在 rank i+1i+1。CSA 的重叠窗口宽 2m2m,跨得更多。
  • 各 rank 的压缩条目数不同。 训练样本是多条序列打包的,每条序列独立压缩、不足 mm 的尾巴丢弃,所以一个 rank 压出来的条目通常少于 s/ms/m,而且各 rank 不等。
rank个 token本地未压缩 KVrank个 token本地未压缩 KV阶段一:点对点rank把最后条发给本地压缩固定产出条(含 padding)本地压缩固定产出阶段二:all-gather收齐所有 rank 的压缩条目select-and-pad共 cp_size ·条,padding 放尾部
CP 下的压缩。压缩窗口可能跨过两个 rank 的边界,所以每个 rank 先把最后 条未压缩 KV 发给下一个 rank,各自压成固定长度,再一次 all-gather 拼成完整的压缩序列。

两阶段通信:先点对点,rank ii 把最后 mm 条未压缩 KV 发给 rank i+1i+1,后者把收到的和自己的 ss 条一起压,固定产出 s/m+1s/m + 1 条(多的是 padding);再 all-gather 收齐所有 rank 的条目,一个融合的 select-and-pad 算子把有效条目排在前面、padding 放尾部,得到总长 cp_sizes/m\text{cp\_size} \cdot s/m 的完整压缩序列。之后 HCA 和 indexer 的可见范围可以按规则预先算出,CSA 的核心注意力由 top-kk 索引显式指定。

对照 Kimi K3 的 KDA 上下文并行:那边传的是固定大小的状态矩阵,难点在 delta rule 让状态不能直接相加;这边传的是 mm 条 KV 和压缩后的条目,难点在窗口对齐。两者都比 softmax 注意力的 CP 传整段 KV 便宜得多。

磁盘前缀缓存:压缩条目好存,滑窗 KV 难存

共享前缀的请求(同一个 system prompt、同一份文档)不想重复 prefill,做法是把 KV cache 落盘。V4 的两类条目要分开处理。

压缩条目。 直接全存。命中前缀时读回压缩条目,用到最后一个完整的压缩块为止;尾部不满一块的 token 要重算,因为未压缩的 KV 没有存。每 4 个 token 才 704 字节(CSA 含索引键),存储便宜。

滑窗 KV。 每个 token 每层一条 576 字节,不压缩,体积约是压缩条目的 8 倍。论文给了三种策略:

策略存什么命中时代价
全存所有 token 的滑窗 KV只读最后 128 个 token 的零重算,但写多读少,SSD 不友好
周期 checkpointpp 个 token 存一次最近 128 条读最近的 checkpoint,重算之后的尾 tokenpp 可调,在存储和计算之间连续折中
不存什么都不存重算最后 128L128 \cdot L 个 token零存储

第三种为什么只要重算 128L128 \cdot L 个 token:第 ll 层某个 token 的滑窗 KV 只依赖第 l1l-1 层最近 128 个 token 的输出,而第 l1l-1 层的输出又只依赖再往前 128 个。LL 层往回推,最后 128 条滑窗 KV 的依赖范围是 128L128 \cdot L 个 token,Pro 是 7808 个。有了落盘的压缩条目,这些 token 的注意力可以照常算,不需要更早的东西。这是滑窗结构给的一个便宜的重建路径。

按部署场景选策略。

mHC 的 6.7% 是怎么压出来的

第 4 篇说过 mHC 的算术量可以忽略,代价在访存、激活内存和流水线通信。V4 论文 §3.4.2 和 mHC 论文 §4.3 讲了三件事:

融合 kernel。 不融合时一个子层要把 4 条流读好几遍:生成系数读一遍,读算子读一遍,混合再读一遍。做法是三个专用 kernel:一个算 24 个系数(把 RMSNorm 的除法挪到矩阵乘之后,前向的两次扫描合成一个 kernel);一个做读;一个把「写 + 混 + 残差合并」合成一次,读的元素从 (3n+1)d(3n+1)d 降到 (n+1)d(n+1)d,写从 3nd3nd 降到 ndnd。Sinkhorn 的 20 轮在一个 kernel 里做,反向 kernel 在片上重算整个迭代。这些 kernel 用 TileLang 写。

重算。 前向后丢掉 mHC kernel 的所有中间结果,反向时不带子层地重跑 mHC。每 LrL_r 个子层只需要存进入时的 4 条流,其余临时重建;V4 的措辞是「重算大部分层间隐状态和所有归一化后的层输入,但不重算计算密集的操作」。最优块大小 LrnL/(n+2)L_r^* \approx \sqrt{nL/(n+2)},和流水线每 stage 的层数差不多,于是重算块直接对齐 stage 边界,重算也不依赖流水线通信,因为每个 stage 的入口激活本来就在本地。

DualPipe。 stage 之间传的激活是 4 倍。DualPipe 的 1F1B 调度被改过,把 mHC 的通信和重算与相邻的计算重叠;FFN 层的写+混 kernel 放在一条高优先级的计算流上,注意力层不用持久化 kernel 以便被抢占。

合起来,mHC 的墙钟开销是重叠后 1F1B 阶段的 6.7%。

其余一句话带过

  • Muon 的分布式实现(§3.4.1):Newton–Schulz 要对完整矩阵做乘法,而参数是分片的。论文说做了高效实现,细节不多。
  • EP 通信重叠(§3.1):去掉节点限制路由之后 all-to-all 的目标节点更多,V4 用细粒度的通信计算重叠把延迟藏起来,并开源了一个 mega-kernel。
  • 激活 checkpoint(§3.4.4):用 TorchFX 追踪计算图,按张量粒度标注要重算的中间结果,自动找最小重算子图。mHC 的重算用的就是这套机制。
  • 确定性内核(§3.3):batch-invariant 的内核库,让训练和推理逐位可复现,对 RL 里的 on-policy 校验有用。

连载收尾

八篇下来,DeepSeek-V4 的结构可以压成三句话:

  1. 序列维度:把 KV 压缩成每 4 个或 128 个 token 一条,在压缩块上做稀疏选择,用 K = V 的单头 MQA 和分组输出投影把 512 维的大头压回合理的代价;滑窗分支补最近的 128 个 token,输出反旋转补回相对位置。1M 上下文的 KV cache 约 5 GB。
  2. 深度维度:残差流拓宽成 4 条,流之间的混合矩阵约束成双随机的,复合映射的增益从 3000 压回 1.6。
  3. 宽度和优化器:DeepSeekMoE 的骨架不动,改打分函数、前三层 hash 路由、SwiGLU 截断;Muon 加混合 Newton–Schulz,Q/KV 归一化让它不需要 QK-Clip。

和同期的 Kimi K3 放在一起看更有意思:两家在序列维度上走了不同的路(K3 用线性注意力换掉 KV cache,V4 用压缩把 KV cache 缩小 50 倍),在深度维度上分别做了「深度上的线性 RNN」和「深度上的 softmax 注意力」,在宽度维度上都被大规模训练的离群激活逼出了截断,在优化器上都选了 Muon 但用不同的办法管住注意力 logit。两份技术报告对照着读,比单独读任何一份都清楚。

资料

  • DeepSeek-V4 报告 §3:arXiv 2606.19348
  • mHC 论文 §4.3 的系统部分:arXiv 2512.24880
  • DualPipe:DeepSeek-V3 报告 §3.2;TileLang:arXiv 2504.17577
  • 官方推理实现里 KV cache 的组织:inference/model.pyAttention.__init__kv_cache 的形状是 window_size + max_seq_len // compress_ratio)和 Compressorkv_state