专栏DeepSeek-V4 模型结构·序列4 / 8
7 min学习

DeepSeek-V4 模型结构(3):序列维度(下),HCA、混合排布与零件

HCA 为什么压缩 128 倍之后不需要再选,CSA 和 HCA 为什么要交错,滑窗分支为什么必须有。K = V 让注意力输出带上了绝对位置,V4 用对输出做反向旋转来抵消,这一步从 RoPE 的乘法性质推出来。再讲 attention sink 从 softmax 的什么缺陷来,Q/KV 归一化为什么让 Muon 不再需要 QK-Clip,最后算清 1M 上下文的 KV cache 和 FLOPs。

目录9 节

对应论文 §2.3.2 到 §2.3.4,公式 (20) 到 (27)。前两篇讲了压缩和选择,这一篇收尾序列维度:HCA,两种注意力的排布,以及论文 §2.3.3 里「为了行文清晰而省略」的几个零件。零件里位置编码那一段最值得推一遍。

文本 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零件
你现在在这里:第 0、1 层和全部奇数层的 HCA,以及每层注意力里的滑窗分支。

HCA:压缩到不用选

压缩分支:1M token 只产生 8192 条,所以不用选128 条全部压缩条目71687168 → 1536RMSNorm1536 → 128 × 512逐头 RMSNorm+ 后 64 维 RoPE7168 → 512RMSNorm + RoPE(64)滑窗 cache最近 128 条压缩 128 → 1,不重叠:7168 → 512RMSNorm + RoPE(块首)softmax 在 128 个槽上压缩 cache条 × 512MQA 核心注意力128 头 × (128 +) 条K = V,每头一个 sink输出反旋转 RoPE(−t)分组投影 16 × (4096 → 1024)再 16384 → 7168
一层 HCA(Pro 维度)。和 CSA 相比只有两处不同:压缩率从 4 变成 128 且不重叠;没有 indexer,query 看全部压缩条目。

HCA 的压缩算子(公式 (20) – (23))和 CSA 是同一种,两处不同:压缩率 m=128m' = 128,不重叠。1M 上下文压完只剩 8192 条,128 个头对 8192 条 512 维向量做注意力,每 token 每层约 1 GFLOP,可以全看,所以没有 indexer。

为什么要有这样一层:CSA 每个 query 只看 1024 个块,是有选择的精读,选错了就看不到;HCA 每个 query 看全部历史的粗粒度摘要,是无遗漏的粗读。两者交错,每两层里总有一层保证全局信息不会因为 indexer 的误判而丢失。这和 Kimi K3 里「KDA 有损记忆 + MLA 无损检索」的 3

混合是同一类设计,只是 V4 的两种注意力都是 softmax、都有 KV cache,区别在粒度。

论文没有给 CSA / HCA 的消融,也没有解释为什么是 1

交错、为什么 Pro 的开头两层是 HCA 而 Flash 是纯滑窗。

排布

