spark_model/model/trait_impl/
mod.rs

1// SPDX-License-Identifier: AGPL-3.0-only
2
3//! `impl Model for TransformerModel` — thin trait impl that delegates to
4//! `<method>_dispatch` helpers split across sibling files for the ≤500
5//! LoC cap. Each sibling adds methods to the `TransformerModel`
6//! inherent impl. The trait impl below is purely one-line delegators.
7
8#![allow(unused_imports, dead_code, clippy::too_many_arguments)]
9
10use anyhow::Result;
11use spark_runtime::gpu::DevicePtr;
12use spark_runtime::kv_cache::PagedKvCache;
13
14use super::types::{PinnedMetaStaging, TransformerModel};
15use crate::layer::{AttnMetadataDev, LayerState};
16use crate::speculative::DraftProposer;
17use crate::traits::{ChunkedPrefillPageMetadata, Model, PrefillSlice, SequenceState};
18use crate::weight_map::{DenseWeight, MtpWeights};
19
20mod async_chkpt;
21mod decode_a;
22mod decode_a2;
23mod decode_a3;
24mod decode_a_diag;
25mod decode_b;
26mod decode_b2;
27pub(in crate::model) mod decode_checkpoint;
28mod decode_graph_key;
29mod decode_multi_seq_gate;
30mod drafter_prefill;
31mod ep_misc;
32mod gdn_woa;
33mod graph_borrow;
34mod lm_head_batched;
35mod meta;
36mod prefill_a;
37mod prefill_b;
38mod prefill_c;
39mod prefill_d;
40mod prefix_reuse;
41mod sequence;
42mod speculative;
43pub(in crate::model) mod ssm_fault_in;
44mod verify_a;
45mod verify_a_ssm;
46mod verify_b;
47mod verify_c;
48mod verify_c2;
49mod verify_d;
50mod verify_e;
51pub(in crate::model) mod verify_e2;
52mod verify_fused;
53
54impl Model for TransformerModel {
55    fn teardown(&mut self) -> Result<()> {
56        self.release_pools()
57    }
58
59    /// Poll this model's own InnerQ driver. A miss is logged, never fatal — it
60    /// is a diagnostic lever, not part of serving.
61    #[cfg(feature = "cuda")]
62    fn poll_innerq(&self) {
63        if let Some(driver) = self.innerq.as_ref()
64            && let Err(e) = driver.maybe_finalize(128)
65        {
66            tracing::warn!("InnerQ maybe_finalize failed: {e:#}");
67        }
68    }
69
70    fn prepare_vision_embed(&self, images: &[crate::VisionItem]) -> Result<()> {
71        self.prepare_vision_embed_dispatch(images)
72    }
73    fn prepare_vision_embed_batched(
74        &self,
75        per_request: &[Vec<crate::VisionItem>],
76    ) -> Result<Vec<(usize, usize, usize, usize)>> {
77        self.prepare_vision_embed_batched_dispatch(per_request)
78    }
79    fn ep_sync_vision_embeds(&self, tokens: &[u32]) -> Result<()> {
80        TransformerModel::ep_sync_vision_embeds(self, tokens)
81    }
82    fn set_vision_slice_base(&self, row_base: usize, grid_base: usize, owned_images: usize) {
83        *self.vision_row_base.lock() = row_base;
84        *self.vision_grid_base.lock() = grid_base;
85        *self.vision_owned_images.lock() = owned_images;
86    }
87    // The four prefill entry points each end with `try_eager_drafter_prefill`:
88    // the whole-prompt drafter capture is a single shared slot, so it must be
89    // consumed while THIS sequence still owns it — one tick later, at the
90    // first propose, a concurrent sequence's prefill has already restarted it
91    // and every sequence but the last-prefilled drafts blind. See
92    // `drafter_prefill.rs`. Kill switch `AVAROK_NO_MTP_EAGER_DRAFTER`.
93    fn tokens_contain_vision_pad(&self, tokens: &[u32]) -> bool {
94        self.tokens_have_vision_pad(tokens)
95    }
96    fn prefill(&self, tokens: &[u32], seq: &mut SequenceState, stream: u64) -> Result<DevicePtr> {
97        self.stamp_overlay_route(seq.adapter_slot);
98        let logits = self.prefill_dispatch(tokens, seq, stream)?;
99        self.try_eager_drafter_prefill(seq, true, stream);
100        Ok(logits)
101    }
102    fn prefill_chunk(
103        &self,
104        tokens: &[u32],
105        seq: &mut SequenceState,
106        chunk_start: usize,
107        chunk_len: usize,
108        is_last_chunk: bool,
109        stream: u64,
110    ) -> Result<DevicePtr> {
111        self.stamp_overlay_route(seq.adapter_slot);
112        let logits = self.prefill_chunk_dispatch(
113            tokens,
114            seq,
115            chunk_start,
116            chunk_len,
117            is_last_chunk,
118            stream,
119        )?;
120        self.try_eager_drafter_prefill(seq, is_last_chunk, stream);
121        Ok(logits)
122    }
123    fn prefill_twophase(
124        &self,
125        tokens: &[u32],
126        seq: &mut SequenceState,
127        chunk_size: usize,
128        stream: u64,
129    ) -> Result<DevicePtr> {
130        self.stamp_overlay_route(seq.adapter_slot);
131        let logits = self.prefill_twophase_dispatch(tokens, seq, chunk_size, stream)?;
132        self.try_eager_drafter_prefill(seq, true, stream);
133        Ok(logits)
134    }
135    fn decode(&self, token: u32, seq: &mut SequenceState, _stream: u64) -> Result<DevicePtr> {
136        self.stamp_overlay_route(seq.adapter_slot);
137        self.stamp_decode_moe_single(seq.adapter_slot);
138        self.decode_dispatch(token, seq, _stream)
139    }
140    fn decode_batch(
141        &self,
142        tokens: &[u32],
143        seqs: &mut [&mut SequenceState],
144        stream: u64,
145    ) -> Result<DevicePtr> {
146        self.stamp_overlay_route_batch(seqs);
147        self.stamp_decode_moe_batch(seqs);
148        let r = self.decode_batch_dispatch(tokens, seqs, stream);
149        if r.is_err() {
150            // A mid-capture refuse (MoE LoRA router/mixed/non-active) in the
151            // batched-decode compute leaves the capture stream recording; release
152            // it so the caller's sequence cleanup doesn't hit
153            // STREAM_CAPTURE_UNSUPPORTED and poison every later op (a single
154            // refused concurrent request would otherwise brick the server). The
155            // batched path captures on the default stream (decode_a2).
156            self.gpu.abort_capture_if_active(self.gpu.default_stream());
157        }
158        r
159    }
160    fn mixed_forward(
161        &self,
162        decode_tokens: &[u32],
163        decode_seqs: &mut [&mut SequenceState],
164        prefill_tokens: &[u32],
165        prefill_seq: &mut SequenceState,
166        prefill_chunk_start: usize,
167        prefill_chunk_len: usize,
168        prefill_is_last: bool,
169        stream: u64,
170    ) -> Result<crate::traits::MixedForwardResult> {
171        // Mixed decode+prefill batch spans multiple adapters ⇒ mark mixed so the
172        // overlay hooks skip (per-token seq_slot routing is SOLID Incr-4).
173        self.overlay_route_slot
174            .store(i32::MIN, std::sync::atomic::Ordering::Relaxed);
175        // Decode portion: Skip only if every decode seq is base, else refuse.
176        self.stamp_decode_moe_batch(decode_seqs);
177        let r = self.mixed_forward_dispatch(
178            decode_tokens,
179            decode_seqs,
180            prefill_tokens,
181            prefill_seq,
182            prefill_chunk_start,
183            prefill_chunk_len,
184            prefill_is_last,
185            stream,
186        );
187        if r.is_err() {
188            // Same brick guard as decode_batch: a refuse in the captured decode
189            // portion must not leave the default stream recording.
190            self.gpu.abort_capture_if_active(self.gpu.default_stream());
191        }
192        let out = r?;
193        self.try_eager_drafter_prefill(prefill_seq, prefill_is_last, stream);
194        Ok(out)
195    }
196
197    /// Q12 Phase 4b override. The concrete dispatcher routes ineligible
198    /// batches to its sequential path before state mutation. Errors from an
199    /// admitted kernel batch must propagate: retrying sequentially can
200    /// reapply prefix-cache and KV state.
201    fn prefill_batch_chunk(
202        &self,
203        streams: &mut [PrefillSlice<'_>],
204        stream: u64,
205    ) -> Result<Vec<DevicePtr>> {
206        self.prefill_batch_chunk_rows(streams, stream, 0)
207    }
208    /// Mixed-step variant: shift the finishing streams' logits rows clear of
209    /// the decode lanes. See the trait docs for the aliasing this prevents.
210    fn prefill_batch_chunk_rows(
211        &self,
212        streams: &mut [PrefillSlice<'_>],
213        stream: u64,
214        row_base: usize,
215    ) -> Result<Vec<DevicePtr>> {
216        self.prefill_batch_chunk_dispatch(streams, stream, row_base)
217    }
218    fn vocab_size(&self) -> usize {
219        self.vocab_size_dispatch()
220    }
221    fn set_active_lora(&mut self, name: &str) -> Result<()> {
222        self.rotate_lora_to(name)
223    }
224    fn adapter_id_for(&self, slot: i32) -> u64 {
225        self.adapter_id_for_slot(slot)
226    }
227    fn acquire_adapter_slot(&self, slot: i32) -> i32 {
228        TransformerModel::acquire_adapter_slot(self, slot)
229    }
230    fn release_adapter_slot(&self, resolved: i32) {
231        TransformerModel::release_adapter_slot(self, resolved)
232    }
233    fn swap_lora_from_disk(
234        &mut self,
235        dir: &std::path::Path,
236        name: &str,
237        slot: usize,
238    ) -> Result<()> {
239        // Disk staging is plain file I/O and is portable; only the PEER path
240        // needs RDMA. Still cuda-gated, since it lands into a device pool.
241        #[cfg(feature = "cuda")]
242        {
243            self.swap_lora_slot_from_disk(dir, name, slot)
244        }
245        #[cfg(not(feature = "cuda"))]
246        {
247            let _ = (dir, name, slot);
248            anyhow::bail!("LoRA disk swap requires the cuda feature")
249        }
250    }
251    fn promote_lora_from_peer(
252        &mut self,
253        peer_addr: &str,
254        adapter_id: &str,
255        name: &str,
256        peft: avarok_core::config::PeftAdapterConfig,
257    ) -> Result<(usize, Option<String>)> {
258        #[cfg(all(feature = "cuda", unix))]
259        {
260            self.promote_lora_slot_from_peer(peer_addr, adapter_id, name, peft)
261        }
262        #[cfg(not(all(feature = "cuda", unix)))]
263        {
264            let _ = (peer_addr, adapter_id, name, peft);
265            anyhow::bail!("LoRA peer promotion stages over RDMA (rdma-core); unix-only")
266        }
267    }
268    fn promote_lora_from_disk(
269        &mut self,
270        dir: &std::path::Path,
271        name: &str,
272    ) -> Result<(usize, Option<String>)> {
273        #[cfg(feature = "cuda")]
274        {
275            self.promote_lora_slot_from_disk(dir, name)
276        }
277        #[cfg(not(feature = "cuda"))]
278        {
279            let _ = (dir, name);
280            anyhow::bail!("LoRA disk promotion requires the cuda feature")
281        }
282    }
283    fn high_speed_swap_dims(&self) -> Option<spark_storage::ModelDims> {
284        self.high_speed_swap_dims_dispatch()
285    }
286    fn normalize_ssm_states(&self, seq: &SequenceState, stream: u64) -> Result<()> {
287        self.normalize_ssm_states_dispatch(seq, stream)
288    }
289    fn bind_gpu_to_thread(&self) -> Result<()> {
290        self.bind_gpu_to_thread_dispatch()
291    }
292    fn alloc_sequence(&self) -> Result<SequenceState> {
293        self.alloc_sequence_dispatch(usize::MAX)
294    }
295
296    fn alloc_sequence_for(&self, budget_tokens: usize) -> Result<SequenceState> {
297        self.alloc_sequence_dispatch(budget_tokens)
298    }
299    fn copy_logits_to_host(&self, logits_ptr: DevicePtr, dst: &mut [u8]) -> Result<()> {
300        self.copy_logits_to_host_dispatch(logits_ptr, dst)
301    }
302    fn logits_ptr_is_fp32(&self, logits_ptr: DevicePtr) -> bool {
303        self.logits_ptr_is_fp32_dispatch(logits_ptr)
304    }
305    fn logits_buffer_ptr(&self) -> DevicePtr {
306        self.logits_buffer_ptr_dispatch()
307    }
308    fn argmax_on_device(&self, logits_ptr: DevicePtr, _stream: u64) -> Result<u32> {
309        self.argmax_on_device_dispatch(logits_ptr, _stream)
310    }
311    fn argmax_batch(&self, logits_ptr: DevicePtr, n: usize, _stream: u64) -> Result<Vec<u32>> {
312        self.argmax_batch_dispatch(logits_ptr, n, _stream)
313    }
314    fn hidden_after_norm(&self) -> DevicePtr {
315        self.hidden_after_norm_dispatch()
316    }
317    fn decode_verify(
318        &self,
319        tokens: &[u32],
320        seq: &mut SequenceState,
321        stream: u64,
322    ) -> Result<Vec<u32>> {
323        self.ssm_pool.require_verify_rollback_supported()?;
324        let r = self.decode_verify_dispatch(tokens, seq, stream);
325        if r.is_err() {
326            // Same brick guard as decode_batch: a refuse mid-verify-capture
327            // (MTP/spec) must not leave the default stream recording. No-op when
328            // not capturing. Verify captures on default_stream (verify_a/b/…).
329            self.gpu.abort_capture_if_active(self.gpu.default_stream());
330        }
331        r
332    }
333    fn checkpoint_ssm_states(&self, seq: &mut SequenceState) -> Result<()> {
334        self.checkpoint_ssm_states_dispatch(seq)
335    }
336    fn rollback_ssm_states(&self, seq: &mut SequenceState, num_accepted: usize) -> Result<()> {
337        self.rollback_ssm_states_dispatch(seq, num_accepted)
338    }
339    fn has_ssm_layers(&self) -> bool {
340        self.ssm_pool.num_ssm_layers > 0
341    }
342    fn decode_rollback_unsupported(&self) -> bool {
343        self.layers.iter().any(|l| l.decode_rollback_unsupported())
344    }
345    fn mtp_slot_draft_capacity(&self, slot_idx: usize) -> usize {
346        self.ssm_pool.verify_draft_capacity(slot_idx)
347    }
348    fn decode_rollback_ring_slots(&self) -> usize {
349        if self.ssm_snapshots.decode_rollback_enabled() {
350            self.ssm_snapshots.decode_ring_slots
351        } else {
352            0
353        }
354    }
355    fn save_decode_ssm_snapshot(&self, seq: &SequenceState, ring_slot: usize) -> Result<()> {
356        self.save_decode_ssm_snapshot_dispatch(seq, ring_slot)
357    }
358    fn restore_decode_ssm_snapshot(&self, seq: &SequenceState, ring_slot: usize) -> Result<()> {
359        self.restore_decode_ssm_snapshot_dispatch(seq, ring_slot)
360    }
361    fn generate_speculative(
362        &self,
363        prompt_tokens: &[u32],
364        params: &spark_runtime::sampler::SamplingParams,
365        num_drafts: usize,
366    ) -> Result<crate::engine::GenerateResult> {
367        self.generate_speculative_dispatch(prompt_tokens, params, num_drafts)
368    }
369    fn has_proposer(&self) -> bool {
370        self.has_proposer_dispatch()
371    }
372    fn dflash_gamma(&self) -> Option<usize> {
373        self.proposer.as_ref().and_then(|p| p.block_gamma())
374    }
375    fn has_self_speculative(&self) -> bool {
376        self.has_self_speculative_dispatch()
377    }
378    fn decode_draft(&self, token: u32, seq: &mut SequenceState, stream: u64) -> Result<DevicePtr> {
379        self.decode_draft_dispatch(token, seq, stream)
380    }
381    fn cache_sequence(&self, seq: &SequenceState) {
382        self.cache_sequence_dispatch(seq)
383    }
384    fn decode_marconi_checkpoint(&self, seq: &mut SequenceState) {
385        self.decode_marconi_checkpoint_dispatch(seq)
386    }
387    fn free_sequence(&self, seq: &mut SequenceState) -> Result<()> {
388        self.free_sequence_dispatch(seq)
389    }
390    fn decode_verify_graphed(
391        &self,
392        tokens: &[u32; 2],
393        seq: &mut SequenceState,
394        _stream: u64,
395    ) -> Result<[u32; 2]> {
396        self.ssm_pool.require_verify_rollback_supported()?;
397        self.decode_verify_graphed_dispatch(tokens, seq, _stream)
398    }
399    fn decode_verify_graphed_k3(
400        &self,
401        tokens: &[u32; 3],
402        seq: &mut SequenceState,
403        _stream: u64,
404    ) -> Result<[u32; 3]> {
405        self.ssm_pool.require_verify_rollback_supported()?;
406        self.decode_verify_graphed_k3_dispatch(tokens, seq, _stream)
407    }
408    fn decode_verify_graphed_k4(
409        &self,
410        tokens: &[u32; 4],
411        seq: &mut SequenceState,
412        _stream: u64,
413    ) -> Result<[u32; 4]> {
414        self.ssm_pool.require_verify_rollback_supported()?;
415        self.decode_verify_graphed_k4_dispatch(tokens, seq, _stream)
416    }
417    fn can_batch_verify(&self, ks: &[usize]) -> bool {
418        self.can_batch_verify_dispatch(ks)
419    }
420    fn decode_verify_batched(
421        &self,
422        tokens: &[u32],
423        ks: &[usize],
424        seqs: &mut [&mut SequenceState],
425        _stream: u64,
426        opts: crate::traits::VerifyBatchedOpts,
427    ) -> Result<Vec<u32>> {
428        self.ssm_pool.require_verify_rollback_supported()?;
429        self.decode_verify_batched_dispatch(tokens, ks, seqs, _stream, opts)
430    }
431    fn stash_verify_hidden_rows(&self, rows: &[usize], _stream: u64) -> Result<()> {
432        self.stash_verify_hidden_rows_dispatch(rows, _stream)
433    }
434    fn save_hidden_for_mtp_from_stash(&self, idx: usize, _stream: u64) -> Result<()> {
435        self.save_hidden_for_mtp_from_stash_dispatch(idx, _stream)
436    }
437    fn run_mtp_propose_batched(
438        &self,
439        tokens: &[u32],
440        positions: &[usize],
441        stash_idx: &[usize],
442        num_drafts: usize,
443        seqs: &mut [&mut SequenceState],
444        _stream: u64,
445        out_conf: Option<&mut Vec<Vec<f32>>>,
446    ) -> Result<Option<Vec<Vec<u32>>>> {
447        self.run_mtp_propose_batched_dispatch(
448            tokens, positions, stash_idx, num_drafts, seqs, out_conf,
449        )
450    }
451    fn mtp_propose_batch_max(&self) -> usize {
452        match &self.proposer {
453            Some(p) => p.propose_batch_max(&self.buffers, &self.config),
454            None => 1,
455        }
456    }
457    fn decode_verify_graphed_kgamma(
458        &self,
459        tokens: &[u32],
460        seq: &mut SequenceState,
461        _stream: u64,
462    ) -> Result<Vec<u32>> {
463        self.ssm_pool.require_verify_rollback_supported()?;
464        self.decode_verify_graphed_kgamma_dispatch(tokens, seq, _stream)
465    }
466    fn decode_and_verify_fused(
467        &self,
468        tokens: &[u32],
469        seq: &mut SequenceState,
470        _stream: u64,
471    ) -> Result<Vec<u32>> {
472        self.ssm_pool.require_verify_rollback_supported()?;
473        self.decode_and_verify_fused_dispatch(tokens, seq, _stream)
474    }
475    fn save_hidden_for_catchup(&self, token_idx: usize, pos: usize) -> Result<()> {
476        self.save_hidden_for_catchup_dispatch(token_idx, pos)
477    }
478
479    fn save_hidden_for_mtp(&self, token_idx: usize, _stream: u64) -> Result<()> {
480        self.save_hidden_for_mtp_dispatch(token_idx, _stream)
481    }
482    fn save_dflash_hidden_for_propose(&self, token_idx: usize, _stream: u64) -> Result<()> {
483        self.save_dflash_hidden_dispatch(token_idx, _stream)
484    }
485
486    fn dflash_accept_append(&self, seq: &mut SequenceState) -> Result<()> {
487        let base = match self.dflash_hidden_save {
488            Some(p) => p,
489            None => return Ok(()),
490        };
491        let prop = match seq.proposer_state.as_mut() {
492            Some(p) => p.as_mut(),
493            None => return Ok(()),
494        };
495        let d = prop
496            .as_any_mut()
497            .downcast_mut::<crate::layers::DflashProposerState>()
498            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
499        let n_layers = self.dflash_capture_layers.len();
500        if n_layers == 0 {
501            return Ok(());
502        }
503        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
504        let save_1 = base.offset(ctx_slot_bytes);
505        let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
506        self.gpu
507            .copy_d2d_async(save_1, dst, ctx_slot_bytes, self.gpu.default_stream())?;
508        d.ctx_positions.push((seq.seq_len as i32).saturating_sub(1));
509        d.ctx_len += 1;
510        Ok(())
511    }
512
513    fn dflash_eagle_accept_append(&self, seq: &mut SequenceState) -> Result<()> {
514        let base = match self.dflash_hidden_save {
515            Some(p) => p,
516            None => return Ok(()),
517        };
518        let prop = match seq.proposer_state.as_mut() {
519            Some(p) => p.as_mut(),
520            None => return Ok(()),
521        };
522        let d = prop
523            .as_any_mut()
524            .downcast_mut::<crate::layers::DflashProposerState>()
525            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
526        let n_layers = self.dflash_capture_layers.len();
527        if n_layers == 0 {
528            return Ok(());
529        }
530        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
531        let stream = self.gpu.default_stream();
532        let pos_row0 = (seq.seq_len as i32).saturating_sub(2);
533        let pos_row1 = (seq.seq_len as i32).saturating_sub(1);
534        // Row 0 @ N
535        let save_0 = base;
536        let dst_0 = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
537        self.gpu
538            .copy_d2d_async(save_0, dst_0, ctx_slot_bytes, stream)?;
539        d.ctx_positions.push(pos_row0);
540        d.ctx_len += 1;
541        // Row 1 @ N+1
542        let save_1 = base.offset(ctx_slot_bytes);
543        let dst_1 = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
544        self.gpu
545            .copy_d2d_async(save_1, dst_1, ctx_slot_bytes, stream)?;
546        d.ctx_positions.push(pos_row1);
547        d.ctx_len += 1;
548        d.skip_next_decode_append = true;
549        Ok(())
550    }
551
552    fn dflash_eagle_kgamma_append(
553        &self,
554        seq: &mut SequenceState,
555        num_accepted: usize,
556        base_pos: usize,
557    ) -> Result<()> {
558        let base = match self.dflash_hidden_save {
559            Some(p) => p,
560            None => return Ok(()),
561        };
562        let prop = match seq.proposer_state.as_mut() {
563            Some(p) => p.as_mut(),
564            None => return Ok(()),
565        };
566        let d = prop
567            .as_any_mut()
568            .downcast_mut::<crate::layers::DflashProposerState>()
569            .ok_or_else(|| anyhow::anyhow!("not DFlash proposer state"))?;
570        let n_layers = self.dflash_capture_layers.len();
571        if n_layers == 0 {
572            return Ok(());
573        }
574        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
575        let stream = self.gpu.default_stream();
576        for t in 0..=num_accepted {
577            let row = base.offset(t * ctx_slot_bytes);
578            let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
579            self.gpu.copy_d2d_async(row, dst, ctx_slot_bytes, stream)?;
580            let pos = (base_pos + t) as i32;
581            d.ctx_positions.push(pos);
582            d.ctx_len += 1;
583        }
584        d.skip_next_decode_append = true;
585        Ok(())
586    }
587
588    fn dflash_capture_band(&self) -> usize {
589        self.dflash_kgamma
590    }
591
592    fn commit_ctx(
593        &self,
594        seq: &mut SequenceState,
595        num_committed: usize,
596        base_pos: usize,
597        scratch_row: usize,
598    ) -> Result<()> {
599        if num_committed == 0 {
600            return Ok(());
601        }
602        let base = match self.dflash_hidden_save {
603            Some(p) => p,
604            None => return Ok(()),
605        };
606        let prop = match seq.proposer_state.as_mut() {
607            Some(p) => p.as_mut(),
608            None => return Ok(()),
609        };
610        // Graceful no-op for non-DFlash proposers (shared bootstrap path).
611        let d = match prop
612            .as_any_mut()
613            .downcast_mut::<crate::layers::DflashProposerState>()
614        {
615            Some(d) => d,
616            None => return Ok(()),
617        };
618        let n_layers = self.dflash_capture_layers.len();
619        if n_layers == 0 {
620            return Ok(());
621        }
622        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
623        let stream = self.gpu.default_stream();
624
625        // Scratch-capacity guard: `try_dflash_capture_all` caps its writes at
626        // `dflash_hidden_save_rows` (γ+1), so a batch row beyond that was
627        // never captured — committing it would append a STALE row (poisoned
628        // ctx is worse than a hole). Skip with a warning; only reachable if
629        // --max-num-seqs exceeds γ+1 on a DFlash serve.
630        if scratch_row + num_committed > self.dflash_hidden_save_rows {
631            tracing::warn!(
632                "commit_ctx: scratch rows {}..{} exceed capture capacity {} — skipping (ctx hole)",
633                scratch_row,
634                scratch_row + num_committed,
635                self.dflash_hidden_save_rows,
636            );
637            return Ok(());
638        }
639
640        // Watermark slide FIRST, on the ctx_len (row-index) axis. If the
641        // incoming rows would exceed capacity, keep the NEWEST rows and drop
642        // the oldest (mirrors dflash_serial_ctx_append). keep is clamped so
643        // drop_n >= keep — the single D2D copy's src/dst can never overlap.
644        // ctx_committed resets to 0 (next propose re-precomputes the slid
645        // rows chunk-wise); ctx_positions values (absolute RoPE positions)
646        // are preserved by the drain, so stamps stay exact across the slide.
647        if d.ctx_len + num_committed > d.max_ctx_len {
648            let keep = (d.max_ctx_len / 2).min(d.max_ctx_len.saturating_sub(num_committed));
649            let drop_n = d.ctx_len.saturating_sub(keep);
650            if drop_n > 0 {
651                let src = d.ctx_hidden_acc.offset(drop_n * ctx_slot_bytes);
652                let dst0 = d.ctx_hidden_acc.offset(0);
653                self.gpu
654                    .copy_d2d_async(src, dst0, keep * ctx_slot_bytes, stream)?;
655                d.ctx_positions.drain(..drop_n);
656                d.ctx_len = keep;
657                d.ctx_committed = 0;
658                tracing::info!(
659                    "DFlash UNIFIED_CTX watermark: slid ctx window (dropped {} oldest, keep {})",
660                    drop_n,
661                    keep,
662                );
663            }
664        }
665
666        // Append num_committed rows at the TAIL (ctx_len axis). dst uses
667        // ctx_len (acc row index); base_pos stamps ctx_positions (RoPE axis).
668        // Conflating the two axes is the DDD §4.1 landmine: they coincide
669        // only until the first slide — and the sliding prompts ARE the reds.
670        debug_assert_eq!(d.ctx_positions.len(), d.ctx_len);
671        for t in 0..num_committed {
672            let row = base.offset((scratch_row + t) * ctx_slot_bytes);
673            let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
674            self.gpu.copy_d2d_async(row, dst, ctx_slot_bytes, stream)?;
675            d.ctx_positions.push((base_pos + t) as i32);
676            d.ctx_len += 1;
677        }
678        // Freshest ctx slot = row (num_committed-1) = the bonus generator
679        // (EAGLE order, matches kgamma_append). Block the next propose()'s
680        // internal decode-append so this capture is never double-appended.
681        d.skip_next_decode_append = true;
682
683        // Per-commit ledger (debug): the C>=2 GAP diagnosis reads this to
684        // find steps whose commits under-append vs the positions committed.
685        tracing::debug!(
686            "CTX_COMMIT slot={} rows={} base_pos={} ctx_len_after={}",
687            seq.slot_idx,
688            num_committed,
689            base_pos,
690            d.ctx_len,
691        );
692        // One-shot activation log so A/B runs can confirm the path is live.
693        if self.stats.once("log:dflash_unified_ctx") {
694            tracing::info!(
695                "DFlash UNIFIED_CTX ACTIVE: first commit_ctx rows={} base_pos={} ctx_len={}",
696                num_committed,
697                base_pos,
698                d.ctx_len,
699            );
700        }
701        Ok(())
702    }
703
704    fn dflash_serial_ctx_append(&self, seq: &mut SequenceState) -> Result<()> {
705        // Ctx-holes fix: append the serial-decoded token's captured hidden.
706        // The decode layer loop (decode_a.rs try_dflash_capture) already
707        // filled `dflash_hidden_save` row 0 with this token's per-layer
708        // hiddens — the same [slot0|..|slot4] layout as one accumulator row.
709        let base = match self.dflash_hidden_save {
710            Some(p) => p,
711            None => return Ok(()),
712        };
713        let prop = match seq.proposer_state.as_mut() {
714            Some(p) => p.as_mut(),
715            None => return Ok(()),
716        };
717        // Graceful no-op for non-DFlash proposers (this bootstrap path is
718        // shared with EAGLE/MTP, unlike the DFlash-only eagle append above).
719        let d = match prop
720            .as_any_mut()
721            .downcast_mut::<crate::layers::DflashProposerState>()
722        {
723            Some(d) => d,
724            None => return Ok(()),
725        };
726        let n_layers = self.dflash_capture_layers.len();
727        if n_layers == 0 {
728            return Ok(());
729        }
730        let ctx_slot_bytes = n_layers * self.config.hidden_size * 2;
731        let stream = self.gpu.default_stream();
732        // Bounded watermark: accumulator full → slide the window. Keep the
733        // NEWEST keep = max/2 rows, drop the oldest (dropping the newest
734        // would starve the drafter of exactly the tokens that drive
735        // acceptance — the 846-token think overrun). drop_n >= keep holds
736        // whenever ctx_len >= max_ctx_len, so src/dst regions of the single
737        // D2D copy can never overlap — no ring arithmetic, no status-1.
738        // ctx_committed resets to 0: the next propose re-precomputes the
739        // slid rows chunk-wise (ctx_window rows/pass) and rewrites their
740        // paged K/V at the new slot indices; ctx_positions values (absolute
741        // positions) are preserved by the drain, so RoPE stamps stay exact.
742        if d.ctx_len >= d.max_ctx_len {
743            let keep = d.max_ctx_len / 2;
744            let drop_n = d.ctx_len - keep;
745            let src = d.ctx_hidden_acc.offset(drop_n * ctx_slot_bytes);
746            let dst0 = d.ctx_hidden_acc.offset(0);
747            self.gpu
748                .copy_d2d_async(src, dst0, keep * ctx_slot_bytes, stream)?;
749            d.ctx_positions.drain(..drop_n);
750            d.ctx_len = keep;
751            d.ctx_committed = 0;
752            tracing::info!(
753                "DFlash SERIAL_APPEND watermark: slid ctx window (dropped {} oldest, keep {})",
754                drop_n,
755                keep,
756            );
757        }
758        let dst = d.ctx_hidden_acc.offset(d.ctx_len * ctx_slot_bytes);
759        self.gpu.copy_d2d_async(base, dst, ctx_slot_bytes, stream)?;
760        // One-shot activation log so A/B runs can confirm the fix is live.
761        if self.stats.once("log:dflash_serial_append") {
762            tracing::info!(
763                "DFlash SERIAL_APPEND ACTIVE: first serial ctx append at ctx_len={} pos={}",
764                d.ctx_len,
765                seq.seq_len.saturating_sub(1),
766            );
767        }
768        // Position convention: decode() advanced seq_len past the token we
769        // just processed, so its true absolute position is seq_len - 1 —
770        // identical to propose.rs's `position.saturating_sub(1)` stamp.
771        debug_assert_eq!(d.ctx_positions.len(), d.ctx_len);
772        d.ctx_positions.push(seq.seq_len.saturating_sub(1) as i32);
773        d.ctx_len += 1;
774        // The latest capture is now in ctx; a propose() firing later (e.g.
775        // adaptive re-probe) must not decode-append it again.
776        d.skip_next_decode_append = true;
777        Ok(())
778    }
779    fn run_mtp_propose(
780        &self,
781        token: u32,
782        position: usize,
783        seq: &mut SequenceState,
784        _stream: u64,
785    ) -> Result<Option<u32>> {
786        self.run_mtp_propose_dispatch(token, position, seq, _stream)
787    }
788    fn run_mtp_propose_multi(
789        &self,
790        token: u32,
791        position: usize,
792        num_drafts: usize,
793        seq: &mut SequenceState,
794        _stream: u64,
795        grammar_bitmask: Option<&[i32]>,
796    ) -> Result<Vec<u32>> {
797        self.run_mtp_propose_multi_dispatch(
798            token,
799            position,
800            num_drafts,
801            seq,
802            _stream,
803            grammar_bitmask,
804        )
805    }
806    fn read_deferred_draft_token(&self) -> Result<u32> {
807        self.read_deferred_draft_token_dispatch()
808    }
809    fn trim_proposer_state(
810        &self,
811        seq: &mut SequenceState,
812        num_accepted: usize,
813        _stream: u64,
814    ) -> Result<()> {
815        self.trim_proposer_state_dispatch(seq, num_accepted, _stream)
816    }
817    fn compact_sequence(&self, seq: &mut SequenceState, new_slot: usize) -> Result<()> {
818        self.compact_sequence_dispatch(seq, new_slot)
819    }
820    fn detach_slot_for_reuse(&self, seq: &mut SequenceState) {
821        self.detach_slot_for_reuse_dispatch(seq)
822    }
823    fn save_sequence_state(
824        &self,
825        seq: &SequenceState,
826        writer: &mut dyn std::io::Write,
827    ) -> Result<()> {
828        self.save_sequence_state_dispatch(seq, writer)
829    }
830    fn restore_sequence_state(
831        &self,
832        seq: &mut SequenceState,
833        num_blocks: usize,
834        reader: &mut dyn std::io::Read,
835    ) -> Result<()> {
836        self.restore_sequence_state_dispatch(seq, num_blocks, reader)
837    }
838    fn num_free_blocks(&self) -> usize {
839        self.num_free_blocks_dispatch()
840    }
841    fn num_total_blocks(&self) -> usize {
842        self.num_total_blocks_dispatch()
843    }
844    fn ssm_snapshot_occupancy(&self) -> Option<(u32, u32)> {
845        let (used, total) = self.ssm_snapshots.occupancy();
846        (total > 0).then_some((used as u32, total as u32))
847    }
848    fn reclaim_prefix_blocks(&self, num_blocks: usize) -> usize {
849        self.reclaim_prefix_blocks_dispatch(num_blocks)
850    }
851    fn start_checkpoint_async(&self, seq: &mut SequenceState) -> Result<()> {
852        self.start_checkpoint_async_dispatch(seq)
853    }
854    fn start_rollback_and_checkpoint_async(
855        &self,
856        seq: &mut SequenceState,
857        num_accepted: usize,
858    ) -> Result<()> {
859        self.start_rollback_and_checkpoint_async_dispatch(seq, num_accepted)
860    }
861    fn sync_secondary(&self) -> Result<()> {
862        self.sync_secondary_dispatch()
863    }
864    fn gdn_fold_accepted(
865        &self,
866        slots: &[usize],
867        accepted_rows: &[u32],
868        k_rows: usize,
869    ) -> Result<bool> {
870        self.gdn_fold_accepted_dispatch(slots, accepted_rows, k_rows)
871    }
872
873    fn commit_accepted_prefix(
874        &self,
875        seq: &mut SequenceState,
876        num_accepted: usize,
877        k: usize,
878    ) -> Result<()> {
879        self.commit_accepted_prefix_dispatch(seq, num_accepted, k)
880    }
881    fn ep_worker_step(&self, slots: &mut [Option<SequenceState>]) -> Result<bool> {
882        self.ep_worker_step_dispatch(slots)
883    }
884    fn is_ep(&self) -> bool {
885        self.is_ep_dispatch()
886    }
887    fn hc_mult(&self) -> usize {
888        self.config.hc_mult
889    }
890
891    fn is_mla(&self) -> bool {
892        self.is_mla_dispatch()
893    }
894
895    fn kv_block_size(&self) -> Option<usize> {
896        Some(self.kv_cache.lock().block_size())
897    }
898    fn decode_logits_fp32(&self) -> bool {
899        self.decode_logits_fp32_dispatch()
900    }
901    fn decode_logits_ptr(&self) -> DevicePtr {
902        self.decode_logits_ptr_dispatch()
903    }
904    fn ep_broadcast_cmd(&self, cmd: u32) -> Result<()> {
905        self.ep_broadcast_cmd_dispatch(cmd)
906    }
907    fn ep_broadcast_cmd_for_seq(&self, seq_id: u32, cmd: u32) -> Result<()> {
908        // Routes to the helper added in 21e2130. Behaviour depends on the
909        // ep_protocol_v2 field set at construction from AVAROK_EP_PROTOCOL.
910        self.ep_broadcast_seq_and_cmd(seq_id, cmd, self.ep_protocol_v2)
911    }
912    fn ep_protocol_v2(&self) -> bool {
913        self.ep_protocol_v2
914    }
915    fn ep_broadcast_tokens(&self, tokens: &[u32]) -> Result<Vec<u32>> {
916        self.ep_broadcast_tokens_dispatch(tokens)
917    }
918    fn default_stream(&self) -> u64 {
919        self.default_stream_dispatch()
920    }
921    fn create_stream(&self) -> Result<u64> {
922        self.create_stream_dispatch()
923    }
924    fn create_event(&self) -> Result<u64> {
925        self.create_event_dispatch()
926    }
927    fn record_event(&self, event: u64, stream: u64) -> Result<()> {
928        self.record_event_dispatch(event, stream)
929    }
930    fn stream_wait_event(&self, stream: u64, event: u64) -> Result<()> {
931        self.stream_wait_event_dispatch(stream, event)
932    }
933    fn synchronize(&self, stream: u64) -> Result<()> {
934        self.synchronize_dispatch(stream)
935    }
936}
937
938impl TransformerModel {
939    /// Collect chunk-boundary aux layer state (PLE, QSA) for a Marconi
940    /// snapshot. Returns the blobs to attach; empty when no layer carries
941    /// aux state.
942    pub(in crate::model) fn collect_aux_states(
943        &self,
944        seq: &SequenceState,
945        stream: u64,
946    ) -> Result<Vec<(u32, Vec<u8>)>> {
947        let mut out = Vec::new();
948        for (i, l) in self.layers.iter().enumerate() {
949            if let Some(blob) =
950                l.snapshot_aux(seq.layer_states[i].as_ref(), self.gpu.as_ref(), stream)?
951            {
952                out.push((i as u32, blob));
953            }
954        }
955        Ok(out)
956    }
957
958    /// Whether restoring a snapshot WITHOUT aux blobs would be unsound for
959    /// this model (some layer carries per-sequence aux state).
960    pub(in crate::model) fn requires_aux_state(&self) -> bool {
961        self.layers.iter().any(|l| l.has_aux_state())
962    }
963
964    /// Apply a snapshot's aux blobs to the owning layers.
965    pub(in crate::model) fn apply_aux_states(
966        &self,
967        seq: &mut SequenceState,
968        blobs: &[(u32, Vec<u8>)],
969        stream: u64,
970    ) -> Result<()> {
971        for (i, blob) in blobs {
972            self.layers[*i as usize].restore_aux(
973                seq.layer_states[*i as usize].as_mut(),
974                blob,
975                self.gpu.as_ref(),
976                stream,
977            )?;
978        }
979        Ok(())
980    }
981}
982
983#[cfg(test)]
984mod gdn_woa_folded_slots_tests {
985    /// The record must mean "the slots folded by the MOST RECENT fold".
986    ///
987    /// Models the two implementations directly, because the real path needs a
988    /// GPU and 36 SSM layers: `record_old` is what shipped
989    /// (`if any { clear(); extend() }`), `record_new` is the fix. The reader is
990    /// `async_chkpt.rs:237` — find your slot, consume it, and if it was there,
991    /// skip restoring `h`.
992    fn record_old(list: &mut Vec<usize>, any: bool, slots: &[usize]) {
993        if any {
994            list.clear();
995            list.extend_from_slice(slots);
996        }
997    }
998    fn record_new(list: &mut Vec<usize>, any: bool, slots: &[usize]) {
999        list.clear();
1000        if any {
1001            list.extend_from_slice(slots);
1002        }
1003    }
1004    /// The reader: true = "folded, do not restore h".
1005    fn consume(list: &mut Vec<usize>, slot: usize) -> bool {
1006        match list.iter().position(|&s| s == slot) {
1007            Some(p) => {
1008                list.swap_remove(p);
1009                true
1010            }
1011            None => false,
1012        }
1013    }
1014
1015    /// A fold that folds NOTHING must not leave a previous batch's slots
1016    /// readable. Slot indices are reused across requests, so a stale entry
1017    /// makes the next occupant of that slot skip its `h` restore.
1018    #[test]
1019    fn a_no_op_fold_does_not_leave_a_stale_claim() {
1020        let mut new = vec![];
1021        record_new(&mut new, true, &[1, 2, 3, 4]); // batch A folded
1022        for s in [1, 2, 3, 4] {
1023            assert!(consume(&mut new, s), "batch A's own slots are folded");
1024        }
1025        record_new(&mut new, false, &[7]); // batch B folded nothing
1026        assert!(
1027            !consume(&mut new, 7),
1028            "slot 7 was NOT folded and must restore h"
1029        );
1030
1031        // CONTROL: the shipped version fails this. Slot 3 is refilled by a new
1032        // request while batch A's entry is still listed, and the no-op fold
1033        // does not clear it.
1034        let mut old = vec![];
1035        record_old(&mut old, true, &[1, 2, 3, 4]);
1036        record_old(&mut old, false, &[7]); // no-op: list survives untouched
1037        assert!(
1038            consume(&mut old, 3),
1039            "the shipped behaviour leaves slot 3 claimed — this assertion \
1040             documents the bug, and its inverse is the fix above"
1041        );
1042    }
1043}