Skip to main content

rlx_runtime/
samplers.rs

1// RLX — versatile ML compiler + runtime.
2// Copyright (C) 2026 Eugene Hauptmann, Nataliya Kosmyna.
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, version 3.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11// GNU General Public License for more details.
12//
13// You should have received a copy of the GNU General Public License
14// along with this program. If not, see <https://www.gnu.org/licenses/>.
15
16//! Advanced token samplers — the long-tail of llama.cpp samplers ported to
17//! a backend-agnostic `Sampler` chain.
18//!
19//! Why backend-agnostic: all samplers operate on a logits row (`&mut [f32]`)
20//! plus a token history slice; nothing about the kernel cares which device
21//! produced the logits. This file owns the algorithms; backend kernels just
22//! emit logits and (for paths like Metal/CUDA) may call into a thin shim
23//! that runs the chain on the CPU after a single device→host copy.
24//!
25//! Available samplers
26//! ------------------
27//!
28//! - [`Temperature`] — uniform `logits[i] /= T` scaling (T → 0 ⇒ greedy).
29//! - [`DynamicTemperature`] — entropy-aware temperature: T scales between
30//!   `min` and `max` based on `entropy(p) / log(vocab)`.
31//! - [`TopK`] — keep the K largest logits; mask the rest to `-inf`.
32//! - [`TopP`] — nucleus sampling: smallest set whose cumulative prob ≥ p.
33//! - [`TopNSigma`] — keep tokens whose logit ≥ `max - n * σ(logits)`.
34//! - [`TypicalP`] — locally-typical sampling (Meister et al. 2022): keep
35//!   tokens whose `|−log p − H(p)|` is smallest until cumulative prob ≥ p.
36//! - [`MirostatV1`] — target-surprise mode-V1 (μ/τ control loop).
37//! - [`MirostatV2`] — simpler logit-based variant: keep tokens with surprise ≤ μ.
38//! - [`Xtc`] — eXclude Top Choices: with probability `prob`, drop top tokens
39//!   above `threshold` so only the lower-rank tokens survive (Q1-style sampling).
40//! - [`Dry`] — DRY (Don't Repeat Yourself) repetition penalty. Penalises
41//!   tokens whose continuation matches an earlier n-gram in the history.
42//! - [`RepetitionPenalty`] — classic frequency/presence penalty (kept here
43//!   so the chain is self-contained).
44//!
45//! Composition: a `SamplerChain` runs samplers in order, each mutating the
46//! logits in place, then the final `Sampler::sample` draws one token. All
47//! samplers are deterministic given the same RNG state.
48
49use rlx_ir::Philox4x32;
50
51/// One row of vocabulary-shaped logits (`[vocab]`).
52pub type Logits<'a> = &'a mut [f32];
53
54/// State that some samplers update each step (e.g. Mirostat μ).
55#[derive(Debug, Default, Clone)]
56pub struct SamplerState {
57    /// Mirostat estimator. NaN until the first apply.
58    pub mirostat_mu: f32,
59}
60
61impl SamplerState {
62    pub fn new() -> Self {
63        Self {
64            mirostat_mu: f32::NAN,
65        }
66    }
67}
68
69/// A logit transform + sample step. Samplers mutate the logits row in place;
70/// `sample` is called at the end of the chain.
71pub trait Sampler: std::fmt::Debug + Send + Sync {
72    /// Apply this sampler's transformation to `logits`. `history` is the
73    /// already-emitted token sequence (newest last). Some samplers (DRY,
74    /// repetition penalty) need it; pure logit transforms ignore it.
75    fn apply(
76        &self,
77        logits: Logits<'_>,
78        history: &[u32],
79        state: &mut SamplerState,
80        rng: &mut Philox4x32,
81    );
82
83    /// Optional name for tracing.
84    fn name(&self) -> &'static str {
85        std::any::type_name::<Self>()
86    }
87}
88
89/// A linear chain of samplers terminated by a `softmax + multinomial`
90/// draw. Build one with [`SamplerChain::builder`] or directly from a Vec.
91#[derive(Debug)]
92pub struct SamplerChain {
93    pub steps: Vec<Box<dyn Sampler>>,
94}
95
96impl SamplerChain {
97    pub fn new() -> Self {
98        Self { steps: Vec::new() }
99    }
100
101    pub fn builder() -> SamplerChainBuilder {
102        SamplerChainBuilder::default()
103    }
104
105    /// Run every sampler in order over `logits`, then draw one token.
106    pub fn sample(
107        &self,
108        logits: Logits<'_>,
109        history: &[u32],
110        state: &mut SamplerState,
111        rng: &mut Philox4x32,
112    ) -> u32 {
113        for step in &self.steps {
114            step.apply(logits, history, state, rng);
115        }
116        sample_from_logits(logits, rng)
117    }
118}
119
120impl Default for SamplerChain {
121    fn default() -> Self {
122        Self::new()
123    }
124}
125
126#[derive(Debug, Default)]
127pub struct SamplerChainBuilder {
128    steps: Vec<Box<dyn Sampler>>,
129}
130
131impl SamplerChainBuilder {
132    pub fn push<S: Sampler + 'static>(mut self, s: S) -> Self {
133        self.steps.push(Box::new(s));
134        self
135    }
136
137    pub fn push_boxed(mut self, s: Box<dyn Sampler>) -> Self {
138        self.steps.push(s);
139        self
140    }
141
142    pub fn build(self) -> SamplerChain {
143        SamplerChain { steps: self.steps }
144    }
145}
146
147// ─── shared utilities ─────────────────────────────────────────────────
148
149/// Stable softmax in place. Tokens at `-inf` get probability 0.
150pub fn softmax_inplace(logits: &mut [f32]) {
151    let mut maxv = f32::NEG_INFINITY;
152    for &x in logits.iter() {
153        if x > maxv {
154            maxv = x;
155        }
156    }
157    if !maxv.is_finite() {
158        let inv = 1.0 / logits.len() as f32;
159        for x in logits.iter_mut() {
160            *x = inv;
161        }
162        return;
163    }
164    let mut s = 0.0f32;
165    for x in logits.iter_mut() {
166        let v = (*x - maxv).exp();
167        *x = v;
168        s += v;
169    }
170    let inv = if s > 0.0 { 1.0 / s } else { 0.0 };
171    for x in logits.iter_mut() {
172        *x *= inv;
173    }
174}
175
176/// Multinomial draw from a probability row (assumed already softmax-ed).
177/// Robust against floating-point drift in the tail.
178pub fn sample_from_probs(probs: &[f32], rng: &mut Philox4x32) -> u32 {
179    let r = rng.next_f32();
180    let mut acc = 0.0f32;
181    for (i, &p) in probs.iter().enumerate() {
182        acc += p;
183        if r <= acc {
184            return i as u32;
185        }
186    }
187    (probs.len() - 1) as u32
188}
189
190/// Apply softmax to logits in place then multinomial-sample one index.
191pub fn sample_from_logits(logits: &mut [f32], rng: &mut Philox4x32) -> u32 {
192    softmax_inplace(logits);
193    sample_from_probs(logits, rng)
194}
195
196/// Sorted-descending index/value pairs. Used by every top-something sampler.
197fn sorted_desc(logits: &[f32]) -> Vec<(usize, f32)> {
198    let mut v: Vec<(usize, f32)> = logits.iter().copied().enumerate().collect();
199    v.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
200    v
201}
202
203// ─── Temperature ─────────────────────────────────────────────────────
204
205#[derive(Debug, Clone, Copy)]
206pub struct Temperature {
207    pub t: f32,
208}
209
210impl Sampler for Temperature {
211    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
212        let t = self.t.max(1e-6);
213        for x in logits.iter_mut() {
214            *x /= t;
215        }
216    }
217}
218
219// ─── DynamicTemperature ──────────────────────────────────────────────
220//
221// Scale temperature between [min, max] based on the entropy of the
222// current logits' softmax: T = min + (max - min) * (H / Hmax)^exponent.
223// `exponent=1.0` matches the original llama.cpp `dynatemp` behavior.
224
225#[derive(Debug, Clone, Copy)]
226pub struct DynamicTemperature {
227    pub min: f32,
228    pub max: f32,
229    pub exponent: f32,
230}
231
232impl Sampler for DynamicTemperature {
233    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
234        let v = logits.len();
235        if v == 0 {
236            return;
237        }
238        let mut tmp: Vec<f32> = logits.to_vec();
239        softmax_inplace(&mut tmp);
240        // Shannon entropy in nats.
241        let mut h = 0.0f32;
242        for &p in tmp.iter() {
243            if p > 0.0 {
244                h -= p * p.ln();
245            }
246        }
247        let hmax = (v as f32).ln().max(1e-6);
248        let norm = (h / hmax).clamp(0.0, 1.0);
249        let t = self.min + (self.max - self.min) * norm.powf(self.exponent);
250        let t = t.max(1e-6);
251        for x in logits.iter_mut() {
252            *x /= t;
253        }
254    }
255}
256
257// ─── TopK ────────────────────────────────────────────────────────────
258
259#[derive(Debug, Clone, Copy)]
260pub struct TopK {
261    pub k: usize,
262}
263
264impl Sampler for TopK {
265    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
266        let v = logits.len();
267        if self.k == 0 || self.k >= v {
268            return;
269        }
270        let sorted = sorted_desc(logits);
271        let cutoff = sorted[self.k - 1].1;
272        for x in logits.iter_mut() {
273            if *x < cutoff {
274                *x = f32::NEG_INFINITY;
275            }
276        }
277    }
278}
279
280// ─── TopP (nucleus) ──────────────────────────────────────────────────
281
282#[derive(Debug, Clone, Copy)]
283pub struct TopP {
284    pub p: f32,
285    /// Always keep at least this many tokens. Avoids degenerate single-token
286    /// nucleus when one logit dominates.
287    pub min_keep: usize,
288}
289
290impl Sampler for TopP {
291    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
292        if self.p >= 1.0 {
293            return;
294        }
295        let v = logits.len();
296        if v == 0 {
297            return;
298        }
299        let mut probs: Vec<f32> = logits.to_vec();
300        softmax_inplace(&mut probs);
301        let sorted = sorted_desc(&probs);
302        let mut keep = vec![false; v];
303        let mut cum = 0.0f32;
304        for (rank, (idx, p)) in sorted.iter().enumerate() {
305            keep[*idx] = true;
306            cum += *p;
307            if cum >= self.p && rank + 1 >= self.min_keep {
308                break;
309            }
310        }
311        for (i, x) in logits.iter_mut().enumerate() {
312            if !keep[i] {
313                *x = f32::NEG_INFINITY;
314            }
315        }
316    }
317}
318
319// ─── MinP ────────────────────────────────────────────────────────────
320//
321// Keep tokens whose probability ≥ p · p_max, where p_max is the top
322// token's probability (Nguyen et al. 2024, "min-p sampling"). The cutoff
323// scales with the model's confidence: permissive when the distribution is
324// flat, strict when one token dominates. A cheaper, often better-behaved
325// alternative to top-p.
326
327#[derive(Debug, Clone, Copy)]
328pub struct MinP {
329    pub p: f32,
330    /// Always keep at least this many tokens (avoids an empty nucleus when
331    /// one token dominates and `p` is large).
332    pub min_keep: usize,
333}
334
335impl Sampler for MinP {
336    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
337        // Reject p outside (0, 1]; the explicit is_nan keeps a NaN p from
338        // slipping past the bounds checks (NaN compares false to everything).
339        if self.p <= 0.0 || self.p > 1.0 || self.p.is_nan() {
340            return;
341        }
342        let v = logits.len();
343        if v == 0 {
344            return;
345        }
346        let mut probs: Vec<f32> = logits.to_vec();
347        softmax_inplace(&mut probs);
348        let p_max = probs.iter().copied().fold(f32::NEG_INFINITY, f32::max);
349        if p_max <= 0.0 || p_max.is_nan() {
350            return;
351        }
352        let threshold = self.p * p_max;
353        let mut keep: Vec<bool> = probs.iter().map(|&p| p >= threshold).collect();
354        // Guarantee at least `min_keep` survivors: re-admit the top probs.
355        let floor = self.min_keep.max(1);
356        if keep.iter().filter(|&&k| k).count() < floor {
357            for (idx, _) in sorted_desc(&probs).into_iter().take(floor) {
358                keep[idx] = true;
359            }
360        }
361        for (i, x) in logits.iter_mut().enumerate() {
362            if !keep[i] {
363                *x = f32::NEG_INFINITY;
364            }
365        }
366    }
367}
368
369/// Apply an additive per-token logit bias in place (OpenAI `logit_bias`
370/// semantics): `logits[id] += bias`. Out-of-range ids are ignored. This is
371/// a host-side logits processor — run it on the raw logits row *before*
372/// the sampler chain. Kept as a free function (not a `SampleOpts` field) so
373/// `SampleOpts` stays `Copy`.
374pub fn apply_logit_bias(logits: &mut [f32], bias: &[(u32, f32)]) {
375    let v = logits.len();
376    for &(id, b) in bias {
377        let i = id as usize;
378        if i < v {
379            logits[i] += b;
380        }
381    }
382}
383
384// ─── TopNSigma ───────────────────────────────────────────────────────
385//
386// Keep tokens whose logit ≥ max_logit − n × σ(logits). Works directly on
387// the logit space, so no softmax is required to mask. (Ref: Hewitt et al.
388// "Top-n-σ" 2024.)
389
390#[derive(Debug, Clone, Copy)]
391pub struct TopNSigma {
392    pub n: f32,
393}
394
395impl Sampler for TopNSigma {
396    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
397        let v = logits.len();
398        if v == 0 || !self.n.is_finite() || self.n <= 0.0 {
399            return;
400        }
401        let mut maxv = f32::NEG_INFINITY;
402        let mut count = 0usize;
403        let mut sum = 0.0f32;
404        for &x in logits.iter() {
405            if x.is_finite() {
406                if x > maxv {
407                    maxv = x;
408                }
409                sum += x;
410                count += 1;
411            }
412        }
413        if count == 0 || !maxv.is_finite() {
414            return;
415        }
416        let mean = sum / count as f32;
417        let mut var = 0.0f32;
418        for &x in logits.iter() {
419            if x.is_finite() {
420                let d = x - mean;
421                var += d * d;
422            }
423        }
424        let sigma = (var / count as f32).sqrt();
425        let cutoff = maxv - self.n * sigma;
426        for x in logits.iter_mut() {
427            if *x < cutoff {
428                *x = f32::NEG_INFINITY;
429            }
430        }
431    }
432}
433
434// ─── TypicalP (locally typical sampling) ────────────────────────────
435//
436// Meister et al. 2022: rank tokens by deviation of their surprisal
437// (−log p) from the distribution's entropy H, ascending. Keep the
438// smallest set whose cumulative prob ≥ p.
439
440#[derive(Debug, Clone, Copy)]
441pub struct TypicalP {
442    pub p: f32,
443    pub min_keep: usize,
444}
445
446impl Sampler for TypicalP {
447    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, _r: &mut Philox4x32) {
448        if self.p >= 1.0 {
449            return;
450        }
451        let v = logits.len();
452        if v == 0 {
453            return;
454        }
455        let mut probs: Vec<f32> = logits.to_vec();
456        softmax_inplace(&mut probs);
457        let mut h = 0.0f32;
458        for &p in probs.iter() {
459            if p > 0.0 {
460                h -= p * p.ln();
461            }
462        }
463        // Score each token by |−log p − H|.
464        let mut scored: Vec<(usize, f32, f32)> = probs
465            .iter()
466            .enumerate()
467            .map(|(i, &p)| {
468                let neg_log = if p > 0.0 { -p.ln() } else { f32::INFINITY };
469                let dev = (neg_log - h).abs();
470                (i, p, dev)
471            })
472            .collect();
473        scored.sort_by(|a, b| a.2.partial_cmp(&b.2).unwrap_or(std::cmp::Ordering::Equal));
474        let mut keep = vec![false; v];
475        let mut cum = 0.0f32;
476        for (rank, (idx, p, _)) in scored.iter().enumerate() {
477            keep[*idx] = true;
478            cum += *p;
479            if cum >= self.p && rank + 1 >= self.min_keep {
480                break;
481            }
482        }
483        for (i, x) in logits.iter_mut().enumerate() {
484            if !keep[i] {
485                *x = f32::NEG_INFINITY;
486            }
487        }
488    }
489}
490
491// ─── Mirostat v1 ─────────────────────────────────────────────────────
492//
493// Original (NeurIPS 2021) paper: maintain a running μ that is the target
494// surprise. Top-k is set so the average surprise of the truncated head
495// matches μ, then μ is updated by the error against τ.
496
497#[derive(Debug, Clone, Copy)]
498pub struct MirostatV1 {
499    pub tau: f32,
500    pub eta: f32,
501    /// Pareto‐tail estimator window. Original paper uses 100.
502    pub m: usize,
503}
504
505impl Default for MirostatV1 {
506    fn default() -> Self {
507        Self {
508            tau: 5.0,
509            eta: 0.1,
510            m: 100,
511        }
512    }
513}
514
515impl Sampler for MirostatV1 {
516    fn apply(
517        &self,
518        logits: Logits<'_>,
519        _h: &[u32],
520        state: &mut SamplerState,
521        rng: &mut Philox4x32,
522    ) {
523        let v = logits.len();
524        if v == 0 {
525            return;
526        }
527        if !state.mirostat_mu.is_finite() {
528            state.mirostat_mu = 2.0 * self.tau;
529        }
530        let mu = state.mirostat_mu.max(1e-6);
531        // Softmax to get probabilities and sort descending.
532        let mut probs = logits.to_vec();
533        softmax_inplace(&mut probs);
534        let sorted = sorted_desc(&probs);
535        // Pareto-tail s-hat from top-m. Surprise[i] = −log p[i].
536        let m = self.m.min(sorted.len()).max(2);
537        let mut num = 0.0f32;
538        let mut den = 0.0f32;
539        for i in 0..(m - 1) {
540            let t = ((i + 2) as f32 / (i + 1) as f32).ln();
541            let b = (sorted[i].1 / sorted[i + 1].1).ln().max(1e-9);
542            num += t * b;
543            den += t * t;
544        }
545        let s_hat = if den > 0.0 { num / den } else { 1.0 };
546        // k = ((eps * 2^μ) / (1 − N^(−eps))) ^ (1 / s_hat)
547        let eps = (s_hat - 1.0).abs().max(1e-3);
548        let k_real = ((eps * (2.0f32.powf(mu))) / (1.0 - (v as f32).powf(-eps)))
549            .powf(1.0 / s_hat)
550            .clamp(1.0, v as f32);
551        let k = k_real as usize;
552        if k < sorted.len() {
553            let cutoff = sorted[k - 1].1;
554            for (i, p) in probs.iter_mut().enumerate() {
555                if *p < cutoff {
556                    *p = 0.0;
557                }
558                let _ = i;
559            }
560            let s: f32 = probs.iter().sum();
561            if s > 0.0 {
562                for p in probs.iter_mut() {
563                    *p /= s;
564                }
565            }
566        }
567        // Draw the token now; convert back into logits by setting one-hot
568        // (downstream samplers softmax+sample again, which is OK since one-hot).
569        let tok = sample_from_probs(&probs, rng) as usize;
570        let surprise = if probs[tok] > 0.0 {
571            -probs[tok].ln() / 2.0f32.ln()
572        } else {
573            mu
574        };
575        state.mirostat_mu = (mu - self.eta * (surprise - self.tau)).max(0.0);
576        for (i, x) in logits.iter_mut().enumerate() {
577            *x = if i == tok {
578                f32::INFINITY
579            } else {
580                f32::NEG_INFINITY
581            };
582        }
583    }
584}
585
586// ─── Mirostat v2 ─────────────────────────────────────────────────────
587//
588// Simpler variant (commonly shipped by llama.cpp): keep tokens whose
589// surprise (in bits) is ≤ μ; sample, then update μ ← μ − η(s − τ).
590
591#[derive(Debug, Clone, Copy)]
592pub struct MirostatV2 {
593    pub tau: f32,
594    pub eta: f32,
595}
596
597impl Default for MirostatV2 {
598    fn default() -> Self {
599        Self { tau: 5.0, eta: 0.1 }
600    }
601}
602
603impl Sampler for MirostatV2 {
604    fn apply(
605        &self,
606        logits: Logits<'_>,
607        _h: &[u32],
608        state: &mut SamplerState,
609        rng: &mut Philox4x32,
610    ) {
611        let v = logits.len();
612        if v == 0 {
613            return;
614        }
615        if !state.mirostat_mu.is_finite() {
616            state.mirostat_mu = 2.0 * self.tau;
617        }
618        let mu = state.mirostat_mu;
619        let mut probs = logits.to_vec();
620        softmax_inplace(&mut probs);
621        // Sort descending; keep tokens with surprise ≤ μ.
622        let mut sorted = sorted_desc(&probs);
623        let ln2 = 2.0f32.ln();
624        let mut keep_n = 0usize;
625        for (i, (_, p)) in sorted.iter().enumerate() {
626            let s = if *p > 0.0 {
627                -p.ln() / ln2
628            } else {
629                f32::INFINITY
630            };
631            if s > mu {
632                break;
633            }
634            keep_n = i + 1;
635        }
636        if keep_n == 0 {
637            keep_n = 1;
638        }
639        let kept: std::collections::HashSet<usize> =
640            sorted.drain(..keep_n).map(|(i, _)| i).collect();
641        for (i, p) in probs.iter_mut().enumerate() {
642            if !kept.contains(&i) {
643                *p = 0.0;
644            }
645        }
646        let s: f32 = probs.iter().sum();
647        if s > 0.0 {
648            for p in probs.iter_mut() {
649                *p /= s;
650            }
651        }
652        let tok = sample_from_probs(&probs, rng) as usize;
653        let surprise = if probs[tok] > 0.0 {
654            -probs[tok].ln() / ln2
655        } else {
656            mu
657        };
658        state.mirostat_mu = (mu - self.eta * (surprise - self.tau)).max(0.0);
659        for (i, x) in logits.iter_mut().enumerate() {
660            *x = if i == tok {
661                f32::INFINITY
662            } else {
663                f32::NEG_INFINITY
664            };
665        }
666    }
667}
668
669// ─── XTC (eXclude Top Choices) ───────────────────────────────────────
670//
671// Drops top tokens whose prob > `threshold`, with probability `prob`, as
672// long as at least one such "high-confidence" token would remain. Forces
673// the model into long-tail tokens to break repetition / mode collapse.
674
675#[derive(Debug, Clone, Copy)]
676pub struct Xtc {
677    pub threshold: f32,
678    pub prob: f32,
679    /// Keep this many top tokens minimum even after exclusion.
680    pub min_keep: usize,
681}
682
683impl Sampler for Xtc {
684    fn apply(&self, logits: Logits<'_>, _h: &[u32], _s: &mut SamplerState, rng: &mut Philox4x32) {
685        if self.prob <= 0.0 {
686            return;
687        }
688        if rng.next_f32() > self.prob {
689            return;
690        }
691        let v = logits.len();
692        if v == 0 {
693            return;
694        }
695        let mut probs = logits.to_vec();
696        softmax_inplace(&mut probs);
697        let sorted = sorted_desc(&probs);
698        // Count tokens above threshold.
699        let n_above = sorted.iter().filter(|(_, p)| *p > self.threshold).count();
700        if n_above < 2 {
701            return; // not enough to exclude
702        }
703        // Exclude all but the lowest-ranked one above threshold so
704        // exactly one survives.
705        let to_kill = n_above.saturating_sub(self.min_keep.max(1));
706        for (idx, _) in sorted.iter().take(to_kill) {
707            logits[*idx] = f32::NEG_INFINITY;
708        }
709    }
710}
711
712// ─── DRY (Don't Repeat Yourself) ─────────────────────────────────────
713//
714// For each suffix of `history` of length n ≥ `allowed_length`, if the
715// next token would extend the suffix into a known n-gram match, scale
716// its logit down by `multiplier * base^(n − allowed_length)`. Caps at
717// `max_ngram` to bound work.
718
719#[derive(Debug, Clone)]
720pub struct Dry {
721    pub multiplier: f32,
722    pub base: f32,
723    pub allowed_length: usize,
724    pub max_ngram: usize,
725    /// Tokens that break a repetition run (e.g. newlines for prose, EOS).
726    pub sequence_breakers: Vec<u32>,
727}
728
729impl Default for Dry {
730    fn default() -> Self {
731        Self {
732            multiplier: 0.8,
733            base: 1.75,
734            allowed_length: 2,
735            max_ngram: 32,
736            sequence_breakers: Vec::new(),
737        }
738    }
739}
740
741impl Sampler for Dry {
742    fn apply(
743        &self,
744        logits: Logits<'_>,
745        history: &[u32],
746        _s: &mut SamplerState,
747        _r: &mut Philox4x32,
748    ) {
749        if self.multiplier <= 0.0 || history.is_empty() {
750            return;
751        }
752        let n = history.len();
753        let max_ngram = self.max_ngram.min(n);
754        let breakers: std::collections::HashSet<u32> =
755            self.sequence_breakers.iter().copied().collect();
756        // For each candidate next token t (only tokens that actually appear
757        // in history can match — bail early on the rest), find the longest
758        // suffix of history that matches a prefix ending at some earlier i.
759        // Track the longest match per token.
760        let mut longest: std::collections::HashMap<u32, usize> = std::collections::HashMap::new();
761        for i in 0..n.saturating_sub(1) {
762            if breakers.contains(&history[i]) {
763                continue;
764            }
765            // Length of common tail starting at history[..=i] (matching i with
766            // n-1, i-1 with n-2, …).
767            let mut l = 0usize;
768            while l < max_ngram && i >= l && n > l && history[i - l] == history[n - 1 - l] {
769                l += 1;
770            }
771            if l >= self.allowed_length && i + 1 < n {
772                let next = history[i + 1];
773                let cur = longest.entry(next).or_insert(0);
774                if l > *cur {
775                    *cur = l;
776                }
777            }
778        }
779        for (tok, l) in longest {
780            let pen = self.multiplier * self.base.powi((l - self.allowed_length) as i32);
781            let idx = tok as usize;
782            if idx < logits.len() {
783                logits[idx] -= pen;
784            }
785        }
786    }
787}
788
789// ─── Repetition penalty ──────────────────────────────────────────────
790
791#[derive(Debug, Clone, Copy)]
792pub struct RepetitionPenalty {
793    pub penalty: f32,
794    pub frequency: f32,
795    pub presence: f32,
796    /// Last N tokens of history to consider.
797    pub last_n: usize,
798}
799
800impl Default for RepetitionPenalty {
801    fn default() -> Self {
802        Self {
803            penalty: 1.0,
804            frequency: 0.0,
805            presence: 0.0,
806            last_n: 64,
807        }
808    }
809}
810
811impl Sampler for RepetitionPenalty {
812    fn apply(
813        &self,
814        logits: Logits<'_>,
815        history: &[u32],
816        _s: &mut SamplerState,
817        _r: &mut Philox4x32,
818    ) {
819        if history.is_empty() {
820            return;
821        }
822        let start = history.len().saturating_sub(self.last_n);
823        let window = &history[start..];
824        let mut counts: std::collections::HashMap<u32, u32> = std::collections::HashMap::new();
825        for &t in window {
826            *counts.entry(t).or_insert(0) += 1;
827        }
828        for (tok, c) in counts {
829            let idx = tok as usize;
830            if idx >= logits.len() {
831                continue;
832            }
833            // OpenAI-style: subtract presence + frequency * count.
834            logits[idx] -= self.presence + self.frequency * c as f32;
835            // Anthropic/llama.cpp-style: divide (for positive logits) or
836            // multiply (negative) by `penalty`.
837            if (self.penalty - 1.0).abs() > 1e-6 {
838                if logits[idx] > 0.0 {
839                    logits[idx] /= self.penalty;
840                } else {
841                    logits[idx] *= self.penalty;
842                }
843            }
844        }
845    }
846}
847
848// ─── tests ───────────────────────────────────────────────────────────
849
850#[cfg(test)]
851mod tests {
852    use super::*;
853
854    fn rng() -> Philox4x32 {
855        Philox4x32::new(0xDEAD_BEEF)
856    }
857
858    #[test]
859    fn temperature_zero_is_greedy_after_chain() {
860        let chain = SamplerChain::builder()
861            .push(Temperature { t: 1e-6 })
862            .build();
863        let mut state = SamplerState::new();
864        let mut r = rng();
865        let mut logits = vec![1.0, 5.0, 2.0, 3.0];
866        let tok = chain.sample(&mut logits, &[], &mut state, &mut r);
867        assert_eq!(tok, 1);
868    }
869
870    #[test]
871    fn top_k_masks_below_kth() {
872        let mut logits = vec![1.0, 5.0, 2.0, 3.0];
873        let mut s = SamplerState::new();
874        let mut r = rng();
875        TopK { k: 2 }.apply(&mut logits, &[], &mut s, &mut r);
876        assert_eq!(logits[1], 5.0);
877        assert_eq!(logits[3], 3.0);
878        assert!(logits[0].is_infinite() && logits[0] < 0.0);
879        assert!(logits[2].is_infinite() && logits[2] < 0.0);
880    }
881
882    #[test]
883    fn top_p_keeps_nucleus() {
884        let mut logits = vec![0.0f32; 4];
885        logits[0] = 10.0;
886        logits[1] = 5.0;
887        let mut s = SamplerState::new();
888        let mut r = rng();
889        TopP {
890            p: 0.5,
891            min_keep: 1,
892        }
893        .apply(&mut logits, &[], &mut s, &mut r);
894        assert!(logits[0].is_finite());
895        // Lowest-probability tokens should be masked.
896        assert!(logits[2].is_infinite() && logits[2] < 0.0);
897        assert!(logits[3].is_infinite() && logits[3] < 0.0);
898    }
899
900    #[test]
901    fn min_p_scales_cutoff_by_top_prob() {
902        // Dominant token: prob ~ exp(10)/Z ≈ 1; a 0.1 cutoff keeps only it.
903        let mut logits = vec![0.0f32; 4];
904        logits[0] = 10.0;
905        logits[1] = 2.0;
906        let mut s = SamplerState::new();
907        let mut r = rng();
908        MinP {
909            p: 0.1,
910            min_keep: 1,
911        }
912        .apply(&mut logits, &[], &mut s, &mut r);
913        assert!(logits[0].is_finite());
914        assert!(logits[1].is_infinite() && logits[1] < 0.0);
915        assert!(logits[2].is_infinite() && logits[2] < 0.0);
916    }
917
918    #[test]
919    fn min_p_min_keep_admits_top_tokens() {
920        // Even with an aggressive cutoff, min_keep survivors are guaranteed.
921        let mut logits = vec![10.0, 9.0, 1.0, 0.0];
922        let mut s = SamplerState::new();
923        let mut r = rng();
924        MinP {
925            p: 0.99,
926            min_keep: 2,
927        }
928        .apply(&mut logits, &[], &mut s, &mut r);
929        assert!(logits[0].is_finite() && logits[1].is_finite());
930    }
931
932    #[test]
933    fn logit_bias_is_additive_and_bounds_checked() {
934        let mut logits = vec![0.0f32, 1.0, 2.0];
935        apply_logit_bias(&mut logits, &[(1, 5.0), (99, 1.0)]); // 99 ignored
936        assert_eq!(logits, vec![0.0, 6.0, 2.0]);
937    }
938
939    #[test]
940    fn top_n_sigma_keeps_top_logits() {
941        // One peak, rest noise.
942        let mut logits = vec![0.0f32; 32];
943        logits[0] = 10.0;
944        logits[1] = 9.5;
945        let mut s = SamplerState::new();
946        let mut r = rng();
947        TopNSigma { n: 1.0 }.apply(&mut logits, &[], &mut s, &mut r);
948        // Peak survives, the long flat tail is killed.
949        assert!(logits[0].is_finite());
950        assert!(logits[5].is_infinite() && logits[5] < 0.0);
951    }
952
953    #[test]
954    fn dynamic_temperature_scales_with_entropy() {
955        // Uniform input → maximum entropy → T = max.
956        let mut logits = vec![1.0f32; 16];
957        let before = logits.clone();
958        let mut s = SamplerState::new();
959        let mut r = rng();
960        DynamicTemperature {
961            min: 0.5,
962            max: 2.0,
963            exponent: 1.0,
964        }
965        .apply(&mut logits, &[], &mut s, &mut r);
966        assert!((logits[0] - before[0] / 2.0).abs() < 1e-5);
967    }
968
969    #[test]
970    fn typical_p_keeps_typical_token() {
971        let mut logits = vec![5.0, 4.0, 0.0, -10.0];
972        let mut s = SamplerState::new();
973        let mut r = rng();
974        TypicalP {
975            p: 0.5,
976            min_keep: 1,
977        }
978        .apply(&mut logits, &[], &mut s, &mut r);
979        // At least one token survives.
980        assert!(logits.iter().any(|x| x.is_finite()));
981    }
982
983    #[test]
984    fn mirostat_v2_keeps_at_least_one() {
985        let mut logits = vec![1.0, 2.0, 3.0, 4.0];
986        let mut s = SamplerState::new();
987        let mut r = rng();
988        MirostatV2 { tau: 5.0, eta: 0.1 }.apply(&mut logits, &[], &mut s, &mut r);
989        // Result is a one-hot encoding of the drawn token.
990        let n_inf = logits
991            .iter()
992            .filter(|x| x.is_infinite() && **x > 0.0)
993            .count();
994        assert_eq!(n_inf, 1);
995    }
996
997    #[test]
998    fn xtc_disabled_when_prob_zero() {
999        let mut logits = vec![10.0, 5.0, 1.0];
1000        let before = logits.clone();
1001        let mut s = SamplerState::new();
1002        let mut r = rng();
1003        Xtc {
1004            threshold: 0.5,
1005            prob: 0.0,
1006            min_keep: 1,
1007        }
1008        .apply(&mut logits, &[], &mut s, &mut r);
1009        assert_eq!(logits, before);
1010    }
1011
1012    #[test]
1013    fn dry_penalises_repeat_continuation() {
1014        // History "A B A B A" → continuing the "B" pattern after "A" should be penalised.
1015        let history = vec![0u32, 1, 0, 1, 0];
1016        let mut logits = vec![0.0, 0.0];
1017        let mut s = SamplerState::new();
1018        let mut r = rng();
1019        Dry {
1020            multiplier: 1.0,
1021            base: 2.0,
1022            allowed_length: 2,
1023            max_ngram: 8,
1024            sequence_breakers: vec![],
1025        }
1026        .apply(&mut logits, &history, &mut s, &mut r);
1027        assert!(logits[1] < 0.0, "B should be penalised; got {}", logits[1]);
1028    }
1029
1030    #[test]
1031    fn repetition_penalty_lowers_repeated_token() {
1032        let history = vec![0u32; 8];
1033        let mut logits = vec![1.0, 1.0];
1034        let mut s = SamplerState::new();
1035        let mut r = rng();
1036        RepetitionPenalty {
1037            penalty: 2.0,
1038            frequency: 0.0,
1039            presence: 0.0,
1040            last_n: 64,
1041        }
1042        .apply(&mut logits, &history, &mut s, &mut r);
1043        assert!(logits[0] < logits[1]);
1044    }
1045}