第 0 层:HCA(m′ = 128),FFN 是 hash-MoE第 1 层:HCA(m′ = 128),FFN 是 hash-MoE第 2 层:CSA(m = 4),FFN 是 hash-MoE第 3 层:HCA(m′ = 128),FFN 是 MoE第 4 层:CSA(m = 4),FFN 是 MoE第 5 层:HCA(m′ = 128),FFN 是 MoE第 6 层:CSA(m = 4),FFN 是 MoE第 7 层:HCA(m′ = 128),FFN 是 MoE第 8 层:CSA(m = 4),FFN 是 MoE第 9 层:HCA(m′ = 128),FFN 是 MoE第 10 层:CSA(m = 4),FFN 是 MoE第 11 层:HCA(m′ = 128),FFN 是 MoE第 12 层:CSA(m = 4),FFN 是 MoE第 13 层:HCA(m′ = 128),FFN 是 MoE第 14 层:CSA(m = 4),FFN 是 MoE第 15 层:HCA(m′ = 128),FFN 是 MoE第 16 层:CSA(m = 4),FFN 是 MoE第 17 层:HCA(m′ = 128),FFN 是 MoE第 18 层:CSA(m = 4),FFN 是 MoE第 19 层:HCA(m′ = 128),FFN 是 MoE第 20 层:CSA(m = 4),FFN 是 MoE第 21 层:HCA(m′ = 128),FFN 是 MoE第 22 层:CSA(m = 4),FFN 是 MoE第 23 层:HCA(m′ = 128),FFN 是 MoE第 24 层:CSA(m = 4),FFN 是 MoE第 25 层:HCA(m′ = 128),FFN 是 MoE第 26 层:CSA(m = 4),FFN 是 MoE第 27 层:HCA(m′ = 128),FFN 是 MoE第 28 层:CSA(m = 4),FFN 是 MoE第 29 层:HCA(m′ = 128),FFN 是 MoE第 30 层:CSA(m = 4),FFN 是 MoE第 31 层:HCA(m′ = 128),FFN 是 MoE第 32 层:CSA(m = 4),FFN 是 MoE第 33 层:HCA(m′ = 128),FFN 是 MoE第 34 层:CSA(m = 4),FFN 是 MoE第 35 层:HCA(m′ = 128),FFN 是 MoE第 36 层:CSA(m = 4),FFN 是 MoE第 37 层:HCA(m′ = 128),FFN 是 MoE第 38 层:CSA(m = 4),FFN 是 MoE第 39 层:HCA(m′ = 128),FFN 是 MoE第 40 层:CSA(m = 4),FFN 是 MoE第 41 层:HCA(m′ = 128),FFN 是 MoE第 42 层:CSA(m = 4),FFN 是 MoE第 43 层:HCA(m′ = 128),FFN 是 MoE第 44 层:CSA(m = 4),FFN 是 MoE第 45 层:HCA(m′ = 128),FFN 是 MoE第 46 层:CSA(m = 4),FFN 是 MoE第 47 层:HCA(m′ = 128),FFN 是 MoE第 48 层:CSA(m = 4),FFN 是 MoE第 49 层:HCA(m′ = 128),FFN 是 MoE第 50 层:CSA(m = 4),FFN 是 MoE第 51 层:HCA(m′ = 128),FFN 是 MoE第 52 层:CSA(m = 4),FFN 是 MoE第 53 层:HCA(m′ = 128),FFN 是 MoE第 54 层:CSA(m = 4),FFN 是 MoE第 55 层:HCA(m′ = 128),FFN 是 MoE第 56 层:CSA(m = 4),FFN 是 MoE第 57 层:HCA(m′ = 128),FFN 是 MoE第 58 层:CSA(m = 4),FFN 是 MoE第 59 层:HCA(m′ = 128),FFN 是 MoE第 60 层:CSA(m = 4),FFN 是 MoEMTP 层:纯滑窗注意力 + MoE02102030405060MTP第 0–2 层:hash-MoE■ CSA ×30(偶数层)■ HCA ×31(第 0、1 层 + 奇数层)■ MTP 层:纯滑窗注意力(config 里 compress_ratios 的最后一项)共 61 层,全部是 MoE;论文说的「前两层」是这里的第 0、1 层。第 60 层(最后一层)是 CSA。
Pro:第 0、1 层 HCA,之后偶数层 CSA、奇数层 HCA,第 60 层 CSA。

规则见总览篇。两点值得注意:

  • 最后一层是 CSA。 交错从第 2 层开始,61 是奇数,最后落在偶数层 60 上。
  • MTP 层是纯滑窗。 config 的 compress_ratios 最后一项是 0,官方代码 MTPBlock(args.n_layers + layer_id) 取到这一项。MTP 层预测下一个 token 的下一个,只用局部信息。

滑窗分支:为什么必须有

压缩有一个副作用:query 看不到自己所在的块。块 t/m\lfloor t/m \rfloor 要等到第 m(t/m+1)1m(\lfloor t/m \rfloor + 1) - 1 个 token 到齐才能压缩,为了严格保持因果性,query tt 只能看编号小于 t/m\lfloor t/m \rfloor 的块。HCA 的 m=128m' = 128 意味着最近的 127 个 token 可能完全不可见,而语言建模里最近的 token 恰恰最重要。

论文的解法是给 CSA 和 HCA 都加一条滑窗分支:每个 query 额外看最近 nwin=128n_{\text{win}} = 128 个 token 的未压缩 KV。这些 KV 就是上一篇第 2 步里那条 512 维向量,写进一个 128 条的环形 cache。在核心注意力里,滑窗的 128 条和压缩条目放进同一个 softmax,不是两个分支各算一次再加权(NSA 是后者)。官方代码里索引数组的前 128 项是滑窗位置、后面是压缩块编号,一次 sparse_attn 调用算完。

Flash 的前两层干脆只有滑窗分支。

