DeepseekV4ForCausalLM model_type · deepseek_v4 HCA 31 层 CSA 30 层 hash 路由 × 3 mHC × 4 MTP × 1

DeepSeek-V4-Pro 架构解剖

61 层文本骨干,隐藏维 7168,每层一条 512 维的 MQA latent。 注意力按 compress_ratios 分成两种并严格交替:31 层 HCA(128 个 token 压一行,全看) 与 30 层 CSA(4 个 token 压一行,indexer 选 top-1024)。 残差是 4 路宽的 mHC,前 3 层的专家不由门控选而是查哈希表,末尾挂 1 层 MTP。

1598.8 B逻辑参数
49.78 B骨干单 token 激活
61 + 1骨干层 + MTP
7168hidden_size
384 / 6专家数 / 每 token
1024index_topk
5.34 KiBKV / token
1 Mmax_position

阅读顺序

先看整网,再拆开大块,最后看特殊模块

主结构里每个模块都标了进出形状。复杂的注意力画成一块,只写它吃什么、吐什么; 紧接着的一节把这块按模块拆开。主图里同样收成一块的 mHC、MoE,以及挂在骨干外面的模块,各自再有一张结构图。

先分清对象

一份 config 里只有两个开关,但它们决定了全部

架构名是 DeepseekV4ForCausalLM,model_type 为 deepseek_v4,纯文本、无视觉塔。 真正决定这个模型长什么样的是 compress_ratios 和 num_hash_layers 两个字段: 前者逐层给出「多少个连续 token 压成一行 KV」,后者说前几层的专家不按门控选。 下面的形状与字节来自 64 个 safetensors 分片的头部(共 145 116 个张量): 逻辑参数按权重元素计,FP4 的一个 I8 字节算 2 个参数,.scale 不进参数量。

compress_ratios:0 / 4 / 128

  • 128 = HCA(31 层):128 个 token 压成一行,不做 top-k,把所有压缩行全看一遍。
  • 4 = CSA(30 层):4 个 token 压成一行,再由 indexer 从中选 index_topk = 1024 行。
  • 0 = Dense:只有滑窗。骨干里一层都没有,只有 MTP / 草稿层是这一档。
  • 排布是严格交替的:层 0、1 是 HCA,之后 CSA 与 HCA 一层隔一层,直到层 60(CSA)。

其余三件事

  • 每一层都另有自己的 128 token 滑窗,与压缩行是两份不同的 KV。
  • 残差从 embedding 起就是 [T, 4, 7168];每个子层用 FP32 的 hc_*_fn [24, 28672] 预测 pre / post / 4×4 混合,Sinkhorn 20 步。
  • MoE 固定 384 选 6 加 1 个共享专家;层 0 / 1 / 2 的选择来自 tid2eid[input_ids],而不是门控 logits。

和 V4.1-Flash 是同一套字段,不同取值

V4.1-Flash 的 compress_ratios 只允许 0/1/2,并引入了 KV owner: 40 层里只有 4 层写压缩 KV,其余层借用。V4-Pro 没有 owner 这一层间接 —— 每个 HCA/CSA 层都自己压、自己写、自己读,CSA 层还各自跑一次 top-1024。 两代共有的是:MQA 单 KV 头、512 维 latent、尾 64 维 RoPE、分组输出 LoRA、attn_sink、4 路 mHC、sqrtsoftplus 路由。

V4-Pro 与 V4-Pro-0813

两份 checkpoint 的骨干完全相同。差别只在尾巴:DeepSeek-V4-Pro 是 1 层 MTP (num_nextn_predict_layers = 1),-0813 把它换成 3 层 DSpark (dspark_block_size = 5,dspark_markov_rank = 512,输入取层 58/59/60), compress_ratios 也相应多出两个 0。


主结构 · 层调度

HCA 和 CSA 一层隔一层,61 层里没有一层是稠密的

compress_ratios 一共 62 项:前 61 项给骨干,最后一项给 MTP 层。 骨干的排布是 [128, 128, 4, 128, 4, …, 128, 4] —— 层 0 和层 1 连着两个 HCA, 从层 2 起 CSA 与 HCA 严格交替,层 60 以 CSA 收尾。 数下来是 31 个 HCA 和 30 个 CSA,一个 Dense 都没有。

attention
routing
02102030405060MTP
HCA · ratio 128 · 31 层 · 不选,全看 CSA · ratio 4 · 30 层 · indexer 选 top-1024 hash 路由 · 层 0–2 MTP · ratio 0 · 只有滑窗
层ratio类型压缩行这一层读什么路由
0, 1128HCA128 token → 1 行128 滑窗 + 全部 (pos+1)/128 行hash
24CSA4 token → 1 行(重叠)128 滑窗 + top-1024hash
3, 5, 7 … 59128HCA128 token → 1 行128 滑窗 + 全部 (pos+1)/128 行sqrtsoftplus + bias
4, 6, 8 … 604CSA4 token → 1 行(重叠)128 滑窗 + top-1024sqrtsoftplus + bias
mtp.00Dense不压缩只有 128 滑窗sqrtsoftplus + bias

1 M 上下文下,两种层看到的行数天差地别

同样是 1 048 576 个 token 的历史:HCA 层把它压成 8192 行,然后一行不落全读; CSA 层把它压成 262 144 行,然后只读 indexer 选中的 1024 行。 两者读的行数是 8192 对 1024,但前者的每一行代表 128 个 token、后者代表 4 个。 粗看全局和细看局部,这就是交替排布要买的东西。

