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