Skip to main content

ik_llama_cpp_2/
sampling.rs

1//! Sampling — a chain-object [`LlamaSampler`] matching `llama-cpp-2`'s API,
2//! emulated over ik_llama.cpp's **legacy** `llama_sample_*` array functions.
3//!
4//! ik has no `llama_sampler_chain_*` and only `grammar`/`dry` sampler-init
5//! functions, so this reconstructs the anchor's chain model in Rust: each
6//! constructor builds a one-stage sampler, [`LlamaSampler::chain_simple`]
7//! concatenates them, and [`apply`](LlamaSampler::apply) runs the stages over a
8//! [`LlamaTokenDataArray`] in order. The legacy transforms accept a null
9//! `llama_context` (ik guards it internally), so no context is needed until the
10//! final draw; the `dist` selector draws in Rust from a seeded RNG.
11
12use std::ffi::CString;
13use std::os::raw::c_char;
14use std::ptr::NonNull;
15
16use ik_llama_cpp_sys as sys;
17
18use crate::context::LlamaContext;
19use crate::grammar::GrammarInitError;
20use crate::model::LlamaModel;
21use crate::token::LlamaToken;
22
23// The candidate array lives in `token::data_array` (matching the anchor);
24// re-exported for ergonomics / back-compat.
25pub use crate::token::data_array::LlamaTokenDataArray;
26
27/// A small deterministic RNG for the `dist` selector, so a stochastic sampler
28/// is reproducible from its seed without pulling in a `rand` dependency
29/// (xorshift64\* seeded via SplitMix64).
30#[derive(Debug, Clone)]
31struct Rng {
32    state: u64,
33}
34
35impl Rng {
36    fn new(seed: u32) -> Self {
37        let mut z = u64::from(seed).wrapping_add(0x9E37_79B9_7F4A_7C15);
38        z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
39        z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
40        Self {
41            state: (z ^ (z >> 31)) | 1,
42        }
43    }
44
45    fn next_u64(&mut self) -> u64 {
46        let mut x = self.state;
47        x ^= x >> 12;
48        x ^= x << 25;
49        x ^= x >> 27;
50        self.state = x;
51        x.wrapping_mul(0x2545_F491_4F6C_DD1D)
52    }
53
54    /// Uniform `f32` in `[0, 1)` (top 24 bits / 2^24).
55    fn next_f32(&mut self) -> f32 {
56        let bits = (self.next_u64() >> 40) as u32; // 24 bits
57        (bits as f32) / (1u32 << 24) as f32
58    }
59}
60
61#[derive(Debug)]
62enum Stage {
63    TopK(i32),
64    TopP {
65        p: f32,
66        min_keep: usize,
67    },
68    MinP {
69        p: f32,
70        min_keep: usize,
71    },
72    Typical {
73        p: f32,
74        min_keep: usize,
75    },
76    TopNSigma(f32),
77    TailFree {
78        z: f32,
79        min_keep: usize,
80    },
81    Temp(f32),
82    TempExt {
83        t: f32,
84        delta: f32,
85        exponent: f32,
86    },
87    Penalties {
88        last_n: i32,
89        repeat: f32,
90        freq: f32,
91        presence: f32,
92    },
93    /// Stochastic selector: softmax + a seeded weighted draw.
94    Dist(Rng),
95    /// Deterministic selector: argmax over logits.
96    Greedy,
97    /// GBNF grammar constraint (masks disallowed tokens). Owns the C grammar;
98    /// `vocab` is stashed from the model so apply/accept need no context.
99    Grammar {
100        grammar: NonNull<sys::llama_grammar>,
101        vocab: *const sys::llama_vocab,
102    },
103}
104
105impl Drop for Stage {
106    fn drop(&mut self) {
107        if let Stage::Grammar { grammar, .. } = self {
108            // SAFETY: `grammar` was produced by `llama_sampler_init_grammar[_lazy]`
109            // and is owned solely by this stage (Stage is not Clone).
110            unsafe { sys::llama_grammar_free(grammar.as_ptr()) };
111        }
112    }
113}
114
115impl Stage {
116    fn run(&mut self, arr: &mut LlamaTokenDataArray, hist: &[sys::llama_token]) {
117        let null = std::ptr::null_mut();
118        match self {
119            Stage::TopK(k) => {
120                // SAFETY: null ctx is guarded by ik; `c` describes `arr.data`.
121                unsafe {
122                    arr.modify_as_c_llama_token_data_array(|c| {
123                        sys::llama_sample_top_k(null, c, *k, 1)
124                    });
125                }
126            }
127            Stage::TopP { p, min_keep } => unsafe {
128                arr.modify_as_c_llama_token_data_array(|c| {
129                    sys::llama_sample_top_p(null, c, *p, *min_keep)
130                });
131            },
132            Stage::MinP { p, min_keep } => unsafe {
133                arr.modify_as_c_llama_token_data_array(|c| {
134                    sys::llama_sample_min_p(null, c, *p, *min_keep)
135                });
136            },
137            Stage::Typical { p, min_keep } => unsafe {
138                arr.modify_as_c_llama_token_data_array(|c| {
139                    sys::llama_sample_typical(null, c, *p, *min_keep)
140                });
141            },
142            Stage::TopNSigma(n) => unsafe {
143                arr.modify_as_c_llama_token_data_array(|c| {
144                    sys::llama_sample_top_n_sigma(null, c, *n)
145                });
146            },
147            Stage::TailFree { z, min_keep } => unsafe {
148                arr.modify_as_c_llama_token_data_array(|c| {
149                    sys::llama_sample_tail_free(null, c, *z, *min_keep)
150                });
151            },
152            Stage::Temp(t) => unsafe {
153                arr.modify_as_c_llama_token_data_array(|c| sys::llama_sample_temp(null, c, *t));
154            },
155            Stage::TempExt { t, delta, exponent } => unsafe {
156                arr.modify_as_c_llama_token_data_array(|c| {
157                    sys::llama_sample_entropy(null, c, *t - *delta, *t + *delta, *exponent);
158                });
159            },
160            Stage::Penalties {
161                last_n,
162                repeat,
163                freq,
164                presence,
165            } => {
166                let use_n = (*last_n).max(0) as usize;
167                let use_n = use_n.min(hist.len());
168                let start = hist.len() - use_n;
169                let window = &hist[start..];
170                // SAFETY: null ctx guarded by ik; `window` lives for the call.
171                unsafe {
172                    arr.modify_as_c_llama_token_data_array(|c| {
173                        sys::llama_sample_repetition_penalties(
174                            null,
175                            c,
176                            window.as_ptr(),
177                            window.len(),
178                            *repeat,
179                            *freq,
180                            *presence,
181                        );
182                    });
183                }
184            }
185            Stage::Dist(rng) => {
186                let r = rng.next_f32();
187                // SAFETY: null ctx guarded; softmax fills `p`; we read the C
188                // array within `[0, size)` and set `selected`.
189                unsafe {
190                    arr.modify_as_c_llama_token_data_array(|c| {
191                        // Guard BEFORE softmax: llama_sample_softmax_impl does
192                        // GGML_ASSERT(size > 0) and would abort on an empty set.
193                        if c.size == 0 {
194                            c.selected = -1;
195                            return;
196                        }
197                        sys::llama_sample_softmax(null, c);
198                        let data = std::slice::from_raw_parts(c.data, c.size);
199                        let mut cum = 0.0_f32;
200                        let mut chosen = (c.size - 1) as i64;
201                        for (i, d) in data.iter().enumerate() {
202                            cum += d.p;
203                            if r < cum {
204                                chosen = i as i64;
205                                break;
206                            }
207                        }
208                        c.selected = chosen;
209                    });
210                }
211            }
212            Stage::Greedy => {
213                // SAFETY: we only read `[0, size)` and set `selected`.
214                unsafe {
215                    arr.modify_as_c_llama_token_data_array(|c| {
216                        if c.size == 0 {
217                            c.selected = -1;
218                            return;
219                        }
220                        let data = std::slice::from_raw_parts(c.data, c.size);
221                        let mut best = 0i64;
222                        let mut best_logit = f32::NEG_INFINITY;
223                        for (i, d) in data.iter().enumerate() {
224                            if d.logit > best_logit {
225                                best_logit = d.logit;
226                                best = i as i64;
227                            }
228                        }
229                        c.selected = best;
230                    });
231                }
232            }
233            Stage::Grammar { grammar, vocab } => {
234                let g = grammar.as_ptr();
235                let v = *vocab;
236                // SAFETY: `g`/`v` valid; the glue applies grammar constraints to
237                // `c` in place (masks disallowed tokens), null-`smpl` internally.
238                unsafe {
239                    arr.modify_as_c_llama_token_data_array(|c| {
240                        sys::ik_llama_rs_grammar_apply(g, v, c);
241                    });
242                }
243            }
244        }
245    }
246}
247
248/// A sampler: an ordered chain of transform stages ending in a selector,
249/// matching `llama-cpp-2`'s `LlamaSampler`.
250///
251/// Build single stages with the constructors ([`greedy`](Self::greedy),
252/// [`temp`](Self::temp), [`top_k`](Self::top_k), …) and compose them with
253/// [`chain_simple`](Self::chain_simple). During generation, either call
254/// [`sample`](Self::sample) (which reads logits from the context) or
255/// [`apply`](Self::apply) on a candidate array you already built, then read
256/// [`LlamaTokenDataArray::selected_token`]. Call [`accept`](Self::accept) with
257/// each drawn token so stateful stages (penalties, grammar) stay in sync.
258///
259/// Not `Clone` (a grammar stage owns a C grammar object freed on drop).
260#[derive(Debug)]
261pub struct LlamaSampler {
262    stages: Vec<Stage>,
263    history: Vec<LlamaToken>,
264}
265
266impl LlamaSampler {
267    fn single(stage: Stage) -> Self {
268        Self {
269            stages: vec![stage],
270            history: Vec::new(),
271        }
272    }
273
274    /// Compose several samplers into one chain, applied left to right.
275    #[must_use]
276    pub fn chain_simple(samplers: impl IntoIterator<Item = LlamaSampler>) -> Self {
277        let mut stages = Vec::new();
278        for s in samplers {
279            stages.extend(s.stages);
280        }
281        Self {
282            stages,
283            history: Vec::new(),
284        }
285    }
286
287    /// Greedy (argmax) selector.
288    #[must_use]
289    pub fn greedy() -> Self {
290        Self::single(Stage::Greedy)
291    }
292
293    /// Stochastic distribution selector seeded by `seed` (softmax + draw).
294    #[must_use]
295    pub fn dist(seed: u32) -> Self {
296        Self::single(Stage::Dist(Rng::new(seed)))
297    }
298
299    /// Temperature scaling.
300    #[must_use]
301    pub fn temp(t: f32) -> Self {
302        Self::single(Stage::Temp(t))
303    }
304
305    /// Dynamic-temperature ("entropy") scaling over `[t-delta, t+delta]`.
306    #[must_use]
307    pub fn temp_ext(t: f32, delta: f32, exponent: f32) -> Self {
308        Self::single(Stage::TempExt { t, delta, exponent })
309    }
310
311    /// Top-k filtering.
312    #[must_use]
313    pub fn top_k(k: i32) -> Self {
314        Self::single(Stage::TopK(k))
315    }
316
317    /// Top-p (nucleus) filtering.
318    #[must_use]
319    pub fn top_p(p: f32, min_keep: usize) -> Self {
320        Self::single(Stage::TopP { p, min_keep })
321    }
322
323    /// Min-p filtering.
324    #[must_use]
325    pub fn min_p(p: f32, min_keep: usize) -> Self {
326        Self::single(Stage::MinP { p, min_keep })
327    }
328
329    /// Locally-typical filtering.
330    #[must_use]
331    pub fn typical(p: f32, min_keep: usize) -> Self {
332        Self::single(Stage::Typical { p, min_keep })
333    }
334
335    /// Top-nσ filtering.
336    #[must_use]
337    pub fn top_n_sigma(n: f32) -> Self {
338        Self::single(Stage::TopNSigma(n))
339    }
340
341    /// Tail-free filtering.
342    #[must_use]
343    pub fn tail_free(z: f32, min_keep: usize) -> Self {
344        Self::single(Stage::TailFree { z, min_keep })
345    }
346
347    /// A GBNF grammar constraint stage.
348    ///
349    /// `grammar_str` is GBNF source, `root` the start symbol (usually `"root"`).
350    /// The stage masks tokens the grammar cannot currently accept; call
351    /// [`accept`](Self::accept) with each drawn token to advance grammar state.
352    ///
353    /// # Safety contract
354    ///
355    /// The returned sampler stashes a pointer into `model`'s vocab, so **`model`
356    /// must outlive this sampler** — using it after the model is dropped is
357    /// undefined behavior. This is not encoded in the type (the sampler stays
358    /// `'static`) so that it drops in for `llama-cpp-2`, which makes the same
359    /// choice; keep the model alive at least as long as any sampler built from
360    /// it (a `'static` model trivially satisfies this). Prefer
361    /// [`crate::grammar::LlamaGrammar`] if you want the model lifetime enforced.
362    ///
363    /// # Errors
364    ///
365    /// [`GrammarInitError::Nul`] on an interior NUL byte; [`GrammarInitError::Parse`]
366    /// if the GBNF fails to parse.
367    pub fn grammar(
368        model: &LlamaModel,
369        grammar_str: &str,
370        root: &str,
371    ) -> Result<Self, GrammarInitError> {
372        let c_gbnf = CString::new(grammar_str)?;
373        let c_root = CString::new(root)?;
374        // SAFETY: model is valid; vocab is const and lives with the model.
375        let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
376        // SAFETY: valid vocab + two valid C strings (copied C-side). Null = parse fail.
377        let raw =
378            unsafe { sys::llama_sampler_init_grammar(vocab, c_gbnf.as_ptr(), c_root.as_ptr()) };
379        let grammar = NonNull::new(raw).ok_or(GrammarInitError::Parse)?;
380        Ok(Self::single(Stage::Grammar { grammar, vocab }))
381    }
382
383    /// A lazy GBNF grammar constraint: it stays inert until one of
384    /// `trigger_words` / `trigger_tokens` appears, then constrains generation.
385    ///
386    /// # Errors
387    ///
388    /// [`GrammarInitError::Nul`] on an interior NUL byte in the grammar, root, or
389    /// a trigger word; [`GrammarInitError::Parse`] if the GBNF fails to parse.
390    pub fn grammar_lazy(
391        model: &LlamaModel,
392        grammar_str: &str,
393        root: &str,
394        trigger_words: impl IntoIterator<Item = impl AsRef<[u8]>>,
395        trigger_tokens: &[LlamaToken],
396    ) -> Result<Self, GrammarInitError> {
397        let c_gbnf = CString::new(grammar_str)?;
398        let c_root = CString::new(root)?;
399        let words: Vec<CString> = trigger_words
400            .into_iter()
401            .map(|w| CString::new(w.as_ref()))
402            .collect::<Result<_, _>>()?;
403        let mut word_ptrs: Vec<*const c_char> = words.iter().map(|c| c.as_ptr()).collect();
404        let tokens: Vec<sys::llama_token> = trigger_tokens.iter().map(|t| t.0).collect();
405        // SAFETY: model valid → const vocab.
406        let vocab = unsafe { sys::llama_model_get_vocab(model.model.as_ptr()) };
407        // SAFETY: valid vocab + C strings + trigger arrays live for the call
408        // (ik copies what it needs). Null = parse failure.
409        let raw = unsafe {
410            sys::llama_sampler_init_grammar_lazy(
411                vocab,
412                c_gbnf.as_ptr(),
413                c_root.as_ptr(),
414                word_ptrs.as_mut_ptr(),
415                word_ptrs.len(),
416                tokens.as_ptr(),
417                tokens.len(),
418            )
419        };
420        let grammar = NonNull::new(raw).ok_or(GrammarInitError::Parse)?;
421        Ok(Self::single(Stage::Grammar { grammar, vocab }))
422    }
423
424    /// Repetition / frequency / presence penalties over the accepted history.
425    #[must_use]
426    pub fn penalties(
427        penalty_last_n: i32,
428        penalty_repeat: f32,
429        penalty_freq: f32,
430        penalty_present: f32,
431    ) -> Self {
432        Self::single(Stage::Penalties {
433            last_n: penalty_last_n,
434            repeat: penalty_repeat,
435            freq: penalty_freq,
436            presence: penalty_present,
437        })
438    }
439
440    /// Run every stage over `arr` in order (transforms + selector). Read the
441    /// result with [`LlamaTokenDataArray::selected_token`].
442    ///
443    /// A no-op on an empty candidate array (leaving `selected = None`): ik's
444    /// legacy samplers `GGML_ASSERT(size > 0)` and would abort the process, so
445    /// this safe wrapper must not forward an empty set into them.
446    pub fn apply(&mut self, arr: &mut LlamaTokenDataArray) {
447        if arr.data.is_empty() {
448            return;
449        }
450        let hist: Vec<sys::llama_token> = self.history.iter().map(|t| t.0).collect();
451        for stage in &mut self.stages {
452            stage.run(arr, &hist);
453        }
454    }
455
456    /// Record `token`: appends to the history (penalties stage) and advances any
457    /// grammar stage.
458    ///
459    /// If a grammar stage is present, only feed tokens the grammar permits at the
460    /// current position (i.e. tokens surviving [`apply`](Self::apply)). Accepting
461    /// a grammar-disallowed end-of-generation token aborts the process (ik's
462    /// `llama_grammar_accept_impl` treats it as fatal).
463    pub fn accept(&mut self, token: LlamaToken) {
464        self.history.push(token);
465        for stage in &mut self.stages {
466            if let Stage::Grammar { grammar, vocab } = stage {
467                // SAFETY: `grammar`/`vocab` valid; the glue advances grammar
468                // state with null `smpl` internally.
469                unsafe { sys::ik_llama_rs_grammar_accept(grammar.as_ptr(), *vocab, token.0) };
470            }
471        }
472    }
473
474    /// Clear the accepted-token history.
475    pub fn reset(&mut self) {
476        self.history.clear();
477    }
478
479    /// Build the candidate array from the context's logits at batch index `idx`,
480    /// run the chain, and return the selected token (argmax fallback if no
481    /// selector ran).
482    pub fn sample(&mut self, ctx: &LlamaContext, idx: i32) -> LlamaToken {
483        let mut arr = ctx.token_data_array_ith(idx);
484        self.apply(&mut arr);
485        arr.selected_token().unwrap_or_else(|| {
486            arr.data
487                .iter()
488                .max_by(|a, b| {
489                    a.logit()
490                        .partial_cmp(&b.logit())
491                        .unwrap_or(std::cmp::Ordering::Equal)
492                })
493                .map_or(LlamaToken(0), crate::token::data::LlamaTokenData::id)
494        })
495    }
496}
497
498#[cfg(test)]
499mod tests {
500    use super::*;
501    use crate::token::data::LlamaTokenData;
502
503    #[test]
504    fn greedy_selects_argmax() {
505        let mut arr = LlamaTokenDataArray::from_logits(&[0.1, 2.5, -1.0, 2.4, 0.0]);
506        let mut s = LlamaSampler::greedy();
507        s.apply(&mut arr);
508        assert_eq!(s.stages.len(), 1);
509        assert_eq!(arr.selected_token(), Some(LlamaToken(1)));
510    }
511
512    #[test]
513    fn dist_is_deterministic_for_a_seed() {
514        let logits = [0.2_f32, 1.0, 0.5, 3.0, -0.5];
515        let mut a = LlamaSampler::dist(1234);
516        let mut b = LlamaSampler::dist(1234);
517        let mut arr_a = LlamaTokenDataArray::from_logits(&logits);
518        let mut arr_b = LlamaTokenDataArray::from_logits(&logits);
519        a.apply(&mut arr_a);
520        b.apply(&mut arr_b);
521        assert!(arr_a.selected_token().is_some());
522        assert_eq!(arr_a.selected_token(), arr_b.selected_token());
523    }
524
525    #[test]
526    fn apply_on_empty_array_is_noop_not_abort() {
527        // ik's legacy samplers GGML_ASSERT(size > 0); apply must short-circuit
528        // an empty candidate set rather than forward it (which would SIGABRT).
529        let mut arr = LlamaTokenDataArray::new(Vec::new(), false);
530        let mut s = LlamaSampler::chain_simple([
531            LlamaSampler::top_k(3),
532            LlamaSampler::top_p(0.9, 1),
533            LlamaSampler::temp(0.8),
534            LlamaSampler::dist(1),
535        ]);
536        s.apply(&mut arr);
537        assert_eq!(arr.selected_token(), None);
538    }
539
540    #[test]
541    fn chain_composes_stages() {
542        let s = LlamaSampler::chain_simple([
543            LlamaSampler::top_k(3),
544            LlamaSampler::temp(0.8),
545            LlamaSampler::greedy(),
546        ]);
547        assert_eq!(s.stages.len(), 3);
548        // history + accept
549        let mut s = s;
550        s.accept(LlamaTokenData::new(LlamaToken(5), 0.0, 0.0).id());
551        assert_eq!(s.history, vec![LlamaToken(5)]);
552    }
553}