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}