GlmMoeDsaForCausalLM model_type · glm_moe_dsa IndexShare MLA + DSA MTP × 1

GLM-5.3 架构解剖

GLM-5.3 在 ATOM 里没有自己的模型文件——它挂在 atom/models/deepseek_v2.py 上,子类 GlmMoeDsaForCausalLM 只覆盖了一张量化排除名映射。把它的 config 与 GLM-5.2 逐字段比对, 只多出一个 moe_router_dtype:78 层、6144、64 头、256 专家、 连 indexer_types 那 21 个 full 的落点都逐项相等。 所以真正要读懂的不是模型定义,而是那 78 项 indexer_types 如何一路改写构造期的模块树、运行期的 buffer 绑定与 MTP 的草稿循环。

753.3 B总参数(含 MTP)
41.25 B单 token 激活
78 + 1主干层 + MTP 层
21 / 78拥有 indexer 的层
6144hidden_size
256 / 8专家数 / 每 token
2048index_topk
1 Mmax_position

定位

它是 DeepSeek-V3.2 的骨架,配 GLM 自己的尺寸

ATOM 里没有 glm5_3.py_CONFIG_REGISTRYglm_moe_dsa 映射到 deepseek_v3 的 config schema, 模型类直接继承 DeepseekV2ForCausalLM,整个子类只有一个类变量。

唯一的子类覆盖

class GlmMoeDsaForCausalLM(DeepseekV2ForCausalLM):
    quant_exclude_name_mapping = {
        "indexers_proj": "indexer.weights_proj",
    }

GLM 的 HF 量化配置用 indexers_proj 这个名字排除索引投影, 而 ATOM 的非融合模块路径叫 indexer.weights_proj。 这张表逐条做 str.replace,好让 FP4/MXFP4 的回退路径不去量化那个 BF16 投影。

与 GLM-5.2 的全部差异

moe_router_dtype     : —      → "float32"
transformers_version : 5.12.0 → 5.15.0

去掉 quantization_config 后逐键比对的结果。结构完全相同, 差别只在权重本身与那一个 dtype 声明(见图 4)。 姊妹型号 GLM-5.3-Flash 则是另一套完全不同的结构—— 45 层混合 KDA/MLA、4 路残差、全 NoPE——单独一份文档: GLM-5.3-Flash 架构解剖


规模

753.3 B 参数落盘,单 token 只碰 41.25 B

下面每个数字都来自 141 个 safetensors 分片的头部(读 shape 相加, fp8 的 weight_scale_inv 单列,不计入参数量)。

MoE routed experts256 专家 × 75 层 × 37.75 M
724.78 B96.2 %
MLA 注意力78 层 × 165.0 M
12.87 B1.7 %
MTP 层 78完整 DSA + 完整 MoE
9.95 B1.3 %
MoE shared expert1 个 × 75 层
2.83 B0.4 %
embed_tokens / lm_head各 154880 × 6144 BF16
1.90 B0.3 %
dense MLP层 0–2,d_ff 12288
0.68 B0.1 %
DSA indexer21 个 full 层 × 9.37 M
0.197 B0.03 %
MoE routergate [256, 6144] + bias
0.118 B0.02 %
合计 753.33 B单 token 激活 41.25 B 96.2 % 的参数在 MoE 专家里

激活量是怎么凑出来的

  • MoE 专家 25.48 B — 75 层 ×(8 routed + 1 shared)× 37.75 M
  • 注意力 12.87 B — 78 层全激活
  • embed + lm_head 1.90 B
  • dense MLP 0.68 B — 只有层 0–2
  • indexer 0.20 B — 只有 21 个 full 层

量化布局

  • FP8 e4m3 block 128×128:MoE 专家、dense MLP、 MLA 全部投影(含 kv_b_proj),以及 indexer 的 wq_b / wk
  • BF16 保留indexer.weights_proj(就是那张排除名映射守住的)、 k_norm、全部 norm、gate.weight、embedding 与 lm_head
  • e_score_correction_bias 在 checkpoint 里是 F32——这不是巧合,见图 4。

核心机制 · 层调度

78 层里只有 21 层真的算 top-k

indexer_types 把每层标成两类:full 本层跑 indexer, 从全部 KV 里选出 2048 个下标写入共享 buffer;shared 磁盘上就没有 indexer 权重,直接复用上一个 full 层选出的下标。 节奏是前 3 层连续 full,之后每 4 层一个 full。

