metrale_model_engine/model/trait_impl/
impl_draft.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-26: `impl ModelDraft for TransformerModel`, mostly delegating to `<method>_dispatch`
4//! helpers in the sibling modules.
5//!
6//! The DFlash context-append methods (`dflash_accept_append`,
7//! `dflash_eagle_accept_append`, `dflash_eagle_kgamma_append`, `commit_ctx`,
8//! `dflash_serial_ctx_append`) are implemented here.
9//!
10//! Owner: model-engine.
11//! Invariants: the ones in `trait_impl/mod.rs`.
12
13use anyhow::Result;
14use metrale_gpu_runtime::gpu::DevicePtr;
15
16use crate::model::types::TransformerModel;
17use crate::traits::{ModelDraft, SequenceState};
18
19impl ModelDraft for TransformerModel {
20    fn mtp_slot_draft_capacity(&self, slot_idx: usize) -> usize {
21        self.ssm_pool.verify_draft_capacity(slot_idx)
22    }
23
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        self.gdn_carry_flush_pending()?;
31        self.generate_speculative_dispatch(prompt_tokens, params, num_drafts)
32    }
33
34    fn has_proposer(&self) -> bool {
35        self.has_proposer_dispatch()
36    }
37
38    fn dflash_gamma(&self) -> Option<usize> {
39        self.proposer.as_ref().and_then(|p| p.block_gamma())
40    }
41
42    fn has_self_speculative(&self) -> bool {
43        self.has_self_speculative_dispatch()
44    }
45
46    fn decode_draft(&self, token: u32, seq: &mut SequenceState, stream: u64) -> Result<DevicePtr> {
47        self.gdn_carry_flush_pending()?;
48        self.decode_draft_dispatch(token, seq, stream)
49    }
50
51    fn stash_verify_hidden_rows(&self, rows: &[usize], _stream: u64) -> Result<()> {
52        self.stash_verify_hidden_rows_dispatch(rows, _stream)
53    }
54
55    fn save_hidden_for_mtp_from_stash(&self, idx: usize, _stream: u64) -> Result<()> {
56        self.save_hidden_for_mtp_from_stash_dispatch(idx, _stream)
57    }
58
59    fn stash_verify_catchup_rows(&self, slot_rows: &[(usize, usize)]) -> Result<()> {
60        self.stash_verify_catchup_rows_dispatch(slot_rows)
61    }
62
63    fn run_mtp_catchup_batched(
64        &self,
65        tokens: &[Vec<u32>],
66        first_slot: &[usize],
67        first_pos: &[usize],
68        seqs: &mut [&mut SequenceState],
69    ) -> Result<usize> {
70        self.run_mtp_catchup_batched_dispatch(tokens, first_slot, first_pos, seqs)
71    }
72
73    fn run_mtp_propose_batched(
74        &self,
75        tokens: &[u32],
76        positions: &[usize],
77        stash_idx: &[usize],
78        num_drafts: usize,
79        seqs: &mut [&mut SequenceState],
80        _stream: u64,
81        out_conf: Option<&mut Vec<Vec<f32>>>,
82    ) -> Result<Option<Vec<Vec<u32>>>> {
83        self.run_mtp_propose_batched_dispatch(
84            tokens, positions, stash_idx, num_drafts, seqs, out_conf,
85        )
86    }
87
88    fn mtp_propose_batch_max(&self) -> usize {
89        match &self.proposer {
90            Some(p) => p.propose_batch_max(&self.buffers, &self.config),
91            None => 1,
92        }
93    }
94
95    fn save_hidden_for_catchup(&self, token_idx: usize, pos: usize) -> Result<()> {
96        self.save_hidden_for_catchup_dispatch(token_idx, pos)
97    }
98
99    fn save_hidden_for_mtp(&self, token_idx: usize, _stream: u64) -> Result<()> {
100        self.save_hidden_for_mtp_dispatch(token_idx, _stream)
101    }
102
103    fn save_dflash_hidden_for_propose(&self, token_idx: usize, _stream: u64) -> Result<()> {
104        self.save_dflash_hidden_dispatch(token_idx, _stream)
105    }
106
107    fn dflash_accept_append(&self, seq: &mut SequenceState) -> Result<()> {
108        let base = match self.dflash_hidden_save {
109            Some(p) => p,
110            None => return Ok(()),
111        };
112        let prop = match seq.proposer_state.as_mut() {
113            Some(p) => p.as_mut(),
114            None => return Ok(()),
115        };
116        let d = prop
117            .as_any_mut()
118            .downcast_mut::<metrale_model_arch::dflash_head::DflashProposerState>()
119            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
120        let n_layers = self.dflash_capture_layers.len();
121        if n_layers == 0 {
122            return Ok(());
123        }
124        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
125        let save_1 = base.offset(ctx_slot_bytes);
126        let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
127        self.gpu
128            .copy_d2d_async(save_1, dst, ctx_slot_bytes, self.gpu.default_stream())?;
129        d.ctx_positions.push((seq.seq_len as i32).saturating_sub(1));
130        d.ctx_len += 1;
131        Ok(())
132    }
133
134    fn dflash_eagle_accept_append(&self, seq: &mut SequenceState) -> Result<()> {
135        let base = match self.dflash_hidden_save {
136            Some(p) => p,
137            None => return Ok(()),
138        };
139        let prop = match seq.proposer_state.as_mut() {
140            Some(p) => p.as_mut(),
141            None => return Ok(()),
142        };
143        let d = prop
144            .as_any_mut()
145            .downcast_mut::<metrale_model_arch::dflash_head::DflashProposerState>()
146            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
147        let n_layers = self.dflash_capture_layers.len();
148        if n_layers == 0 {
149            return Ok(());
150        }
151        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
152        let stream = self.gpu.default_stream();
153        let pos_row0 = (seq.seq_len as i32).saturating_sub(2);
154        let pos_row1 = (seq.seq_len as i32).saturating_sub(1);
155        let save_0 = base;
156        let dst_0 = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
157        self.gpu
158            .copy_d2d_async(save_0, dst_0, ctx_slot_bytes, stream)?;
159        d.ctx_positions.push(pos_row0);
160        d.ctx_len += 1;
161        let save_1 = base.offset(ctx_slot_bytes);
162        let dst_1 = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
163        self.gpu
164            .copy_d2d_async(save_1, dst_1, ctx_slot_bytes, stream)?;
165        d.ctx_positions.push(pos_row1);
166        d.ctx_len += 1;
167        d.skip_next_decode_append = true;
168        Ok(())
169    }
170
171    fn dflash_eagle_kgamma_append(
172        &self,
173        seq: &mut SequenceState,
174        num_accepted: usize,
175        base_pos: usize,
176    ) -> Result<()> {
177        let base = match self.dflash_hidden_save {
178            Some(p) => p,
179            None => return Ok(()),
180        };
181        let prop = match seq.proposer_state.as_mut() {
182            Some(p) => p.as_mut(),
183            None => return Ok(()),
184        };
185        let d = prop
186            .as_any_mut()
187            .downcast_mut::<metrale_model_arch::dflash_head::DflashProposerState>()
188            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
189        let n_layers = self.dflash_capture_layers.len();
190        if n_layers == 0 {
191            return Ok(());
192        }
193        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
194        let stream = self.gpu.default_stream();
195        for t in 0..=num_accepted {
196            let row = base.offset(t * ctx_slot_bytes);
197            let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
198            self.gpu.copy_d2d_async(row, dst, ctx_slot_bytes, stream)?;
199            let pos = (base_pos + t) as i32;
200            d.ctx_positions.push(pos);
201            d.ctx_len += 1;
202        }
203        d.skip_next_decode_append = true;
204        Ok(())
205    }
206
207    fn dflash_capture_band(&self) -> usize {
208        self.dflash_kgamma
209    }
210
211    fn commit_ctx(
212        &self,
213        seq: &mut SequenceState,
214        num_committed: usize,
215        base_pos: usize,
216        scratch_row: usize,
217    ) -> Result<()> {
218        if num_committed == 0 {
219            return Ok(());
220        }
221        let base = match self.dflash_hidden_save {
222            Some(p) => p,
223            None => return Ok(()),
224        };
225        let prop = match seq.proposer_state.as_mut() {
226            Some(p) => p.as_mut(),
227            None => return Ok(()),
228        };
229        let d = match prop
230            .as_any_mut()
231            .downcast_mut::<metrale_model_arch::dflash_head::DflashProposerState>()
232        {
233            Some(d) => d,
234            None => return Ok(()),
235        };
236        let n_layers = self.dflash_capture_layers.len();
237        if n_layers == 0 {
238            return Ok(());
239        }
240        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
241        let stream = self.gpu.default_stream();
242
243        // 2026-09-25: Capture writes stop at `dflash_hidden_save_rows`
244        // (`try_dflash_capture_all_at` caps its row count), so a row past it
245        // was never captured. Appending it would put a stale hidden into the
246        // context, so skip the commit with a warning instead.
247        if scratch_row + num_committed > self.dflash_hidden_save_rows {
248            tracing::warn!(target: "metrale_model_engine::model::trait_impl", "commit_ctx: scratch rows {}..{} exceed capture capacity {} — skipping (ctx hole)",
249                scratch_row,
250                scratch_row + num_committed,
251                self.dflash_hidden_save_rows,
252            );
253            return Ok(());
254        }
255
256        // 2026-09-25: If the incoming rows would overflow the accumulator,
257        // slide first: keep the newest `keep` rows and drop the oldest.
258        // `ctx_committed = 0` makes the next propose recompute K/V for every
259        // kept row, and `ctx_positions` keeps absolute positions. `drop_n >=
260        // keep` holds only when `ctx_len >= 2 * keep`; when `ctx_len` is below
261        // that, the source and destination ranges of the single copy overlap.
262        if d.ctx_len + num_committed > d.max_ctx_len {
263            let keep = (d.max_ctx_len / 2).min(d.max_ctx_len.saturating_sub(num_committed));
264            let drop_n = d.ctx_len.saturating_sub(keep);
265            if drop_n > 0 {
266                let src = d.ctx_hidden_acc.offset(drop_n * ctx_slot_bytes);
267                let dst0 = d.ctx_hidden_acc.offset(0);
268                self.gpu
269                    .copy_d2d_async(src, dst0, keep * ctx_slot_bytes, stream)?;
270                d.ctx_positions.drain(..drop_n);
271                d.ctx_len = keep;
272                d.ctx_committed = 0;
273                tracing::info!(target: "metrale_model_engine::model::trait_impl", "DFlash UNIFIED_CTX watermark: slid ctx window (dropped {} oldest, keep {})",
274                    drop_n,
275                    keep,
276                );
277            }
278        }
279
280        // 2026-09-25: Append at the tail. The destination row is indexed by
281        // `ctx_len`, while `ctx_positions` records the absolute position
282        // `base_pos + t`. The two differ once a slide has run.
283        debug_assert_eq!(d.ctx_positions.len(), d.ctx_len);
284        for t in 0..num_committed {
285            let row = base.offset((scratch_row + t) * ctx_slot_bytes);
286            let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
287            self.gpu.copy_d2d_async(row, dst, ctx_slot_bytes, stream)?;
288            d.ctx_positions.push((base_pos + t) as i32);
289            d.ctx_len += 1;
290        }
291        // 2026-09-25: The latest capture is in the context now, so the next
292        // propose skips its own decode-append of it.
293        d.skip_next_decode_append = true;
294
295        tracing::debug!(target: "metrale_model_engine::model::trait_impl", "CTX_COMMIT slot={} rows={} base_pos={} ctx_len_after={}",
296            seq.slot_idx,
297            num_committed,
298            base_pos,
299            d.ctx_len,
300        );
301        if self.stats.once("log:dflash_unified_ctx") {
302            tracing::info!(target: "metrale_model_engine::model::trait_impl", "DFlash UNIFIED_CTX ACTIVE: first commit_ctx rows={} base_pos={} ctx_len={}",
303                num_committed,
304                base_pos,
305                d.ctx_len,
306            );
307        }
308        Ok(())
309    }
310
311    fn dflash_serial_ctx_append(&self, seq: &mut SequenceState) -> Result<()> {
312        // 2026-09-25: Append the hidden captured for the token just decoded
313        // serially. The decode layer loop (`try_dflash_capture(i, 0, ..)` in
314        // decode_a3.rs) writes it to row 0 of `dflash_hidden_save`, one slot per
315        // capture layer, which is the layout of one accumulator row.
316        let base = match self.dflash_hidden_save {
317            Some(p) => p,
318            None => return Ok(()),
319        };
320        let prop = match seq.proposer_state.as_mut() {
321            Some(p) => p.as_mut(),
322            None => return Ok(()),
323        };
324        // 2026-09-25: A non-DFlash proposer state returns `Ok` here, whereas
325        // `dflash_eagle_accept_append` returns an error for one.
326        let d = match prop
327            .as_any_mut()
328            .downcast_mut::<metrale_model_arch::dflash_head::DflashProposerState>()
329        {
330            Some(d) => d,
331            None => return Ok(()),
332        };
333        let n_layers = self.dflash_capture_layers.len();
334        if n_layers == 0 {
335            return Ok(());
336        }
337        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
338        let stream = self.gpu.default_stream();
339        // 2026-09-25: When the accumulator is full, slide the window: keep the
340        // newest `max_ctx_len / 2` rows and drop the oldest. Here `ctx_len >=
341        // max_ctx_len`, so `drop_n >= keep` and the single copy's source and
342        // destination ranges do not overlap. `ctx_committed = 0` makes the next
343        // propose recompute K/V for every kept row, and `ctx_positions` keeps
344        // absolute positions.
345        if d.ctx_len >= d.max_ctx_len {
346            let keep = d.max_ctx_len / 2;
347            let drop_n = d.ctx_len - keep;
348            let src = d.ctx_hidden_acc.offset(drop_n * ctx_slot_bytes);
349            let dst0 = d.ctx_hidden_acc.offset(0);
350            self.gpu
351                .copy_d2d_async(src, dst0, keep * ctx_slot_bytes, stream)?;
352            d.ctx_positions.drain(..drop_n);
353            d.ctx_len = keep;
354            d.ctx_committed = 0;
355            tracing::info!(target: "metrale_model_engine::model::trait_impl", "DFlash SERIAL_APPEND watermark: slid ctx window (dropped {} oldest, keep {})",
356                drop_n,
357                keep,
358            );
359        }
360        let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
361        self.gpu.copy_d2d_async(base, dst, ctx_slot_bytes, stream)?;
362        if self.stats.once("log:dflash_serial_append") {
363            tracing::info!(target: "metrale_model_engine::model::trait_impl", "DFlash SERIAL_APPEND ACTIVE: first serial ctx append at ctx_len={} pos={}",
364                d.ctx_len,
365                seq.seq_len.saturating_sub(1),
366            );
367        }
368        // 2026-09-25: Decode has already advanced `seq_len` past this token, so
369        // its position is `seq_len - 1`.
370        debug_assert_eq!(d.ctx_positions.len(), d.ctx_len);
371        d.ctx_positions.push(seq.seq_len.saturating_sub(1) as i32);
372        d.ctx_len += 1;
373        // 2026-09-25: The latest capture is in the context now, so the next
374        // propose skips its own decode-append of it.
375        d.skip_next_decode_append = true;
376        Ok(())
377    }
378
379    fn run_mtp_propose(
380        &self,
381        token: u32,
382        position: usize,
383        seq: &mut SequenceState,
384        _stream: u64,
385    ) -> Result<Option<u32>> {
386        self.run_mtp_propose_dispatch(token, position, seq, _stream)
387    }
388
389    fn run_mtp_propose_multi(
390        &self,
391        token: u32,
392        position: usize,
393        num_drafts: usize,
394        seq: &mut SequenceState,
395        _stream: u64,
396        grammar_bitmask: Option<&[i32]>,
397    ) -> Result<Vec<u32>> {
398        self.run_mtp_propose_multi_dispatch(
399            token,
400            position,
401            num_drafts,
402            seq,
403            _stream,
404            grammar_bitmask,
405        )
406    }
407
408    fn read_deferred_draft_token(&self) -> Result<u32> {
409        self.read_deferred_draft_token_dispatch()
410    }
411
412    fn trim_proposer_state(
413        &self,
414        seq: &mut SequenceState,
415        num_accepted: usize,
416        _stream: u64,
417    ) -> Result<()> {
418        self.trim_proposer_state_dispatch(seq, num_accepted, _stream)
419    }
420}