DeepseekV41ForCausalLM model_type · deepseek_v41 CSA2 mHC × 4 Engram DSpark × 3

DeepSeek-V4.1-Flash 架构解剖

40 层文本骨干,隐藏维 5120,单层一条 512 维的 MQA latent。压缩比只有 0、1、2: 2 层纯滑窗,18 层把 2 个 token 收成 1 行,20 层一行对一个 token。 残差是 4 路宽的,层 1 和层 14 各插一张宿主机上的 Engram 表,末尾 3 层 DSpark 用同一份 checkpoint 起草。 注意力按 WINDOW / FULL / REINDEX / REUSE 分成四种,Q 和输出投影四种一模一样, 差别只在「写哪几份缓存」和「top-512 从哪来」。

763.21 B逻辑参数,含 Engram 表
16.79 B文本骨干单 token 激活
40 + 3骨干层 + DSpark 层
5120hidden_size
384 / 6专家数 / 每 token
512index_topk
30 / 40层既不压缩也不打分
2.82 KiBBF16 池每 token
1 Mmax_position

阅读顺序

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

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

先分清对象

一份 checkpoint 里叠了四套机制

架构名是 DeepseekV41ForCausalLM,model_type 为 deepseek_v41。 文本配置在 text_config(deepseek_v41_text),视觉塔在 vision_config。 ATOM 的 normalize_hf_config 把文本字段摊平给骨干,同时保留一份独立的多模态根配置。 下面的数字来自 48 个 safetensors 分片的头部:逻辑参数按权重元素计,FP4 的一个 I8 字节算 2 个参数,.scale 单列、不进参数量。 按这个口径把全部张量的存储字节加总,等于索引里的 total_size = 510,286,023,000。

CSA2 注意力

  • 每个 token、每一层先投影出一条 [512] latent。64 个 query 头各是 [512],KV 头数是 1。
  • RoPE 只转 latent 和 query 的最后 64 维。前 448 维是 NoPE。滑窗用 rope_theta = 10000;压缩行用 compress_rope_theta = 160000 的 YaRN(factor 16,原长 65536)。
  • 每一层都有自己的 128-token 滑窗。压缩层再额外看所属 KV owner 上最多 512 行压缩 KV。

mHC、Engram、DSpark

  • embedding 在进第一层之前被复制成 [T, 4, 5120]。每个子层用 FP32 的 hc_*_fn [24, 20480] 预测 pre / post / 4×4 混合,Sinkhorn 20 步。
  • Engram 在层 1、层 14,表在宿主机 mmap。层 1 的表是 [384006168, 256] FP8,层 14 是 [384016682, 256]。
  • DSpark 三层只做滑窗,专家缩小到 128 选 3,一次起草 5 个 token。输入取层 37、38、39 的残差均值。

主结构 · 层调度

40 层里只有 4 层真正写下压缩 KV

compress_ratios 覆盖 40 层骨干加 3 层草稿,取值只允许 0、1、2。 build_attention_topology 用 kv_source_layer_ids = [2, 8, 14, 20] 和 index_source_layer_ids = [2, 8, 14, 20, 24, 28, 32, 36] 把每一层标成四种模式。 悬停色块可以看到层号。

attention
engram
0
2
8
14
20
24
36
39
D
WINDOW · 层 0–1 FULL · 写 KV + 打分 · 2 / 8 / 14 / 20 REINDEX · 重打分 · 24 / 28 / 32 / 36 REUSE · 沿用本段 top-k Engram · 层 1、14 DSpark · mtp.0–2,滑窗
层ratioKV owner谁打分模式这一段在看什么
0–10——WINDOW只有本层 128 滑窗。层 1 在注意力之前做 Engram
2–7222FULL + 5×REUSE每 2 token 一行。层 2 写平面并选出 top-512,层 3–7 直接用这份选择
8–13288FULL + 5×REUSE新的压缩平面,结构与上一段相同
14–1921414FULL + 5×REUSE第三段 ratio-2。层 14 同时是 Engram 层
20–2312020FULL + 3×REUSE一行一个 token。层 20 写出候选块:2048 × 8 行
24–2712024REINDEX + 3×REUSEKV 仍是层 20 的平面,top-k 在候选块里重算
28–3112028REINDEX + 3×REUSE同上,打分点前移到 28
32–3512032REINDEX + 3×REUSE同上
36–3912036REINDEX + 3×REUSEDSpark 的三路输入取 37、38、39 的残差,这三层本身是 REUSE
mtp.0–20——WINDOW草稿层禁止出现非零压缩比

四种模式在磁盘上差哪些张量

WINDOW 与 REUSE 没有 compressor,也没有 indexer。 REINDEX 有 indexer.wq_b 和 weights_proj,没有 indexer.wk。 FULL 才有 compressor.wkv、indexer.wk 和 k_norm。 ratio 2 的 FULL 层(2、8、14)多一个 compressor.wgate [512, 5120];ratio 1 的层 20 没有这张门。


主结构

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

文本骨干一次前向的模块顺序是固定的。Attention 在这里是一块:进去是 mHC 之后的 [T, 5120],出来还是 [T, 5120]。四种内部结构在下一节。 Engram、mHC、MoE、DSpark、视觉塔在主图里也先收成块,形状写在块上,内部见后面的单独拆解。

主结构 · DeepSeek-V4.1-Flash