full 层落点:0, 1, 2, 6, 10, 14, 18, 22, 26, 30, 34, 38, 42, 46, 50, 54, 58, 62, 66, 70, 74

图 1 · IndexShare

一个 4 层周期:一次 top-k,四层共用 indexer_types = [... full, shared, shared, shared, full ...] layer 6 · full 自带完整 indexer 权重 wq_b [4096, 2048] wk [128, 6144] · k_norm 128 weights_proj [32, 6144] 9.37 M 1 layer 7 · shared 磁盘上没有 indexer 权重 构造期不建 indexer 模块 直接读上一个 full 层的结果 0 M layer 8 · shared 磁盘上没有 indexer 权重 构造期不建 indexer 模块 直接读上一个 full 层的结果 0 M layer 9 · shared 磁盘上没有 indexer 权重 构造期不建 indexer 模块 直接读上一个 full 层的结果 0 M _sparse_kv_indices_gpu · 每层复用同一块 buffer [T, 2048] int32 —— 被选中的 KV 槽位下标,写一次读四次 写一次 读三次 21 个 full · 57 个 shared —— 稀疏选择的算力只付 21/78
这是 GLM-5.2 起引入、5.3 原样继承的唯一结构性改动。shared 层不是“跳过计算”,而是磁盘上就没有那份权重_indexer_weights_shared 让构造期不为它们建模块,_should_skip_index_topk 让运行期不为它们跑 top-k。indexer_types 存在时是唯一权威;缺失时才回退到 index_topk_freq / index_skip_topk_offset 的公式。

结构全图

从 input_ids 到 logits,再放大其中一层

图 2 · 结构全图

完整模型栈 GlmMoeDsaForCausalLM · 753.3 B · 78 + 1 层 input_ids [T] embed_tokens [154880, 6144] BF16 951.5 M · 每 rank 全表 layers 0 – 2 · dense MLP 6144 ↔ 12288 679.5 M layers 3 – 77 · MoE × 75 层 256 routed (top-8) + 1 shared moe_intermediate_size 2048 每层 self_attn 165.0 M 每层 mlp 9.70 B 21 层带 indexer 57 层复用(IndexShare) KV cache 576 / token / 层 742.6 B 参数 · 激活 25.5 B model.norm · RMSNorm(6144) lm_head [154880, 6144] BF16 · 951.5 M logits [T, 154880] layer 78 · MTP draft eh_proj [6144, 12288] enorm · hnorm · shared_head.norm 自带完整 indexer + 完整 MoE 9.95 B 077 DecoderLayer 展开(层 3–77 之一) 一层 9.88 B(full 索引层)/ 9.87 B(shared 索引层) residual stream · [T, 6144] input_layernorm · RMSNorm(6144) fused_qkv_a_proj · 一次 GEMM,输出按列切三份 [2624, 6144] 2624 = 2048 + 512 + 64 · 16.1 M ATOM 构造期把 q_a_proj 与 kv_a_proj_with_mqa 合并;checkpoint 里它们是分开的两张量 1 q_a_layernorm RMSNorm(2048) kv_a_layernorm RMSNorm(512) RoPE 交错式 · θ = 8e6 q_b_proj [16384, 2048] → 64 × 256 33.6 M kv_b_proj [28672, 512] → 64 × (192+256) 14.7 M k_pe [T, 64] 旋转后入 cache 与 512 latent 拼成 576 indexer(仅 21 个 full 层拥有) wq_b [4096, 2048] → 32 头 × 128 wk [128, 6144] → k_norm(128) weights_proj [32, 6144] · FP8 打分 → top-k(2048) → _sparse_kv_indices_gpu 9.37 M / 层 —— shared 层这一格整块不存在 2 MLA · 稀疏因果注意力 查询 [T, 64, 576] ⟷ KV entry [slots, 576] = 512 latent + 64 rope 只读 indexer 选出的 2048 个槽;seq ≤ 2048 时 top-k 选中全部,等价稠密 3 o_proj [6144, 16384] = 64 × v_head_dim 256 post_attention_layernorm · RMSNorm(6144) mlp · MoE gate [256, 6144] → sigmoid → noaux_tc top-8 / 256 e_score_correction_bias [256] fp32 —— 路由必须在 fp32 里做(见图 4) 每专家 gate/up [2048, 6144] · down [6144, 2048] = 37.75 M + 1 shared expert · × routed_scaling_factor 2.5 4 这一层的账 · self_attn 165.0 M(full 层 174.4 M,多出来的就是那个 indexer) · mlp 9.70 B —— 256 个专家占了全模型 96.2 % 的参数 · 单 token 只碰 8 + 1 个专家:339.8 M,其余 9.36 B 一动不动 · KV 只存 576 宽的 latent,不存 64 头 × 256 的展开形态
请盯住 fused_qkv_a_proj 那一格:它是一次 [2624, 6144] 的 GEMM,输出按列切成三份2624 = 2048 + 512 + 64),而不是投影到某个三维形状。checkpoint 里它本是 q_a_proj [2048, 6144]kv_a_proj_with_mqa [576, 6144] 两张量,合并发生在 ATOM 构造期。右侧第 2 格(indexer)在 57 个 shared 层里整块不存在。

