spark_model/layers/glm5next_dsa/
binding.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! GLM-5.3 DSA checkpoint binding: the 14 `self_attn` tensors of a DSA block,
4//! their expected dtype and shape, and a verifier that fails loudly.
5//!
6//! Scoped to `LibertAIDAI/GLM-5.3-Flash-NVFP4@9e0d74e3`. Mirrors
7//! [`crate::layers::glm5next_kda::binding`] and deliberately reuses its
8//! `RawTensor` / `TensorSource` / dtype types instead of growing a parallel set.
9//!
10//! Shapes are expressed against [`Glm5NextDsaConfig`], so a geometry change fails
11//! here rather than at kernel launch — the same contract the KDA binder holds.
12//!
13//! # ðŸŠĪ What this exists to catch
14//!
15//! * **`indexer.k_norm` has a `bias`.** It is an `nn.LayerNorm`, not an RMSNorm —
16//!   the only other norm in GLM-5.3 with a bias. A binder that loads only
17//!   `.weight` drops mean-subtraction *and* the bias, and nothing about the shapes
18//!   says so. The spec table lists the bias as REQUIRED so its absence is an error,
19//!   not a silent zero.
20//! * **`index_kpool_compress_ape` is `[kpool, index_head_dim]`** — indexed by pool
21//!   SLOT, not by head and not by pool. Its 1 KB size makes a wrong-axis bind easy
22//!   to miss.
23//! * **`q_b_proj` and `kv_b_proj` carry different per-head widths** —
24//!   `qk_head_dim` (256) vs `nope + v_head_dim` (512). Both are `[heads * w, lora]`,
25//!   so a swapped width still yields a well-formed 2-D tensor.
26//! * **NoPE**: there is no `wkv_a_rope` / `wq_b_rope` here at all, and
27//!   `kv_a_proj_with_mqa` is `kv_lora_rank` wide (512), not `+ rope` (576).
28//!   A spec that expects 576 rejects the real checkpoint.
29
30use std::collections::BTreeSet;
31
32use anyhow::{Result, bail};
33
34use super::Glm5NextDsaConfig;
35use crate::layers::glm5next_kda::binding::{KdaDtype as Dtype, KdaTensorSource as TensorSource};
36
37/// One expected tensor: layer-relative name, dtype, and full (unsharded) shape.
38#[derive(Debug, Clone, PartialEq, Eq)]
39pub struct DsaSpec {
40    pub name: String,
41    pub dtype: Dtype,
42    pub shape: Vec<usize>,
43}
44
45/// Every `self_attn` tensor a DSA block has — and the complete list of what it may
46/// have. `full_heads` is the pre-shard head count; the checkpoint is never sharded
47/// on disk, so binding always validates against the full geometry and slices after.
48pub fn dsa_tensor_specs(cfg: &Glm5NextDsaConfig, full_heads: usize) -> Vec<DsaSpec> {
49    let x = cfg.hidden;
50    let ql = cfg.q_lora_rank;
51    let kvl = cfg.kv_lora_rank;
52    let qk = cfg.qk_head_dim();
53    let kvb = cfg.qk_nope_head_dim + cfg.v_head_dim;
54    let ihd = cfg.index_head_dim;
55    let ih = cfg.index_heads;
56
57    let s = |name: &str, shape: Vec<usize>| DsaSpec {
58        name: name.to_string(),
59        dtype: Dtype::Bf16,
60        shape,
61    };
62
63    vec![
64        // ── MLA ──
65        s("self_attn.q_a_proj.weight", vec![ql, x]),
66        s("self_attn.q_a_layernorm.weight", vec![ql]),
67        s("self_attn.q_b_proj.weight", vec![full_heads * qk, ql]),
68        // ðŸŠĪ NoPE: kv_cache_dim == kv_lora_rank, no rope section.
69        s(
70            "self_attn.kv_a_proj_with_mqa.weight",
71            vec![cfg.kv_cache_dim(), x],
72        ),
73        s("self_attn.kv_a_layernorm.weight", vec![kvl]),
74        s("self_attn.kv_b_proj.weight", vec![full_heads * kvb, kvl]),
75        s(
76            "self_attn.o_proj.weight",
77            vec![x, full_heads * cfg.v_head_dim],
78        ),
79        // ── indexer ──
80        s("self_attn.indexer.wq_b.weight", vec![ih * ihd, ql]),
81        s("self_attn.indexer.wk.weight", vec![ihd, x]),
82        s("self_attn.indexer.k_norm.weight", vec![ihd]),
83        // ðŸŠĪ REQUIRED: LayerNorm bias. Its absence is an error, never a silent zero.
84        s("self_attn.indexer.k_norm.bias", vec![ihd]),
85        s("self_attn.indexer.weights_proj.weight", vec![ih, x]),
86        s("self_attn.indexer.index_kpool_compress_gate", vec![ihd, x]),
87        // ðŸŠĪ indexed by pool SLOT: [kpool, index_head_dim].
88        s(
89            "self_attn.indexer.index_kpool_compress_ape",
90            vec![cfg.index_kpool, ihd],
91        ),
92    ]
93}
94
95/// What a successful bind saw.
96#[derive(Debug, Clone, PartialEq, Eq)]
97pub struct DsaBindReport {
98    pub bound: usize,
99    pub total_bytes: usize,
100    /// `self_attn.*` names present in the source that no spec claims. Never empty-
101    /// tolerated: an unclaimed attention tensor means the architecture moved.
102    pub unclaimed: Vec<String>,
103}
104
105/// Verify one DSA block against the spec table.
106///
107/// Every spec must be present with the exact dtype and shape, and no `self_attn.*`
108/// tensor may be left unclaimed. Both directions matter: a missing tensor is a
109/// broken layer, and an unexpected one means the checkpoint is not the model we
110/// think it is.
111pub fn verify_dsa_block(
112    cfg: &Glm5NextDsaConfig,
113    full_heads: usize,
114    source: &dyn TensorSource,
115) -> Result<DsaBindReport> {
116    cfg.validate()?;
117    let specs = dsa_tensor_specs(cfg, full_heads);
118    let mut total_bytes = 0usize;
119
120    for spec in &specs {
121        let Some(raw) = source.get(&spec.name) else {
122            bail!("DSA bind: missing required tensor `{}`", spec.name);
123        };
124        if raw.dtype != spec.dtype {
125            bail!(
126                "DSA bind: `{}` is {:?}, expected {:?}",
127                spec.name,
128                raw.dtype,
129                spec.dtype
130            );
131        }
132        if raw.shape != spec.shape {
133            bail!(
134                "DSA bind: `{}` has shape {:?}, expected {:?}",
135                spec.name,
136                raw.shape,
137                spec.shape
138            );
139        }
140        let elems: usize = spec.shape.iter().product();
141        if raw.bytes.len() != elems * 2 {
142            bail!(
143                "DSA bind: `{}` carries {} bytes, expected {} for {:?} BF16",
144                spec.name,
145                raw.bytes.len(),
146                elems * 2,
147                spec.shape
148            );
149        }
150        total_bytes += raw.bytes.len();
151    }
152
153    let claimed: BTreeSet<&str> = specs.iter().map(|s| s.name.as_str()).collect();
154    let unclaimed: Vec<String> = source
155        .names()
156        .into_iter()
157        .filter(|n| n.starts_with("self_attn.") && !claimed.contains(n.as_str()))
158        .collect();
159    if !unclaimed.is_empty() {
160        bail!(
161            "DSA bind: {} unclaimed self_attn tensor(s), first: {:?}. An unexpected \
162             attention tensor means the architecture moved — do not skip it.",
163            unclaimed.len(),
164            &unclaimed[..unclaimed.len().min(5)]
165        );
166    }
167
168    Ok(DsaBindReport {
169        bound: specs.len(),
170        total_bytes,
171        unclaimed,
172    })
173}
174
175#[cfg(test)]
176mod tests;