文本骨干 · 单次前向 input_ids 离线接口 [B, T] 运行时拉成 [T] embed · BF16 weight [129280, 5120] hidden [T, 5120] 展开成 4 路残差 pre_mix [T, 4] FP32,第 0 路初值 = 1 residual [T, 4, 5120] BF16 × 40 Block · 子层顺序固定 Engram 只在层 1、层 14 插入,并且插在注意力的 pre 之前。其余层跳过最左边这块。 Engram 仅层 1、14 先 settle 四路 gather [T, 6144] wkv [25600, 6144] → [T, 4, 5120] 详见 Engram mHC · attn 缝 吃 residual [T, 4, 5120] hc_fn [24, 20480] normed [T, 5120] post 折进下一缝 详见 mHC CSA2 Attention · 大块 输入 [T, 5120] 输出 [T, 5120] 内部 Q [T, 64, 512] 滑窗 ≤128 + 前缀 ≤512 WINDOW / FULL / REINDEX / REUSE 整块拆解见下一节 MoE mHC ffn 缝之后 输入 [T, 5120] 384 选 6 + 1 shared gate [384, 5120] 输出 [T, 5120] 详见 MoE 块的返回值带着 FFN 的 post,留给下一层的 pre 一起结算。滑窗走 window_rope,压缩行走 global_rope,两套表。 层 37–39 的残差先 settle,形状 [T, 4, 5120],对 hc 取均值后是 [T, 5120] × 3,送给 DSpark。 图像 token 的 embedding 被视觉塔替换,进来时已经是 [5120]。文本走 gate.bias,图像走 gate.bias_vl,两套都是 [384] FP32。 视觉塔、DSpark 不在这 40 层的循环里,各自的结构在特殊模块。 collapse Σ pre_mix[i] · stream[i] [T, 5120] BF16 final RMSNorm weight [5120],eps 1e-20 [T, 5120] lm_head · BF16 [129280, 5120],与 embed 不共享 logits [T, 129280] 词表 129280。image_token_id = 129264。bos / eos / pad = 0 / 1 / 2。DSpark 的噪声 token 是 128799。 collapse 用当前的 FP32 pre_mix 做加权和,没有额外的输出投影。四路宽度从 embedding 一直保持到最后一层结束。
粗框的是 Attention 大块。主图不展开它的投影、滑窗和压缩行;下一节是这块的内部结构,再下面四张是 WINDOW / FULL / REINDEX / REUSE 各自多出来的权重与读写。

Attention 块

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

主结构里那块 Attention 拆开之后,是下面这条链。四种模式的差别不在这条链上: build_attention_topology 只决定这一层额外带哪些权重、 往哪几份缓存写、top-512 从哪来。 compressor 与 indexer 接在链的侧面,它们是特殊模块,不画进这条主链。

Attention 块 · 共用通路

输入是 mHC 之后的 normed hidden。TP 切的是头和 wo 的组,下面按未切的逻辑形状画。 hidden [T, 5120] BF16 attn_norm 的输出 eps 1e-20 wqkv_a · 一次 GEMM [1792, 5120] FP8 32×32 1792 = 1280 + 512 输出 [T, 1792] 按列切开 q_lora [T, 1280] kv [T, 512] 磁盘上仍是 wq_a 与 wkv 两份 DualRMSNorm · 一次发射 q_norm.weight [1280],顺手量化成 FP8 qr [T, 1280] + scale kv_norm.weight [512] → kv_normed [T, 512] BF16 wq_b → Q [32768, 1280] FP8,列并行 Q [T, 64, 512] 32768 = 64 头 × 512 RoPE · 只转尾 64 维 前 448 维 NoPE Q 与滑窗 KV 同一次旋转 ratio 0 用 window_rope ratio 1/2 用 global_rope 读两份 KV,都是 [512] 滑窗:本层自己的,≤ 128 行 前缀:owner 的压缩行,≤ 512 attn_sink [64] FP32 只加在 softmax 分母上 分组输出 注意力出 [T, 64, 512] 逆 RoPE,按 32 分组量化 wo_a:8 × [1024, 4096] wo_b [5120, 8192] → [T, 5120] softmax 温度是 1/√512。wo_a 的权重加载后洗成 16×16,组内 GEMM;wo_b 做 TP 的 all-reduce。 侧面接口,不在这条主链上:FULL 从 hidden 分出 compressor;FULL 与 REINDEX 从 hidden 和 qr 分出 indexer。打分必须等 compressor 把这一行写完。 REUSE 层的前缀下标不在本层算,按 index_group_start 取本段第一层已经写好的 selected。WINDOW 没有前缀那一项。 下面四张图只画每种模式相对这条通路多出来的模块、写入和读取。
ATOM_DSV41_SIDE_STREAMS 默认是 0:compressor、这条主链、indexer 顺序排在同一条流上。 开到 1 或 2 只改变它们占不占旁路队列,依赖边不变:投影不能早于 qk_norm,打分不能早于 compressor 写下的那一行。

Attention 块 · 四种内部结构

同一套 Q/O,四种「写什么、读什么、谁打分」

上一张图是主结构里那块 Attention 的内部。下面四张只标每种模式多出来的权重、写入和读取。投影路径四种相同:wqkv_a 一次 GEMM 出 q_lora [T, 1280] 与 kv [T, 512], DualRMSNormMXFP8 一次归一化并顺手把 qr 量化成 FP8,wq_b 展开成 Q [T, 64, 512], 输出端一律是「逆 RoPE 融合分组量化 → wo_a 8 组 → wo_b」。 build_attention_topology 给每层贴的标签只决定三件事:这一层额外带哪些权重、 往哪几份缓存写、top-512 从哪来。 40 层里有 30 层既不压缩也不打分。

图 2a · WINDOW · 层 0–1,以及 3 层 DSpark 草稿 · ratio = 0

共用前段:wqkv_a [1792, 5120] FP8 → q_lora [T, 1280] + kv [T, 512] → DualRMSNorm → wq_b [32768, 1280] → Q [T, 64, 512],RoPE 只转尾 64 维 共用后段:attn_sink [64] FP32 加在 logit 上 → 输出 [T, 64, 512] → 逆 RoPE + 每 32 维一组量化 → wo_a 8 × [1024, 4096] → wo_b [5120, 8192] → [T, 5120] 额外模块 没有 compressor 没有 indexer 磁盘上这些张量根本不存在 额外权重 0 写入 只写本层滑窗 ring [512] BF16 / token ring 深 128(开 DSpark 时 133) 在 STATE 里,完全不碰 PAGE 读取 本层滑窗,最多 128 行 prefix = min(pos + 1, 128) 没有 top-k 那一项 rope = window_rope,θ = 10000 这是全模型最便宜的一层:权重集合就是共用前后段,KV 只有一条 128 行的环,长度不随上下文增长。 草稿层被配置强制钉在这里 —— compress_ratios 的末 3 位必须为 0,否则 build_attention_topology 直接报错。