滑窗是第三份,和上面两份都不一样

不管哪种层,都另有一条 sliding_window = 128 的环,存的是没压过的原始 latent。 所以一个 CSA 层的 query 实际看到的是「最近 128 个原始 token」加「从历史里挑出来的 1024 行压缩块」, 两份 KV 物理上在同一个池子里,但一个按 slot 寻址、一个按 block 寻址。


主结构

从 token 到 logits,注意力是其中一块

61 层里每个子层的进出形状都标在这张图上。注意力子层是一块:进去是 [T, 7168],出来还是 [T, 7168]。 HCA、CSA、Dense 三种内部结构在下一节拆开;mHC、compressor、indexer、哈希路由和 MTP 各自单独成图。

图 1 · 文本前向的形状

token_ids [T] embed.weight [129280, 7168] BF16 hidden [T, 7168] 展成 4 路残差 [T, 4, 7168] pre_mix [T, 4] FP32 初值 (1, 0, 0, 0) 61 层,每层两个子层缝 注意力子层 hc_pre → [T, 7168] → attn_norm Attention 大块 · 进出都是 [T, 7168] hc_post → 回到 [T, 4, 7168] FFN 子层 hc_pre → [T, 7168] → ffn_norm 384 选 6 + 1 共享 → [T, 7168] hc_post → 回到 [T, 4, 7168] 层内三份 KV:本层 128 行滑窗(未压缩)· 本层自己的压缩行(HCA 1/128 或 CSA 1/4)· CSA 另有一份只用来打分的 index KV 层 0 / 1 / 2 的 FFN 用哈希表选专家;层 0 的两个 mHC 缝不能融合,其余层的 post + pre 合成一次发射 层类型按 compress_ratios 交替:0,1 是 HCA,之后 CSA 与 HCA 一层隔一层,层 60 是 CSA 残差四路宽度自始至终不变,中间没有任何一步把它收掉再展开 收尾 hc_head_fn [4, 28672] FP32 [T, 4, 7168] → [T, 7168] norm.weight [7168] 最后一次 RMSNorm head.weight [129280, 7168] logits [T, 129280] 与 embed 不共享权重 MTP 层不在这条链里:它接的是「这一步的隐藏态」和「下一个 token 的 embedding」,自己再跑一遍注意力和 MoE,共用这里的 embed 与 head。 单个 token 在这条链上碰到的权重合计 49.78 B —— 其中 24.18 B 是那 6 个路由专家,19.47 B 是 61 层注意力。

Attention 块

这块里面,61 层走的是同一条投影通路

主结构里的注意力子层拆开之后是下面这条链。HCA、CSA、Dense 的前后段完全相同, 差别只在中间:有没有 compressor、有没有 indexer、读哪些行。 那三件东西在下一组图里按层类型展开,compressor 与 indexer 再各自单独拆开。

Attention 块 · 共用通路

输入是 hc_pre 再过 attn_norm 的 [T, 7168]。下面按未切 TP 的逻辑形状画。 hidden [T, 7168] BF16 attn_norm 之后 单 KV 头,head_dim 512 wqkv_a · 一次 GEMM [2048, 7168] FP8 128×128 2048 = 1536 + 512 输出 [T, 2048] 按列切开 q_lora [T, 1536] kv [T, 512] 磁盘上仍是 wq_a 与 wkv q_norm + kv_norm · 一次发射 q_norm 无单独权重,融合量化成 FP8 qr [T, 1536] + kv_normed [T, 512] kv_norm.weight [512] wq_b → 逐头 RMSNorm → RoPE wq_b [65536, 1536] FP8 Q [T, 128, 512] 逐头 RMSNorm 无权重;尾 64 维 RoPE 滑窗 · 每种层都有 未压缩 latent [512] 本层 ≤ 128 行 和压缩行不是同一份 KV window_size = 128 压缩行 · 按层类型 HCA:全部可见行,1/128 CSA:indexer 的 top-1024 Dense:没有这一项 见下面三张变体图 分组输出 出 [T, 128, 512] sink [128] 只进分母 wo_a:16 × [1024, 4096] wo_b [7168, 16384] softmax 温度 1/√512。压缩层的 RoPE 走 YaRN(θ = 160000,factor 16);Dense / MTP 用普通 θ = 10000。 wo 之后回到 [T, 7168],交给 hc_post 摊回 [T, 4, 7168]。每个 HCA/CSA 层自己压、自己写、自己读,没有 V4.1 那种 KV owner。
下面三张图是这块的三种实例。HCA 多一个 compressor;CSA 多两个 compressor 再加 indexer;Dense 两侧都没有,骨干里一层都不是 Dense。

Attention 块 · 三种内部结构

Q 和输出投影三种一模一样,差别全在中间那段 KV

上一张图是主结构里那块 Attention 的内部。下面三张是它的三种实例,前后段相同:wqkv_a 一次 GEMM 出 q_lora [T, 1536] 与 kv [T, 512], 一个融合 kernel 同时做逐头无权重的 Q RMSNorm、带权重的 KV RMSNorm 和尾 64 维的 GPT-J 交错 RoPE(decode 时连滑窗写入也折进这一次发射); 输出端一律是 wo_a 16 组分组 LoRA 加 wo_b。 不同的只有三件事:有没有 compressor、有没有 indexer、读哪些行。

