spark_model/layers/mtp_head/
draft_proposer.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `DraftProposer` implementation for [`MtpHead`] (split from mtp_head.rs for the 500-LoC cap).
4use super::*;
5
6impl DraftProposer for MtpHead {
7    fn alloc_state(&self, _gpu: &dyn GpuBackend) -> Result<Box<dyn ProposerState>> {
8        Ok(Box::new(MtpProposerState {
9            block_table: Vec::new(),
10            seq_len: 0,
11            last_num_drafted: 0,
12            last_pair_key: None,
13        }))
14    }
15
16    fn propose(
17        &self,
18        last_token: u32,
19        target_hidden: DevicePtr,
20        position: usize,
21        num_drafts: usize,
22        state: &mut dyn ProposerState,
23        ctx: &ForwardContext,
24        stream: u64,
25        draft_embed_target: Option<DevicePtr>,
26        grammar_bitmask: Option<&[i32]>,
27        _target_hidden_stack: Option<DevicePtr>,
28    ) -> Result<Vec<u32>> {
29        let mtp_state = state
30            .as_any_mut()
31            .downcast_mut::<MtpProposerState>()
32            .ok_or_else(|| anyhow::anyhow!("Invalid MTP proposer state"))?;
33
34        // Reset chain confidence for this propose (forward_one mins into it).
35        self.last_conf_bits
36            .store(1.0f32.to_bits(), std::sync::atomic::Ordering::Relaxed);
37        let mut drafts = Vec::with_capacity(num_drafts);
38        let mut current_token = last_token;
39        let mut current_hidden = target_hidden;
40
41        for i in 0..num_drafts {
42            // Only the LAST draft gets GPU-side embedding (it's the one
43            // used in the next verify step).
44            let embed_target = if i == num_drafts - 1 {
45                draft_embed_target
46            } else {
47                None
48            };
49            // Grammar-masked drafting (num_drafts==1 path only for now).
50            // For num_drafts > 1 we would need to speculatively advance the
51            // matcher between drafts and roll back before returning; the
52            // current scheduler only uses num_drafts==1, so we pass the same
53            // mask for every i and warn loudly if K>1 + grammar combine.
54            if grammar_bitmask.is_some() && i > 0 {
55                tracing::warn!(
56                    "MTP grammar-masked drafting called with num_drafts>1 (i={i}); \
57                     mask held fixed across draft positions — acceptance may drop."
58                );
59            }
60            let mask_for_draft = grammar_bitmask;
61            let draft = self.forward_one(
62                current_token,
63                current_hidden,
64                position + i,
65                mtp_state,
66                ctx,
67                stream,
68                embed_target,
69                mask_for_draft,
70            )?;
71            tracing::debug!(
72                "MTP propose[{i}]: token={current_token} pos={} mtp_seq_len={} → draft={draft}",
73                position + i,
74                mtp_state.seq_len,
75            );
76            drafts.push(draft);
77            current_token = draft;
78            // For subsequent drafts, use the MTP head's own hidden state
79            current_hidden = ctx.buffers.hidden_states();
80        }
81
82        mtp_state.last_num_drafted = drafts.len();
83        Ok(drafts)
84    }
85
86    fn propose_batch(
87        &self,
88        last_tokens: &[u32],
89        target_hiddens: &[DevicePtr],
90        positions: &[usize],
91        num_drafts: usize,
92        states: &mut [&mut dyn ProposerState],
93        ctx: &ForwardContext,
94        stream: u64,
95        out_conf: Option<&mut Vec<Vec<f32>>>,
96    ) -> Result<Option<Vec<Vec<u32>>>> {
97        if !self.can_propose_batch(last_tokens.len(), ctx.buffers, ctx.config) {
98            return Ok(None);
99        }
100        // All states must downcast to MtpProposerState; any miss means a
101        // mixed-proposer batch — unsupported, fall back per-seq.
102        let mut mtp_states: Vec<&mut MtpProposerState> = Vec::with_capacity(states.len());
103        for s in states.iter_mut() {
104            match s.as_any_mut().downcast_mut::<MtpProposerState>() {
105                Some(st) => mtp_states.push(st),
106                None => return Ok(None),
107            }
108        }
109        self.propose_batch_impl(
110            last_tokens,
111            target_hiddens,
112            positions,
113            num_drafts,
114            &mut mtp_states,
115            ctx,
116            stream,
117            out_conf,
118        )
119        .map(Some)
120    }
121
122    fn propose_batch_max(
123        &self,
124        buffers: &spark_runtime::buffers::BufferArena,
125        config: &atlas_core::config::ModelConfig,
126    ) -> usize {
127        MtpHead::propose_batch_max(self, buffers, config)
128    }
129
130    fn prefill_drafter(
131        &self,
132        prompt_tokens: &[u32],
133        hiddens: DevicePtr,
134        state: &mut dyn ProposerState,
135        ctx: &ForwardContext,
136        stream: u64,
137    ) -> Result<usize> {
138        self.prefill_drafter_impl(prompt_tokens, hiddens, state, ctx, stream)
139    }
140
141    fn drafter_rows(&self, state: &mut dyn ProposerState) -> usize {
142        state
143            .as_any_mut()
144            .downcast_mut::<MtpProposerState>()
145            .map(|s| s.seq_len)
146            .unwrap_or(0)
147    }
148
149    fn last_pair_key(&self, state: &mut dyn ProposerState) -> Option<usize> {
150        state
151            .as_any_mut()
152            .downcast_mut::<MtpProposerState>()
153            .and_then(|s| s.last_pair_key)
154    }
155
156    fn take_drafter_kv(
157        &self,
158        state: &mut dyn ProposerState,
159    ) -> Option<(Vec<u32>, usize, Option<usize>)> {
160        let st = state.as_any_mut().downcast_mut::<MtpProposerState>()?;
161        if st.block_table.is_empty() || st.seq_len == 0 {
162            return None;
163        }
164        let blocks = std::mem::take(&mut st.block_table);
165        let rows = st.seq_len;
166        let key = st.last_pair_key;
167        // Leave the state exactly as `alloc_state` would: no blocks, no rows,
168        // no pair key. `free_state` then has nothing to release.
169        st.seq_len = 0;
170        st.last_pair_key = None;
171        st.last_num_drafted = 0;
172        Some((blocks, rows, key))
173    }
174
175    fn install_drafter_kv(
176        &self,
177        state: &mut dyn ProposerState,
178        blocks: Vec<u32>,
179        rows: usize,
180        last_pair_key: Option<usize>,
181    ) -> bool {
182        let Some(st) = state.as_any_mut().downcast_mut::<MtpProposerState>() else {
183            return false;
184        };
185        // Only ever into a fresh state — otherwise the old blocks would leak.
186        if !st.block_table.is_empty() || st.seq_len != 0 {
187            return false;
188        }
189        st.block_table = blocks;
190        st.seq_len = rows;
191        st.last_pair_key = last_pair_key;
192        true
193    }
194
195    fn free_drafter_kv(&self, blocks: &[u32]) {
196        if !blocks.is_empty() {
197            self.kv_cache.lock().free_blocks(blocks);
198        }
199    }
200
201    fn catchup_drafter(
202        &self,
203        tokens: &[u32],
204        hiddens: DevicePtr,
205        row_base: usize,
206        pos_base: usize,
207        state: &mut dyn ProposerState,
208        ctx: &ForwardContext,
209        stream: u64,
210    ) -> Result<usize> {
211        self.drafter_rows_impl(tokens, hiddens, row_base, pos_base, state, ctx, stream)
212    }
213
214    fn read_deferred_draft_token(&self, gpu: &dyn GpuBackend) -> Result<u32> {
215        self.read_deferred_draft_token(gpu)
216    }
217
218    fn last_confidence(&self) -> Option<f32> {
219        if crate::speculative::draft_conf_tau() <= 0.0 {
220            return None;
221        }
222        Some(f32::from_bits(
223            self.last_conf_bits
224                .load(std::sync::atomic::Ordering::Relaxed),
225        ))
226    }
227
228    fn after_verify(
229        &self,
230        num_accepted: usize,
231        state: &mut dyn ProposerState,
232        _stream: u64,
233    ) -> Result<()> {
234        let mtp_state = state
235            .as_any_mut()
236            .downcast_mut::<MtpProposerState>()
237            .ok_or_else(|| anyhow::anyhow!("Invalid MTP proposer state"))?;
238
239        // Trim rejected drafts from MTP KV cache.
240        // num_drafted was recorded in the last propose() call.
241        // We trim `num_drafted - num_accepted` entries.
242        // e.g. K=2: drafted 1, accepted 0 → trim 1. accepted 1 → trim 0.
243        // e.g. K=3: drafted 2, accepted 0 → trim 2. accepted 1 → trim 1. accepted 2 → trim 0.
244        let num_drafted = mtp_state.last_num_drafted.max(1);
245        let num_to_trim = mtp_rows_to_trim(
246            num_drafted,
247            num_accepted,
248            crate::speculative::mtp_refeed_accepted_enabled(),
249        );
250        let old_sl = mtp_state.seq_len;
251        if num_to_trim > 0 {
252            mtp_state.seq_len = mtp_state.seq_len.saturating_sub(num_to_trim);
253            // Trimmed rows have consecutive pair keys; the newest surviving
254            // key moves back by the same count.
255            if let Some(k) = mtp_state.last_pair_key {
256                mtp_state.last_pair_key = Some(k.saturating_sub(num_to_trim));
257            }
258        }
259        tracing::debug!(
260            "MTP after_verify: accepted={num_accepted} drafted={num_drafted} trim={num_to_trim} mtp_seq_len: {old_sl} → {}",
261            mtp_state.seq_len,
262        );
263        Ok(())
264    }
265
266    fn free_state(&self, _gpu: &dyn GpuBackend, state: &mut dyn ProposerState) -> Result<()> {
267        let mtp_state = state
268            .as_any_mut()
269            .downcast_mut::<MtpProposerState>()
270            .ok_or_else(|| anyhow::anyhow!("Invalid MTP proposer state"))?;
271        if !mtp_state.block_table.is_empty() {
272            self.kv_cache.lock().free_blocks(&mtp_state.block_table);
273            mtp_state.block_table.clear();
274        }
275        mtp_state.seq_len = 0;
276        Ok(())
277    }
278}