图 2b · FULL · 层 2 / 8 / 14(ratio 2)与层 20(ratio 1)· 唯一会写 PAGE 的层

共用前后段同图 2a。下面三栏是 FULL 独有的部分。 额外模块 compressor.wkv_gate [1024, 5120] BF16(ratio 2) 层 20 只有 [512, 5120],无 gate indexer 全套 wk [128, 512] + k_norm wq_b [4096, 1280]、weights_proj [32, 5120] 额外权重 10.71 M(层 20 为 8.09 M) 写入(三处) ① 本层滑窗 [512] BF16 ② owner 主平面 每 ratio 个 token 汇成一行 [512],1024 B ③ owner index 平面 index key [128],FP8 下 132 B / 行 层 20 另写候选块 [rows, 2048] 读取 本层滑窗 ≤ 128 行 加上自己刚写的平面上 top-512 prefix = 滑窗 + min((pos+1)/ratio, 512) rope = global_rope(YaRN,θ = 160000) 打分范围 = 本请求整条上下文 这是唯一一种「本层刚写的行,本层这一步就要读」的层:compressor 写进 index 平面的那一行,indexer 的打分必须等它落地,两者之间是一条实打实的依赖边。 ratio 2 的池化在 FP32 里做:wkv / wgate 两半,组内 softmax 加权求和,再 RMSNorm;未满的一组留在每请求的 FP32 ring 里,不写 PAGE。

图 2c · REINDEX · 层 24 / 28 / 32 / 36 · 借层 20 的 KV,自己重算 top-512

共用前后段同图 2a。ratio = 1,KV owner 固定是层 20。 额外模块 没有 compressor 没有 wk / k_norm 它不造 key,只造 query indexer.wq_b [4096, 1280] weights_proj [32, 5120] 额外权重 5.41 M 写入 只写本层滑窗 主平面和 index 平面都是层 20 的, 这一层一个字节都不往里写 本层的 selected 会被后面 3 层 REUSE 读取 · 被候选块夹住 KV:层 20 平面上自己选的 512 行 打分范围不是整条上下文 = 层 20 留下的 2048 块 × 8 行 = 最多 16 384 行 在这 16 384 行里重算 top-512 重打分不是冗余:候选块是层 20 按它自己的分数挑的,这一层排名很高的行完全可能落在层 20 没留的块里,丢掉它正是这套机制要的效果。 约束写在配置校验里:candidate_topk_blocks × candidate_block_size ≥ index_topk,候选块装不下一整个 top-k 就拒绝启动。

图 2d · REUSE · 30 层 · 不打分,直接取本段第一层的选择

共用前后段同图 2a。层 3–7 / 9–13 / 15–19 / 21–23 / 25–27 / 29–31 / 33–35 / 37–39。 额外模块 一个都没有 权重集合与 WINDOW 层完全相同 区别只在它还看 owner 的压缩行 额外权重 0 · 这样的层有 30 个 写入 只写本层滑窗 既不写主平面,也不写 index 平面 也不往 step 里写新的 selected 读取 本层滑窗 ≤ 128 行 selected 取自 index_group_start 那一层 [rows, 512] 升序,-1 填充 本层不发任何打分 kernel prefix 公式与本段 owner 的 ratio 一致 一段 = (ratio, kv_owner, topk_owner) 三元组相同的一串连续层,_tag_index_groups 给它们打上同一个 index_group_start。 decode 时整段的下标一次建完 —— 这就是「一次打分、最多 5 层共用」能省下来的那部分。
模式层数额外权重 / 层写主平面写 index 平面发打分 kernel打分宽度
WINDOW20否否否—
FULL410.71 M / 8.09 M是是是整条上下文
REINDEX45.41 M否否是2048 × 8 = 16 384 行
REUSE300否否否沿用本段

这四个标签是算出来的,不是配置里写死的

配置只给三样东西:compress_ratios、kv_source_layer_ids、index_source_layer_ids。 build_attention_topology 从前往后扫:ratio 为 0 判 WINDOW;否则沿用最近一个 KV owner, layer_id == kv_owner 判 FULL,layer_id == topk_owner 判 REINDEX,剩下的是 REUSE。 校验也在这里:每个 KV source 必须同时是 index source,window-only 的层不能当 owner,候选块要能装下一整个 top-k。

所以「稀疏」省在哪

省的不是注意力本身的行数 —— 四种模式读的都是「≤128 滑窗 + ≤512 压缩行」这个固定预算。 省的是造 KV 的次数(40 层只有 4 层写)和打分的次数(40 层只有 8 层算,其中 4 层还被夹在 16 384 行里)。 1 M 上下文下,注意力每层读的行数是常数,这是 CSA2 的整个要点。

四种层合并出来的是什么形状

不管哪种模式,滑窗行和压缩行都被拼进同一份 CSR,进同一次 softmax,没有门控也没有加权合并。 decode 一个 token 的形状是 Q [1, 64, 512] 对 KV [N, 512] —— num_key_value_heads = 1,64 个 query 头共用同一份 N 行,而且 K 和 V 是同一行(kernel 只收一个 KV 张量)。 N 最多是 128 滑窗 + 512 压缩 = 640,WINDOW 层只有 ≤128。 attn_sink [64] 每头一个,只进 softmax 分母。

出来之后

out [1, 64, 512] → 逆 RoPE(在 query 自己的位置上转回去,因为 K = V 且 V 是旋转过的) → wo_a 8 组 × [1024, 4096] → [1, 8192] → wo_b [5120, 8192] → [1, 5120]。 inverse_rope_group_quant 把逆 RoPE 和 wo_a 的 per-32 分组量化融成一发,省一次 BF16 往返。

CSA2 的池化比 V4 简单一截