图 2a · HCA · 31 层 · ratio = 128 · 压得狠,但一行不落

共用前段:wqkv_a [2048, 7168] FP8 → q_lora [T, 1536] + kv [T, 512] → q_norm(融合量化) → wq_b [65536, 1536] → Q [T, 128, 512] → 逐头 RMSNorm(无权重)+ 尾 64 维 RoPE 共用后段:attn_sink [128] FP32 只加在 softmax 分母 → 输出 [T, 128, 512] → 逆 RoPE → wo_a 16 × [1024, 4096] → wo_b [7168, 16384] → [T, 7168] 额外模块:一个 compressor wkv / wgate 各 [512, 7168] BF16 overlap = False,组与组不重叠 ape [128, 512] FP32 位置编码加在门上 128 个 token 一组做 FP32 softmax 加权 没有 indexer · 额外权重 7.41 M 写入 ① 本层 128 行滑窗(未压缩 latent) ② 每 128 个 token 一行压缩 KV 写进 HCA 信封区,一行 [512] 未满的一组留在 FP32 ring(128 槽) YaRN RoPE,θ = 160000,factor 16 读取 · 不做任何选择 滑窗 ≤ 128 行 加上全部可见压缩行 (pos+1)/128 1 M 上下文 = 8192 行,全读 kv_indices_hca / kv_indptr_hca 没有 top-k,也就没有打分 kernel HCA 是「全局低分辨率」那一路:它不需要知道哪一段历史重要,因为它把每一段都看了,只是每一行代表 128 个 token。 代价是读的行数随上下文线性增长 —— 只是斜率被 128 除过,1 M 上下文也才 8192 行。

图 2b · CSA · 30 层 · ratio = 4 · 压得细,但只挑 1024 行

共用前后段同图 2a。下面三栏是 CSA 独有的部分 —— 注意它有两个 compressor。 额外模块:两个 compressor + indexer 主 compressor wkv / wgate 各 [1024, 7168],ape [4, 1024] overlap = True:一行 reduce 的是 8 个 token indexer 自己的 compressor wkv / wgate 各 [256, 7168],head_dim 128 逐行量化后写进另一个池 额外权重 31.4 M(比 HCA 多 24.0 M) 写入(三处) ① 本层 128 行滑窗 ② 每 4 个 token 一行主压缩 KV 写进 CSA 信封区,一行 [512] ③ 每 4 个 token 一行 index KV 另一个池子,一行 [128] FP4 默认(gfx942 仍 FP8) 读取 · 先打分再取行 滑窗 ≤ 128 行 加上 indexer 选出的 top-1024 候选池是 (pos+1)/4 行 1 M 上下文 = 262 144 选 1024 选出的是 seq-local 行号, 由 csa_translate_pack 翻成物理偏移 两个 compressor 不是冗余:主 compressor 的输出是真正要被注意力读的 KV,indexer 的那个只用来打分,所以它可以压到 128 维、可以更激进地量化。 overlap = True 是 CSA 独有的:每行池化的窗口是 2 × ratio = 8 个 token,相邻两行重叠一半;同一个 token 在自己那组用投影的后 512 列、在下一组用前 512 列 —— 这就是宽度翻倍的来源。

图 2c · Dense · ratio = 0 · 骨干里一层都没有,只有 MTP / 草稿层

共用前后段同图 2a,但 RoPE 换成普通的 θ = 10000,不走 YaRN —— 这一层本来就看不远。 额外模块 没有 compressor 没有 indexer 额外权重 0 写入 只写本层 128 行滑窗 信封区里连一行都不占 读取 滑窗 min(pos+1, 128) 行 kv_indices_swa / kv_indptr_swa KV 长度与上下文无关 草稿层要的是便宜和短:它只负责在已经定好的上下文上往前猜几个 token,历史由被它验证的骨干负责。
类型层数额外权重 / 层压缩行 / token打分1 M 上下文下一个 query 读的行数
HCA317.41 M1 / 128无≤128 + 8192
CSA3031.40 M1 / 4 ×2 个池子top-1024≤128 + 1024
Dense0(骨干)00无≤128

Attention 块 · 合并

1152 行不是两次注意力,是一次 softmax

滑窗行和压缩行被拼进同一份 CSR 索引表,交给同一个 kernel。 没有门控,没有加权合并,两类行在 softmax 里平等竞争。 能这样做的前提是它们在池子里结构同构:都是一条 [512] 的 latent, 而且 K 和 V 是同一行 —— kernel 只收一个 KV 张量。

图 2d · 一个 decode token 的完整形状(CSA 层,pos = 20000)

