pub trait Sampler {
fn sample(&mut self, logits: &[f32]) -> u32;
}
#[derive(Debug, Clone, Copy, Default)]
pub struct GreedySampler;
impl Sampler for GreedySampler {
fn sample(&mut self, logits: &[f32]) -> u32 {
greedy_argmax(logits)
}
}
pub fn greedy_argmax(logits: &[f32]) -> u32 {
assert!(!logits.is_empty(), "greedy_argmax requires at least one logit");
let mut best_idx = 0usize;
let mut best_val = logits[0];
for (idx, &val) in logits.iter().enumerate().skip(1) {
if val > best_val {
best_idx = idx;
best_val = val;
}
}
best_idx as u32
}
#[derive(Debug, Clone)]
struct Rng(u64);
impl Rng {
fn new(seed: u64) -> Self {
Self(if seed == 0 { 0x9E37_79B9_7F4A_7C15 } else { seed })
}
fn next_u64(&mut self) -> u64 {
let mut x = self.0;
x ^= x >> 12;
x ^= x << 25;
x ^= x >> 27;
self.0 = x;
x.wrapping_mul(0x2545_F491_4F6C_DD1D)
}
fn next_unit_f32(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
}
}
#[derive(Debug, Clone)]
pub struct SamplingConfig {
pub temperature: f32,
pub top_k: Option<usize>,
pub top_p: Option<f32>,
pub min_p: Option<f32>,
pub repeat_penalty: f32,
pub repeat_last_n: usize,
pub seed: u64,
}
impl Default for SamplingConfig {
fn default() -> Self {
Self {
temperature: 1.0,
top_k: None,
top_p: None,
min_p: None,
repeat_penalty: 1.0,
repeat_last_n: 64,
seed: 0,
}
}
}
pub struct StochasticSampler {
config: SamplingConfig,
rng: Rng,
history: Vec<u32>,
}
impl StochasticSampler {
pub fn new(config: SamplingConfig) -> Self {
let rng = Rng::new(config.seed);
Self { config, rng, history: Vec::new() }
}
}
impl Sampler for StochasticSampler {
fn sample(&mut self, logits: &[f32]) -> u32 {
assert!(!logits.is_empty(), "StochasticSampler::sample requires at least one logit");
let mut work = logits.to_vec();
apply_repetition_penalty(&mut work, &self.history, self.config.repeat_penalty);
let chosen = if self.config.temperature <= 0.0 {
greedy_argmax(&work)
} else {
if let Some(k) = self.config.top_k {
apply_top_k(&mut work, k);
}
if let Some(p) = self.config.top_p {
apply_top_p(&mut work, p);
}
if let Some(mp) = self.config.min_p {
apply_min_p(&mut work, mp);
}
apply_temperature(&mut work, self.config.temperature);
sample_from_logits(&work, &mut self.rng)
};
self.history.push(chosen);
let window = self.config.repeat_last_n.max(1);
if self.history.len() > window {
self.history.remove(0);
}
chosen
}
}
fn apply_repetition_penalty(logits: &mut [f32], history: &[u32], penalty: f32) {
if penalty == 1.0 {
return;
}
let seen: std::collections::HashSet<u32> = history.iter().copied().collect();
for &id in &seen {
let Some(logit) = logits.get_mut(id as usize) else { continue };
*logit = if *logit > 0.0 { *logit / penalty } else { *logit * penalty };
}
}
fn apply_top_k(logits: &mut [f32], k: usize) {
if k == 0 || k >= logits.len() {
return;
}
let mut order: Vec<usize> = (0..logits.len()).collect();
order.sort_unstable_by(|&a, &b| logits[b].partial_cmp(&logits[a]).unwrap());
for &idx in &order[k..] {
logits[idx] = f32::NEG_INFINITY;
}
}
fn apply_top_p(logits: &mut [f32], p: f32) {
if p >= 1.0 {
return;
}
let probs = softmax_ignoring_masked(logits);
let mut order: Vec<usize> = (0..logits.len()).filter(|&i| probs[i] > 0.0).collect();
order.sort_unstable_by(|&a, &b| probs[b].partial_cmp(&probs[a]).unwrap());
let mut cumulative = 0.0f32;
let mut keep = order.len();
for (rank, &idx) in order.iter().enumerate() {
cumulative += probs[idx];
if cumulative > p {
keep = rank + 1; break;
}
}
for &idx in &order[keep..] {
logits[idx] = f32::NEG_INFINITY;
}
}
fn apply_min_p(logits: &mut [f32], min_p: f32) {
if min_p <= 0.0 {
return;
}
let probs = softmax_ignoring_masked(logits);
let max_prob = probs.iter().copied().fold(0.0f32, f32::max);
let threshold = min_p * max_prob;
for (idx, &pr) in probs.iter().enumerate() {
if pr < threshold {
logits[idx] = f32::NEG_INFINITY;
}
}
}
fn apply_temperature(logits: &mut [f32], temperature: f32) {
for logit in logits.iter_mut() {
if logit.is_finite() {
*logit /= temperature;
}
}
}
fn softmax_ignoring_masked(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
if !max.is_finite() {
let n = logits.len().max(1);
return vec![1.0 / n as f32; logits.len()];
}
let exps: Vec<f32> = logits.iter().map(|&l| if l.is_finite() { (l - max).exp() } else { 0.0 }).collect();
let sum: f32 = exps.iter().sum();
if sum <= 0.0 {
let n = logits.len().max(1);
return vec![1.0 / n as f32; logits.len()];
}
exps.into_iter().map(|e| e / sum).collect()
}
fn sample_from_logits(logits: &[f32], rng: &mut Rng) -> u32 {
let probs = softmax_ignoring_masked(logits);
let draw = rng.next_unit_f32();
let mut cumulative = 0.0f32;
for (idx, &pr) in probs.iter().enumerate() {
cumulative += pr;
if draw < cumulative {
return idx as u32;
}
}
greedy_argmax(&probs)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greedy_argmax_picks_the_highest_scoring_index() {
assert_eq!(greedy_argmax(&[0.1, 5.0, -3.0, 2.0]), 1);
}
#[test]
fn greedy_argmax_breaks_ties_toward_the_first_occurrence() {
assert_eq!(greedy_argmax(&[1.0, 3.0, 3.0, 0.0]), 1);
}
#[test]
fn greedy_argmax_handles_a_single_element() {
assert_eq!(greedy_argmax(&[42.0]), 0);
}
#[test]
fn greedy_sampler_is_deterministic_across_repeated_calls() {
let mut sampler = GreedySampler;
let logits = [0.5, 2.0, 1.0];
assert_eq!(sampler.sample(&logits), sampler.sample(&logits));
}
#[test]
#[should_panic]
fn greedy_argmax_rejects_empty_input() {
greedy_argmax(&[]);
}
#[test]
fn rng_with_the_same_seed_produces_the_same_sequence() {
let mut a = Rng::new(42);
let mut b = Rng::new(42);
let seq_a: Vec<u64> = (0..10).map(|_| a.next_u64()).collect();
let seq_b: Vec<u64> = (0..10).map(|_| b.next_u64()).collect();
assert_eq!(seq_a, seq_b);
}
#[test]
fn rng_with_different_seeds_produces_different_sequences() {
let mut a = Rng::new(1);
let mut b = Rng::new(2);
let seq_a: Vec<u64> = (0..10).map(|_| a.next_u64()).collect();
let seq_b: Vec<u64> = (0..10).map(|_| b.next_u64()).collect();
assert_ne!(seq_a, seq_b);
}
#[test]
fn rng_zero_seed_does_not_get_stuck_at_the_fixed_point() {
let mut rng = Rng::new(0);
let values: Vec<u64> = (0..5).map(|_| rng.next_u64()).collect();
assert!(values.iter().all(|&v| v != 0), "a stuck-at-zero PRNG would emit all zeros: {values:?}");
}
#[test]
fn rng_unit_f32_stays_within_zero_one() {
let mut rng = Rng::new(7);
for _ in 0..1000 {
let v = rng.next_unit_f32();
assert!((0.0..1.0).contains(&v), "{v} outside [0, 1)");
}
}
#[test]
fn repetition_penalty_of_one_is_a_no_op() {
let mut logits = vec![1.0, -1.0, 2.0];
apply_repetition_penalty(&mut logits, &[0, 1, 2], 1.0);
assert_eq!(logits, vec![1.0, -1.0, 2.0]);
}
#[test]
fn repetition_penalty_divides_positive_and_multiplies_negative_logits() {
let mut logits = vec![4.0, -4.0, 10.0];
apply_repetition_penalty(&mut logits, &[0, 1], 2.0);
assert_eq!(logits[0], 2.0); assert_eq!(logits[1], -8.0); assert_eq!(logits[2], 10.0); }
#[test]
fn repetition_penalty_ignores_duplicate_history_entries() {
let mut logits = vec![8.0];
apply_repetition_penalty(&mut logits, &[0, 0, 0], 2.0);
assert_eq!(logits[0], 4.0, "repeated history entries must not compound the penalty");
}
#[test]
fn top_k_keeps_exactly_the_k_highest_logits() {
let mut logits = vec![1.0, 5.0, 3.0, 4.0, 2.0];
apply_top_k(&mut logits, 2);
let kept: Vec<usize> = (0..5).filter(|&i| logits[i].is_finite()).collect();
assert_eq!(kept, vec![1, 3]); }
#[test]
fn top_k_zero_is_a_no_op() {
let mut logits = vec![1.0, 2.0, 3.0];
apply_top_k(&mut logits, 0);
assert!(logits.iter().all(|v| v.is_finite()));
}
#[test]
fn top_k_at_or_above_the_length_is_a_no_op() {
let mut logits = vec![1.0, 2.0, 3.0];
apply_top_k(&mut logits, 3);
assert!(logits.iter().all(|v| v.is_finite()));
let mut logits2 = vec![1.0, 2.0, 3.0];
apply_top_k(&mut logits2, 10);
assert!(logits2.iter().all(|v| v.is_finite()));
}
#[test]
fn top_p_includes_the_crossing_token() {
let logits = to_logits_from_probs(&[0.5, 0.3, 0.2]);
let mut work = logits.clone();
apply_top_p(&mut work, 0.6);
assert!(work[0].is_finite(), "token 0 (below the threshold on its own) must be kept");
assert!(work[1].is_finite(), "token 1 (the crossing token) must be kept, not excluded");
assert!(!work[2].is_finite(), "token 2 (after the crossing point) must be excluded");
}
#[test]
fn top_p_keeps_only_the_single_token_when_it_alone_exceeds_p() {
let logits = to_logits_from_probs(&[0.9, 0.05, 0.05]);
let mut work = logits.clone();
apply_top_p(&mut work, 0.5);
assert!(work[0].is_finite());
assert!(!work[1].is_finite());
assert!(!work[2].is_finite());
}
#[test]
fn top_p_of_one_or_above_is_a_no_op() {
let logits = to_logits_from_probs(&[0.5, 0.3, 0.2]);
let mut work = logits.clone();
apply_top_p(&mut work, 1.0);
assert!(work.iter().all(|v| v.is_finite()));
}
fn to_logits_from_probs(probs: &[f32]) -> Vec<f32> {
probs.iter().map(|p| p.ln()).collect()
}
#[test]
fn min_p_keeps_only_tokens_within_the_relative_threshold_of_the_max() {
let logits = to_logits_from_probs(&[0.7, 0.2, 0.1]);
let mut work = logits.clone();
apply_min_p(&mut work, 0.5);
assert!(work[0].is_finite());
assert!(!work[1].is_finite());
assert!(!work[2].is_finite());
}
#[test]
fn min_p_always_keeps_the_top_token() {
let logits = to_logits_from_probs(&[0.4, 0.35, 0.25]);
let mut work = logits.clone();
apply_min_p(&mut work, 0.99);
assert!(work[0].is_finite(), "the single most likely token can never be excluded by its own threshold");
}
#[test]
fn min_p_of_zero_or_below_is_a_no_op() {
let logits = to_logits_from_probs(&[0.7, 0.2, 0.1]);
let mut work = logits.clone();
apply_min_p(&mut work, 0.0);
assert!(work.iter().all(|v| v.is_finite()));
}
#[test]
fn softmax_ignoring_masked_treats_negative_infinity_as_zero_probability() {
let probs = softmax_ignoring_masked(&[1.0, f32::NEG_INFINITY, 1.0]);
assert_eq!(probs[1], 0.0);
assert!((probs[0] - 0.5).abs() < 1e-6);
assert!((probs[2] - 0.5).abs() < 1e-6);
}
#[test]
fn softmax_ignoring_masked_rows_sum_to_one() {
let probs = softmax_ignoring_masked(&[2.0, -1.0, 0.5, f32::NEG_INFINITY]);
let sum: f32 = probs.iter().sum();
assert!((sum - 1.0).abs() < 1e-5);
}
#[test]
fn temperature_zero_degrades_exactly_to_greedy() {
let logits = [0.1, 5.0, -3.0, 2.0, 5.0]; let mut greedy = GreedySampler;
let mut stochastic = StochasticSampler::new(SamplingConfig { temperature: 0.0, ..SamplingConfig::default() });
for _ in 0..5 {
assert_eq!(stochastic.sample(&logits), greedy.sample(&logits));
}
}
#[test]
fn negative_temperature_also_degrades_to_greedy() {
let logits = [1.0, 9.0, 2.0];
let mut sampler = StochasticSampler::new(SamplingConfig { temperature: -1.0, ..SamplingConfig::default() });
assert_eq!(sampler.sample(&logits), greedy_argmax(&logits));
}
#[test]
fn a_fixed_seed_reproduces_a_fixed_token_sequence() {
let logits_sequence: Vec<Vec<f32>> =
(0..20).map(|i| vec![1.0 + i as f32 * 0.1, 2.0, 0.5, 3.0 - i as f32 * 0.05, 1.5]).collect();
let config = SamplingConfig { temperature: 0.8, top_k: Some(3), seed: 12345, ..SamplingConfig::default() };
let run = |cfg: SamplingConfig| -> Vec<u32> {
let mut sampler = StochasticSampler::new(cfg);
logits_sequence.iter().map(|l| sampler.sample(l)).collect()
};
let first = run(config.clone());
let second = run(config);
assert_eq!(first, second, "the same seed must reproduce the exact same token sequence");
}
#[test]
fn different_seeds_usually_produce_different_sequences() {
let logits_sequence: Vec<Vec<f32>> = (0..20).map(|i| vec![1.0, 2.0 + i as f32 * 0.01, 1.5, 1.8]).collect();
let run = |seed: u64| -> Vec<u32> {
let cfg = SamplingConfig { temperature: 1.0, seed, ..SamplingConfig::default() };
let mut sampler = StochasticSampler::new(cfg);
logits_sequence.iter().map(|l| sampler.sample(l)).collect()
};
assert_ne!(run(1), run(2));
}
#[test]
fn stochastic_sampler_only_ever_emits_in_range_token_ids() {
let vocab = 7usize;
let cfg = SamplingConfig {
temperature: 1.2,
top_k: Some(4),
top_p: Some(0.9),
min_p: Some(0.05),
repeat_penalty: 1.1,
seed: 999,
..SamplingConfig::default()
};
let mut sampler = StochasticSampler::new(cfg);
let mut rng_for_logits = Rng::new(42);
for _ in 0..200 {
let logits: Vec<f32> = (0..vocab).map(|_| rng_for_logits.next_unit_f32() * 10.0 - 5.0).collect();
let id = sampler.sample(&logits);
assert!((id as usize) < vocab, "sampled id {id} out of range for vocab {vocab}");
}
}
#[test]
fn repetition_penalty_can_flip_which_token_greedy_decoding_picks() {
let mut sampler = StochasticSampler::new(SamplingConfig {
temperature: 0.0, repeat_penalty: 2.0,
repeat_last_n: 4,
..SamplingConfig::default()
});
let logits = [0.1, 5.0, 0.2, 4.9];
assert_eq!(sampler.sample(&logits), 1, "unpenalized, token 1 has the highest raw logit");
assert_eq!(
sampler.sample(&logits),
3,
"token 1's history-penalized logit (5.0 / 2.0 = 2.5) must now lose to token 3's untouched 4.9"
);
}
}