同一个 fused_compress kernel,但 CSA2 从不重叠(compress_ratios 恒返回 (ratio, False)):ratio 2 的一行只池化 2 个 token,ratio 1 根本不池化。 所以没有 V4 那套「同一个 token 在两行里用不同半边列」的角色切换,投影宽度就是 head_dim。 池化本身仍是逐通道的 softmax —— (values * scores.softmax(dim=2)).sum(dim=2), softmax 沿组内位置,每个通道独立。

ape 在这里是一串零

V4 给池化的门加了一个学出来的组内位置偏置 ape(checkpoint 里是真张量)。 CSA2 不用它,但为了共用同一个 kernel,仍然挂了一个全零的 register_buffer("ape", ..., persistent=False) —— 不在 checkpoint 里,也不会被当成未加载的参数报警。 反过来说:compressor.ape 在不在 safetensors 里,是区分 V4 与 V4.1 谱系的一个便宜判据。


特殊模块 · compressor

只有 FULL 层有它,ratio 1 和 ratio 2 差一个门

compressor 从 Attention 块的 hidden 侧面分出去,不经过 wqkv_a。 它写出的是 owner 平面上的压缩行;indexer 吃的是 RoPE 之前的那份 latent。 CSA2 的组与组不重叠(overlap = False),半成品留在每请求的 FP32 ring 里,不进 PAGE。

compressor · ratio 2 与 ratio 1

ratio 2 · 层 2、8、14 hidden [T, 5120] wkv、wgate 各 [512, 5120] BF16 values、scores 各 [T, 512] FP32 每 2 个 token 一组做 softmax latent [512] → RMSNorm [512] RoPE 尾 64 维后写入 owner 平面 一行 1024 B。未满的一组留在 ring ring 深 2;开 DSpark 时再加 5,深 7 ratio 1 · 层 20 hidden [T, 5120] 只有 wkv [512, 5120] BF16 没有 wgate。一行对应一个源 token 投影 [T, 512] → RMSNorm [512] RoPE 尾 64 维后写入 owner 20 softmax 只有一个元素,权重恒为 1 后面的 REINDEX 层借用这一份 KV ape 是全零,CSA2 不加位置偏置 写进 PAGE 的是旋转之后的 latent。indexer 的 wk 吃的是旋转之前的那一份,所以两条支路在 RMSNorm 之后分叉。 WINDOW、REINDEX、REUSE 没有这组权重。40 层里只有这 4 个 owner 会写压缩行。
融合内核一次处理整个 batch 的压缩边界。ratio 只允许 0、1、2;草稿层的 ratio 必须是 0,否则 build_attention_topology 直接拒绝。

特殊模块 · 压缩平面与候选块

4 个 owner 的平面,8 个层负责打分

图 3 · ratio 2 的池化,以及层 20 之后的候选块

ratio 2 · 不重叠 token 2t [512] FP32 token 2t+1 [512] FP32 沿组内维 softmax 门是 wgate 的分数 APE 缓冲是全 0,不学习 一行 latent [512] RMSNorm 后,尾 64 维 RoPE BF16 主平面 1024 B / 行 ratio 1(层 20 起)没有组、没有门:wkv 的 [512] 做完 RMSNorm 就是一行,与源 token 一一对应。 层 20 给后面所有 ratio-1 层圈一块候选区 层 20 · FULL 扫自己的整张平面 选出 2048 个块 每块 8 行 = 16384 行 层 24 / 28 / 32 / 36 KV 仍是层 20 的平面 只在这 16384 行里打分 再取 top-512 同段的 REUSE 例如 25–27 用层 24 不再读 index 平面 直接用本段 selected ratio 2 的三段 层 2、8、14 没有候选块 对全历史取 top-512 index 平面跟主平面用同一个 block id,但字节区域分开。一行 index key:128 个 FP8,再加 1 个 FP32 scale,共 132 字节。 query 侧是 32 头 × 128 维,由 q_lora [1280] 投出来,再乘 weights_proj 的 32 个标量。缩放是 head_dim⁻⁰·⁵ × n_heads⁻⁰·⁵。 候选块数 × 块大小必须装得下整个 top-512:2048 × 8 = 16384 > 512。拓扑校验会拒绝装不下的配置,因为下标长度按闭式填写,少打分就会在自己的切片里留洞。 block 256 时,ratio 2 的一页是 128 行,ratio 1 的一页是 256 行。index 块固定 8 行,所以一页正好是整数个打分块。
层 20 是唯一的 candidate_source_layer_id。从这一层起,所有消费者和层 20 必须落在同一个 KV owner 上——这里就是层 20 自己。 ratio 2 的三段各自拥有独立平面,互不复用候选块。

特殊模块 · indexer

top-512 是怎么从一个分页平面里选出来的

打分从来不把 key 重新物化出来:query 量化之后直接交给分页 MQA logits kernel, 平面本身就是 PAGE 里那块 index 区域。每一个 query 行都是自己的一个「批次项」, 带自己的可见长度和自己的块表 —— 所以 prefill 的 token、decode 的 token、草稿的 token 在打分器眼里是同一种行, ragged 批次对它不构成特殊情况。

图 3b · 从 hidden 到 selected [rows, 512]