Q 和 KV 的 RMSNorm

query 在展开成 128 头之后逐头做一次无权重的 RMSNorm,KV 那条 512 维向量也做 RMSNorm(有权重),都在 RoPE 之前。论文说目的是防止注意力 logit 爆炸。

这一步在 V4 里比通常更重要,因为它和优化器绑定:Muon 的更新是正交化过的,每一步对权重矩阵的所有奇异方向推同样的距离,WUQW^{UQ}WKVW^{KV} 会比 Adam 下更快地长大,logit 随之爆炸。Kimi K2 用 QK-Clip 事后压 WQ,WKW_Q, W_K 的范数;V4 因为 q 和 kv 都归一化了,logit 的尺度被结构锁死,不用 QK-Clip(论文 §2.4)。第 6 篇再展开。

部分 RoPE 与输出反旋转

这是零件里最值得推的一段。

部分 RoPE。 query 的每个头和那条 KV 向量都只在最后 64 维加 RoPE(qk_rope_head_dim = 64),前 448 维不带位置。这是 MLA 的老做法:位置信息只需要一小部分维度承载。

问题:K = V 让输出带上了绝对位置。RjR_j 是位置 jj 的旋转矩阵(作用在那 64 维上)。核心注意力对 rope 维的输出是

ot=jαt,jRjvj,αt,j=softmaxj((Rtqt)(Rjkj))=softmaxj(qtRjtkj).\mathbf{o}_t = \sum_j \alpha_{t,j}\, R_j \mathbf{v}_j, \qquad \alpha_{t,j} = \operatorname{softmax}_j\big((R_t \mathbf{q}_t)^\top (R_j \mathbf{k}_j)\big) = \operatorname{softmax}_j\big(\mathbf{q}_t^\top R_{j-t} \mathbf{k}_j\big).

注意力权重 αt,j\alpha_{t,j} 只依赖相对位置 jtj - t,这是 RoPE 的设计。但普通注意力里 V 不带 RoPE,输出是 αvj\sum \alpha \mathbf{v}_j,干净的内容加权和。V4 里 V 就是 K,K 已经被旋转过了,所以输出里每一项都带着 RjR_j,也就是绝对位置 jj。同一段内容出现在序列的不同位置,注意力输出会不一样,模型学到的东西就和绝对位置绑定了,长度外推时尤其麻烦。

解法:把输出反向旋转回来。 RoPE 是乘法的:RaRb=Ra+bR_a R_b = R_{a+b}Ra=Ra1=RaR_{-a} = R_a^{-1} = R_a^\top。对输出乘 RtR_{-t}

Rtot=jαt,jRtRjvj=jαt,jRjtvj.R_{-t}\, \mathbf{o}_t = \sum_j \alpha_{t,j}\, R_{-t} R_j \mathbf{v}_j = \sum_j \alpha_{t,j}\, R_{j-t}\, \mathbf{v}_j .

现在每个条目对输出的贡献是 vj\mathbf{v}_j 被旋转了 jtj - t,只和相对距离有关。论文说的「对每个 ot,i\mathbf{o}_{t,i} 的最后 64 维施加位置 i-i 的 RoPE」就是这一步。官方代码是 apply_rotary_emb(o[..., -rd:], freqs_cis, inverse=True)inverse 取共轭。

这个副作用甚至可以看成一个特性:普通注意力的输出完全不含位置,V4 的输出里带着「被取回的内容离我多远」,下游的 WOAW^{OA} 可以利用这个信息。

