Skip to main content

llama_cpp_4/
mtp.rs

1//! Safe wrapper around the C++ MTP draft session.
2//!
3//! [`MtpSession`] pairs a target [`LlamaContext`] with an MTP draft
4//! [`LlamaContext`] (built with
5//! [`crate::context::params::LlamaContextType::Mtp`]) and drives the
6//! multi-token-prediction speculative-decoding loop introduced in upstream
7//! llama.cpp [PR #22673](https://github.com/ggml-org/llama.cpp/pull/22673).
8//!
9//! The draft algorithm lives in upstream's
10//! `common/speculative.cpp` (`common_speculative_impl_draft_mtp`). This module
11//! wraps it through a stable C shim in `llama-cpp-sys-4/mtp_shim/`.
12//!
13//! # Upstream behaviour (llama.cpp #23269+)
14//!
15//! After [MTP clean-up #23269](https://github.com/ggml-org/llama.cpp/pull/23269):
16//!
17//! - Draft sampling uses `top_k = 10` inside upstream (not configurable from Rust).
18//! - [`MtpSessionConfig::p_min`] filters low-confidence draft tokens (default `0.0`).
19//! - Upstream CLI default for `n_max` is `3`; set [`MtpSessionConfig::n_draft_max`]
20//!   explicitly — optimal values are model/quant dependent ([`MTP.md`] on GitHub).
21//!
22//! [`MTP.md`]: https://github.com/eugenehp/llama-cpp-rs/blob/main/MTP.md
23//!
24//! # Quick start
25//!
26//! ```ignore
27//! use llama_cpp_4::context::params::{LlamaContextParams, LlamaContextType};
28//! use llama_cpp_4::mtp::{MtpSession, MtpSessionConfig};
29//!
30//! let n_draft_max = 3;
31//!
32//! let mut target = model.new_context(&backend, LlamaContextParams::default())?;
33//! let mut draft = model.new_context(
34//!     &backend,
35//!     LlamaContextParams::default()
36//!         .with_ctx_type(LlamaContextType::Mtp)
37//!         .with_n_rs_seq(n_draft_max.max(4)),
38//! )?;
39//!
40//! let config = MtpSessionConfig::new(1, n_draft_max).with_p_min(0.0);
41//! let mut session = MtpSession::new_with_config(&mut target, &mut draft, config)?;
42//! ```
43//!
44//! # Speculative loop
45//!
46//! For each generation step, after decoding on the **target** context:
47//!
48//! ```ignore
49//! // 1. Target prefill or verify decode (you build the batch)
50//! session.decode_target_and_process(&mut batch)?;
51//!
52//! // 3. Ask for draft tokens starting from the last accepted token
53//! let drafts = session.draft(0, n_past, last_token)?;
54//!
55//! // 4. Verify drafts on the target (compare logits / sample — your code)
56//! let n_accepted: u16 = /* ... */;
57//!
58//! // 5. Sync draft recurrent state with what the target accepted
59//! session.accept(0, n_accepted)?;
60//! ```
61//!
62//! Call [`MtpSession::begin`] once per fresh generation if you want upstream
63//! prompt tracking (optional for MTP). Call [`MtpSession::print_stats`] when
64//! finished to log draft/accept counters via llama.cpp's log callback.
65//!
66//! A full runnable implementation is in `examples/mtp/`.
67//!
68//! # Embedding requirements
69//!
70//! | Method | MTP typical value | Meaning |
71//! |---|---|---|
72//! | [`MtpSession::need_embd_pre_norm`] | `true` | Next-n hidden states (upstream name) |
73//! | [`MtpSession::need_embd`] | `false` | Post-norm / seq embeddings not used |
74//!
75//! # Multi-head `NextN` (Step3.5+)
76//!
77//! When [`crate::model::LlamaModel::n_layer_nextn`] returns a value greater than `1`, set the
78//! draft context head before each [`MtpSession::draft`] call:
79//!
80//! ```ignore
81//! for head in 0..model.n_layer_nextn() {
82//!     draft.set_nextn_layer_offset(head);
83//!     let drafts = session.draft(0, n_past, last_token)?;
84//!     // verify on target ...
85//! }
86//! draft.set_nextn_layer_offset(0); // restore default
87//! ```
88//!
89
90use std::marker::PhantomData;
91use std::ptr::NonNull;
92use std::rc::Rc;
93
94use crate::context::params::LlamaContextType;
95use crate::context::LlamaContext;
96use crate::llama_batch::LlamaBatch;
97use crate::speculative::MAX_SPECULATIVE_PROMPT_TOKENS;
98use crate::speculative::{
99    capture_state, restore_state, validate_config, validate_context_capacities,
100    SpeculativeContextCapacity, SpeculativeStateError,
101};
102use crate::token::LlamaToken;
103
104/// Errors raised by the MTP draft session.
105#[derive(Debug, thiserror::Error)]
106pub enum MtpSessionError {
107    /// Returned when `mtp_session_new` fails (typically: model lacks MTP heads,
108    /// or one of the contexts is incompatible).
109    #[error("failed to create MTP draft session — check that ctx_dft was built with LlamaContextType::Mtp and the model has MTP heads")]
110    Init,
111
112    /// `mtp_session_process` returned false.
113    #[error("mtp_session_process failed (see llama.cpp logs)")]
114    Process,
115
116    /// Native prompt initialization failed or raised a contained exception.
117    #[error("mtp_session_begin failed")]
118    Begin,
119
120    /// Native draft generation failed or raised a contained exception.
121    #[error("mtp_session_draft failed")]
122    Draft,
123
124    /// Native proposal acceptance failed or raised a contained exception.
125    #[error("mtp_session_accept failed")]
126    Accept,
127
128    /// Prompt storage exceeds the safe speculative-session bound.
129    #[error("prompt has {size} tokens, exceeding the {maximum}-token bound")]
130    PromptTooLong {
131        /// Caller-supplied prompt-token count.
132        size: usize,
133        /// Inclusive safe prompt-token bound.
134        maximum: usize,
135    },
136
137    /// The supplied contexts do not satisfy the native MTP contract.
138    #[error("incompatible MTP contexts: {0}")]
139    IncompatibleContexts(&'static str),
140
141    /// Caller passed a sequence id outside `[0, n_seq)`.
142    #[error("sequence id {seq_id} out of range (n_seq = {n_seq})")]
143    BadSeqId {
144        /// the offending seq id
145        seq_id: i32,
146        /// configured number of sequences
147        n_seq: u32,
148    },
149
150    /// Invalid session configuration (e.g. `n_draft_max <= 0`).
151    #[error("invalid MTP session config: {0}")]
152    InvalidConfig(&'static str),
153
154    /// The target context failed to decode.
155    #[error("target decode failed: {0}")]
156    Decode(#[from] crate::DecodeError),
157
158    /// An operation requires all draft proposals to be completed first.
159    #[error("sequence {seq_id} still has an unaccepted draft proposal")]
160    ProposalPending {
161        /// Sequence with a pending proposal.
162        seq_id: i32,
163    },
164
165    /// `accept` was called without a preceding nonempty draft.
166    #[error("sequence {seq_id} has no draft proposal to accept")]
167    NoPendingProposal {
168        /// Sequence without a pending proposal.
169        seq_id: i32,
170    },
171
172    /// The accepted prefix exceeds the proposal length.
173    #[error("accepted {accepted} tokens from a {proposed}-token proposal")]
174    AcceptedTooMany {
175        /// Accepted prefix length.
176        accepted: u16,
177        /// Exact proposal length.
178        proposed: usize,
179    },
180
181    /// Exact speculative-state capture or restore failed.
182    #[error(transparent)]
183    State(#[from] SpeculativeStateError),
184}
185
186/// Parameters for [`MtpSession::new_with_config`].
187///
188/// Maps directly to upstream `common_params_speculative_draft`.
189///
190/// # Examples
191///
192/// ```ignore
193/// // Defaults: n_min = 0, p_min = 0.0 (aligned with upstream #23269+)
194/// let cfg = MtpSessionConfig::new(1, 3);
195///
196/// // Stricter drafts: skip tokens below 10% draft-model probability
197/// let cfg = MtpSessionConfig::new(1, 1).with_p_min(0.10);
198/// ```
199#[derive(Debug, Clone, Copy, PartialEq)]
200pub struct MtpSessionConfig {
201    /// Number of concurrent sequences (usually `1`).
202    pub n_seq: u32,
203    /// Maximum tokens drafted per [`MtpSession::draft`] call (`n_max` upstream).
204    pub n_draft_max: i32,
205    /// Minimum draft tokens to propose (`n_min` upstream, default `0`).
206    pub n_min: i32,
207    /// Greedy probability floor; drafts below this are dropped (`p_min` upstream, default `0.0`).
208    pub p_min: f32,
209}
210
211impl MtpSessionConfig {
212    /// Build config with upstream-aligned defaults for `n_min` (`0`) and `p_min` (`0.0`).
213    ///
214    /// # Examples
215    ///
216    /// ```ignore
217    /// let cfg = MtpSessionConfig::new(1, 3); // one sequence, up to 3 draft tokens
218    /// ```
219    #[must_use]
220    pub fn new(n_seq: u32, n_draft_max: i32) -> Self {
221        Self {
222            n_seq,
223            n_draft_max,
224            n_min: 0,
225            p_min: 0.0,
226        }
227    }
228
229    /// Set minimum draft tokens (`n_min` upstream).
230    #[must_use]
231    pub fn with_n_min(mut self, n_min: i32) -> Self {
232        self.n_min = n_min;
233        self
234    }
235
236    /// Set draft probability floor (`p_min` upstream).
237    ///
238    /// Draft tokens whose greedy probability falls below this value are dropped.
239    /// Upstream default is `0.0` after #23269 (was `0.75` in older builds).
240    ///
241    /// # Examples
242    ///
243    /// ```ignore
244    /// let cfg = MtpSessionConfig::new(1, 1).with_p_min(0.10);
245    /// ```
246    #[must_use]
247    pub fn with_p_min(mut self, p_min: f32) -> Self {
248        self.p_min = p_min;
249        self
250    }
251}
252
253/// Owned MTP draft session.
254///
255/// Drops the underlying `mtp_session *` (and the C++ `common_speculative *`
256/// it holds) when freed.
257///
258/// The session exclusively borrows both contexts for its Rust lifetime, so
259/// neither can be moved, accessed mutably, or dropped while native code retains
260/// their pointers. It is deliberately neither `Send` nor `Sync`.
261pub struct MtpSession<'ctx, 'model> {
262    raw: NonNull<llama_cpp_sys_4::mtp_session>,
263    config: MtpSessionConfig,
264    target: &'ctx mut LlamaContext<'model>,
265    draft: &'ctx mut LlamaContext<'model>,
266    pending_proposals: Vec<Option<usize>>,
267    not_send_sync: PhantomData<Rc<()>>,
268}
269
270impl<'ctx, 'model> MtpSession<'ctx, 'model> {
271    /// Construct an MTP draft session with upstream defaults for `n_min` and
272    /// `p_min`.
273    ///
274    /// Equivalent to `new_with_config(MtpSessionConfig::new(n_seq, n_draft_max))`.
275    ///
276    /// # Examples
277    ///
278    /// ```ignore
279    /// let mut session = MtpSession::new(&mut target, &mut draft, 1, 3)?;
280    /// ```
281    ///
282    /// # Errors
283    ///
284    /// Returns [`MtpSessionError::Init`] or [`MtpSessionError::InvalidConfig`].
285    pub fn new(
286        target: &'ctx mut LlamaContext<'model>,
287        draft: &'ctx mut LlamaContext<'model>,
288        n_seq: u32,
289        n_draft_max: i32,
290    ) -> Result<Self, MtpSessionError> {
291        Self::new_with_config(target, draft, MtpSessionConfig::new(n_seq, n_draft_max))
292    }
293
294    /// Construct an MTP draft session with full speculative draft parameters.
295    ///
296    /// `target` must be a [`LlamaContextType::Default`](crate::context::params::LlamaContextType::Default) context.
297    /// `draft` must be a [`LlamaContextType::Mtp`](crate::context::params::LlamaContextType::Mtp) context from the same model,
298    /// with [`LlamaContextParams::with_n_rs_seq`](crate::context::params::LlamaContextParams::with_n_rs_seq)
299    /// `>= config.n_draft_max`.
300    ///
301    /// # Examples
302    ///
303    /// ```ignore
304    /// let config = MtpSessionConfig::new(1, 1)
305    ///     .with_p_min(0.0); // match upstream default after #23269
306    /// let session = MtpSession::new_with_config(&mut target, &mut draft, config)?;
307    /// ```
308    ///
309    /// # Errors
310    ///
311    /// Returns [`MtpSessionError::Init`] or [`MtpSessionError::InvalidConfig`].
312    pub fn new_with_config(
313        target: &'ctx mut LlamaContext<'model>,
314        draft: &'ctx mut LlamaContext<'model>,
315        config: MtpSessionConfig,
316    ) -> Result<Self, MtpSessionError> {
317        validate_config(config.n_seq, config.n_draft_max, config.n_min, config.p_min)
318            .map_err(MtpSessionError::InvalidConfig)?;
319        validate_contexts(target, draft, config)?;
320        let sequence_slots = usize::try_from(config.n_seq)
321            .map_err(|_| MtpSessionError::InvalidConfig("n_seq exceeds usize"))?;
322
323        // `MTP_SPEC_TYPE_*` is `c_uint` under clang/gcc and `c_int` under MSVC;
324        // `as i32` compiles on both. The allow covers the clang/gcc case.
325        #[allow(clippy::cast_possible_wrap)]
326        let c_config = llama_cpp_sys_4::mtp_session_config {
327            n_seq: config.n_seq,
328            n_draft_max: config.n_draft_max,
329            n_min: config.n_min,
330            p_min: config.p_min,
331            spec_type: llama_cpp_sys_4::MTP_SPEC_TYPE_MTP as i32,
332        };
333
334        let raw = unsafe {
335            llama_cpp_sys_4::mtp_session_new(
336                target.context.as_ptr(),
337                draft.context.as_ptr(),
338                &raw const c_config,
339            )
340        };
341        let raw = NonNull::new(raw).ok_or(MtpSessionError::Init)?;
342        Ok(Self {
343            raw,
344            config,
345            target,
346            draft,
347            pending_proposals: vec![None; sequence_slots],
348            not_send_sync: PhantomData,
349        })
350    }
351
352    /// Session configuration passed at construction.
353    #[must_use]
354    pub fn config(&self) -> MtpSessionConfig {
355        self.config
356    }
357
358    /// True when the speculative backend needs post-norm embeddings on the
359    /// target context (`llama_set_embeddings`).
360    ///
361    /// MTP returns **false**; use [`Self::need_embd_pre_norm`] for MTP.
362    #[must_use]
363    pub fn need_embd(&self) -> bool {
364        unsafe { llama_cpp_sys_4::mtp_session_need_embd(self.raw.as_ptr()) }
365    }
366
367    /// True when the speculative backend needs pre-norm hidden states on the
368    /// target context (`llama_set_embeddings_pre_norm`).
369    ///
370    /// MTP returns **true**. Upstream configures this on both contexts during
371    /// session init; callers normally do not need to set it manually.
372    #[must_use]
373    pub fn need_embd_pre_norm(&self) -> bool {
374        unsafe { llama_cpp_sys_4::mtp_session_need_embd_pre_norm(self.raw.as_ptr()) }
375    }
376
377    /// Configured maximum number of tokens drafted per [`draft`](Self::draft)
378    /// call.
379    #[must_use]
380    pub fn n_draft_max(&self) -> i32 {
381        self.config.n_draft_max
382    }
383
384    /// Configured minimum draft tokens (`n_min`).
385    #[must_use]
386    pub fn n_min(&self) -> i32 {
387        self.config.n_min
388    }
389
390    /// Configured draft probability floor (`p_min`).
391    #[must_use]
392    pub fn p_min(&self) -> f32 {
393        self.config.p_min
394    }
395
396    /// Configured number of sequences.
397    #[must_use]
398    pub fn n_seq(&self) -> u32 {
399        self.config.n_seq
400    }
401
402    /// Returns shared access to the target context for reading logits,
403    /// embeddings, and model metadata.
404    #[must_use]
405    pub fn target_context(&self) -> &LlamaContext<'model> {
406        self.target
407    }
408
409    /// Returns exclusive access to the target context while this wrapper
410    /// retains native pointer ownership.
411    #[must_use]
412    pub fn target_context_mut(&mut self) -> &mut LlamaContext<'model> {
413        self.target
414    }
415
416    /// Returns shared access to the draft context for metadata inspection.
417    #[must_use]
418    pub fn draft_context(&self) -> &LlamaContext<'model> {
419        self.draft
420    }
421
422    /// Returns exclusive access to the draft context while this wrapper
423    /// retains native pointer ownership.
424    #[must_use]
425    pub fn draft_context_mut(&mut self) -> &mut LlamaContext<'model> {
426        self.draft
427    }
428
429    /// Decodes on the target and immediately harvests the same batch into MTP.
430    ///
431    /// This is the causal target boundary required by the native draft
432    /// implementation. A failed decode is never passed to `process`.
433    ///
434    /// # Errors
435    ///
436    /// Returns a target [`crate::DecodeError`] or native process failure.
437    pub fn decode_target_and_process(
438        &mut self,
439        batch: &mut LlamaBatch,
440    ) -> Result<(), MtpSessionError> {
441        self.decode_target(batch)?;
442        self.process(batch)
443    }
444
445    /// Decodes one batch on the exclusively held target context.
446    ///
447    /// Use [`Self::decode_target_and_process`] unless mechanics must run
448    /// between target decode and draft-state harvesting. This method remains
449    /// available while a draft proposal is pending because that is the target
450    /// verification phase; proposal creation, begin, and state access retain
451    /// their stricter lifecycle checks.
452    ///
453    /// # Errors
454    ///
455    /// Returns a target [`crate::DecodeError`].
456    pub fn decode_target(&mut self, batch: &mut LlamaBatch) -> Result<(), MtpSessionError> {
457        self.target.decode(batch)?;
458        Ok(())
459    }
460
461    /// Log speculative-decoding statistics (draft/accept counts and timings) via
462    /// llama.cpp `LOG_INF`. Install a log callback with [`crate::log_set`] to
463    /// capture output.
464    ///
465    /// # Examples
466    ///
467    /// ```ignore
468    /// // After your generation loop:
469    /// session.print_stats();
470    /// ```
471    pub fn print_stats(&self) {
472        unsafe { llama_cpp_sys_4::mtp_session_print_stats(self.raw.as_ptr()) }
473    }
474
475    /// Optional: call once at the start of a fresh generation with the
476    /// prompt tokens that were just decoded into the target context.
477    ///
478    /// Upstream uses this for prompt tracking; MTP speculative loops often
479    /// work without it if you call [`Self::process`] after every target decode.
480    ///
481    /// # Examples
482    ///
483    /// ```ignore
484    /// session.begin(0, &prompt_tokens)?;
485    /// ```
486    ///
487    /// # Errors
488    ///
489    /// Returns [`MtpSessionError::BadSeqId`] if `seq_id` is out of range.
490    pub fn begin(&mut self, seq_id: i32, prompt: &[LlamaToken]) -> Result<(), MtpSessionError> {
491        self.check_seq(seq_id)?;
492        self.require_quiescent()?;
493        if prompt.len() > MAX_SPECULATIVE_PROMPT_TOKENS {
494            return Err(MtpSessionError::PromptTooLong {
495                size: prompt.len(),
496                maximum: MAX_SPECULATIVE_PROMPT_TOKENS,
497            });
498        }
499        let ok = unsafe {
500            llama_cpp_sys_4::mtp_session_begin(
501                self.raw.as_ptr(),
502                seq_id,
503                prompt.as_ptr().cast(),
504                prompt.len(),
505            )
506        };
507        if !ok {
508            return Err(MtpSessionError::Begin);
509        }
510        Ok(())
511    }
512
513    /// Hand the session a batch that was just decoded on the target context.
514    ///
515    /// Call this after every successful `target.decode(batch)` so upstream can
516    /// sync draft recurrent state with the target KV cache.
517    ///
518    /// # Examples
519    ///
520    /// ```ignore
521    /// target.decode(&mut batch)?;
522    /// session.process(&batch)?;
523    /// ```
524    ///
525    /// # Errors
526    ///
527    /// Returns [`MtpSessionError::Process`] when upstream rejects the batch.
528    pub fn process(&mut self, batch: &LlamaBatch) -> Result<(), MtpSessionError> {
529        let ok = unsafe {
530            llama_cpp_sys_4::mtp_session_process(self.raw.as_ptr(), &raw const batch.llama_batch)
531        };
532        if ok {
533            Ok(())
534        } else {
535            Err(MtpSessionError::Process)
536        }
537    }
538
539    /// Generate up to [`n_draft_max`](Self::n_draft_max) speculative tokens.
540    ///
541    /// `n_past` is the number of tokens already in the target KV cache for
542    /// `seq_id`. `id_last` is the last token accepted on the target (usually
543    /// the token you just sampled).
544    ///
545    /// # Examples
546    ///
547    /// ```ignore
548    /// let drafts = session.draft(0, n_past, last_token)?;
549    /// for draft in &drafts {
550    ///     // verify each draft against target logits ...
551    /// }
552    /// ```
553    ///
554    /// # Errors
555    ///
556    /// Returns [`MtpSessionError::BadSeqId`] if `seq_id` is out of range.
557    pub fn draft(
558        &mut self,
559        seq_id: i32,
560        n_past: i32,
561        id_last: LlamaToken,
562    ) -> Result<Vec<LlamaToken>, MtpSessionError> {
563        self.check_seq(seq_id)?;
564        let sequence_index = self.sequence_index(seq_id)?;
565        if self.pending_proposals[sequence_index].is_some() {
566            return Err(MtpSessionError::ProposalPending { seq_id });
567        }
568
569        let cap = usize::try_from(self.config.n_draft_max.max(0)).unwrap_or(0);
570        let mut buf: Vec<i32> = vec![0; cap];
571        let mut out_n = i32::try_from(cap).unwrap_or(i32::MAX);
572
573        let ok = unsafe {
574            llama_cpp_sys_4::mtp_session_draft(
575                self.raw.as_ptr(),
576                seq_id,
577                n_past,
578                id_last.0,
579                buf.as_mut_ptr(),
580                &raw mut out_n,
581            )
582        };
583        if !ok {
584            return Err(MtpSessionError::Draft);
585        }
586
587        let n = usize::try_from(out_n.max(0)).unwrap_or(0);
588        buf.truncate(n);
589        if n > 0 {
590            self.pending_proposals[sequence_index] = Some(n);
591        }
592        Ok(buf.into_iter().map(LlamaToken).collect())
593    }
594
595    /// Inform the session how many draft tokens the target verifier accepted.
596    ///
597    /// Pass `0` when every draft was rejected. Upstream rolls back draft
598    /// recurrent state accordingly.
599    ///
600    /// # Examples
601    ///
602    /// ```ignore
603    /// session.accept(0, n_accepted)?;
604    /// ```
605    ///
606    /// # Errors
607    ///
608    /// Returns [`MtpSessionError::BadSeqId`] if `seq_id` is out of range.
609    pub fn accept(&mut self, seq_id: i32, n_accepted: u16) -> Result<(), MtpSessionError> {
610        self.check_seq(seq_id)?;
611        let sequence_index = self.sequence_index(seq_id)?;
612        let proposed = self.pending_proposals[sequence_index]
613            .ok_or(MtpSessionError::NoPendingProposal { seq_id })?;
614        if usize::from(n_accepted) > proposed {
615            return Err(MtpSessionError::AcceptedTooMany {
616                accepted: n_accepted,
617                proposed,
618            });
619        }
620        let ok =
621            unsafe { llama_cpp_sys_4::mtp_session_accept(self.raw.as_ptr(), seq_id, n_accepted) };
622        if !ok {
623            return Err(MtpSessionError::Accept);
624        }
625        self.pending_proposals[sequence_index] = None;
626        Ok(())
627    }
628
629    /// Returns `true` when every draft proposal has been completed.
630    #[must_use]
631    pub fn is_quiescent(&self) -> bool {
632        self.pending_proposals.iter().all(Option::is_none)
633            && unsafe { llama_cpp_sys_4::mtp_session_is_quiescent(self.raw.as_ptr()) }
634    }
635
636    /// Captures versioned per-sequence speculative continuation state.
637    ///
638    /// Target and draft context bytes are separate and must be checkpointed at
639    /// the same quiescent boundary.
640    ///
641    /// # Errors
642    ///
643    /// Returns an error for an invalid sequence, pending proposal, incomplete
644    /// native support, or excessive state.
645    pub fn speculative_state(&self, seq_id: i32) -> Result<Vec<u8>, MtpSessionError> {
646        self.check_seq(seq_id)?;
647        self.require_quiescent()?;
648        Ok(capture_state(self.raw, seq_id)?)
649    }
650
651    /// Restores versioned per-sequence speculative continuation state.
652    ///
653    /// Restore the corresponding target and draft context bytes before calling
654    /// this method.
655    ///
656    /// # Errors
657    ///
658    /// Returns an error for an invalid sequence, pending proposal, excessive
659    /// input, or any version/configuration/state mismatch.
660    pub fn restore_speculative_state(
661        &mut self,
662        seq_id: i32,
663        state: &[u8],
664    ) -> Result<(), MtpSessionError> {
665        self.check_seq(seq_id)?;
666        self.require_quiescent()?;
667        restore_state(self.raw, seq_id, state)?;
668        Ok(())
669    }
670
671    /// Removes a target-context KV range.
672    ///
673    /// # Errors
674    ///
675    /// Returns a conversion error when an identifier or position exceeds
676    /// native `i32` bounds.
677    pub fn clear_target_kv_cache_seq(
678        &mut self,
679        seq_id: Option<u32>,
680        p0: Option<u32>,
681        p1: Option<u32>,
682    ) -> Result<bool, crate::context::kv_cache::KvCacheConversionError> {
683        self.target.clear_kv_cache_seq(seq_id, p0, p1)
684    }
685
686    /// Removes a draft-context KV range.
687    ///
688    /// # Errors
689    ///
690    /// Returns a conversion error when an identifier or position exceeds
691    /// native `i32` bounds.
692    pub fn clear_draft_kv_cache_seq(
693        &mut self,
694        seq_id: Option<u32>,
695        p0: Option<u32>,
696        p1: Option<u32>,
697    ) -> Result<bool, crate::context::kv_cache::KvCacheConversionError> {
698        self.draft.clear_kv_cache_seq(seq_id, p0, p1)
699    }
700
701    /// Returns the target context's exact sequence-state byte count.
702    pub fn target_state_seq_get_size_ext(&mut self, seq_id: i32, flags: u32) -> usize {
703        self.target.state_seq_get_size_ext(seq_id, flags)
704    }
705
706    /// Copies target context sequence state with exact native flags.
707    pub fn target_state_seq_get_data_ext(
708        &mut self,
709        dst: &mut [u8],
710        seq_id: i32,
711        flags: u32,
712    ) -> usize {
713        self.target.state_seq_get_data_ext(dst, seq_id, flags)
714    }
715
716    /// Restores target context sequence state with exact native flags.
717    pub fn target_state_seq_set_data_ext(&mut self, src: &[u8], seq_id: i32, flags: u32) -> usize {
718        self.target.state_seq_set_data_ext(src, seq_id, flags)
719    }
720
721    /// Returns the draft context's exact sequence-state byte count.
722    pub fn draft_state_seq_get_size_ext(&mut self, seq_id: i32, flags: u32) -> usize {
723        self.draft.state_seq_get_size_ext(seq_id, flags)
724    }
725
726    /// Copies draft context sequence state with exact native flags.
727    pub fn draft_state_seq_get_data_ext(
728        &mut self,
729        dst: &mut [u8],
730        seq_id: i32,
731        flags: u32,
732    ) -> usize {
733        self.draft.state_seq_get_data_ext(dst, seq_id, flags)
734    }
735
736    /// Restores draft context sequence state with exact native flags.
737    pub fn draft_state_seq_set_data_ext(&mut self, src: &[u8], seq_id: i32, flags: u32) -> usize {
738        self.draft.state_seq_set_data_ext(src, seq_id, flags)
739    }
740
741    fn require_quiescent(&self) -> Result<(), MtpSessionError> {
742        if let Some((index, _)) = self
743            .pending_proposals
744            .iter()
745            .enumerate()
746            .find(|(_, proposal)| proposal.is_some())
747        {
748            return Err(MtpSessionError::ProposalPending {
749                seq_id: i32::try_from(index).unwrap_or(i32::MAX),
750            });
751        }
752        if !unsafe { llama_cpp_sys_4::mtp_session_is_quiescent(self.raw.as_ptr()) } {
753            return Err(MtpSessionError::State(SpeculativeStateError::NotQuiescent));
754        }
755        Ok(())
756    }
757
758    fn check_seq(&self, seq_id: i32) -> Result<(), MtpSessionError> {
759        if seq_id < 0 || seq_id.cast_unsigned() >= self.config.n_seq {
760            return Err(MtpSessionError::BadSeqId {
761                seq_id,
762                n_seq: self.config.n_seq,
763            });
764        }
765        Ok(())
766    }
767
768    fn sequence_index(&self, seq_id: i32) -> Result<usize, MtpSessionError> {
769        self.check_seq(seq_id)?;
770        usize::try_from(seq_id)
771            .map_err(|_| MtpSessionError::InvalidConfig("sequence id exceeds usize"))
772    }
773}
774
775fn validate_contexts(
776    target: &LlamaContext<'_>,
777    draft: &LlamaContext<'_>,
778    config: MtpSessionConfig,
779) -> Result<(), MtpSessionError> {
780    if target.context_type() != LlamaContextType::Default
781        || draft.context_type() != LlamaContextType::Mtp
782    {
783        return Err(MtpSessionError::IncompatibleContexts(
784            "target must be Default and draft must be Mtp",
785        ));
786    }
787    if target.n_seq_max() < config.n_seq || draft.n_seq_max() != config.n_seq {
788        return Err(MtpSessionError::IncompatibleContexts(
789            "target sequence capacity is too small or draft capacity differs from n_seq",
790        ));
791    }
792    let required_draft = u32::try_from(config.n_draft_max)
793        .map_err(|_| MtpSessionError::InvalidConfig("n_draft_max exceeds u32"))?;
794    validate_context_capacities(
795        SpeculativeContextCapacity {
796            batch: target.n_batch(),
797            micro_batch: target.n_ubatch(),
798            recurrent_slots: target.n_rs_seq(),
799            recurrent_or_hybrid: target.model.is_recurrent() || target.model.is_hybrid(),
800        },
801        SpeculativeContextCapacity {
802            batch: draft.n_batch(),
803            micro_batch: draft.n_ubatch(),
804            recurrent_slots: draft.n_rs_seq(),
805            recurrent_or_hybrid: draft.model.is_recurrent() || draft.model.is_hybrid(),
806        },
807        required_draft,
808    )
809    .map_err(MtpSessionError::IncompatibleContexts)?;
810    if draft.model.n_embd_out() != target.model.n_embd() {
811        return Err(MtpSessionError::IncompatibleContexts(
812            "draft output width differs from target hidden width",
813        ));
814    }
815    Ok(())
816}
817
818impl Drop for MtpSession<'_, '_> {
819    fn drop(&mut self) {
820        unsafe { llama_cpp_sys_4::mtp_session_free(self.raw.as_ptr()) }
821    }
822}
823
824impl std::fmt::Debug for MtpSession<'_, '_> {
825    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
826        f.debug_struct("MtpSession")
827            .field("config", &self.config)
828            .field("need_embd_pre_norm", &self.need_embd_pre_norm())
829            .finish_non_exhaustive()
830    }
831}