query 侧 · 每层自己算 hidden [T, 5120] weights_proj [32, 5120] → w [T, 32] qr [T, 1280] FP8(qk_norm 顺手量化) indexer.wq_b [4096, 1280] → q [T, 32, 128] RoPE 只转尾 64 维(global_rope) FP8 平面:按 (token, head) 行量化 那个 FP32 scale 折进 w,不单独传 key 侧 · FULL 层早就写好了 压缩后、RoPE 前的 latent [512] BF16 indexer.wk [128, 512] → k_norm → RoPE 写进 owner 的 index 平面: FP8:132 B / 行,8 行一块 (值在块内交错,scale 放块尾) FP4:68 B / 行(64 B 值 + 4 B scale) 两张平面,页高 8 行 主 KV 池与这张平面互不影响 第一级 · 行 top-k deepgemm_fp8_paged_mqa_logits FP4 走 flydsl_pa_mqa_logits_fp4 logits [rows, width] width = 本请求整条上下文 或 = 候选 2048 块 × 8 行 每行只算到自己的 visible 为止 top_k_per_row_decode → k = 512 selected [rows, 512] 升序,-1 填充 第二级 · 只有层 20 跑:把 16 384 行的「窗口」挑出来给后面 4 个 REINDEX 层用 _block_maxima_kernel:同一个 logits,每 8 行一块取块内最大值(NaN 当 -inf,其余截到最大有限值)→ maxima [rows, blocks] 最新的那一块钉成 +inf:它装的是最近的 token,只填了一部分,不能让它被别的块比下去 top_k_per_row_decode(k = 2048) → candidates [rows, 2048] 升序、-1 填充;这张表随后当块表用,所以候选块高度必须正好等于 index 平面的分页高度 8 打分用的那个数 logits[r, c] = Σ(h < 32) w[r, h] · ⟨ q[r, h], k[c] ⟩ ÷ (√128 · √32) w 没有激活函数,就是 weights_proj 的原始输出;两个 1/√· 合起来就是代码里的 weights_scale = head_dim^-0.5 · heads^-0.5。 注意这和注意力自己的 1/√512 是两套温度:indexer 是 32 头 128 维的 MQA 打分器,注意力是 64 头 512 维。

visible 是每行自己的前缀长度

打分器要求可见性是「一个前缀」,不是一张掩码。一行看不到的列压根不被读,也不被写 —— 所以 selected 里的 -1 不是「选了个空」, 而是「这一行的历史还不够 512 行」。下游的 _indptr_scan 据此用闭式公式 min((pos+1)/ratio, 512) 直接算出每行占多少格,不用真去数一遍。

FP8 与 FP4 平面的区别只在字节

--index_cache_dtype 选 fp8(默认)或 fp4,选哪个都不改变上面这条链路的形状, 只改变 key 的落盘格式、query 的量化方式,以及一件实质的事:平面精度不同,选出来的行就不同。 值得记一笔:V41PoolGeometry.layout_id —— 前缀复用和 lmcache_mp 状态键都按它匹配 —— 里那一段目前是写死的 index-fp8,注释也还停留在「FP8 是唯一平面」的年代, 所以换成 fp4 平面并不会改变这个串。


特殊模块 · mHC

子层之间传的是一份还没结算的 post

图 4 · 四路残差怎么收、怎么放

进入子层 residual [T, 4, 5120] flatten → [T, 20480] hc_fn [24, 20480] FP32 mixes [T, 24] 拆成三份 pre [T, 4] post [T, 4],门是 2·sigmoid comb [T, 4, 4] Sinkhorn × 20,eps 1e-6 子层本体 先 RMSNorm 到 [T, 5120] 注意力或 MoE 输出 [T, 5120] post 先欠着 不在本子层结算 下一子层的 pre 把它折进去 一次 kernel 做完 必须当场结算的两个缝 Engram 读的是 post 与 pre 之间的残差,所以层 1、层 14 先 settle。 栈的末尾 collapse 之前也要 settle:把欠着的 FFN post 加回去, 再用 pre_mix 把 4 路收成 [T, 5120]。 DSpark 截取层输入时同样先 settle,再对 hc 维取均值。 系数必须留在 FP32 hc_fn、hc_base、hc_scale 从 checkpoint 起就是 FP32。 hc_base [24],hc_scale [3],和 24 维 mixes 对齐。 24 = 4 路 pre + 4 路 post + 16 个混合系数。 初始 pre_mix 只有第 0 路为 1,其余为 0。
注意力和 FFN 各有一套 hc_*,形状相同,所以 mHC 与这一层是 WINDOW、FULL 还是 REUSE 无关。 norm 权重 [5120] 不单独跑,它被吃进 pre_delayed 那一次投影里。

特殊模块 · MoE

384 个 FP4 路由专家,加一个同形的 FP8 共享专家

MoE 接在 Attention 块之后、mHC 的 FFN 缝把残差收成 [T, 5120] 的地方。 路由专家与共享专家形状相同、dtype 不同,所以共享专家是独立模块,每个 token 都过一遍。 图像 token 只换选择用的偏置,不换专家权重。

MoE · 从 normed hidden 到 [T, 5120]

gate weight [384, 5120] BF16 logits [T, 384] sqrtsoftplus bias [384] FP32 只参与选择 图像 token 改用 bias_vl bias_vl [384] FP32 top-6 ids [T, 6] · 权重 [T, 6] 权重再归一化 × routed_scaling_factor 1.5 decode 窄宽度走固定顺序 的 router GEMV,和 mono 的 MoE kernel 同一条实现 专家 路由 ×384:FP4,块 1×32 w1 / w3 [2304, 5120] w2 [5120, 2304] SwiGLU,输出钳到 ±10 共享 ×1:同形,FP8 32×32 激活是 MXFP8,不是 MXFP4 输出加回四路残差之前仍是 [T, 5120]。草稿层的 MoE 同形,只是 128 个专家、每 token 选 3 个。 路由专家若喂 MXFP4 激活,输出相对参考偏约 12%。moe.py 把 moe_fp8_activations 钉成 True。

特殊模块 · Engram

两张宿主机大表,每个 token 只取 24 行

图 5 · n-gram 哈希到残差门控

token 与历史 n-gram 阶 2、3、4 每阶 8 个 hash 头 24 个行号 / token host 表 · FP8 层 1 [384006168, 256] 层 14 [384016682, 256] gather 后 [T, 6144] wkv [25600, 6144] FP8 4 个 key + 1 个 value 各 [T, 5120] 写回残差 每路用自己的 key 对 residual [T, 4, 5120] 6144 = (4 − 1) × 8 × 256。阶 1 不取行,hash 从第二个 lookback 才写出列。 q_weight、k_weight 各 [4, 5120] BF16,加载后乘成 gate。图像位置可以被 mask 掉,不参与这张表。 表为什么这么大,激活为什么几乎不涨 逻辑参数 196.61 B 两张表的行 × 256 scale 另计,1×32 沿 head 落盘 188.83 GiB 一个 token 的读 24 × 256 × 2 层 = 12 288 个表元素 投影矩阵则整块参与 和 DSpark 的约束 截取点不能落在 Engram 层 37–39 在层 14 之后,安全 层 1 的 Engram 在滑窗层上
行数不是词表,是每层、每个 (n-gram, head) 用不同素数切出来的哈希桶之和。 两层的行数差 10514,因为后一层的素数要避开前一层已经用过的桶。 压缩词表大小是 99092,pad id 是 2。