Q · 1 个 token [1, 128, 512] 128 个 query 头 每头 512 维 尾 64 维已在 pos 20000 旋转 KV · 1 个 KV 头(MQA) [1152, 512] 1024 行压缩(indexer 选的) + 128 行滑窗(未压缩) K = V,同一行,不是两个张量 128 个头共用这同一份 1152 行 所以索引表是 per-token 的,不是 per-head indexer 把 64 个打分头加权求和成一个 logit, 也是因为选择必须全头共用 KV 只读一次,摊给 128 个头 —— MQA 的全部好处 一次 softmax,对第 h 个头 S = {1024 压缩行} ∪ {128 滑窗行},共 1152 项 s_j = 512^-0.5 · ⟨ q_h , k_j ⟩ out_h = Σ(j∈S) exp(s_j)·v_j ÷ ( exp(sink_h) + Σ(j∈S) exp(s_j) ) sink 只进分母 没有对应 value,概率质量可以漏掉 分数矩阵 [128, 1152] 128 × 1152 次长度 512 的点积 ≈ 75.5 M MAC(QK)+ 同样多(AV) KV 读取 1152 × 640 B ≈ 737 KB(FP8 池) 出来之后 out [1, 128, 512] → 逆 RoPE(在 query 自己的位置上转回去)→ wo_a 16 组 × [1024, 4096] → [1, 16384] → wo_b [7168, 16384] → [1, 7168] 逆 RoPE 是必须的:K = V 且 V 是旋转过的,输出会带进绝对位置分量。mxscale 路径把它和 wo_a 的分组量化融成一发。
1152 行不等于 1152 个 token。滑窗一行 = 1 个 token;压缩一行 = 8 个 token 的加权平均,且相邻行相隔 4 个 token。 这 1024 行是从 5000 行候选里挑的,覆盖的源 token 最多约 4096 个,且不连续 —— 所以这个 query 看到的历史远不止 1152 个 token,只是分辨率分两档。

三种层只有行数不同

Q 和 KV 的维度三种层完全一样:[1, 128, 512] 对 [N, 512]。 变的只是 N —— CSA 是 ≤1024+128,HCA 是「全部 (pos+1)/128 行 + ≤128」(1 M 上下文 = 8192+128), Dense 只剩 ≤128。同一个 kernel、同一条输出通路。

prefill 是两源,仍是一次 softmax

prefill 走 sparse_attn_v4_paged_prefill,收两套 indptr:prefix(池子里前面 chunk 的滑窗 + 压缩行) 和 extend(本 chunk 还没进池的 K)。kernel 内部按 flash 的方式分段算再用 log-sum-exp 归并, 数学上仍是对 prefix ∪ extend 这一个集合的一次 softmax。


特殊模块 · compressor

HCA 一个,CSA 两个,宽度差在 overlap

compressor 接在 Attention 块的 hidden 侧面,不经过 wqkv_a。 HCA 把 128 个 token 压成一行,组与组不重叠。CSA 的主 compressor 打开 overlap: 每行池化的窗口是 2 × ratio = 8 个 token,相邻两行重叠一半,所以投影从 512 宽变成 1024。 indexer 自己还有一个更窄的 compressor,它的输出只用来打分。

compressor · HCA、CSA 主通路、CSA 打分通路

HCA · ratio 128 wkv / wgate [512, 7168] BF16,ape [128, 512] FP32 overlap = False 128 个 token 一组 softmax 写出一行 [512] 写进 HCA 信封区 未满一组留在 FP32 ring ring 深 128 没有 indexer · 31 层各一份 CSA 主 compressor · ratio 4 wkv / wgate [1024, 7168] BF16,ape [4, 1024] FP32 overlap = True 一行 reduce 8 个 token 相邻行重叠 4 个 token 写出一行 [512],供注意力读 同一个 token 在本组用后 512 列 在下一组用前 512 列 这就是宽度翻倍的来源 indexer 的 compressor wkv / wgate [256, 7168] BF16,head_dim 128 逐行 amax → ue8m0 → e4m3 (rotate 标志目前不生效) 每 4 token 一行 [128] 写进另一份 index 池 注意力不读这行 只给 top-1024 打分 30 个 CSA 层各一份 额外权重:HCA 层 7.34 M,CSA 层在主 compressor 之外再加 indexer 那套,合计比 HCA 多 24.0 M。 Dense(只有 MTP / 草稿)没有这三块里的任何一块。组内累加指定在 FP32。

图 3b · 一行压缩 KV 是怎么算出来的(CSA,边界 pos = 7,写出 row 1)

