Skip to main content

llama_cpp_2/
speculative.rs

1//! Experimental wrappers for llama.cpp speculative decoding helpers.
2
3use std::ptr::NonNull;
4
5use crate::context::LlamaContext;
6use crate::llama_batch::LlamaBatch;
7use crate::status_is_ok;
8use crate::token::LlamaToken;
9
10/// Parameters for same-model MTP speculative decoding.
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct MtpSpeculativeParams {
13    /// Maximum number of draft tokens to propose.
14    pub n_max: i32,
15    /// Minimum number of draft tokens required before returning a draft.
16    pub n_min: i32,
17    /// Minimum draft probability accepted by llama.cpp's MTP drafter.
18    pub p_min: f32,
19}
20
21impl Default for MtpSpeculativeParams {
22    fn default() -> Self {
23        Self {
24            n_max: 3,
25            n_min: 0,
26            p_min: 0.0,
27        }
28    }
29}
30
31/// Errors returned by the MTP speculative wrapper.
32#[derive(Debug, Eq, PartialEq, thiserror::Error)]
33pub enum MtpSpeculativeError {
34    /// Invalid parameters were provided.
35    #[error("invalid MTP speculative parameters")]
36    InvalidParams,
37    /// llama.cpp returned a null speculative handle.
38    #[error("llama.cpp failed to initialize MTP speculative decoding")]
39    InitFailed,
40    /// llama.cpp rejected a wrapper call.
41    #[error("llama.cpp MTP speculative call failed with status {0}")]
42    Status(i32),
43    /// The draft output exceeded the caller-provided bound.
44    #[error("llama.cpp MTP draft exceeded configured maximum")]
45    DraftOverflow,
46}
47
48/// RAII owner for a same-model MTP speculative context.
49///
50/// This wrapper currently binds llama.cpp's speculative state to sequence 0.
51/// Batches passed to [`Self::process`] must therefore contain only sequence 0.
52#[derive(Debug)]
53pub struct MtpSpeculative<'model> {
54    raw: NonNull<llama_cpp_sys_2::llama_rs_mtp_speculative>,
55    target_context: LlamaContext<'model>,
56    draft_context: LlamaContext<'model>,
57    n_max: usize,
58}
59
60impl<'model> MtpSpeculative<'model> {
61    /// Create a new MTP speculative helper from a target context and an MTP
62    /// draft context.
63    ///
64    /// # Errors
65    ///
66    /// Returns an error if parameters are invalid or llama.cpp cannot
67    /// initialize the speculative implementation for the loaded model.
68    pub fn new(
69        target_context: LlamaContext<'model>,
70        draft_context: LlamaContext<'model>,
71        params: MtpSpeculativeParams,
72    ) -> Result<Self, MtpSpeculativeError> {
73        if params.n_max <= 0 || params.n_min < 0 || params.n_min > params.n_max {
74            return Err(MtpSpeculativeError::InvalidParams);
75        }
76        let n_max =
77            usize::try_from(params.n_max).map_err(|_| MtpSpeculativeError::InvalidParams)?;
78
79        let raw = unsafe {
80            llama_cpp_sys_2::llama_rs_mtp_speculative_init(
81                target_context.context.as_ptr(),
82                draft_context.context.as_ptr(),
83                params.n_max,
84                params.n_min,
85                params.p_min,
86            )
87        };
88        let raw = NonNull::new(raw).ok_or(MtpSpeculativeError::InitFailed)?;
89
90        Ok(Self {
91            raw,
92            target_context,
93            draft_context,
94            n_max,
95        })
96    }
97
98    /// Access the target context.
99    #[must_use]
100    pub fn target_context(&self) -> &LlamaContext<'model> {
101        &self.target_context
102    }
103
104    /// Access the target context for decode and cache rollback operations.
105    pub fn target_context_mut(&mut self) -> &mut LlamaContext<'model> {
106        &mut self.target_context
107    }
108
109    /// Access the draft context for cache rollback operations.
110    pub fn draft_context_mut(&mut self) -> &mut LlamaContext<'model> {
111        &mut self.draft_context
112    }
113
114    /// Begin a new generation from the given prompt tokens.
115    ///
116    /// # Errors
117    ///
118    /// Returns an error if llama.cpp rejects the call.
119    pub fn begin(&mut self, prompt_tokens: &[LlamaToken]) -> Result<(), MtpSpeculativeError> {
120        let prompt = tokens_to_raw(prompt_tokens);
121        let status = unsafe {
122            llama_cpp_sys_2::llama_rs_mtp_speculative_begin(
123                self.raw.as_ptr(),
124                prompt.as_ptr(),
125                prompt.len(),
126            )
127        };
128        status_to_result(status)
129    }
130
131    /// Process a batch that was just decoded by the target context.
132    ///
133    /// The batch must contain token input for sequence 0 only.
134    ///
135    /// # Errors
136    ///
137    /// Returns an error if llama.cpp cannot update the MTP draft context.
138    pub fn process(&mut self, batch: &LlamaBatch<'_>) -> Result<(), MtpSpeculativeError> {
139        let status = unsafe {
140            llama_cpp_sys_2::llama_rs_mtp_speculative_process(
141                self.raw.as_ptr(),
142                std::ptr::from_ref(&batch.llama_batch),
143            )
144        };
145        status_to_result(status)
146    }
147
148    /// Generate draft tokens after `id_last`.
149    ///
150    /// # Errors
151    ///
152    /// Returns an error if llama.cpp rejects the draft operation or emits more
153    /// draft tokens than requested.
154    pub fn draft(
155        &mut self,
156        n_past: i32,
157        id_last: LlamaToken,
158        prompt_tokens: &[LlamaToken],
159    ) -> Result<Vec<LlamaToken>, MtpSpeculativeError> {
160        if n_past < 0 {
161            return Err(MtpSpeculativeError::InvalidParams);
162        }
163
164        let prompt = tokens_to_raw(prompt_tokens);
165        let mut raw_out = vec![0; self.n_max];
166        let mut out_len = 0_usize;
167        let status = unsafe {
168            llama_cpp_sys_2::llama_rs_mtp_speculative_draft(
169                self.raw.as_ptr(),
170                n_past,
171                id_last.0,
172                prompt.as_ptr(),
173                prompt.len(),
174                raw_out.as_mut_ptr(),
175                raw_out.len(),
176                &raw mut out_len,
177            )
178        };
179        if status == llama_cpp_sys_2::LLAMA_RS_STATUS_ALLOCATION_FAILED {
180            return Err(MtpSpeculativeError::DraftOverflow);
181        }
182        status_to_result(status)?;
183        raw_out.truncate(out_len);
184        Ok(raw_out.into_iter().map(LlamaToken).collect())
185    }
186
187    /// Notify llama.cpp how many draft tokens the target context accepted.
188    ///
189    /// # Errors
190    ///
191    /// Returns an error if llama.cpp rejects the call.
192    pub fn accept(&mut self, n_accepted: u16) -> Result<(), MtpSpeculativeError> {
193        let status = unsafe {
194            llama_cpp_sys_2::llama_rs_mtp_speculative_accept(self.raw.as_ptr(), n_accepted)
195        };
196        status_to_result(status)
197    }
198}
199
200impl Drop for MtpSpeculative<'_> {
201    fn drop(&mut self) {
202        unsafe {
203            llama_cpp_sys_2::llama_rs_mtp_speculative_free(self.raw.as_ptr());
204        }
205    }
206}
207
208fn tokens_to_raw(tokens: &[LlamaToken]) -> Vec<llama_cpp_sys_2::llama_token> {
209    tokens.iter().map(|token| token.0).collect()
210}
211
212fn status_to_result(status: llama_cpp_sys_2::llama_rs_status) -> Result<(), MtpSpeculativeError> {
213    if status_is_ok(status) {
214        Ok(())
215    } else {
216        Err(MtpSpeculativeError::Status(status as i32))
217    }
218}