特殊模块 · DSpark

三层滑窗草稿。视觉塔是下一张图,两套权重不共享

DSpark · 块宽 5 · mtp.0–2

DSpark · 块宽 5 三路目标 层 37、38、39 的残差 先 settle,再对 hc 取均值 拼接 [T, 15360] mtp.0.main_proj [5120, 15360] FP8 main_norm → [T, 5120] 这是三层草稿共享的输入 mtp.0 / 1 / 2 滑窗注意力,头数仍是 64 MoE 128 选 3,中间维 2304 每层 Q [T, 64, 512] mtp.2 的两个头 Markov [129280, 256] × 2 confidence [1, 5376] 5376 = 5120 + 256 视觉 · 32 层,单图内双向 patch embed 14×14×3 = 588 → hidden 1024,带 bias [N, 1024] ViT block × 32 16 头 × 64,wqkv [3072, 1024] MLP 2816,SwiGLU 式分半 2D RoPE 宽 32,θ = 10000 aligner 3×3 折进通道 w1 [5120, 9216] w2 [5120, 5120] · GELU 送进语言模型 每行追加 newline 前后是 start / end 三个向量都是 [5120] 视觉权重全部 BF16,合计 485.3 M。max_image_tokens = 1024,min_pixels = 295936,只接受单帧(temporal = 1)。 图像 token 在 MoE 里用 bias_vl,文本用 bias。两套偏置都是 [384] FP32,选择时加、不进专家权重。 DSpark 与多模态请求不能同时出现。草稿层的专家逻辑形状与骨干相同,只是 128 个而不是 384 个,每 token 激活 3 个。
草稿和目标共用 embed 与 lm_head。Markov 头把上一个 token 嵌成 256 维,再投回整个词表作为 logits 偏置;confidence 头吃的是隐藏态和这份 256 维拼接起来的 5376 维。

视觉塔 · 32 层 ViT + aligner

DSpark · 块宽 5 三路目标 层 37、38、39 的残差 先 settle,再对 hc 取均值 拼接 [T, 15360] mtp.0.main_proj [5120, 15360] FP8 main_norm → [T, 5120] 这是三层草稿共享的输入 mtp.0 / 1 / 2 滑窗注意力,头数仍是 64 MoE 128 选 3,中间维 2304 每层 Q [T, 64, 512] mtp.2 的两个头 Markov [129280, 256] × 2 confidence [1, 5376] 5376 = 5120 + 256 视觉 · 32 层,单图内双向 patch embed 14×14×3 = 588 → hidden 1024,带 bias [N, 1024] ViT block × 32 16 头 × 64,wqkv [3072, 1024] MLP 2816,SwiGLU 式分半 2D RoPE 宽 32,θ = 10000 aligner 3×3 折进通道 w1 [5120, 9216] w2 [5120, 5120] · GELU 送进语言模型 每行追加 newline 前后是 start / end 三个向量都是 [5120] 视觉权重全部 BF16,合计 485.3 M。max_image_tokens = 1024,min_pixels = 295936,只接受单帧(temporal = 1)。 图像 token 在 MoE 里用 bias_vl,文本用 bias。两套偏置都是 [384] FP32,选择时加、不进专家权重。 DSpark 与多模态请求不能同时出现。草稿层的专家逻辑形状与骨干相同,只是 128 个而不是 384 个,每 token 激活 3 个。
视觉权重全部 BF16。patch 14×14×3 = 588 进 1024 维,32 层双向注意力,aligner 把 3×3 折进 5120。 图像 token 在 MoE 里用 bias_vl。DSpark 与多模态请求不能同时出现。

规模

763.21 B 逻辑参数,单 token 碰 16.79 B

参数量的大头是两块稀疏存储:40 层 × 384 个 FP4 路由专家,加上两张 Engram 表。 单 token 激活按「这一步真正参与乘加的权重」计:6 个路由专家加 1 个共享专家、全部注意力、embed 与 lm_head。 Engram 表每个 token 只 gather 24 行,不计入激活;视觉塔和 DSpark 是另外的前向。

路由专家384 × 40 × 35.39 M · FP4
543.58 B71.22%
Engram 表两层 host mmap · FP8
196.61 B25.76%
DSpark 路由专家128 × 3 层 · FP4
13.59 B1.78%
骨干注意力40 层 × 127.11 M 起
5.08 B0.67%
共享专家1 × 40 层 · FP8 32×32
1.42 B0.19%
embed + lm_head129280 × 5120 · BF16 × 2
1.32 B0.17%
DSpark 其余注意力、Markov、main_proj
0.64 B0.08%
视觉塔 + aligner32 层 ViT · BF16
0.49 B0.06%
Engram 投影wkv [25600, 6144] × 2
0.31 B0.04%
逻辑参数 763.21 B 落盘 475.24 GiB(含 scale) 骨干单 token 激活 16.79 B

16.79 B 是怎么凑出来的

  • 路由专家 8.49 B — 40 层 × 6 × 35.39 M
  • 注意力 5.08 B — 40 层全部参与,另加 compressor 18.4 M、indexer 43.5 M
  • 共享专家 1.42 B — 每层 1 个,FP8
  • embed + lm_head + 最终 norm 1.32 B
  • Engram 投影 0.31 B — 两层的 wkv 整矩阵都乘
  • 路由门 + mHC 0.099 B

