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 的草稿循环。
定位
ATOM 里没有 glm5_3.py。_CONFIG_REGISTRY 把
glm_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 架构解剖。
规模
下面每个数字都来自 141 个 safetensors 分片的头部(读 shape 相加,
fp8 的 weight_scale_inv 单列,不计入参数量)。
激活量是怎么凑出来的
量化布局
kv_b_proj),以及 indexer 的
wq_b / wk。indexer.weights_proj(就是那张排除名映射守住的)、
k_norm、全部 norm、gate.weight、embedding 与 lm_head。e_score_correction_bias 在 checkpoint 里是 F32——这不是巧合,见图 4。核心机制 · 层调度
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
_indexer_weights_shared 让构造期不为它们建模块,_should_skip_index_topk 让运行期不为它们跑 top-k。indexer_types 存在时是唯一权威;缺失时才回退到 index_topk_freq / index_skip_topk_offset 的公式。结构全图
图 2 · 结构全图
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 层里整块不存在。稀疏注意力
图 3 · 稀疏度
容易踩的地方:上下文不到 2048 时 indexer 是 no-op
index_topk = 2048 意味着历史长度 ≤ 2048 时 top-k 会选中每一个 token,
稀疏路径与稠密因果注意力逐位相同。此时 indexer 照算不误,结果却不改变任何东西——
所以短上下文下的对比说明不了稀疏选择是对的,只能说明它没把事情弄坏。
GLM-5.3 特有
图 4 · fp32 路由
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_topk 按 gating_output.dtype() 分发,
然后把 correction_bias reinterpret_cast 成同一个 scalar_t。
两者必须同宽,否则内核会按错误的宽度去读偏置缓冲区。
所以这一个 dtype 同时管住了两件事。
权重实测
| checkpoint 张量 | dtype | shape | 说明 |
|---|---|---|---|
| model.embed_tokens.weight | BF16 | [154880, 6144] | 951.5 M |
| 注意力(每层都有) | |||
| self_attn.q_a_proj | F8_E4M3 | [2048, 6144] | 与下一行在 ATOM 里合并成 fused_qkv_a_proj [2624, 6144] |
| self_attn.kv_a_proj_with_mqa | F8_E4M3 | [576, 6144] | 576 = 512 latent + 64 rope |
| self_attn.q_a_layernorm / kv_a_layernorm | BF16 | [2048] / [512] | |
| self_attn.q_b_proj | F8_E4M3 | [16384, 2048] | 64 头 × 256(192 nope + 64 rope) |
| self_attn.kv_b_proj | F8_E4M3 | [28672, 512] | 64 × (192 + 256) |
| self_attn.o_proj | F8_E4M3 | [6144, 16384] | 64 × v_head_dim 256 |
| indexer(只有 21 个 full 层 + MTP 层有) | |||
| indexer.wq_b | F8_E4M3 | [4096, 2048] | 32 头 × 128 |
| indexer.wk | F8_E4M3 | [128, 6144] | |
| indexer.weights_proj | BF16 | [32, 6144] | 就是量化排除名映射守住的那一个 |
| indexer.k_norm.weight / .bias | BF16 | [128] / [128] | 带 bias 的 LayerNorm,不是 RMSNorm |
| layers.5.self_attn.indexer.* | — | 不存在 | shared 层,一个 indexer 张量都没有 |
| FFN | |||
| mlp.gate.weight / e_score_correction_bias | BF16 / F32 | [256, 6144] / [256] | 偏置的 fp32 不是可选项 |
| mlp.experts.{0..255}.gate_proj / up_proj | F8_E4M3 | [2048, 6144] | 单专家 37.75 M |
| mlp.experts.*.down_proj | F8_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_proj | BF16 | [6144, 12288] | 拼接“末层 hidden + 下一 token embed”后降回 6144 |
| layers.78.enorm / hnorm / shared_head.norm | BF16 | [6144] | 草稿块自带完整 indexer 与完整 MoE,9.95 B |
显存
| 缓存 | 计费单位 | 大小 | 备注 |
|---|---|---|---|
| MLA KV | 78 层 × 576 × dtype / token | 89 856 B/token = 87.8 KiB · BF16 |
FP8 时减半;存的是 latent,不是 64 头 × 256 的展开 |
| 索引 key | 21 个 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 倍。
投机解码
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 的。
把它理解反了,草稿层就会读到目标模型上一层留下的下标。
读码路线
| # | 文件 · 位置 | 看什么 |
|---|---|---|
| 1 | atom/config.py · _CONFIG_REGISTRY | glm_moe_dsa → deepseek_v3:为什么没有 GLM 自己的 config 类 |
| 2 | atom/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 为什么必须同宽 |
| 5 | DeepseekV2Attention.__init__ · fused_qkv_a_proj | 2624 = 2048 + 512 + 64 的合并发生在哪一步 |
| 6 | model_ops/attentions/aiter_mla.py | mla_kv_entry_dim、aligned_index_cache_dim、
以及 _global_index_cache_layer_ids 如何按 indexer_types 排掉 shared 层 |