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}