三笔存储不要混

  • 骨干 GPU 权重 278.12 GiB:注意力、MoE、embed、Engram 投影。两张表、视觉塔、DSpark 都不在这笔里。
  • Engram 表 188.83 GiB,宿主机 mmap,按 FP8 行存。按 float32 展开单层大约 393 GB,所以 gather 之前不做全表反量化。
  • 另外两笔:视觉塔 0.90 GiB,挂在多模态模型上一起加载;DSpark 7.39 GiB,只在打开草稿时从同一份 checkpoint 再读。落盘合计 475.24 GiB。路由专家占逻辑参数的 71%,占磁盘大约 57%。
张量形状dtype出现位置
embed.weight / head.weight[129280, 5120]BF16各一份,不共享
attn.wq_a / attn.wkv[1280, 5120] / [512, 5120]FP8 32×32运行时合成一次 GEMM,输出 [T, 1792]
attn.wq_b[32768, 1280]FP8 32×3264 头 × 512,列并行
attn.wo_a / attn.wo_b[8192, 4096] / [5120, 8192]FP8 32×328 组,每组 [1024, 4096]
attn.attn_sink[64]FP32每头一个 logit 偏置
hc_attn_fn / hc_ffn_fn[24, 20480]FP32每层各一份;base [24],scale [3]
compressor.wkv / wgate[512, 5120]BF16FULL 层。ratio 2 才有 wgate;层 20 没有
indexer.wq_b[4096, 1280]FP8 32×328 个打分层:2, 8, 14, 20, 24, 28, 32, 36
indexer.wk[128, 512]BF16只有 4 个 FULL 层。REINDEX 层磁盘上没有这份权重
indexer.weights_proj[32, 5120]BF168 个打分层
ffn.gate.weight[384, 5120]BF16bias、bias_vl 各 [384] FP32
experts.e.w1 / w3[2304, 2560]I8 打包 FP4逻辑列数 5120。scale [2304, 160] E8M0
experts.e.w2[5120, 1152]I8 打包 FP4逻辑列数 2304。scale [5120, 72]
shared_experts.w1[2304, 5120]FP8 32×32与路由专家同形、不同 dtype,所以不进同一个 buffer
engram.wkv[25600, 6144]FP8 32×324 路 key + 1 路 value,各 5120
mtp.0.main_proj[5120, 15360]FP8 32×32三层目标隐藏态拼起来
markov embed / head[129280, 256]BF16只在 mtp.2

图 6 · KV

历史走 PAGE,滑窗和半成品组走 STATE

默认池是 BF16 主平面加 FP8 index(--kv-cache-dtype auto / bfloat16)。 页大小在 vLLM 插件里固定为 ATOM_DEEPSEEK_V41_BLOCK_SIZE = 256。 下面的字节数是 V41PoolGeometry 对这份 config 的结果:paged_bytes = 739840,正好是 256 × 2890。

图 6 · 一个 256-token 页里有什么

PAGE · 739 840 B / block · 随上下文增长 main 层 2 128 行 × 1024 B 131 072 B main 层 8 ratio 2,128 行 131 072 B main 层 14 ratio 2,128 行 131 072 B main 层 20 ratio 1,256 行 262 144 B 4 张 index 平面 132 B / 行 · 与主平面同行数 84 480 B 主平面小计 655 360 B。一行 BF16 latent = 512 × 2 = 1024 B。摊到源 token:三个 ratio-2 owner 各 512 B,加上 ratio-1 的 1024 B,再加 index 330 B,合计 2890 B/token。 1 048 576 token 的 PAGE 是 2.822 GiB。这不含每层自己的滑窗。 STATE · 5 276 672 B / 请求 · 不随上下文变长 40 层滑窗 每层 128 × [512] BF16 40 × 128 × 1024 = 5 242 880 B 压缩 ring × 4 owner kv 与 score 各 [2, 512] FP32 32 768 B,对齐后仍在这笔里 Engram cursor [4] int64:位置 + 3 个历史 id 整段 STATE 含对齐 5.03 MiB
滑窗是每层自己的近期 latent,压缩平面只有 4 个 owner。把 PAGE 前缀恢复回来、却留下旧的 STATE,游标对不上调度器宣称的边界,引擎会直接拒绝这次请求。 DSpark 把滑窗 ring 加深到 133,压缩 ring 加深到 7,并把层数扩到 43(含 3 层草稿)。

换成 packed FP4 主池

nvfp4 把主平面改成每行 288 B(512 维 E2M1 加 group-16 的 E4M3 scale),滑窗行变成 528 B(512 个 FP8 加 16 个 E8M0)。 index 平面不受这个开关影响。按同样的 4 个 owner 摊下来是 1050 B/token,一页 268 800 B。 主池只认 bf16 和 fp4 两种,没有单独的 FP8 KV 池。 index 平面是另一个开关 --index_cache_dtype:fp8(默认,132 B/行)或 fp4(68 B/行), 后者把 BF16 池压到 2730 B/token,把 packed 池压到 890 B/token。

DSpark 要求 BF16 KV

投机配置一旦打开,kv_cache_dtype 必须是 bf16,草稿宽度必须是 5,方法名必须是 dspark,而且草稿模型和目标模型是同一份 checkpoint。 另外:main 上的 validate_runtime_config 直接拒绝 plugin mode —— V4.1 目前只有 ATOM 原生这一条执行路径,vLLM 插件桥接(页大小固定 256、STATE 另占 proxy block)还在未合入的分支上。


量化

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

权重大类存储激活备注
wq_a、wq_b、wkv、wo_a、wo_b、indexer.wq_b、Engram wkv、main_projFP8 e4m3,块 32×32,scale E8M0动态,按 32 分组wo_a 在组内做 GEMM,权重加载后洗成 16×16
共享专家 w1/w2/w3FP8 e4m3,32×32MXFP8与路由专家 dtype 不同,保持独立模块
路由专家(骨干 384,草稿 128)FP4 e2m1,块 1×32,I8 打包MXFP8逻辑形状 [2304, 5120] 等;磁盘 K 维减半
compressor、indexer.wk、weights_proj、gate、embed、head、视觉、MarkovBF16BF16compressor 的组内累加指定为 FP32
hc_*、attn_sink、gate.bias、bias_vlFP32FP32mHC 系数不允许降精度
Engram 表FP8 行,scale 沿 256 维每 32 一个gather 后转不进 GPU 参数表

