metrale_model_arch/glm5next_skeleton/
budget.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: `StateBudget` (per-sequence state bytes) and `StructuralAccounting` (checkpoint names against the skeleton's contract).
4//!
5//! Owner: model-arch (GLM-5.3).
6//! Invariants: none beyond the types.
7
8/// 2026-09-25: Per-sequence state bytes, built by `Glm5NextTextSkeleton::state_budget` from
9/// the config's geometry. `per_rank` divides only the KDA state and the DSA KV.
10#[derive(Debug, Clone, Copy)]
11pub struct StateBudget {
12    /// 2026-09-25: KDA recurrent state, `[heads, head_dim, head_dim]` fp32 per KDA layer,
13    /// summed over the KDA layers. Does not grow with sequence length.
14    pub kda_recurrent: usize,
15    /// 2026-09-25: KDA causal-conv window, bf16, `conv_dim x (kernel - 1 + num_spec)` per KDA
16    /// layer. Does not grow with sequence length.
17    pub kda_conv: usize,
18    /// 2026-09-25: DSA MLA KV per token across the text-stack DSA layers, `kv_lora_rank` x 2
19    /// bytes each. Grows with sequence length.
20    pub dsa_kv_per_token: usize,
21    /// 2026-09-25: Indexer key and gate state per token across the text-stack DSA layers.
22    /// Grows with sequence length. The indexer projections are `DsaShard::Replicated`
23    /// (`glm5next_dsa/tp.rs`), so every rank holds the whole cache and [`Self::per_rank`]
24    /// does not divide it.
25    pub dsa_indexer_per_token: usize,
26    /// 2026-09-25: mHC highway per token, `hc_mult x hidden` fp32. An activation, so neither
27    /// `fixed` nor `per_token` counts it.
28    pub mhc_highway_per_token: usize,
29    /// 2026-09-25: MoE routing scratch per token: f32 logits plus 4-byte top-k ids and weights.
30    pub moe_routing_per_token: usize,
31}
32
33impl StateBudget {
34    /// 2026-09-25: Fixed (sequence-length-independent) bytes per sequence.
35    pub fn fixed(&self) -> usize {
36        self.kda_recurrent + self.kda_conv
37    }
38    /// 2026-09-25: Bytes that grow with every token of context.
39    pub fn per_token(&self) -> usize {
40        self.dsa_kv_per_token + self.dsa_indexer_per_token
41    }
42    /// 2026-09-25: Total persistent state for a sequence of `tokens`.
43    pub fn for_sequence(&self, tokens: usize) -> usize {
44        self.fixed() + tokens * self.per_token()
45    }
46    /// 2026-09-25: Divides `kda_recurrent`, `kda_conv` and `dsa_kv_per_token` by `ep`; the
47    /// indexer cache, the mHC highway and the routing scratch stay whole.
48    pub fn per_rank(&self, ep: usize) -> StateBudget {
49        StateBudget {
50            kda_recurrent: self.kda_recurrent / ep,
51            kda_conv: self.kda_conv / ep,
52            dsa_kv_per_token: self.dsa_kv_per_token / ep,
53            // 2026-09-25: Not divided: the indexer projections are `DsaShard::Replicated`, so
54            // every rank holds the whole cache.
55            dsa_indexer_per_token: self.dsa_indexer_per_token,
56            mhc_highway_per_token: self.mhc_highway_per_token,
57            moe_routing_per_token: self.moe_routing_per_token,
58        }
59    }
60}
61
62#[derive(Debug)]
63pub struct StructuralAccounting {
64    pub required: usize,
65    pub bound: usize,
66    /// 2026-09-25: Structural tensors the checkpoint does not have. `is_complete` requires
67    /// it empty.
68    pub missing: Vec<String>,
69    /// 2026-09-25: Names that are neither structural nor MLP/vision. `is_complete` requires
70    /// it empty.
71    pub unexpected: Vec<String>,
72    /// 2026-09-25: Count of MLP/MoE and vision names, which the skeleton does not bind.
73    pub deferred: usize,
74}
75
76impl StructuralAccounting {
77    pub fn is_complete(&self) -> bool {
78        self.missing.is_empty() && self.unexpected.is_empty() && self.bound == self.required
79    }
80}