压缩条目的位置。 压缩条目由 2m2m(或 mm')个 token 合成,它的「位置」取块的第一个 token(代码里 freqs_cis[:cutoff:ratio]),RoPE 在压缩、RMSNorm 之后加,压缩时 Ca,CbC^a, C^b 本身不带旋转。所以压缩是在无位置的表示上做的,位置只在条目层面加一次。

两套 RoPE。 压缩层的 θ=160000\theta = 160000 加 YaRN(factor 16,原始长度 64K);纯滑窗层的 θ=10000\theta = 10000,不用 YaRN,因为它只看 128 个 token。indexer 用压缩层那套。

Attention sink

softmax 的一个结构性缺陷:权重必须加起来等于 1。即使当前 query 和所有条目都无关,它也得把注意力分出去,于是模型学会把没用的注意力堆到某个固定 token 上(通常是第一个 token),这就是 attention sink 现象。稀疏注意力下更麻烦:被选中的 1024 个块可能全是不相关的,但仍然要分完 1。

解法(公式 (27),和 gpt-oss 相同):给每个头一个可学习的 logit zhz'_h,加进分母但不加进分子:

sh,i,j=exp(zh,i,j)kexp(zh,i,k)+exp(zh).s_{h,i,j} = \frac{\exp(z_{h,i,j})}{\sum_k \exp(z_{h,i,k}) + \exp(z'_h)} .

等价于多了一个 value 为零的虚拟条目。当所有真实 logit 都远小于 zhz'_h 时,权重之和趋近 0,这个头这一步输出接近零。模型不再需要牺牲一个真实 token 来当垃圾桶。

官方 kernel 里它是 online softmax 结束后的一行:sum_exp[i] += exp(attn_sink[i] - scores_max[i])。128 个头共 128 个参数。

1M 上下文的账

KV cache。 一条压缩条目 512 维:64 维 rope 位存 BF16,448 维存 FP8,共 576 字节(忽略 scale)。indexer 的键 128 维,按 FP8 算 128 字节。

层类型每 token 摊到的字节层数(Pro)1M token 合计
CSA:(576 + 128) / 4176 B305.16 GiB
HCA:576 / 1284.5 B310.14 GiB
滑窗:128 条 × 576,不随长度增长常数614.4 MiB
合计≈ 5.4 KB≈ 5.3 GiB
GQA-8,头维 128,BF16,61 层8 × 128 × 2 × 2 B × 61244 KB / token,244 GiB @ 1MDeepSeek-V3.2(MLA + DSA)(512 FP8 + 64 BF16 + indexer 128 FP8) × 6146 KB / token,46 GiB @ 1MDeepSeek-V4-ProCSA 30 层 × (576 + 128)/4 + HCA 31 层 × 576/128 + 滑窗5.3 KB / token,5.3 GiB @ 1MDeepSeek-V4-FlashCSA 21 层、HCA 20 层3.7 KB / token,3.7 GiB @ 1MV4-Pro ≈ 基线的 2.2%,≈ V3.2 的 12%(论文 Figure 1 的口径分别是 2% 和 10%;差别来自 scale 与 indexer 精度的记法)
1M 上下文时每个 token 摊到的 KV cache。V4 的条目是 64 维 BF16 加 448 维 FP8,一条 576 B;CSA 每 4 个 token 一条,HCA 每 128 个 token 一条,滑窗固定 128 条不随长度增长。

对照:BF16 GQA-8、头维 128、61 层的基线是 250 KB/token,1M 时 244 GiB,V4-Pro 是它的 2%,和论文一致。DeepSeek-V3.2 每 token 每层 576 维 latent 加 128 维索引键,约 47 KB/token,V4 约是它的 11%,论文说 10%,差别在 scale 和 indexer 精度的记法。Flash 约 3.7 GiB。

FLOPs。 核心注意力每 token 每层(Pro):CSA 看 1152 条,2×128×1152×512=1512 \times 128 \times 1152 \times 512 = 151 MFLOP;HCA 在 1M 时看 8320 条,1.09 GFLOP。61 层合计约 38 GFLOP,加上 CSA 的 indexer 打分 30 层 × 4.3 GFLOP(FP4)。V3.2 在 1M 时每层要对 2048 个 token 做 128 头 × 192 维的 QK 和 128 × 128 的 PV,加上对全部 1M token 的 indexer 打分,后者是 61 层 × 2×64×106×12812 \times 64 \times 10^6 \times 128 \approx 1 TFLOP。V4 把 indexer 的 nn 除以 4、层数减半,是 27% 这个数的主要来源。

术语坑

  • nwinn_{\text{win}} 是 token 数,不是条目数。 滑窗里的 128 条是未压缩的原始 KV。
  • HCA 的「不重叠」和 CSA 的「重叠」是同一个 Compressor,用 compress_ratio == 4 判断。
  • sink 加在分母。 不是一个可学习的 key,是一个常数 logit。
  • RoPE 的反旋转只作用在 64 维上。 前 448 维本来就不带位置。
  • YaRN 只在压缩层。 original_max_position_embeddings = 65536factor = 16,对应训练时 64K 到 1M 的扩展。

下一篇

序列维度讲完了。第 4 篇进入深度维度:mHC。它和这一篇的 Q/KV 归一化、attention sink 一样,都是在给一个无界的量加上界。