Skip to main content

rlx_runtime/
samplers.rs

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