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 从哪来」。
阅读顺序
主结构里每个模块都标了进出形状。复杂的注意力画成一块,只写它吃什么、吐什么; 紧接着的一节把这块按模块拆开。主图里同样收成一块的 mHC、MoE,以及挂在骨干外面的模块,各自再有一张结构图。
先分清对象
架构名是 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 注意力
rope_theta = 10000;压缩行用 compress_rope_theta = 160000 的 YaRN(factor 16,原长 65536)。mHC、Engram、DSpark
hc_*_fn [24, 20480] 预测 pre / post / 4×4 混合,Sinkhorn 20 步。主结构 · 层调度
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] 把每一层标成四种模式。
悬停色块可以看到层号。
| 层 | ratio | KV owner | 谁打分 | 模式 | 这一段在看什么 |
|---|---|---|---|---|---|
| 0–1 | 0 | — | — | WINDOW | 只有本层 128 滑窗。层 1 在注意力之前做 Engram |
| 2–7 | 2 | 2 | 2 | FULL + 5×REUSE | 每 2 token 一行。层 2 写平面并选出 top-512,层 3–7 直接用这份选择 |
| 8–13 | 2 | 8 | 8 | FULL + 5×REUSE | 新的压缩平面,结构与上一段相同 |
| 14–19 | 2 | 14 | 14 | FULL + 5×REUSE | 第三段 ratio-2。层 14 同时是 Engram 层 |
| 20–23 | 1 | 20 | 20 | FULL + 3×REUSE | 一行一个 token。层 20 写出候选块:2048 × 8 行 |
| 24–27 | 1 | 20 | 24 | REINDEX + 3×REUSE | KV 仍是层 20 的平面,top-k 在候选块里重算 |
| 28–31 | 1 | 20 | 28 | REINDEX + 3×REUSE | 同上,打分点前移到 28 |
| 32–35 | 1 | 20 | 32 | REINDEX + 3×REUSE | 同上 |
| 36–39 | 1 | 20 | 36 | REINDEX + 3×REUSE | DSpark 的三路输入取 37、38、39 的残差,这三层本身是 REUSE |
| mtp.0–2 | 0 | — | — | 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 没有这张门。
主结构
文本骨干一次前向的模块顺序是固定的。Attention 在这里是一块:进去是 mHC 之后的
[T, 5120],出来还是 [T, 5120]。四种内部结构在下一节。
Engram、mHC、MoE、DSpark、视觉塔在主图里也先收成块,形状写在块上,内部见后面的单独拆解。
主结构 · DeepSeek-V4.1-Flash
Attention 块
主结构里那块 Attention 拆开之后,是下面这条链。四种模式的差别不在这条链上:
build_attention_topology 只决定这一层额外带哪些权重、
往哪几份缓存写、top-512 从哪来。
compressor 与 indexer 接在链的侧面,它们是特殊模块,不画进这条主链。
Attention 块 · 共用通路
ATOM_DSV41_SIDE_STREAMS 默认是 0:compressor、这条主链、indexer 顺序排在同一条流上。
开到 1 或 2 只改变它们占不占旁路队列,依赖边不变:投影不能早于 qk_norm,打分不能早于 compressor 写下的那一行。
Attention 块 · 四种内部结构
上一张图是主结构里那块 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
图 2b · FULL · 层 2 / 8 / 14(ratio 2)与层 20(ratio 1)· 唯一会写 PAGE 的层
图 2c · REINDEX · 层 24 / 28 / 32 / 36 · 借层 20 的 KV,自己重算 top-512
图 2d · REUSE · 30 层 · 不打分,直接取本段第一层的选择
| 模式 | 层数 | 额外权重 / 层 | 写主平面 | 写 index 平面 | 发打分 kernel | 打分宽度 |
|---|---|---|---|---|---|---|
| WINDOW | 2 | 0 | 否 | 否 | 否 | — |
| FULL | 4 | 10.71 M / 8.09 M | 是 | 是 | 是 | 整条上下文 |
| REINDEX | 4 | 5.41 M | 否 | 否 | 是 | 2048 × 8 = 16 384 行 |
| REUSE | 30 | 0 | 否 | 否 | 否 | 沿用本段 |
这四个标签是算出来的,不是配置里写死的
配置只给三样东西: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
compressor 从 Attention 块的 hidden 侧面分出去,不经过 wqkv_a。
它写出的是 owner 平面上的压缩行;indexer 吃的是 RoPE 之前的那份 latent。
CSA2 的组与组不重叠(overlap = False),半成品留在每请求的 FP32 ring 里,不进 PAGE。
compressor · ratio 2 与 ratio 1
ratio 只允许 0、1、2;草稿层的 ratio 必须是 0,否则 build_attention_topology 直接拒绝。
特殊模块 · 压缩平面与候选块
图 3 · ratio 2 的池化,以及层 20 之后的候选块
candidate_source_layer_id。从这一层起,所有消费者和层 20 必须落在同一个 KV owner 上——这里就是层 20 自己。
ratio 2 的三段各自拥有独立平面,互不复用候选块。
特殊模块 · indexer
打分从来不把 key 重新物化出来:query 量化之后直接交给分页 MQA logits kernel, 平面本身就是 PAGE 里那块 index 区域。每一个 query 行都是自己的一个「批次项」, 带自己的可见长度和自己的块表 —— 所以 prefill 的 token、decode 的 token、草稿的 token 在打分器眼里是同一种行, ragged 批次对它不构成特殊情况。
图 3b · 从 hidden 到 selected [rows, 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
图 4 · 四路残差怎么收、怎么放
hc_*,形状相同,所以 mHC 与这一层是 WINDOW、FULL 还是 REUSE 无关。
norm 权重 [5120] 不单独跑,它被吃进 pre_delayed 那一次投影里。
特殊模块 · MoE
MoE 接在 Attention 块之后、mHC 的 FFN 缝把残差收成 [T, 5120] 的地方。
路由专家与共享专家形状相同、dtype 不同,所以共享专家是独立模块,每个 token 都过一遍。
图像 token 只换选择用的偏置,不换专家权重。
MoE · 从 normed hidden 到 [T, 5120]
特殊模块 · Engram
图 5 · n-gram 哈希到残差门控
特殊模块 · DSpark
DSpark · 块宽 5 · mtp.0–2
视觉塔 · 32 层 ViT + aligner
bias_vl。DSpark 与多模态请求不能同时出现。
规模
参数量的大头是两块稀疏存储:40 层 × 384 个 FP4 路由专家,加上两张 Engram 表。 单 token 激活按「这一步真正参与乘加的权重」计:6 个路由专家加 1 个共享专家、全部注意力、embed 与 lm_head。 Engram 表每个 token 只 gather 24 行,不计入激活;视觉塔和 DSpark 是另外的前向。
16.79 B 是怎么凑出来的
wkv 整矩阵都乘三笔存储不要混
| 张量 | 形状 | 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×32 | 64 头 × 512,列并行 |
| attn.wo_a / attn.wo_b | [8192, 4096] / [5120, 8192] | FP8 32×32 | 8 组,每组 [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] | BF16 | FULL 层。ratio 2 才有 wgate;层 20 没有 |
| indexer.wq_b | [4096, 1280] | FP8 32×32 | 8 个打分层:2, 8, 14, 20, 24, 28, 32, 36 |
| indexer.wk | [128, 512] | BF16 | 只有 4 个 FULL 层。REINDEX 层磁盘上没有这份权重 |
| indexer.weights_proj | [32, 5120] | BF16 | 8 个打分层 |
| ffn.gate.weight | [384, 5120] | BF16 | bias、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×32 | 4 路 key + 1 路 value,各 5120 |
| mtp.0.main_proj | [5120, 15360] | FP8 32×32 | 三层目标隐藏态拼起来 |
| markov embed / head | [129280, 256] | BF16 | 只在 mtp.2 |
图 6 · KV
默认池是 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 页里有什么
换成 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)还在未合入的分支上。
量化
| 权重大类 | 存储 | 激活 | 备注 |
|---|---|---|---|
| wq_a、wq_b、wkv、wo_a、wo_b、indexer.wq_b、Engram wkv、main_proj | FP8 e4m3,块 32×32,scale E8M0 | 动态,按 32 分组 | wo_a 在组内做 GEMM,权重加载后洗成 16×16 |
| 共享专家 w1/w2/w3 | FP8 e4m3,32×32 | MXFP8 | 与路由专家 dtype 不同,保持独立模块 |
| 路由专家(骨干 384,草稿 128) | FP4 e2m1,块 1×32,I8 打包 | MXFP8 | 逻辑形状 [2304, 5120] 等;磁盘 K 维减半 |
| compressor、indexer.wk、weights_proj、gate、embed、head、视觉、Markov | BF16 | BF16 | compressor 的组内累加指定为 FP32 |
| hc_*、attn_sink、gate.bias、bias_vl | FP32 | FP32 | mHC 系数不允许降精度 |
| 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 + FULL | level=0, cudagraph=FULL | decode 整个前向进图 | 当前验收用的配置 |
| level 0 + PIECEWISE | level=0, cudagraph=PIECEWISE | 分段 | 允许,但 level 3 下不接受 |
| level 3 + FULL | level=3, cudagraph=FULL | decode 整个前向进图 | 编译器路径;level 3 只认 FULL 或 eager |
会被直接拒绝的
kv_cache_dtype 不是 bf16 或 fp4;index_cache_dtype 不是 fp8 或 fp4。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 里的位置
| # | 文件 | 看什么 |
|---|---|---|
| 1 | atom/models/deepseek_v41/config.py | build_attention_topology:四种模式、KV owner、index 段、候选块能不能装下 top-512 |
| 2 | atom/models/deepseek_v41/weights.py | checkpoint_schema:每个张量的形状、dtype、TP 轴。Engram 表标成 host |
| 3 | atom/models/deepseek_v41/model.py | Block.forward:Engram settle,然后注意力 pre、FFN pre、欠着的 post |
| 4 | atom/models/deepseek_v41/attention.py | 三条流的 join 点;wqkv_a 的切分;wo 的分组 GEMM |
| 5 | atom/model_ops/deepseek_v41/compressor.py | ratio 1 无门;ratio 2 两个 token 一组,overlap=False |
| 6 | atom/model_ops/attentions/deepseek_v41/indices.py | 前缀长度 = 滑窗 + min(pos/ratio, 512) |
| 7 | atom/model_ops/attentions/pool_layout/v41_pool_geometry.py | PAGE 与 STATE 的字段、2890 B/token、5.03 MiB/请求 |
| 8 | atom/models/deepseek_v41/moe.py | bias_vl 只换图像 token 的路由偏置,专家计算仍走 V4 的 MoE |
| 9 | atom/model_ops/engram/device/layer.py | wkv 的 5 段切分,以及 host 表不在这个模块里 |
| 10 | atom/models/deepseek_v41/dspark.py · vision.py | 草稿层复用同一套 Block;视觉是另一套 ViT + aligner |
| 11 | atom/model_ops/deepseek_v41/paged_scoring.py · indexer.py | 两级 top-k:行打分走分页 MQA logits,块打分把最新一块钉成 +inf |
| 12 | atom/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.*。
和字节相关的拒绝