metrale_model_engine/traits/model/
draft.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `ModelDraft`, 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, bail};
11use metrale_gpu_runtime::gpu::DevicePtr;
12
13/// 2026-09-26: Draft proposers: MTP and DFlash proposals, the hidden-state saves and stashes they read,
14/// and the DFlash context appends.
15pub trait ModelDraft {
16    /// 2026-09-25: The most drafts a speculative step may send to the sequence in SSM pool slot
17    /// `slot_idx` without overflowing that slot's per-token intermediate state. The scheduler
18    /// uses the minimum over the active slots. Default `usize::MAX` (no SSM verify pools).
19    fn mtp_slot_draft_capacity(&self, _slot_idx: usize) -> usize {
20        usize::MAX
21    }
22
23    /// 2026-09-25: Generate for one prompt with the model's own draft proposer.
24    fn generate_speculative(
25        &self,
26        prompt_tokens: &[u32],
27        params: &metrale_sampling::SamplingParams,
28        num_drafts: usize,
29    ) -> Result<crate::engine::GenerateResult>;
30
31    /// 2026-09-25: Whether a draft proposer or self-speculative decoding is available.
32    fn has_proposer(&self) -> bool;
33
34    /// 2026-09-25: The installed DFlash drafter's block size γ, read from the drafter; the serve
35    /// layer derives `num_drafts = γ - 1` from it. Default `None`.
36    fn dflash_gamma(&self) -> Option<usize> {
37        None
38    }
39
40    fn has_self_speculative(&self) -> bool;
41
42    /// 2026-09-25: The draft decode step for self-speculative decoding; returns logits.
43    fn decode_draft(&self, token: u32, seq: &mut SequenceState, stream: u64) -> Result<DevicePtr>;
44
45    /// 2026-09-25: Copy hidden row `rows[i]` of the last batched verify into stash slot `i`,
46    /// before a propose overwrites the shared hidden buffer. Default: an error.
47    fn stash_verify_hidden_rows(&self, rows: &[usize], stream: u64) -> Result<()> {
48        let _ = (rows, stream);
49        bail!("stash_verify_hidden_rows: unsupported by this model")
50    }
51
52    /// 2026-09-25: Copy batched-verify rows into drafter catch-up stash slots,
53    /// `slot_rows[j] = (slot, row)`. Default: `Ok(())`.
54    fn stash_verify_catchup_rows(&self, slot_rows: &[(usize, usize)]) -> Result<()> {
55        let _ = slot_rows;
56        Ok(())
57    }
58
59    /// 2026-09-25: Append one drafter row per accepted draft before the next propose (see
60    /// [`Self::stash_verify_catchup_rows`]); returns the rows written. Default: 0.
61    fn run_mtp_catchup_batched(
62        &self,
63        tokens: &[Vec<u32>],
64        first_slot: &[usize],
65        first_pos: &[usize],
66        seqs: &mut [&mut SequenceState],
67    ) -> Result<usize> {
68        let _ = (tokens, first_slot, first_pos, seqs);
69        Ok(0)
70    }
71
72    /// 2026-09-25: [`Self::save_hidden_for_mtp`] from stash slot `idx` (written by
73    /// [`Self::stash_verify_hidden_rows`]) instead of the live verify rows, which a propose may
74    /// have overwritten. Default: an error.
75    fn save_hidden_for_mtp_from_stash(&self, idx: usize, stream: u64) -> Result<()> {
76        let _ = (idx, stream);
77        bail!("save_hidden_for_mtp_from_stash: unsupported by this model")
78    }
79
80    /// 2026-09-25: `num_drafts` MTP drafts for each of `tokens.len()` sequences in one drafter
81    /// forward per draft position. `stash_idx[i]` is the verify-stash slot holding sequence
82    /// `i`'s hidden, and `positions[i]` its propose position, as for
83    /// [`Self::run_mtp_propose_multi`].
84    ///
85    /// `out_conf`, when `Some`, receives each draft's top-1 log-probability in the shape of the
86    /// returned drafts, or zeros when the drafter cannot measure it. `Ok(None)` means
87    /// unsupported, and the caller proposes per sequence. Default: `Ok(None)`.
88    #[allow(clippy::too_many_arguments)]
89    fn run_mtp_propose_batched(
90        &self,
91        tokens: &[u32],
92        positions: &[usize],
93        stash_idx: &[usize],
94        num_drafts: usize,
95        seqs: &mut [&mut SequenceState],
96        stream: u64,
97        out_conf: Option<&mut Vec<Vec<f32>>>,
98    ) -> Result<Option<Vec<Vec<u32>>>> {
99        let _ = (
100            tokens, positions, stash_idx, num_drafts, seqs, stream, out_conf,
101        );
102        Ok(None)
103    }
104
105    /// 2026-09-25: The widest batch [`Self::run_mtp_propose_batched`] takes in one drafter
106    /// forward. Default `1` (per sequence only).
107    fn mtp_propose_batch_max(&self) -> usize {
108        1
109    }
110
111    /// 2026-09-25: Copy the hidden state at `token_idx` into the MTP input buffer, which
112    /// `run_mtp_propose` and `run_mtp_propose_multi` read. `TransformerModel` copies the hidden
113    /// state before the final norm, because the MTP head applies its own norm.
114    fn save_hidden_for_mtp(&self, token_idx: usize, stream: u64) -> Result<()>;
115
116    /// 2026-09-25: Store a serially decoded token's final hidden at `pos` in the drafter catch-up
117    /// ring. Default: `Ok(())`.
118    fn save_hidden_for_catchup(&self, _token_idx: usize, _pos: usize) -> Result<()> {
119        Ok(())
120    }
121
122    /// 2026-09-25: Capture row `token_idx` from every DFlash capture layer for the next propose.
123    /// Default: `Ok(())`.
124    fn save_dflash_hidden_for_propose(&self, _token_idx: usize, _stream: u64) -> Result<()> {
125        Ok(())
126    }
127
128    /// 2026-09-25: Append row 1 of the DFlash hidden scratch to the proposer context. Default:
129    /// `Ok(())`.
130    fn dflash_accept_append(&self, _seq: &mut SequenceState) -> Result<()> {
131        Ok(())
132    }
133
134    /// 2026-09-25: Append scratch rows 0 and 1 to the DFlash context at positions `seq_len - 2`
135    /// and `seq_len - 1`, row 1 last, before the propose. Default: `Ok(())`.
136    fn dflash_eagle_accept_append(&self, _seq: &mut SequenceState) -> Result<()> {
137        Ok(())
138    }
139
140    /// 2026-09-25: Append scratch rows `0..=num_accepted` to the DFlash context at positions
141    /// `base_pos..=base_pos + num_accepted`, row `num_accepted` last. Default: `Ok(())`.
142    fn dflash_eagle_kgamma_append(
143        &self,
144        _seq: &mut SequenceState,
145        _num_accepted: usize,
146        _base_pos: usize,
147    ) -> Result<()> {
148        Ok(())
149    }
150
151    /// 2026-09-25: Append the just-decoded token's captured hidden (scratch row 0) to the DFlash
152    /// context at position `seq_len - 1`, for a sequence decoded serially while speculation is
153    /// suspended; otherwise that capture is overwritten and the context has a hole. It sets
154    /// `skip_next_decode_append` so a later propose does not append it again. No-op without a
155    /// DFlash proposer state. Default: `Ok(())`.
156    fn dflash_serial_ctx_append(&self, _seq: &mut SequenceState) -> Result<()> {
157        Ok(())
158    }
159
160    /// 2026-09-25: Append `num_committed` DFlash scratch rows, from `scratch_row`, at the end of
161    /// the context, stamped with positions `base_pos..base_pos + num_committed`, after sliding
162    /// the window if it would overflow. `base_pos` is a position, not a context row: the two
163    /// differ after a slide. Default: `Ok(())`.
164    fn commit_ctx(
165        &self,
166        _seq: &mut SequenceState,
167        _num_committed: usize,
168        _base_pos: usize,
169        _scratch_row: usize,
170    ) -> Result<()> {
171        Ok(())
172    }
173
174    /// 2026-09-25: Rows per sequence band in the DFlash hidden scratch: sequence `i` of a batched
175    /// verify captures into band `i`, so its `commit_ctx` `scratch_row` is
176    /// `i * dflash_capture_band()`. Default `0` (no DFlash drafter).
177    fn dflash_capture_band(&self) -> usize {
178        0
179    }
180
181    /// 2026-09-25: One MTP draft from the saved hidden state; `None` without a proposer.
182    fn run_mtp_propose(
183        &self,
184        token: u32,
185        position: usize,
186        seq: &mut SequenceState,
187        stream: u64,
188    ) -> Result<Option<u32>>;
189
190    /// 2026-09-25: `num_drafts` MTP drafts from the hidden state saved by `save_hidden_for_mtp`.
191    ///
192    /// `grammar_bitmask`, when `Some`, limits drafts to a grammar's allowed tokens:
193    /// `ceil(vocab_size / 32)` words, bit `tok` set when `tok` is allowed.
194    fn run_mtp_propose_multi(
195        &self,
196        token: u32,
197        position: usize,
198        num_drafts: usize,
199        seq: &mut SequenceState,
200        stream: u64,
201        grammar_bitmask: Option<&[i32]>,
202    ) -> Result<Vec<u32>>;
203
204    /// 2026-09-25: The draft id the proposer left on the device. Default `0`.
205    fn read_deferred_draft_token(&self) -> Result<u32> {
206        Ok(0)
207    }
208
209    /// 2026-09-25: Tell the proposer how many drafts the last verify accepted, so it can trim its
210    /// per-sequence state.
211    fn trim_proposer_state(
212        &self,
213        seq: &mut SequenceState,
214        num_accepted: usize,
215        stream: u64,
216    ) -> Result<()>;
217}