use super::penalties::apply_history_penalties;
use super::{filtered_distribution, greedy_choice, SamplingParams};
use crate::penalty_window::PenaltyWindow;
pub type LogitMask<'a> = &'a mut dyn FnMut(&mut [f32]);
pub struct Sampler {
state: u64,
}
impl Sampler {
pub fn new(seed: u64) -> Self {
Sampler {
state: if seed == 0 { 0x9E3779B97F4A7C15 } else { seed },
}
}
fn next_u64(&mut self) -> u64 {
self.state ^= self.state << 13;
self.state ^= self.state >> 7;
self.state ^= self.state << 17;
self.state.wrapping_mul(0x2545F491_4F6CDD1D)
}
fn next_f32(&mut self) -> f32 {
(self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
}
pub fn xtc_roll(&mut self, params: &SamplingParams) -> Option<f32> {
if params.xtc_can_fire() {
Some(self.next_f32())
} else {
None
}
}
pub fn sample(
&mut self,
logits: &[f32],
params: &SamplingParams,
history: PenaltyWindow<'_>,
) -> usize {
self.sample_with_mask(logits, params, history, None)
}
pub fn sample_with_mask(
&mut self,
logits: &[f32],
params: &SamplingParams,
history: PenaltyWindow<'_>,
mut mask: Option<LogitMask<'_>>,
) -> usize {
let xtc_roll = self.xtc_roll(params);
if params.temperature <= 0.0 && mask.is_none() {
if logits.len() == 1 {
return logits[0] as usize;
}
let mut scores = logits.to_vec();
apply_history_penalties(&mut scores, params, history);
return greedy_choice(scores, params, history, xtc_roll);
}
let mut scores: Vec<f32> = logits.to_vec();
apply_history_penalties(&mut scores, params, history);
if let Some(m) = mask.as_mut() {
m(&mut scores);
}
if params.temperature <= 0.0 {
if scores.len() == 1 {
return scores[0] as usize;
}
return greedy_choice(scores, params, history, xtc_roll);
}
let probs = filtered_distribution(scores, params, history, xtc_roll);
self.sample_from(&probs)
}
pub fn uniform(&mut self) -> f32 {
self.next_f32()
}
pub fn sample_from(&mut self, probs: &[f32]) -> usize {
let draw = self.next_f32();
let mut cumulative = 0.0f32;
for (i, &p) in probs.iter().enumerate() {
cumulative += p;
if draw < cumulative {
return i;
}
}
probs
.iter()
.enumerate()
.rev()
.find(|&(_, &p)| p > 0.0)
.map(|(i, _)| i)
.unwrap_or(0)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sampling::{sampling_distribution, spread_logits};
#[test]
fn a_chain_without_xtc_does_not_consume_a_draw_for_it() {
let params = SamplingParams {
temperature: 1.0,
..SamplingParams::default()
};
let logits = spread_logits(32);
assert!(
Sampler::new(99).xtc_roll(¶ms).is_none(),
"the guard must refuse the draw, not merely ignore it"
);
let mut sampled_by_chain = Sampler::new(99);
let mut by_hand = Sampler::new(99);
for step in 0..16 {
let sampled = sampled_by_chain.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[]));
let probs = sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &[]), None);
assert_eq!(
sampled,
by_hand.sample_from(&probs),
"token {step} came off a different position in the stream"
);
}
assert_eq!(sampled_by_chain.uniform(), by_hand.uniform());
}
#[test]
fn temperature_zero_accepts_precomputed_argmax_singleton() {
let mut sampler = Sampler::new(1);
let params = SamplingParams::default();
assert_eq!(
sampler.sample(&[42.0], ¶ms, PenaltyWindow::new(&[], &[])),
42
);
let sampled = SamplingParams {
temperature: 0.8,
..SamplingParams::default()
};
assert_eq!(
sampler.sample(&[42.0], &sampled, PenaltyWindow::new(&[], &[])),
0
);
}
#[test]
fn temperature_zero_is_deterministic_greedy_argmax() {
let logits = vec![0.1, 0.9, 0.3, -0.2];
let params = SamplingParams::default();
let mut sampler = Sampler::new(42);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1
);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1
);
}
#[test]
fn high_temperature_can_pick_a_non_argmax_token_over_many_draws() {
let logits = vec![1.0, 1.0, 1.0, 1.0];
let params = SamplingParams {
temperature: 1.0,
..SamplingParams::default()
};
let mut sampler = Sampler::new(7);
let mut seen = std::collections::HashSet::new();
for _ in 0..200 {
seen.insert(sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])));
}
assert!(
seen.len() > 1,
"uniform logits at temperature=1.0 must produce more than one distinct token across 200 draws"
);
}
#[test]
fn top_k_one_is_equivalent_to_greedy() {
let logits = vec![0.1, 0.9, 0.3, -0.2];
let params = SamplingParams {
temperature: 1.0,
top_k: 1,
..SamplingParams::default()
};
let mut sampler = Sampler::new(123);
for _ in 0..20 {
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1
);
}
}
#[test]
fn top_p_near_zero_is_equivalent_to_greedy() {
let logits = vec![0.1, 5.0, 0.3, -0.2];
let params = SamplingParams {
temperature: 1.0,
top_p: 0.001,
..SamplingParams::default()
};
let mut sampler = Sampler::new(9);
for _ in 0..20 {
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1
);
}
}
#[test]
fn presence_and_frequency_penalties_reduce_seen_token_logits() {
let logits = vec![0.0, 5.0, 0.0];
let params = SamplingParams {
temperature: 1.0,
presence_penalty: 10.0,
frequency_penalty: 0.0,
..SamplingParams::default()
};
let mut sampler = Sampler::new(1);
let mut counts = [0usize; 3];
for _ in 0..500 {
counts[sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[1]))] += 1;
}
assert!(
counts[1] < 250,
"presence_penalty should discourage token 1; counts={counts:?}"
);
let params = SamplingParams {
temperature: 1.0,
presence_penalty: 0.0,
frequency_penalty: 10.0,
..SamplingParams::default()
};
let mut sampler = Sampler::new(2);
counts = [0; 3];
for _ in 0..500 {
counts[sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[1, 1, 1]))] += 1;
}
assert!(
counts[1] < 250,
"frequency_penalty should discourage repeated token 1; counts={counts:?}"
);
}
#[test]
fn repetition_penalty_reduces_probability_of_recently_seen_token() {
let logits = vec![0.0, 5.0, 0.0];
let params = SamplingParams {
temperature: 1.0,
repetition_penalty: 1000.0,
..SamplingParams::default()
};
let mut sampler = Sampler::new(3);
let mut counts = [0usize; 3];
for _ in 0..500 {
counts[sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[1]))] += 1;
}
assert!(
counts[1] < 250,
"heavily penalizing token 1 (already in history) should make it far less likely than its raw logit alone would suggest; got counts={counts:?}"
);
}
#[test]
fn low_seeds_do_not_bias_the_first_draw() {
let vocab = 8;
let logits = vec![0.0f32; vocab];
let params = SamplingParams {
temperature: 1.0,
..SamplingParams::default()
};
let seeds = 4_000u64;
let mut counts = vec![0usize; vocab];
for seed in 1..=seeds {
counts[Sampler::new(seed).sample(&logits, ¶ms, PenaltyWindow::new(&[], &[]))] += 1;
}
let expected = seeds as f64 / vocab as f64;
for (token, &c) in counts.iter().enumerate() {
assert!(
(c as f64 - expected).abs() < expected * 0.25,
"uniform logits: token {token} came up {c} times across {seeds} seeds, \
expected about {expected:.0} (counts={counts:?})"
);
}
}
#[test]
fn the_published_distribution_is_the_one_sample_actually_draws_from() {
let logits = vec![0.4, 2.0, -1.0, 1.2, 0.9, -0.3];
let params = SamplingParams {
temperature: 0.8,
top_p: 0.9,
top_k: 4,
repetition_penalty: 1.3,
..SamplingParams::default()
};
let history = [1usize, 4];
let claimed =
sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &history), None);
assert!((claimed.iter().sum::<f32>() - 1.0).abs() < 1e-5);
let draws = 100_000;
let mut counts = vec![0usize; logits.len()];
let mut sampler = Sampler::new(0xC0FFEE);
for _ in 0..draws {
counts[sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &history))] += 1;
}
for (i, &c) in counts.iter().enumerate() {
let empirical = c as f64 / draws as f64;
assert!(
(empirical - claimed[i] as f64).abs() < 0.01,
"token {i}: sample() draws it {empirical:.4} of the time but \
sampling_distribution claims {:.4}",
claimed[i]
);
}
}
#[test]
fn degenerate_all_zero_probability_falls_back_to_greedy() {
let logits = vec![0.1, 0.9, 0.3, -0.2];
let params = SamplingParams {
temperature: 1.0,
top_k: 1,
top_p: 1.0,
..SamplingParams::default()
};
let mut sampler = Sampler::new(1);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1
);
}
}