metrale_model_arch/
seq_state_reserve.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Per-sequence device state the serve must reserve, computed from
4//! the config before the model exists.
5//!
6//! The server's `preflight_reserve` adds this charge (`per_sequence_reserve`)
7//! before the model is built, when there is no layer list, proposer or
8//! `GpuBackend`, so it is a function of the config alone, like the
9//! `ssm_reserve` terms.
10//!
11//! Charged, per sequence:
12//!   * the target stack's DSA indexer caches (`Glm5NextDsaState`), one per text DSA layer;
13//!   * the draft proposer's own state (what `Glm5NextMtpHead`'s `alloc_state` allocates),
14//!     in its own field: the proposer is not a `TransformerLayer` and is not in the layer list.
15//!
16//! Not charged here:
17//!   * KDA recurrent and conv state: pool-owned (`ssm_reserve::PoolPlan`);
18//!     `meta.rs` gives pool-backed linear-attention layers pool addresses instead of calling
19//!     `alloc_state`;
20//!   * the paged KV pool: this reserve is subtracted from the KV budget.
21//!
22//! The result is per-rank bytes; nothing here divides by a world size.
23//!
24//! Owner: model-arch (serve memory reserve).
25//! Invariants:
26//! - Any `model_type` other than `glm5_next` is charged zero.
27
28use anyhow::Result;
29use metrale_config::{LayerType, ModelConfig};
30
31use crate::glm5next_dsa::state::{dsa_capacity, indexer_state_bytes};
32use crate::glm5next_skeleton::{Glm5NextTextSkeleton, Mixer};
33
34/// 2026-09-25: Per-sequence, per-rank device state, split by owner.
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct PerSequenceState {
37    /// 2026-09-25: Indexer caches owned by the target stack's DSA layers.
38    pub target_layers: usize,
39    /// 2026-09-25: State owned by the draft proposer (a `DraftProposer`, not a `TransformerLayer`).
40    pub proposer: usize,
41}
42
43impl PerSequenceState {
44    pub fn total(&self) -> usize {
45        self.target_layers + self.proposer
46    }
47
48    /// 2026-09-25: What the reserve must hold for `max_batch_size` concurrent
49    /// sequences (0 counts as 1).
50    pub fn for_batch(&self, max_batch_size: usize) -> usize {
51        self.total() * max_batch_size.max(1)
52    }
53}
54
55/// 2026-09-25: Per-sequence owned device state for `config`, at a context of `max_seq_len`
56/// tokens.
57///
58/// Returns zeros for any `model_type` other than `glm5_next`. The proposer term needs
59/// `spec_on` and a sparse-attention MTP layer. Errors when the GLM skeleton cannot be built
60/// from `config`.
61pub fn per_sequence_state_bytes(
62    config: &ModelConfig,
63    max_seq_len: usize,
64    spec_on: bool,
65) -> Result<PerSequenceState> {
66    if config.model_type != "glm5_next" {
67        return Ok(PerSequenceState::default());
68    }
69    let capacity = dsa_capacity(max_seq_len, config.index_kpool);
70    let per_layer = indexer_state_bytes(capacity, config.index_head_dim);
71
72    // 2026-09-25: The skeleton, built from the config alone, says which layers carry a DSA
73    // mixer. The per-layer bytes use the pool-rounded capacity (`dsa_capacity`), the same
74    // capacity `Glm5NextDsaState::alloc` allocates.
75    let skeleton = Glm5NextTextSkeleton::from_config(config)?;
76    let dsa_layers = skeleton
77        .layers
78        .iter()
79        .filter(|l| l.mixer == Mixer::Dsa)
80        .count();
81
82    let target_layers = dsa_layers * per_layer;
83
84    // 2026-09-25: The GLM MTP head's `alloc_state` (`glm5next_mtp_head/proposer.rs`)
85    // allocates one DSA indexer block for its drafter layer and five buffers: `concat`,
86    // `x`, `logits`, `arg`, `head_xchg`, in that order below. Its `block_table` is a host
87    // `Vec<u32>`, not device memory.
88    let proposer = if spec_on && config.mtp_layer_types.contains(&LayerType::SparseAttention) {
89        per_layer
90            + 2 * config.hidden_size * 2
91            + config.hidden_size * 2
92            + config.vocab_size * 2
93            + 4
94            + 16
95    } else {
96        0
97    };
98
99    Ok(PerSequenceState {
100        target_layers,
101        proposer,
102    })
103}
104
105#[cfg(test)]
106#[path = "seq_state_reserve_tests.rs"]
107mod tests;