metrale_model_arch/glm5next_dsa/state.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: Per-sequence DSA indexer cache: the `k_normed`, `gate` and `valid` rows the
4//! selector reads.
5//!
6//! Owner: model-arch (GLM-5.3 DSA).
7//! Invariants:
8//! - `len <= capacity`: `advance` and `sync_to` refuse to pass `capacity`, and `rewind_to`
9//! only shrinks.
10//!
11//! The indexer keys and gates are projections of the hidden state (`indexer.wk`,
12//! `index_kpool_compress_gate`), which the MLA latent cache does not hold, so they get their
13//! own cache. It is a `LayerState`, allocated per sequence by `TransformerLayer::alloc_state`.
14//!
15//! # Flat, not paged
16//!
17//! `dsa_kpool_compress` reads `k[raw * D + d]` and `gate[raw * D + d]` at absolute
18//! positions, so this is one contiguous buffer per sequence. The MLA latent cache stays
19//! paged.
20
21use anyhow::{Result, bail};
22use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
23
24use super::Glm5NextDsaConfig;
25use super::select::DsaSelectGeometry;
26use metrale_model_layers::layer::LayerState;
27
28/// 2026-09-25: Rows a sequence's indexer cache holds: `cfg.max_context` rounded down to
29/// whole pools ([`dsa_capacity`]).
30///
31/// Each row costs `2 · index_head_dim` BF16 plus 1 B per DSA layer (513 B at
32/// `index_head_dim` 128, 5,643 B over GLM-5.3's 11 DSA layers), and every rank holds a full
33/// copy. `Glm5NextTextSkeleton::state_budget` charges the same bytes per token.
34pub fn max_dsa_context(cfg: &Glm5NextDsaConfig) -> usize {
35 dsa_capacity(cfg.max_context, cfg.index_kpool)
36}
37
38/// 2026-09-25: Indexer cache rows for `max_context` tokens, rounded down to whole pools (a
39/// trailing partial pool is not a pool).
40///
41/// The allocation ([`max_dsa_context`]) and the serve's per-sequence reserve
42/// (`seq_state_reserve::per_sequence_state_bytes`) both call it. `index_kpool` 0 is treated
43/// as 1, so it is total for callers that have not validated the config.
44pub fn dsa_capacity(max_context: usize, index_kpool: usize) -> usize {
45 let kpool = index_kpool.max(1);
46 (max_context / kpool) * kpool
47}
48
49/// 2026-09-25: Bytes one sequence's indexer cache takes for one DSA layer at `capacity` rows:
50/// `capacity * (4 * index_head_dim + 1)`, the sizes [`Glm5NextDsaState::alloc`] allocates
51/// for `k_normed`, `gate` and `valid`. The serve's per-sequence reserve uses it.
52pub fn indexer_state_bytes(capacity: usize, index_head_dim: usize) -> usize {
53 capacity * index_head_dim * 2 // 2026-09-25: k_normed and gate (BF16), then valid (u8)
54 + capacity * index_head_dim * 2
55 + capacity
56}
57
58/// 2026-09-25: One sequence's indexer cache for one DSA layer, allocated once at
59/// [`max_dsa_context`] rows and never grown.
60pub struct Glm5NextDsaState {
61 /// 2026-09-25: `[capacity, index_head_dim]` BF16 indexer keys, after the `k_norm`
62 /// LayerNorm.
63 pub k_normed: DevicePtr,
64 /// 2026-09-25: `[capacity, index_head_dim]` BF16 compress-gate projection.
65 pub gate: DevicePtr,
66 /// 2026-09-25: `[capacity]` u8 per-position validity.
67 pub valid: DevicePtr,
68 /// 2026-09-25: Tokens written so far. The selector reads `[0, len)`.
69 len: usize,
70 capacity: usize,
71 index_head_dim: usize,
72 /// 2026-09-25: Set by [`Self::free`], which then does nothing on a second call.
73 released: bool,
74}
75
76impl Glm5NextDsaState {
77 /// 2026-09-25: Reserve for the whole addressable context. `alloc_state` has no length
78 /// argument, so the cap, not the prompt, sizes this.
79 pub fn alloc(gpu: &dyn GpuBackend, cfg: &Glm5NextDsaConfig) -> Result<Self> {
80 cfg.validate()?;
81 let capacity = max_dsa_context(cfg);
82 let d = cfg.index_head_dim;
83 Ok(Self {
84 k_normed: gpu.alloc(capacity * d * 2)?,
85 gate: gpu.alloc(capacity * d * 2)?,
86 valid: gpu.alloc(capacity)?,
87 len: 0,
88 capacity,
89 index_head_dim: d,
90 released: false,
91 })
92 }
93
94 pub fn len(&self) -> usize {
95 self.len
96 }
97 pub fn is_empty(&self) -> bool {
98 self.len == 0
99 }
100 pub fn capacity(&self) -> usize {
101 self.capacity
102 }
103 /// 2026-09-25: Indexer key width. The aux-state codec (`aux_state.rs`) refuses a blob
104 /// taken at a different width.
105 pub fn index_head_dim(&self) -> usize {
106 self.index_head_dim
107 }
108
109 /// 2026-09-25: Byte offset of row `pos` in `k_normed` / `gate`.
110 pub fn row_offset(&self, pos: usize) -> usize {
111 pos * self.index_head_dim * 2
112 }
113
114 /// 2026-09-25: Fails when `n` more rows would pass `capacity`.
115 ///
116 /// `Glm5NextDsaLayer::indexer_forward` writes row `len()` before it advances, so it calls
117 /// this first: at `len == capacity` that row is past the end of every buffer.
118 pub fn ensure_room(&self, n: usize) -> Result<()> {
119 self.ensure_room_through(self.len + n)
120 }
121
122 /// 2026-09-25: The same refusal for an absolute end row.
123 ///
124 /// Before a graph replay the counter may still be ahead of the sequence (a rejected
125 /// draft; `sync_to` fixes it after the replay), so the replay check
126 /// (`check_replay_room`) asks about the end row `seq_len + k` instead.
127 pub fn ensure_room_through(&self, end: usize) -> Result<()> {
128 if end > self.capacity {
129 bail!(
130 "DSA indexer cache: {end} tokens exceeds the {} rows reserved for this \
131 sequence. The indexer cache is sized from --max-seq-len at sequence \
132 creation and never grows; raise --max-seq-len (and re-check the memory \
133 budget) to serve a longer context.",
134 self.capacity
135 );
136 }
137 Ok(())
138 }
139
140 /// 2026-09-25: Advance after writing `n` rows at `[len, len + n)`. Fails, without
141 /// changing `len`, when that would pass `capacity`.
142 pub fn advance(&mut self, n: usize) -> Result<()> {
143 self.ensure_room(n)?;
144 self.len += n;
145 Ok(())
146 }
147
148 /// 2026-09-25: Rewind to `n` rows after a rejected speculative draft.
149 ///
150 /// The rows in `[n, len)` are left in the cache but become unreachable: the selector reads
151 /// `[0, len)` and the next write starts at `n`, so they are overwritten before anything
152 /// can select over them. Only shrinks — growing is `advance`'s job, and a request to
153 /// "rewind" forward would mean the caller lost track of where the sequence is.
154 pub fn rewind_to(&mut self, n: usize) -> Result<()> {
155 if n > self.len {
156 bail!(
157 "DSA indexer rewind to {n} from {}: rewind only shrinks; a forward 'rewind' \
158 means the caller lost the sequence position",
159 self.len
160 );
161 }
162 self.len = n;
163 Ok(())
164 }
165
166 /// 2026-09-25: Put the counter where `decode_k` would have left it, for a step served by
167 /// a replayed CUDA graph. `seq_len` is the sequence length before this step's `k` rows.
168 ///
169 /// The same reconcile `decode_k` does on entry: the counter is rewound to `seq_len` when
170 /// it is ahead (a rejected draft), a counter behind `seq_len` is an error, and then it
171 /// advances by `k`.
172 pub fn sync_to(&mut self, seq_len: usize, k: usize) -> Result<()> {
173 match self.len.cmp(&seq_len) {
174 std::cmp::Ordering::Greater => self.rewind_to(seq_len)?,
175 std::cmp::Ordering::Less => bail!(
176 "DSA indexer cache holds {} tokens but the replayed step starts at {seq_len} \
177 — rows are MISSING, not merely stale.",
178 self.len
179 ),
180 std::cmp::Ordering::Equal => {}
181 }
182 self.advance(k)
183 }
184
185 /// 2026-09-25: Plan a selection over everything cached so far.
186 pub fn geometry(&self, cfg: &Glm5NextDsaConfig, q_rows: usize) -> Result<DsaSelectGeometry> {
187 DsaSelectGeometry::plan(cfg, self.len, q_rows)
188 }
189
190 /// 2026-09-25: Release the per-sequence device buffers.
191 ///
192 /// Takes `&mut self` because callers hold the state behind a `dyn` state object. A second
193 /// call does nothing (`released` is set first), since `release_state` on the layer and the
194 /// MTP proposer's `free_state` can both reach one state. After the three frees succeed the
195 /// pointers are nulled and `len` is 0.
196 pub fn free(&mut self, gpu: &dyn GpuBackend) -> Result<()> {
197 if self.released {
198 return Ok(());
199 }
200 self.released = true;
201 for p in [self.k_normed, self.gate, self.valid] {
202 gpu.free(p)?;
203 }
204 self.k_normed = DevicePtr(0);
205 self.gate = DevicePtr(0);
206 self.valid = DevicePtr(0);
207 self.len = 0;
208 Ok(())
209 }
210}
211
212impl LayerState for Glm5NextDsaState {
213 fn as_any(&self) -> &dyn std::any::Any {
214 self
215 }
216 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
217 self
218 }
219}
220
221#[cfg(test)]
222mod tests;