稀疏注意力

2048 这个数字在不同上下文下的含义完全不同

图 3 · 稀疏度

一次 forward 里,注意力实际读了多少 index_topk = 2048 · max_position_embeddings = 1 048 576 indexer 打分 q [T, 32, 128] · k [ctx, 128] 全部 FP8 logits [T, ctx] fp32 每 token 对全历史打一次分 1 top-k(2048) → [T, 2048] int32 槽位下标 写入 _sparse_kv_indices_gpu 后面 3 个 shared 层直接读它 2 MLA 只读这 2048 个 KV entry [2048, 576] ctx ≤ 2048 时选中全部 → 等价稠密 ctx = 1 M 时读 0.2 % 3 同一个 2048,在不同上下文长度下占的比例 2 048 全部选中 · 与稠密因果注意力逐位相同 8 192 每 4 个 token 里有 1 个进入注意力 131 072 1.6 % 1 048 576 0.2 % —— KV cache 仍然是全量 89.9 KB/token
注意最后一行:被读的只有 2048 个,但 KV cache 仍然按全长存。稀疏省的是注意力的计算与带宽,不是显存。1 M 上下文的 KV 依然是 89.9 KB × 1 M ≈ 87.7 GiB(BF16)。

容易踩的地方:上下文不到 2048 时 indexer 是 no-op

index_topk = 2048 意味着历史长度 ≤ 2048 时 top-k 会选中每一个 token, 稀疏路径与稠密因果注意力逐位相同。此时 indexer 照算不误,结果却不改变任何东西—— 所以短上下文下的对比说明不了稀疏选择是对的,只能说明它没把事情弄坏。


GLM-5.3 特有

唯一新增的 config 字段,指向一个 4 个数量级的精度问题

图 4 · fp32 路由

noaux_tc 的 e_score_correction_bias 落进 bf16 会发生什么 model.layers.40.mlp.gate.e_score_correction_bias · 实测 256 个值 fp32(checkpoint 里的样子) 225 个互不相同的值 round to nearest bf16 bf16(模型 dtype,ULP = 1/16) 7.84375 7.96875 8 8.0625 8.125 8.1875 8.25 8.3125 只剩 8 个 —— 256 个专家挤在 8 个刻度上 后果 这张表存在的意义就是给 256 个专家排序 塌到 8 个值之后,绝大多数专家在偏置上完全同分 top-8 的选择信号几乎被丢光 ATOM 的处理 _moe_router_dtype() 对 glm_moe_dsa 无条件返回 fp32 gate(hidden, otype=fp32),logits 与 bias 同宽 两者必须同宽:biased_grouped_topk 按 logits.dtype reinterpret_cast bias,宽度不一致就读错缓冲区
这是 GLM-5.3 相对 GLM-5.2 唯一新增的 config 字段moe_router_dtype: "float32")背后的东西。实测全部 78 层:每层 115–229 个互不相同的 fp32 偏置值,落到 bf16 后只剩 3–9 个。ATOM 不看这个字段而是按 model_type 判定——GLM-5 / 5.1 / 5.2 的 config 早于这个字段,没法自己声明,但它们的偏置有一模一样的分布。

为什么 gate 的输出 dtype 不是一个独立选择

把 logits 舍到 bf16 本身几乎不动 top-k,但它与偏置的宽度不是独立的: aiter 的 biased_grouped_topkgating_output.dtype() 分发, 然后把 correction_bias reinterpret_cast 成同一个 scalar_t。 两者必须同宽,否则内核会按错误的宽度去读偏置缓冲区。 所以这一个 dtype 同时管住了两件事。


权重实测

从 141 个 safetensors 分片头部读到的真实形状

