use crate::config::GenerateOptions;
use crate::logits::{ProcessorContext, TokenId};
use rand::rngs::StdRng;
use rand::{Rng, SeedableRng};
pub(crate) struct SamplingRng {
rng: StdRng,
}
impl SamplingRng {
pub(crate) fn new(seed: Option<u64>) -> Self {
let rng = seed.map_or_else(StdRng::from_os_rng, StdRng::seed_from_u64);
Self { rng }
}
pub(crate) fn for_row(seed: Option<u64>, row_index: usize) -> Self {
Self::new(seed.map(|seed| seed.wrapping_add(row_index as u64)))
}
pub(crate) fn value_for(&mut self, options: &GenerateOptions) -> f32 {
if options.greedy || options.temperature == 0.0 {
0.0
} else {
self.rng.random()
}
}
}
pub trait Sampler: Send {
fn sample(&mut self, logits: &[f32], context: &ProcessorContext) -> TokenId;
fn name(&self) -> &str;
}
#[derive(Debug, Default, Clone, Copy)]
pub struct GreedySampler;
impl Sampler for GreedySampler {
fn sample(&mut self, logits: &[f32], _context: &ProcessorContext) -> TokenId {
sample_greedy(logits)
}
fn name(&self) -> &str {
"greedy"
}
}
#[derive(Debug, Clone, Copy)]
pub struct CategoricalSampler {
rng_value: f32,
}
impl CategoricalSampler {
pub fn new(rng_value: f32) -> Self {
Self { rng_value }
}
}
impl Sampler for CategoricalSampler {
fn sample(&mut self, logits: &[f32], _context: &ProcessorContext) -> TokenId {
sample_categorical(logits, self.rng_value)
}
fn name(&self) -> &str {
"categorical"
}
}
pub(crate) enum DefaultSampler {
Greedy(GreedySampler),
Categorical(CategoricalSampler),
}
impl Sampler for DefaultSampler {
fn sample(&mut self, logits: &[f32], context: &ProcessorContext) -> TokenId {
match self {
Self::Greedy(sampler) => sampler.sample(logits, context),
Self::Categorical(sampler) => sampler.sample(logits, context),
}
}
fn name(&self) -> &str {
match self {
Self::Greedy(sampler) => sampler.name(),
Self::Categorical(sampler) => sampler.name(),
}
}
}
pub(crate) fn default_sampler_for_options(
options: &GenerateOptions,
rng_value: f32,
) -> DefaultSampler {
if options.greedy || options.temperature == 0.0 {
DefaultSampler::Greedy(GreedySampler)
} else {
DefaultSampler::Categorical(CategoricalSampler::new(rng_value))
}
}
pub fn sample_greedy(logits: &[f32]) -> u32 {
let max_logit = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
if max_logit == f32::NEG_INFINITY {
return 0;
}
logits
.iter()
.position(|&logit| logit == max_logit)
.unwrap_or(0) as u32
}
pub fn sample_categorical(logits: &[f32], rng_value: f32) -> u32 {
if logits.is_empty() {
return 0;
}
let max_logit = logits
.iter()
.copied()
.filter(|v| !v.is_nan())
.fold(f32::NEG_INFINITY, f32::max);
if !max_logit.is_finite() {
return sample_greedy(logits);
}
let weights: Vec<f32> = logits
.iter()
.map(|&logit| {
if logit.is_nan() {
0.0
} else {
(logit - max_logit).exp()
}
})
.collect();
let exp_sum: f32 = weights.iter().sum();
if !exp_sum.is_finite() || exp_sum <= 0.0 {
return sample_greedy(logits);
}
let target = rng_value.clamp(0.0, 1.0);
let mut cumulative = 0.0;
for (i, weight) in weights.iter().enumerate() {
cumulative += *weight / exp_sum;
if target < cumulative {
return i as u32;
}
}
(logits.len() - 1) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greedy_chooses_lowest_id_for_ties() {
assert_eq!(sample_greedy(&[1.0, 3.0, 3.0]), 1);
}
#[test]
fn categorical_respects_rng_value() {
assert_eq!(sample_categorical(&[10.0, 0.0], 0.99), 0);
}
#[test]
fn sampler_trait_dispatch_matches_free_functions() {
let context = ProcessorContext::default();
let mut greedy = GreedySampler;
assert_eq!(greedy.sample(&[1.0, 2.0, 2.0], &context), 1);
let mut categorical = CategoricalSampler::new(0.75);
assert_eq!(categorical.sample(&[0.0, 0.0], &context), 1);
}
#[test]
fn seeded_sampling_is_reproducible_and_seed_sensitive() {
let options = GenerateOptions {
greedy: false,
seed: Some(42),
..Default::default()
};
let draw = |seed| {
let mut rng = SamplingRng::new(Some(seed));
(0..64)
.map(|_| sample_categorical(&[0.0, 0.0, 0.0], rng.value_for(&options)))
.collect::<Vec<_>>()
};
assert_eq!(draw(42), draw(42));
assert_ne!(draw(42), draw(43));
}
#[test]
fn categorical_sampling_matches_softmax_distribution() {
let logits = [0.0_f32, 1.0, 2.0];
let max_logit = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let weights = logits.map(|logit| (logit - max_logit).exp());
let total: f32 = weights.iter().sum();
let expected = weights.map(|weight| weight / total);
let options = GenerateOptions {
greedy: false,
..Default::default()
};
let mut rng = SamplingRng::new(Some(7));
let draws = 100_000;
let mut counts = [0_usize; 3];
for _ in 0..draws {
counts[sample_categorical(&logits, rng.value_for(&options)) as usize] += 1;
}
for (count, probability) in counts.into_iter().zip(expected) {
let observed = count as f32 / draws as f32;
assert!(
(observed - probability).abs() < 0.01,
"observed {observed}, expected {probability}"
);
}
assert!(counts.into_iter().all(|count| count > 0));
}
#[test]
fn greedy_is_seed_independent_and_does_not_advance_rng() {
let greedy = GenerateOptions {
greedy: true,
seed: Some(11),
..Default::default()
};
let sampled = GenerateOptions {
greedy: false,
seed: Some(11),
..Default::default()
};
let mut after_greedy = SamplingRng::new(greedy.seed);
for _ in 0..32 {
assert_eq!(sample_greedy(&[1.0, 3.0, 2.0]), 1);
assert_eq!(after_greedy.value_for(&greedy), 0.0);
}
let mut untouched = SamplingRng::new(greedy.seed);
assert_eq!(
after_greedy.value_for(&sampled),
untouched.value_for(&sampled)
);
}
}