Skip to main content

ik_llama_cpp_2/
speculative.rs

1//! Multi-Token Prediction (MTP / NextN) support.
2//!
3//! ik_llama.cpp's MTP is an **embedded** mechanism: a single model carrying
4//! NextN prediction layers (loaded with [`crate::LlamaModelParams::with_mtp`]),
5//! driven by a low-level op-type state machine plus a `common_speculative` driver.
6//!
7//! * [`MtpOpType`] + [`crate::LlamaContext::set_mtp_op_type`] expose the raw
8//!   op-type primitive (available without `common`).
9//! * [`MtpSpeculative`] (requires the **`common`** feature) is the full
10//!   draft/accept driver, backed by a C++ glue over ik's `common_speculative_*`
11//!   (`ik_llama_rs_mtp_*`). It reproduces the native `--spec-type mtp` flow.
12
13use ik_llama_cpp_sys as sys;
14
15/// MTP decode-graph operation mode (maps to ik's `llama_mtp_op_type`).
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum MtpOpType {
18    /// Normal decode (no MTP graph).
19    None,
20    /// Prompt warmup — populate MTP KV + hidden state (no logits).
21    Warmup,
22    /// Advance MTP state after accepted tokens.
23    UpdateAccepted,
24    /// Generate draft tokens from the NextN head.
25    DraftGen,
26}
27
28impl MtpOpType {
29    /// Raw `llama_mtp_op_type` value.
30    #[must_use]
31    pub fn to_raw(self) -> sys::llama_mtp_op_type {
32        match self {
33            MtpOpType::None => sys::MTP_OP_NONE,
34            MtpOpType::Warmup => sys::MTP_OP_WARMUP,
35            MtpOpType::UpdateAccepted => sys::MTP_OP_UPDATE_ACCEPTED,
36            MtpOpType::DraftGen => sys::MTP_OP_DRAFT_GEN,
37        }
38    }
39}
40
41/// Parameters for MTP speculative decoding. Mirrors the shape of llama-cpp-2's
42/// `MtpSpeculativeParams` (plus `temp`/`mtp_heads` for ik's driver).
43#[derive(Debug, Clone, Copy)]
44pub struct MtpSpeculativeParams {
45    /// Maximum draft tokens per step (n_max=1 = one NextN head).
46    pub n_max: i32,
47    /// Minimum draft tokens per step.
48    pub n_min: i32,
49    /// Minimum draft acceptance probability (0.0 = never early-stop drafting).
50    pub p_min: f32,
51    /// MTP heads to use (1 = default; >1 experimental).
52    pub mtp_heads: i32,
53    /// Target-sampler temperature (<= 0 = greedy).
54    pub temp: f32,
55}
56
57impl Default for MtpSpeculativeParams {
58    fn default() -> Self {
59        Self {
60            n_max: 1,
61            n_min: 0,
62            p_min: 0.0,
63            mtp_heads: 1,
64            temp: 0.0,
65        }
66    }
67}
68
69#[cfg(feature = "common")]
70pub use driver::{MtpSpeculative, MtpStep};
71
72#[cfg(feature = "common")]
73mod driver {
74    use super::MtpSpeculativeParams;
75    use ik_llama_cpp_sys as sys;
76    use std::ptr::NonNull;
77
78    use crate::context::LlamaContext;
79    use crate::model::LlamaModel;
80    use crate::token::LlamaToken;
81    use crate::LlamaError;
82
83    /// Result of one [`MtpSpeculative::step`].
84    #[derive(Debug, Clone)]
85    pub struct MtpStep {
86        /// Tokens emitted this step — append to the output; each token is emitted
87        /// exactly once across steps.
88        pub tokens: Vec<LlamaToken>,
89        /// Number of draft tokens accepted this step (0..=n_max).
90        pub n_accepted: i32,
91    }
92
93    /// Full MTP speculative driver (requires the `common` feature).
94    ///
95    /// Wraps ik's `common_speculative_*` via the `ik_llama_rs_mtp_*` C++ glue,
96    /// which OWNS the draft→verify→accept→commit critical section (multiple
97    /// internal `llama_decode`s on the target + companion contexts). The borrow of
98    /// the [`LlamaContext`] enforces that Rust does not touch it mid-loop; drive it
99    /// as `new` → `begin(prompt)` → `step()`* .
100    #[derive(Debug)]
101    pub struct MtpSpeculative<'ctx, 'model> {
102        raw: NonNull<sys::ik_llama_rs_mtp>,
103        ctx: &'ctx mut LlamaContext<'model>,
104        params: MtpSpeculativeParams,
105    }
106
107    impl<'ctx, 'model> MtpSpeculative<'ctx, 'model> {
108        /// Initialize the driver over a NextN model + its (mtp-enabled) context.
109        ///
110        /// `model` must have been loaded with `.with_mtp(true)` and `ctx` created
111        /// with `.with_mtp(true)`. Fails (`MtpInit`) for a model with 0 NextN
112        /// layers or an openPangu/recurrent target (unsupported in v1).
113        pub fn new(
114            model: &LlamaModel,
115            ctx: &'ctx mut LlamaContext<'model>,
116            params: MtpSpeculativeParams,
117        ) -> Result<Self, LlamaError> {
118            let cparams = ctx.raw_params; // the exact params ctx was built with
119                                          // SAFETY: valid model/ctx; cparams matches ctx; glue validates preconditions.
120            let raw = unsafe {
121                sys::ik_llama_rs_mtp_init(
122                    model.model.as_ptr(),
123                    ctx.context.as_ptr(),
124                    &cparams,
125                    params.n_max,
126                    params.n_min,
127                    params.p_min,
128                    params.mtp_heads,
129                    params.temp,
130                )
131            };
132            NonNull::new(raw)
133                .map(|raw| Self { raw, ctx, params })
134                .ok_or(LlamaError::MtpInit)
135        }
136
137        /// Prompt warmup + begin. Runs the prompt through the MTP path and prepares
138        /// the first draft. Returns `n_past`. Call once before [`Self::step`].
139        pub fn begin(&mut self, prompt: &[LlamaToken]) -> Result<usize, LlamaError> {
140            let toks: Vec<sys::llama_token> = prompt.iter().map(|t| t.0).collect();
141            // SAFETY: valid handle + token buffer.
142            let n =
143                unsafe { sys::ik_llama_rs_mtp_begin(self.raw.as_ptr(), toks.as_ptr(), toks.len()) };
144            if n < 0 {
145                return Err(LlamaError::MtpBegin(n as i32));
146            }
147            Ok(n as usize)
148        }
149
150        /// Run one draft→verify→accept→commit cycle. Returns the tokens emitted
151        /// this step (to append to the output) and how many drafts were accepted.
152        pub fn step(&mut self) -> Result<MtpStep, LlamaError> {
153            let cap = self.params.n_max.max(1) as usize + 2;
154            let mut out = vec![0 as sys::llama_token; cap];
155            let mut n_out: usize = 0;
156            let mut n_accepted: i32 = 0;
157            // SAFETY: out has `cap` slots; n_out/n_accepted are valid out-params.
158            let st = unsafe {
159                sys::ik_llama_rs_mtp_step(
160                    self.raw.as_ptr(),
161                    out.as_mut_ptr(),
162                    cap,
163                    &mut n_out,
164                    &mut n_accepted,
165                )
166            };
167            if st as i32 != 0 {
168                // LLAMA_RS_STATUS_OK == 0
169                return Err(LlamaError::MtpStep(st as i32));
170            }
171            out.truncate(n_out);
172            Ok(MtpStep {
173                tokens: out.into_iter().map(LlamaToken).collect(),
174                n_accepted,
175            })
176        }
177
178        /// The configured parameters.
179        #[must_use]
180        pub fn params(&self) -> MtpSpeculativeParams {
181            self.params
182        }
183
184        /// Mutable access to the wrapped context (only between full generations).
185        pub fn context_mut(&mut self) -> &mut LlamaContext<'model> {
186            self.ctx
187        }
188    }
189
190    impl Drop for MtpSpeculative<'_, '_> {
191        fn drop(&mut self) {
192            // SAFETY: frees the common_speculative + common_sampler owned by the glue.
193            unsafe { sys::ik_llama_rs_mtp_free(self.raw.as_ptr()) };
194        }
195    }
196}