checkpoint 张量dtypeshape说明
model.embed_tokens.weightBF16[154880, 6144]951.5 M
注意力(每层都有)
self_attn.q_a_projF8_E4M3[2048, 6144]与下一行在 ATOM 里合并成 fused_qkv_a_proj [2624, 6144]
self_attn.kv_a_proj_with_mqaF8_E4M3[576, 6144]576 = 512 latent + 64 rope
self_attn.q_a_layernorm / kv_a_layernormBF16[2048] / [512]
self_attn.q_b_projF8_E4M3[16384, 2048]64 头 × 256(192 nope + 64 rope)
self_attn.kv_b_projF8_E4M3[28672, 512]64 × (192 + 256)
self_attn.o_projF8_E4M3[6144, 16384]64 × v_head_dim 256
indexer(只有 21 个 full 层 + MTP 层有)
indexer.wq_bF8_E4M3[4096, 2048]32 头 × 128
indexer.wkF8_E4M3[128, 6144]
indexer.weights_projBF16[32, 6144]就是量化排除名映射守住的那一个
indexer.k_norm.weight / .biasBF16[128] / [128]带 bias 的 LayerNorm,不是 RMSNorm
layers.5.self_attn.indexer.*不存在shared 层,一个 indexer 张量都没有
FFN
mlp.gate.weight / e_score_correction_biasBF16 / F32[256, 6144] / [256]偏置的 fp32 不是可选项
mlp.experts.{0..255}.gate_proj / up_projF8_E4M3[2048, 6144]单专家 37.75 M
mlp.experts.*.down_projF8_E4M3[6144, 2048]
mlp.shared_experts.*F8_E4M3同上1 个,与 routed 输出相加
mlp.gate_proj / up_proj(层 0–2)F8_E4M3[12288, 6144]稠密 MLP,每层 226.5 M
MTP
layers.78.eh_projBF16[6144, 12288]拼接“末层 hidden + 下一 token embed”后降回 6144
layers.78.enorm / hnorm / shared_head.normBF16[6144]草稿块自带完整 indexer 与完整 MoE,9.95 B

显存

缓存账

缓存计费单位大小备注
MLA KV78 层 × 576 × dtype / token 89 856 B/token
= 87.8 KiB · BF16
FP8 时减半;存的是 latent,不是 64 头 × 256 的展开
索引 key21 个 full 层 × 144 B / token 3 024 B/token 144 = (128 + 4) 向上对齐到 16 B;57 个 shared 层不占
合计每 token 92 880 B ≈ 90.7 KiB 1 M 上下文 ≈ 88.6 GiB(BF16 KV)

索引缓存只由 full 层承担,是 IndexShare 除算力之外的第二项收益: 若 78 层都建索引缓存,这一项会是 11 232 B/token,涨到 3.7 倍。


投机解码

MTP 层自带 indexer,不复用主模型的 top-k

checkpoint 层 78 是一个完整的解码层,外加 eh_proj / enorm / hnorm / shared_head.norm,并且自带完整的 indexer 权重。 ATOM 的 _should_skip_index_topk 对层号 ≥ num_hidden_layers 的层直接返回 False——也就是不跳过:草稿块要为它自己那个被草拟的位置算 top-k。

index_share_for_mtp_iteration 说的不是这件事

这个字段只管多个草稿步之间的共享(num_speculative_tokens > 1), 不表示 MTP 复用目标模型的索引。上游 vLLM 与 ATOM 的 sglang 插件都是独立跑 MTP indexer 的。 把它理解反了,草稿层就会读到目标模型上一层留下的下标。


读码路线

按这个顺序看

#文件 · 位置看什么
1atom/config.py · _CONFIG_REGISTRY glm_moe_dsa → deepseek_v3:为什么没有 GLM 自己的 config 类
2atom/models/deepseek_v2.py · GlmMoeDsaForCausalLM 整个子类只有一张量化排除名映射
3_should_skip_index_topk / _indexer_weights_shared IndexShare 的两处落点:运行期跳过 top-k,构造期不建模块。 注意 MTP 层的提前返回
4_moe_router_dtype 为什么按 model_type 判定而不是读 moe_router_dtype, 以及 logits 与 bias 为什么必须同宽
5DeepseekV2Attention.__init__ · fused_qkv_a_proj 2624 = 2048 + 512 + 64 的合并发生在哪一步
6model_ops/attentions/aiter_mla.py mla_kv_entry_dimaligned_index_cache_dim、 以及 _global_index_cache_layer_ids 如何按 indexer_types 排掉 shared 层