Skip to main content

llama_cpp_2/
speculative.rs

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