validate_native_quantization 只接受这一套:quant_method = fp8、weight_block_size = [32, 32]、 scale_fmt = ue8m0、activation_scheme = dynamic、expert_dtype = fp4。 路由专家若走默认的 MXFP4 激活,输出会偏离参考,因此这里把激活钉在 MXFP8。


执行模式

同一份权重,四种跑法;不支持的组合在读权重之前就被拒绝

validate_runtime_config 在加载权重和开池子之前跑一遍,把没实现的执行模式整条拒掉, 而不是降级成某个「能跑但不对」的路径。下面这张表是目前允许的四种跑法,右边那张是会直接抛错的清单。

跑法怎么开注意力在图里吗用途
eager--enforce-eager不捕获定位故障时的基准;数值上的参照系
level 0 + FULLlevel=0, cudagraph=FULLdecode 整个前向进图当前验收用的配置
level 0 + PIECEWISElevel=0, cudagraph=PIECEWISE分段允许,但 level 3 下不接受
level 3 + FULLlevel=3, cudagraph=FULLdecode 整个前向进图编译器路径;level 3 只认 FULL 或 eager

会被直接拒绝的

  • pipeline / context / data parallel,TBO,EPLB,RapidServe。V4.1 只跑 TP(专家可整块 EP,也可切 TP)。
  • vLLM 插件模式、在线量化。
  • kv_cache_dtype 不是 bf16 或 fp4;index_cache_dtype 不是 fp8 或 fp4。
  • PAGE 的 token 数是奇数(ratio 2 要能整除)。
  • KV 传输不是 lmcache_mp:只有它会把 STATE 一起过一遍 PAGE 备份;只搬 PAGE 的 connector 拿不到压缩 ring 和游标。
  • 投机不是 dspark / 不是 5 个 token / KV 不是 bf16 / 开了宽松 MTP 验证。

旁路流是可选的,默认一条都不开

ATOM_DSV41_SIDE_STREAMS 取 0 / 1 / 2,默认 0:compressor 和 indexer 都排在主流上,Attention 块的主链、compressor、indexer 三支是顺序执行的。 1 让 compressor 借 MoE 那条 alt_stream(它的生命期在打分处就结束,早于共享专家发出,不额外占队列); 2 再给 indexer 一条自己的流 —— 只有它会和别的分支同时在飞,所以只有它值一个硬件队列。 把 indexer 挪开本身不赚钱,赚的是另一边:query 投影和那次融合的 rope/window 不再堵在它前面。

mono:把一层压成两个 kernel

ATOM_MONO_ENABLE 默认是 1,针对 ≤ 12 行的 decode(DSpark 的验证步和草稿骨干): K1 从 mHC 的缝一路做到旋转好的 query 和滑窗行;K2 把打分、稀疏 decode、输出投影、它的 all-reduce、FFN 的缝和 MoE 全放进一次发射 (ATOM_MONO_CHECK 可以把 K2a/K2b 拆开逐段对比)。compressor 和 Engram 仍由原模块在两个 kernel 之间跑。 注意:它目前会被拒绝 —— 里面的 MoE kernel 还在跑 MXFP4 激活,而 V4.1 的路由专家要的是 MXFP8。

为什么路由专家必须喂 MXFP8

专家权重是 FP4,但激活不是。aiter 在 gfx950 上对这种专家的默认是 MXFP4 激活, 那样算出来的路由输出比参考实现偏 ~12%,GSM8K 掉约 1.4 pp。 moe.py 因此显式把 moe_fp8_activations 钉成 True。 这也是上面 mono 被拒的同一个原因。


在 ATOM 里的位置

按这个顺序读,拓扑和字节不会先分叉

#文件看什么
1atom/models/deepseek_v41/config.pybuild_attention_topology:四种模式、KV owner、index 段、候选块能不能装下 top-512
2atom/models/deepseek_v41/weights.pycheckpoint_schema:每个张量的形状、dtype、TP 轴。Engram 表标成 host
3atom/models/deepseek_v41/model.pyBlock.forward:Engram settle,然后注意力 pre、FFN pre、欠着的 post
4atom/models/deepseek_v41/attention.py三条流的 join 点;wqkv_a 的切分;wo 的分组 GEMM
5atom/model_ops/deepseek_v41/compressor.pyratio 1 无门;ratio 2 两个 token 一组,overlap=False
6atom/model_ops/attentions/deepseek_v41/indices.py前缀长度 = 滑窗 + min(pos/ratio, 512)
7atom/model_ops/attentions/pool_layout/v41_pool_geometry.pyPAGE 与 STATE 的字段、2890 B/token、5.03 MiB/请求
8atom/models/deepseek_v41/moe.pybias_vl 只换图像 token 的路由偏置,专家计算仍走 V4 的 MoE
9atom/model_ops/engram/device/layer.pywkv 的 5 段切分,以及 host 表不在这个模块里
10atom/models/deepseek_v41/dspark.py · vision.py草稿层复用同一套 Block;视觉是另一套 ViT + aligner
11atom/model_ops/deepseek_v41/paged_scoring.py · indexer.py两级 top-k:行打分走分页 MQA logits,块打分把最新一块钉成 +inf
12atom/models/deepseek_v41/mono/runner.py≤ 12 行 decode 的 K1 / K2a / K2b 融合,以及它为什么现在被拒

权重进哪一块内存

骨干和视觉走通用的 load_model。Engram 表在 weights_mapper 里被映成空,不注册成参数,由 model_loader/deepseek_v41.py 的 mmap 一直持有。 DSpark 是另一个模块,只在投机打开时再扫同一份 checkpoint 里的 mtp.*。

和字节相关的拒绝

  • 压缩比出现 0/1/2 以外的值,或者草稿层的 ratio 不是 0。
  • 候选块装不下 512 个选中行。
  • 量化格式不是上面那一套 native FP8 + FP4。
  • KV 传输只搬 PAGE、不搬 STATE。这样的 connector 会被拒绝,而不是降级运行。