metrale_model_layers/speculative.rs
1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-25: The draft-proposer interface for speculative decoding
4//! ([`DraftProposer`], [`ProposerState`]), and the MTP knobs, ladder, tree
5//! shape and verify keys in its submodules.
6//!
7//! Owner: model-layers (speculative).
8//! Invariants: none beyond the types.
9
10pub mod draft_stop;
11pub mod ladder;
12pub mod tree_shape;
13pub mod verify_key;
14
15pub use ladder::{
16 DEFAULT_MTP_MAX_SEQS, MAX_MTP_MAX_SEQS, MtpMaxSeqsSource, ladder_drafts_from_steps,
17 mtp_ladder_disabled, mtp_ladder_drafts, mtp_ladder_pinned, mtp_max_seqs, parse_mtp_k_ladder,
18 resolve_mtp_max_seqs, set_mtp_k_ladder, set_mtp_max_seqs,
19};
20mod knobs;
21pub use knobs::{
22 EP_CMD_MTP_PROPOSE, hidden_fingerprint, mtp_accept_debug, mtp_catchup_enabled,
23 mtp_ep_propose_enabled, mtp_multi_seq_mode, mtp_refeed_accepted_enabled, mtp_refeed_debug,
24 mtp_refeed_shift, shadow_topk,
25};
26
27use std::any::Any;
28
29use anyhow::Result;
30use metrale_config::ModelConfig;
31use metrale_gpu_runtime::buffers::BufferArena;
32use metrale_gpu_runtime::gpu::{DevicePtr, GpuBackend};
33
34use crate::layer::ForwardContext;
35
36/// 2026-09-25: Per-sequence state of a [`DraftProposer`], reached through
37/// `as_any` downcasts like `LayerState`.
38pub trait ProposerState: Send + Sync {
39 fn as_any(&self) -> &dyn Any;
40 fn as_any_mut(&mut self) -> &mut dyn Any;
41}
42
43/// 2026-09-25: A draft-token proposer. The model calls `propose` for draft
44/// tokens, verifies them with the target, then calls `after_verify` with the
45/// accepted count so the proposer can trim its state.
46pub trait DraftProposer: Send + Sync {
47 /// 2026-09-25: Allocate per-sequence proposer state.
48 fn alloc_state(&self, gpu: &dyn GpuBackend) -> Result<Box<dyn ProposerState>>;
49
50 /// 2026-09-25: [`Self::alloc_state`] given the sequence's token budget
51 /// (prompt length plus `max_tokens`), so state that scales with context
52 /// can be sized to the request instead of `--max-seq-len`. `usize::MAX`
53 /// means unknown. The default ignores the budget.
54 fn alloc_state_for(
55 &self,
56 gpu: &dyn GpuBackend,
57 budget_tokens: usize,
58 ) -> Result<Box<dyn ProposerState>> {
59 let _ = budget_tokens;
60 self.alloc_state(gpu)
61 }
62
63 /// 2026-09-25: The block size γ of a block-diffusion (DFlash) drafter;
64 /// `None` for other proposers. With `--dflash`, serve sets the draft count
65 /// to γ - 1, at least 1.
66 fn block_gamma(&self) -> Option<usize> {
67 None
68 }
69
70 /// 2026-09-25: Rows of `mtp_prefill_hidden` this proposer can use, given
71 /// the served `--max-seq-len`; the model sizes that `[rows, hidden]` BF16
72 /// buffer from it. The default is `max_seq_len`.
73 ///
74 /// Return less only when the proposer can never be given a later
75 /// position. A smaller cap corrupts nothing: a prompt longer than the
76 /// buffer is not fully captured, so its drafter prefill is skipped, which
77 /// costs acceptance.
78 fn prefill_hidden_rows(&self, max_seq_len: usize) -> usize {
79 max_seq_len
80 }
81
82 /// 2026-09-25: Whether this proposer's block is sharded across ranks, so
83 /// its forward is given the communicator. The default is false; the
84 /// GLM-5.3 MTP head returns [`mtp_ep_propose_enabled`]. When it is true on
85 /// a multi-rank serve, the model sends the worker [`EP_CMD_MTP_PROPOSE`]
86 /// before each propose, so both ranks run the same forward and
87 /// collectives.
88 fn needs_comm(&self) -> bool {
89 false
90 }
91
92 /// 2026-09-25: Whether this proposer's context prefill uses the shared
93 /// forward scratch (`ctx.buffers`). If so, the model skips the prefill at
94 /// the end of the target's prefill, while the target still owns those
95 /// buffers, and it runs at the first `propose` instead. Measured
96 /// 2026-08-29 on GLM-5.3 (2x GB10): running it at the end of prefill
97 /// changed the target's output on 2 of 6 probes.
98 fn prefill_uses_shared_buffers(&self) -> bool {
99 false
100 }
101
102 /// 2026-09-25: Drafter KV rows held in `state`; 0 when unknown.
103 fn drafter_rows(&self, _state: &mut dyn ProposerState) -> usize {
104 0
105 }
106
107 /// 2026-09-25: Sequence-space pair key of the newest drafter row; `None`
108 /// when untracked. Rows are compacted, so the row count does not give the
109 /// sequence position.
110 fn last_pair_key(&self, _state: &mut dyn ProposerState) -> Option<usize> {
111 None
112 }
113
114 /// 2026-09-25: Move this sequence's drafter KV blocks out of its state, so
115 /// `free_state` does not release them and the model can carry them to the
116 /// next turn. Returns `(blocks, rows, last_pair_key)`, or `None` when
117 /// unsupported or empty. Afterwards the state must behave as freshly
118 /// allocated.
119 fn take_drafter_kv(
120 &self,
121 _state: &mut dyn ProposerState,
122 ) -> Option<(Vec<u32>, usize, Option<usize>)> {
123 None
124 }
125
126 /// 2026-09-25: Install carried blocks into a fresh state, the inverse of
127 /// [`Self::take_drafter_kv`]. Returns false when unsupported; the caller
128 /// then still owns the blocks and must free them.
129 fn install_drafter_kv(
130 &self,
131 _state: &mut dyn ProposerState,
132 _blocks: Vec<u32>,
133 _rows: usize,
134 _last_pair_key: Option<usize>,
135 ) -> bool {
136 false
137 }
138
139 /// 2026-09-25: Release drafter KV blocks that no proposer state owns, such
140 /// as a carried entry being replaced or dropped.
141 fn free_drafter_kv(&self, _blocks: &[u32]) {}
142
143 /// 2026-09-25: Append drafter rows at KV slots `row_base ..` with RoPE
144 /// positions `pos_base ..` from `(tokens, hiddens)` pairs. Returns the
145 /// rows written; the default writes none.
146 #[allow(clippy::too_many_arguments)]
147 fn catchup_drafter(
148 &self,
149 _tokens: &[u32],
150 _hiddens: DevicePtr,
151 _row_base: usize,
152 _pos_base: usize,
153 _state: &mut dyn ProposerState,
154 _ctx: &ForwardContext,
155 _stream: u64,
156 ) -> Result<usize> {
157 Ok(0)
158 }
159
160 /// 2026-09-25: Propose up to `num_drafts` tokens autoregressively.
161 ///
162 /// - `last_token`: the last verified token.
163 /// - `target_hidden`: the target's `[1, hidden_size]` BF16 hidden for it,
164 /// saved before the final norm.
165 /// - `position`: the sequence position, for RoPE.
166 /// - `grammar_bitmask`: when `Some`, one bit per token id in i32 words;
167 /// drafts are limited to tokens whose bit is set.
168 /// - `target_hidden_stack`: the model's DFlash hidden capture, when it has
169 /// one; the MTP head ignores it.
170 fn propose(
171 &self,
172 last_token: u32,
173 target_hidden: DevicePtr,
174 position: usize,
175 num_drafts: usize,
176 state: &mut dyn ProposerState,
177 ctx: &ForwardContext,
178 stream: u64,
179 draft_embed_target: Option<DevicePtr>,
180 grammar_bitmask: Option<&[i32]>,
181 target_hidden_stack: Option<DevicePtr>,
182 ) -> Result<Vec<u32>>;
183
184 /// 2026-09-25: Propose `num_drafts` tokens for each of
185 /// `last_tokens.len()` sequences at once. Index i of every slice belongs to
186 /// sequence i; `target_hiddens[i]` is its `[1, hidden]` BF16 hidden, and
187 /// the pointers need not be contiguous. There is no grammar mask.
188 ///
189 /// `Ok(None)` when unsupported, and the caller proposes per sequence;
190 /// otherwise `drafts[i]` holds sequence i's drafts.
191 #[allow(clippy::too_many_arguments)]
192 fn propose_batch(
193 &self,
194 _last_tokens: &[u32],
195 _target_hiddens: &[DevicePtr],
196 _positions: &[usize],
197 _num_drafts: usize,
198 _states: &mut [&mut dyn ProposerState],
199 _ctx: &ForwardContext,
200 _stream: u64,
201 _out_conf: Option<&mut Vec<Vec<f32>>>,
202 ) -> Result<Option<Vec<Vec<u32>>>> {
203 Ok(None)
204 }
205
206 /// 2026-09-25: The widest batch [`Self::propose_batch`] accepts; 1 means
207 /// per-sequence only. The scheduler groups batched proposes by it.
208 fn propose_batch_max(&self, _buffers: &BufferArena, _config: &ModelConfig) -> usize {
209 1
210 }
211
212 /// 2026-09-25: Append one drafter row per accepted draft of the last
213 /// verify, for several sequences; the model calls it only with
214 /// `ModelLevers::mtp_kv_exact` on. `tokens[i]` are sequence i's accepted
215 /// drafts, `hiddens[i][k]` the target hidden paired with `tokens[i][k]`,
216 /// and `first_pos[i]` the RoPE position of `tokens[i][0]`. Returns the rows
217 /// written; the default writes none.
218 fn catchup_batch(
219 &self,
220 _tokens: &[Vec<u32>],
221 _hiddens: &[Vec<DevicePtr>],
222 _first_pos: &[usize],
223 _states: &mut [&mut dyn ProposerState],
224 _ctx: &ForwardContext,
225 _stream: u64,
226 ) -> Result<usize> {
227 Ok(0)
228 }
229
230 /// 2026-09-25: Prefill the drafter's KV over the prompt before the
231 /// sequence's first `propose`. `hiddens` is `[P, hidden_size]` BF16, row
232 /// `i` the target's hidden for `prompt_tokens[i]` before the final norm.
233 /// Returns the drafter rows written; the default writes none.
234 fn prefill_drafter(
235 &self,
236 prompt_tokens: &[u32],
237 hiddens: DevicePtr,
238 state: &mut dyn ProposerState,
239 ctx: &ForwardContext,
240 stream: u64,
241 ) -> Result<usize> {
242 let _ = (prompt_tokens, hiddens, state, ctx, stream);
243 Ok(0)
244 }
245
246 /// 2026-09-25: The draft token id the last `propose` with
247 /// `draft_embed_target` left on the device; the default returns 0.
248 fn read_deferred_draft_token(&self, gpu: &dyn GpuBackend) -> Result<u32> {
249 let _ = gpu;
250 Ok(0)
251 }
252
253 /// 2026-09-25: Trim the proposer's state after a verify that accepted
254 /// `num_accepted` drafts.
255 fn after_verify(
256 &self,
257 num_accepted: usize,
258 state: &mut dyn ProposerState,
259 stream: u64,
260 ) -> Result<()>;
261
262 /// 2026-09-25: Free per-sequence proposer state when a sequence finishes.
263 /// `DevicePtr` has no `Drop`, so device memory `alloc_state` allocated
264 /// leaks unless it is freed here; the default frees nothing.
265 fn free_state(&self, gpu: &dyn GpuBackend, state: &mut dyn ProposerState) -> Result<()> {
266 let _ = (gpu, state);
267 Ok(())
268 }
269
270 /// 2026-09-29: This proposer's draft head as the circuit executor binds it; `None` for a
271 /// proposer the circuit does not model.
272 fn circuit_draft(
273 &self,
274 _config: &ModelConfig,
275 _levers: &crate::layers::ops::ModelLevers,
276 ) -> Option<crate::circuit_exec::DraftBinding> {
277 None
278 }
279
280 /// 2026-09-29: Run `runner` in place of the draft head's own forward from now on, or its
281 /// own forward again with `None`. A proposer without a circuit binding ignores it.
282 fn set_circuit_draft(
283 &self,
284 _runner: Option<std::sync::Arc<dyn crate::circuit_exec::DraftRunner>>,
285 ) {
286 }
287}