① 每个 token 的投影:1024 宽 = 两种角色各 512 wkv_gate(x) → kv [T, 1024] 与 score [T, 1024] 列 [0:512] = 当「旧组成员」时用 列 [512:1024] = 当「新组成员」时用 这一步没有 norm、没有 RoPE,是原始 GEMM 输出 ② 窗口是 8 个源位置,角色决定读哪半列 k_static0 1 2 34 5 6 7 源位置 s0 1 2 34 5 6 7 col_off0 0 0 0512 512 512 512 ape 行0 1 2 30 1 2 3 旧组(上一行也用过它们) 新组(这一行才闭合) window_len 决定前几个从 FP32 状态环取、后几个从本次输入取 ape:加在门上,只加一次 score_k = score + ape[k%4][col_off:] 写环时就已经加好(ape[pos%4]) 所以 kernel 只对本次输入行加 4 个组内位置 × 2 种角色 = 8 个偏置向量 ③ 512 个独立的 softmax —— 不是每个 token 一个标量权重 系数表 w[k][d] 是 [8, 512]:每一列沿 8 个位置归一化,列与列之间彼此独立 out[d] = Σ(k<8) softmax_k( score[s_k][off_k+d] + ape[k%4][off_k+d] ) · kv[s_k][off_k+d] 同一个 token 可以在通道 0 上占 0.31、在通道 1 上只占 0.02 —— 没有哪个标量代表「这个 token 的重要性」 在线 softmax,FP32;除以 w_acc 说明是加权平均,padding 位 score = -inf 分子分母都不进 ④ 归一与旋转,轴不一样 RMSNorm:沿全部 512 维(除以 head_dim) RoPE:只转 448–511,GPT-J 相邻配对 位置取组首 (pos//4)×4 = 4,不是边界 7 ⑤ 落盘:ci = pos//4 → block_table[bid][ci//64] 的第 ci%64 行。主通路写 BF16 1024 B(或 FP8 512+128 B);indexer 通路逐行量化后写另一个池。整条链在一个 kernel 的寄存器里跑完,只有最后这次 scatter 写内存。
这张图画的是 CSA 主 compressor。indexer 的那个逐字相同,只是 head_dim 128、投影 [256, 7168]、ape [4, 256]、 RoPE 转的是 128 维里的 64 维(一半,主通路只有八分之一),最后多一步逐行量化。 HCA 把 col_off 恒设为 0、窗口换成 128 个不重叠的 token,其余完全一致。

特殊模块 · indexer

CSA 的 top-1024 是在一份独立的、压得更狠的 KV 上选出来的

indexer 不在主 KV 上打分。它有自己的 compressor(head_dim = 128), 把同样 4 个 token 一组压成一行 128 维的 index key,写进另一个池子; query 侧也单独投影成 64 头 × 128 维。打分完只输出行号,真正的 KV 仍从主池取。

图 3 · 从 hidden 到 top-1024 行号

query 侧 · 每个 token 一次 hidden [T, 7168] weights_proj [64, 7168] → w [T, 64] qr [T, 1536] FP8(q_norm 顺手量化) indexer.wq_b [8192, 1536] → q [T, 64, 128] RoPE + 量化融合成一次发射 每 (token, head) 一个 scale,折进 w key 侧 · indexer 自己的压缩器 同样 4 个 token 一组 wkv / wgate [256, 7168] BF16 FP32 softmax 池化 → RMSNorm → RoPE 逐行 amax → ue8m0 → e4m3 → 预洗牌 FP4:一行 68 B(64 B 值 + 4 B e8m0) FP8:一行 132 B(128 B + FP32 scale) 写入与量化都在 fused_compress_attn 里 打分与 top-k deepgemm_fp8_paged_mqa_logits FP4 走 pa_mqa_logits_fp4 / OPUS logits [T, (pos+1)/4] 每行只算到自己的可见长度为止 top_k_per_row_{prefill,decode} topk_in_seq [T, 1024] int32 看不到那么多的行用 -1 填充 选出来的还不能直接用:csa_translate_pack top-k 给的是「这条序列自己的第几行压缩 KV」,而主池是分页的。翻译这一步把 seq-local 行号换成物理分页偏移,并跳过 -1。 翻译之后得到的 kv_indices_csa / kv_indptr_csa,和滑窗那段一起喂给同一个 CSR 形式的稀疏注意力 kernel。 打分用的那个数 logits[t, c] = Σ(h < 64) w[t, h] · ⟨ q[t, h], k[c] ⟩ ÷ (√128 · √64) 两个 1/√· 合起来就是代码里的 _weights_scale = softmax_scale · n_heads^-0.5;w 没有激活函数。 注意温度有三套:注意力 1/√512(128 头 × 512 维),indexer 1/√128 再除 √64,压缩器内部的池化用的是 softmax 不是温度。

indexer 的 Q 不切 TP

indexer.wq_b 是 ReplicatedLinear:64 个头在每个 rank 上都要有。 理由很直接 —— 打分要对 64 个头求和才得到一个 logit,切了就得为 index_score 再加一次 all-reduce。 这张表只有 12.58 M,复制一份比多一次通信便宜。

index KV 的精度是单独的开关

原生单节点默认 FP4(gfx942 例外,仍 FP8),插件路径保持 FP8。 --index-cache-dtype fp8 可以强制回退。 gfx950 与 gfx1250 的 FP4 布局故意不兼容:前者是 pa_mqa_logits_fp4 的预洗牌布局,后者是 OPUS 自然行 —— 所以 indexer_layout 要和 quant_mode 分开记,一个说怎么量化,一个说怎么摆。

rotate = True 是个还没接上的标志

Compressor.__init__ 收一个 rotate 参数,indexer 那份传的是 True。 但 rotate_activation(atom/model_ops/quant_v4.py 的 Walsh-Hadamard) 在整个 V4 路径里没有任何调用者 —— 全仓只有 GLM-5.3 的 k-pool 和 V4.1 的融合 RoPE 用到 Hadamard, 后者还显式传 do_rotate_act=False。 源码注释说明了它是给后续 PR 留的判别位:参考实现训练时做的是 rotate_activation(kv) 再 FP4 量化的 QAT 往返,这一步目前没有移植。 看到这个标志不要当成「跑了 Hadamard」。

稀疏要到 4K 之后才真的生效

CSA 的候选行数是 (pos+1)/4,要超过 index_topk = 1024 才会丢东西 —— 也就是 pos ≥ 4095。在那之前 top-1024 选中的是全部候选行, CSA 层等价于「滑窗 + 全部压缩历史」。 4K 以内的 benchmark 测不到这套机制的稀疏行为,只测到它的压缩损失。

打分读的字节和它要剪枝的那次注意力差不多

同样在 pos = 20000:打分要扫 5000 行 index K,FP8 下 5000 × 132 B ≈ 660 KB; 而被它剪到 1024 行之后,注意力读主 KV 是 1024 × 640 B ≈ 655 KB。 换成 FP4 index 平面(68 B/行)降到 340 KB,约为注意力的一半 —— 这大概就是原生单节点默认选 FP4 的原因(recipe 写了 gfx942 除外)。 只是字节数对比,不是时延对比;两者访存模式不同,没有实测。

压缩器状态环比滑窗还大

每请求、每层、FP32、kv_state 与 score_state 各一份: CSA 主环 (8, 1024) = 64 KiB、CSA index 环 (8, 256) = 16 KiB、 HCA 主环 (128, 512) = 512 KiB。 合计约 17.8 MiB / 请求,其中 HCA 占 15.5 MiB —— 比 FP8 滑窗的 4.77 MiB 还大 3.7 倍。 HCA 环必须 128 深,因为它的池化窗口就是 128,跨 forward 边界时最多要补 127 个历史位置。


特殊模块 · mHC

残差一路是四路宽的,每个子层进出都要过一次 Sinkhorn

embedding 进第一层之前被展成 [T, 4, 7168]。每个子层(注意力、FFN 各算一个)做两件事: hc_pre 把 4 路残差按预测出来的权重收成一路 [T, 7168] 喂给子层, hc_post 把子层输出按另一组权重摊回 4 路,同时用一个 4×4 组合矩阵重新混合旧残差。 这两组权重和那个矩阵都是同一个投影出来的,再经 20 步 Sinkhorn 投影到双随机流形上。

图 5 · 一个子层的 mHC 缝

残差 [T, 4, 7168] BF16 从 embedding 起就是 4 路 四路之间没有共享存储 hc_mult = 4 预测三样东西 先对 [T, 28672] 做 RMSNorm(无权重) hc_fn [24, 28672] FP32 → [T, 24] 24 = (2 + 4) × 4 Sinkhorn 20 步,eps 1e-6 hc_scale [3]、hc_base [24] 也是 FP32 拆成三份 pre [T, 4] 把 4 路收成 1 路 → [T, 7168] post [T, 4] 子层输出摊回 4 路,门是 2·sigmoid comb [T, 4, 4] 旧残差自己重新混合一次 三者都是 FP32,且不允许降精度 一层里实际发生的顺序 hc_pre(attn) → attn_norm → 注意力 → hc_post(attn) → hc_pre(ffn) → ffn_norm → MoE → hc_post(ffn) 层 0 以外,相邻的 hc_post + hc_pre 被 aiter 的 mhc_fused_post_pre 合成一次发射;层 0 没有前一个 post 可折,所以单独跑。 收尾 最后一层出来仍然是 [T, 4, 7168]。hc_head_fn [4, 28672] 再预测一次权重,把 4 路收成 [T, 7168],过 norm 后才进 lm_head。
V4 的 mHC 是「收—算—摊」的直写形式。 V4.1-Flash 把它改成了延迟形式:post 欠着不结算,折进下一个子层的 pre 里一起算 —— 那是 V4.1 的 pre_delayed,V4 这里没有。

特殊模块 · MoE 与 MTP

前 3 层查表选专家,后 58 层才用门控

图 6 · 两种路由,以及挂在尾巴上的 MTP

层 0 / 1 / 2 · hash 路由 选哪 6 个:tid2eid[input_ids] 表是 [129280, 6] int64,与 token id 绑定,和隐藏态无关 权重还是 sqrtsoftplus(gate_logits) 在这 6 个位置上取值 再归一化、乘 routed_scaling_factor = 2.5 这 3 层没有 gate.bias —— checkpoint 里就没有 层 3–60 · 门控路由 gate [384, 7168] BF16 → logits [T, 384] sqrtsoftplus 打分,gate.bias [384] FP32 只参与选择 noaux_tc 取 top-6,权重再归一化 × 2.5 bias 不进权重,只把选择推偏 —— 负载均衡用 专家本身:两种 dtype,一个形状 384 个路由专家:w1 / w3 [3072, 7168]、w2 [7168, 3072],FP4 e2m1 按 1×32 配 e8m0 scale;SwiGLU 钳到 ±10 1 个共享专家:同样的形状,但 FP8 e4m3 按 128×128 —— 一个 buffer 装不下两种 dtype,所以它是独立模块,每个 token 都走 MTP · 1 层 · 不在主循环里 enorm(embedding) 与 hnorm(上一层隐藏态) 各过一次 RMSNorm,再由 e_proj / h_proj [7168, 7168] FP8 投影相加 之后是一整层 Dense 注意力 + 它自己的 384 个专家 + 它自己的 hc_head —— 25.84 B 参数,和骨干共享 embed 与 lm_head
哈希路由只在最靠近输入的 3 层用:那里的表示还非常接近 token 本身,用 token id 直接定专家既稳定又省掉一次门控。 往后表示混合得越来越多,才切到按隐藏态打分。-0813 把 MTP 换成 3 层 DSpark,骨干一个字节都不改。

规模

1598.8 B 逻辑参数,单 token 碰 49.78 B

参数几乎全在一个地方:61 层 × 384 个 FP4 路由专家,占 96.78%。 单 token 激活按「这一步真正参与乘加的权重」计:6 个路由专家加 1 个共享专家、全部注意力(含 compressor 与 indexer)、embed 与 lm_head。 MTP 是另一个前向,不计入。

路由专家384 × 61 层 × 66.06 M · FP4
1547.40 B96.78%
MTP 一层含它自己的 384 专家
25.84 B1.62%
注意力主干61 层 × 299.9 M · FP8
18.29 B1.14%
共享专家1 × 61 层 · FP8 128×128
4.03 B0.25%
embed + lm_head129280 × 7168 · BF16 × 2
1.85 B0.12%
compressorHCA 7.34 M / CSA 14.68 M · BF16
0.67 B0.04%
indexer只有 30 个 CSA 层有
0.50 B0.03%
路由门gate [384, 7168] × 61
0.17 B0.01%
mHC 系数hc_*_fn [24, 28672] FP32
0.08 B0.01%
逻辑参数 1598.84 B 落盘 805.32 GiB(含 scale,64 个分片) 骨干单 token 激活 49.78 B

49.78 B 是怎么凑出来的

  • 路由专家 24.18 B — 61 层 × 6 × 66.06 M
  • 注意力 19.47 B — HCA 层 307.31 M,CSA 层 331.30 M(差的 24.0 M 就是 indexer 加更宽的 compressor)
  • 共享专家 4.03 B — 每层 1 个,和路由专家同形但 FP8
  • embed + lm_head + 最终 norm 1.85 B
  • 路由门 0.17 B + mHC 0.08 B

落盘 805.32 GiB 怎么分

  • 路由专家 765.60 GiB —— 95.1% 的磁盘。逻辑上占 96.8%,因为 FP4 两个参数共用一个字节。
  • 注意力 17.04 GiB、MTP 13.00 GiB、共享专家 3.75 GiB、embed/head 3.45 GiB。
  • compressor 1.25 GiB、indexer 0.58 GiB、mHC 0.31 GiB、门控 0.31 GiB、哈希表 0.02 GiB。
  • 哈希表 tid2eid 是 [129280, 6] 的 int64,3 层各一张 —— 它是查表不是乘加,不进激活量。
张量形状dtype出现位置
embed.weight / head.weight[129280, 7168]BF16各一份,不共享
attn.wq_a / attn.wkv[1536, 7168] / [512, 7168]FP8 128×128运行时合成一次 GEMM,输出 [T, 2048]
attn.wq_b[65536, 1536]FP8 128×128128 头 × 512,列并行
attn.wo_a / attn.wo_b[16384, 4096] / [7168, 16384]FP8 128×12816 组,每组 [1024, 4096]
attn.attn_sink[128]FP32每头一个 logit,只进 softmax 分母
compressor.wkv / wgate[512, 7168]BF16HCA 层(ratio 128,不重叠);ape [128, 512]
compressor.wkv / wgate[1024, 7168]BF16CSA 层(ratio 4,重叠窗口,宽度翻倍);ape [4, 1024]
indexer.wq_b[8192, 1536]FP8 128×128只有 30 个 CSA 层。64 头 × 128,不切 TP
indexer.weights_proj[64, 7168]BF16每头一个打分权重
indexer.compressor.wkv / wgate[256, 7168]BF16indexer 自己的 压缩器,head_dim 128,输出只给打分
ffn.gate.weight[384, 7168]BF16层 3–60 另有 gate.bias [384] FP32
ffn.gate.tid2eid[129280, 6]INT64只有层 0 / 1 / 2,与 gate.bias 互斥
ffn.experts.e.w1 / w3[3072, 3584]I8 打包 FP4逻辑列数 7168。scale [3072, 224] E8M0,块 1×32
ffn.experts.e.w2[7168, 1536]I8 打包 FP4逻辑列数 3072
ffn.shared_experts.w1 / w3[3072, 7168]FP8 128×128与路由专家同形、不同 dtype
hc_attn_fn / hc_ffn_fn[24, 28672]FP32每层各一份;base [24]、scale [3]
hc_head_fn[4, 28672]FP32只有一份,收尾时把 4 路残差收成 1 路
mtp.0.e_proj / h_proj[7168, 7168]FP8 128×128MTP 的两路输入投影,各配 enorm / hnorm

图 4 · KV 池

一个行空间,两头生长:历史按块、滑窗按槽

V4 把两种 KV 放进同一个行空间:压缩历史按 block 寻址,从 0 往上长; 每请求的滑窗和压缩器状态按 slot 寻址,从高位往下长。 「一行」在所有平面上指同一个意思,但一行的字节数各平面不同 —— 这是硬要求, 因为稀疏 kernel 拿的是一份共享的 index 缓冲区,NoPE 和 RoPE 两个平面必须用同一个行号。

图 4 · 一个 token 要多少字节,一个请求要多少

行空间:两端相向生长 num_blocks 个信封 · 压缩历史 按 block 寻址,向上长 num_slots 个槽 · 状态 + 滑窗 按 slot 寻址,向下长 gap 信封:一个 256-token 的块里有 1982 行 CSA · 30 层 每层 256 / 4 = 64 行 30 × 64 = 1920 行 HCA · 31 层 每层 256 / 128 = 2 行 31 × 2 = 62 行 合计 1982 行 / 256 token = 7.742 行 / token 一行多少字节,取决于池子的 dtype BF16 主池:一行 [512] BF16 = 1024 B,RoPE 就写在这一行里 → 7.742 × 1024 = 7928 B / token FP8 主池:NoPE 平面一行 512 B,RoPE 64 维另走一张 BF16 平面 128 B,合计 640 B → 7.742 × 640 = 4955 B / token index 池(只有 CSA 的 7.5 行 / token):FP4 68 B/行 = 510 B / token;FP8 132 B/行 = 990 B / token recipe 的默认组合(FP8 主池 + FP4 index)= 4955 + 510 = 5465 B / token,1 M 上下文约 5.09 GiB 滑窗不随上下文变长:61 层 × 128 行 = 7808 行 / 请求 —— FP8 下 4.77 MiB,BF16 下 7.63 MiB。压缩器的未满组另有一条 FP32 ring。 槽位在高端、从高往低分配,是为了让两类 KV 互不搬动对方已经放好的东西;移动边界只是主机端改一个计数,不用重新切分,也不用重捕获。

为什么 NoPE 和 RoPE 不能是同一个信封的两段

假设一个信封 E 字节,那么块 b 的第 r 行在一个平面上是 b·E/512 + r, 在另一个平面上是 b·E/128 + r —— 两者只有在 E = 0 时才相等。 而 mla_decode_fwd_v4_nm 这类 kernel 只收一份 index 缓冲区, 所以两个平面必须各自是一张密排的平面,行号才能通用。

MQA 单 KV 头 = KV 不随 TP 分摊

num_key_value_heads = 1,所以 TP8 不会把 KV 切成 8 份: 上面那 5465 B/token 是每个 rank 都要存一份的量。 省显存的开关因此是 --kv_cache_dtype fp8(1024 → 640 B/行)和 FP4 index 平面,而不是加 TP。


量化

密集投影是 128×128 FP8,路由专家是 1×32 FP4

权重大类存储激活备注
wq_a、wq_b、wkv、wo_a、wo_b、indexer.wq_b、e_proj、h_projFP8 e4m3,块 128×128,scale E8M0动态V4.1-Flash 用的是 32×32,这是两代之间最容易看错的一处
路由专家 w1 / w2 / w3FP4 e2m1,块 1×32,I8 打包MXFP8磁盘 K 维减半;AITER_BF16_FP8_MOE_BOUND=0 + ATOM_MOE_GU_ITLV=1 走对内核
共享专家 w1 / w2 / w3FP8 e4m3,128×128动态与路由专家同形不同 dtype,因此是独立模块
compressor(两个)、weights_proj、gate、embed、headBF16BF16压缩器的组内累加指定在 FP32
hc_*_fn / base / scale、attn_sink、gate.biasFP32FP32mHC 系数不允许降精度;ATOM_MHC_USE_BF16 只改打包布局
gate.tid2eidINT64—查表,不参与乘加
主 KV 池BF16 1024 B/行 或 FP8 512+128 B/行—--kv_cache_dtype fp8;RoPE 永不量化
index KV 池FP4 68 B/行(默认)或 FP8 132 B/行—gfx942 回退 FP8;gfx950 与 gfx1250 的 FP4 布局互不兼容

在 ATOM 里的位置

按这个顺序读,层类型和字节不会先分叉

#文件看什么
1atom/models/deepseek_v4.py · DeepseekV4Argscompress_ratios 的三种取值注释:0 = Dense、4 = CSA、128 = HCA
2atom/models/deepseek_v4.py · DeepseekV4Attention.__init__按 ratio 决定建不建 compressor / indexer;YaRN 还是普通 RoPE
3atom/models/deepseek_v4.py · Compressoroverlap = (ratio == 4),以及它为什么让投影宽度翻倍
4atom/models/deepseek_v4.py · Indexer自带的 compressor、复制而非切分的 wq_b、FP4/FP8 两种布局
5atom/models/deepseek_v4.py · _sparse_attention三套 kv_indices_{swa,csa,hca} 怎么按 ratio 选
6atom/models/deepseek_v4.py · MoE._hash_topktid2eid[input_ids] 定选择、sqrtsoftplus 定权重
7atom/models/deepseek_v4.py · Blockhc_pre / hc_post,以及层 0 为什么不能用融合版
8atom/model_ops/attentions/pool_layout/v4_pool_geometry.py行空间、信封与槽、visible_csa / visible_hca
9atom/model_ops/attentions/pool_layout/v4_pool_fields.py一行有什么:NoPE / RoPE 两个平面,indexer 块的数据与 scale 分区
10atom/model_ops/v4_kernels/fused_compress.py · csa_translate_pack.py压缩与写入融合成一个 kernel;seq-local 行号翻成物理偏移
11recipes/DeepSeek-V4.mdTP8 + FP8 KV 的启动命令、MoE 环境变量、测得的 GSM8K

怎么跑起来

recipe 给的是 8 × MI355X、TP8、FP8 KV: AITER_BF16_FP8_MOE_BOUND=0 ATOM_MOE_GU_ITLV=1 加 --kv_cache_dtype fp8 -tp 8。 不需要 --trust-remote-code,DeepseekV4ForCausalLM 是 ATOM 内置架构。 这个配置在 MI355X / gfx950 上测得 GSM8K(1319 题,3-shot flexible-extract)0.9522。

可选的 EP / DP 路线

--enable-expert-parallel --enable-dp-attention --all2all-backend rccl 走原生 RCCL 的专家派发: 均匀 decode 用图安全的预路由 AllGather/ReduceScatter 快路径,prefill 与混合批走变长版本。 它只消费既有路由产出的物理专家 ID,不改 EPLB 策略;TBO 在这个后端上不支持。