metrale_model_engine/traits/model/ssm_state.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `ModelSsmState`, one of the supertraits `Model` is made of. Its methods, default
4//! bodies and docs are the ones `Model` declared before the split.
5//!
6//! Owner: model-engine.
7//! Invariants: none beyond the types.
8
9use crate::traits::SequenceState;
10use anyhow::Result;
11
12/// 2026-09-26: Recurrent (SSM) state: checkpoint and rollback around a verify, decode-rollback
13/// snapshots, and the commit of an accepted prefix on the secondary stream.
14pub trait ModelSsmState {
15 fn checkpoint_ssm_states(&self, seq: &mut SequenceState) -> Result<()>;
16
17 /// 2026-09-25: Restore the SSM state to where `num_accepted` tokens of the last verify were
18 /// accepted.
19 fn rollback_ssm_states(&self, seq: &mut SequenceState, num_accepted: usize) -> Result<()>;
20
21 /// 2026-09-25: Whether the model has recurrent SSM layers, whose state is advanced in place
22 /// each token. Lowering `seq_len` rewinds paged KV but not that state, so a decode rollback
23 /// must also restore it from a snapshot. Default `false`.
24 fn has_ssm_layers(&self) -> bool {
25 false
26 }
27
28 /// 2026-09-26: Whether some layer keeps per-sequence state that lowering the KV cursor does
29 /// not rewind (`LayerCapabilities::decode_rollback_unsupported`); the scheduler then declines
30 /// a decode rollback (`rollback_to_boundary`). Default `false`.
31 fn decode_rollback_unsupported(&self) -> bool {
32 false
33 }
34
35 /// 2026-09-25: Decode-rollback SSM snapshot slots per active sequence; the scheduler sizes
36 /// each sequence's snapshot ring from it. `0` (the default) means no decode-rollback
37 /// snapshots. `TransformerModel` returns the depth `ssm_reserve::decode_rollback_ring_slots`
38 /// chose.
39 fn decode_rollback_ring_slots(&self) -> usize {
40 0
41 }
42
43 /// 2026-09-25: Save `seq`'s SSM `h_state` and `conv_state` (every SSM layer) into its
44 /// decode-rollback ring slot `ring_slot`, in `0..decode_rollback_ring_slots()`. Default:
45 /// `Ok(())`.
46 fn save_decode_ssm_snapshot(&self, _seq: &SequenceState, _ring_slot: usize) -> Result<()> {
47 Ok(())
48 }
49
50 /// 2026-09-25: Restore `seq`'s SSM state from ring slot `ring_slot`, written earlier by
51 /// [`Self::save_decode_ssm_snapshot`]. Default: `Ok(())`.
52 fn restore_decode_ssm_snapshot(&self, _seq: &SequenceState, _ring_slot: usize) -> Result<()> {
53 Ok(())
54 }
55
56 /// 2026-09-25: During decode, save a block-aligned SSM snapshot at a checkpoint boundary, so
57 /// the next turn's prefix-cache hit can restore from decode-produced state. The scheduler
58 /// calls it after a step's SSM state is final. Default: no-op.
59 fn decode_marconi_checkpoint(&self, _seq: &mut SequenceState) {}
60
61 /// 2026-09-25: Start the SSM checkpoint copies on the secondary stream; call
62 /// [`Self::sync_secondary`] before the next verify. Default: the synchronous
63 /// [`Self::checkpoint_ssm_states`].
64 fn start_checkpoint_async(&self, seq: &mut SequenceState) -> Result<()> {
65 self.checkpoint_ssm_states(seq)
66 }
67
68 /// 2026-09-25: Roll the SSM state back to `num_accepted` and checkpoint it again, on the
69 /// secondary stream. Default: the synchronous rollback, then checkpoint.
70 fn start_rollback_and_checkpoint_async(
71 &self,
72 seq: &mut SequenceState,
73 num_accepted: usize,
74 ) -> Result<()> {
75 self.rollback_ssm_states(seq, num_accepted)?;
76 self.checkpoint_ssm_states(seq)
77 }
78
79 /// 2026-09-25: Wait for the secondary stream's work. Default: `Ok(())`.
80 fn sync_secondary(&self) -> Result<()> {
81 Ok(())
82 }
83
84 /// 2026-09-25: Commit the accepted prefix of a verify onto the live SSM `h_state` and
85 /// `conv_state`, on the secondary stream (pair with [`Self::sync_secondary`]). Default:
86 /// `Ok(())`.
87 fn commit_accepted_prefix(
88 &self,
89 _seq: &mut SequenceState,
90 _num_accepted: usize,
91 _k: usize,
92 ) -> Result<()> {
93 Ok(())
94 }
95
96 /// 2026-09-25: SSM snapshot-pool occupancy `(used, total)`, or `None` without a snapshot
97 /// pool (the default; `TransformerModel` also returns `None` when the pool has no slots).
98 fn ssm_snapshot_occupancy(&self) -> Option<(u32, u32)> {
99 None
100 }
101}