use infernum_core::SamplingParams;
pub struct Sampler {
params: SamplingParams,
rng: Option<fastrand::Rng>,
}
impl Sampler {
#[must_use]
pub fn new(params: SamplingParams) -> Self {
let rng = params.seed.map(|seed| {
let mut rng = fastrand::Rng::new();
rng.seed(seed);
rng
});
Self { params, rng }
}
#[must_use]
pub fn sample(&mut self, logits: &[f32]) -> u32 {
if self.params.temperature == 0.0 {
return self.argmax(logits);
}
let scaled: Vec<f32> = logits
.iter()
.map(|&l| l / self.params.temperature)
.collect();
let filtered = if self.params.top_k > 0 {
self.top_k_filter(&scaled, self.params.top_k as usize)
} else {
scaled
};
let filtered = if self.params.top_p < 1.0 {
self.top_p_filter(&filtered, self.params.top_p)
} else {
filtered
};
let filtered = if self.params.min_p > 0.0 {
self.min_p_filter(&filtered, self.params.min_p)
} else {
filtered
};
self.categorical_sample(&filtered)
}
fn argmax(&self, logits: &[f32]) -> u32 {
logits
.iter()
.enumerate()
.max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i as u32)
.unwrap_or(0)
}
fn top_k_filter(&self, logits: &[f32], k: usize) -> Vec<f32> {
let mut indexed: Vec<(usize, f32)> = logits.iter().copied().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let mut result = vec![f32::NEG_INFINITY; logits.len()];
for (i, v) in indexed.into_iter().take(k) {
result[i] = v;
}
result
}
fn top_p_filter(&self, logits: &[f32], p: f32) -> Vec<f32> {
let probs = self.softmax(logits);
let mut indexed: Vec<(usize, f32)> = probs.iter().copied().enumerate().collect();
indexed.sort_by(|(_, a), (_, b)| b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal));
let mut cumsum = 0.0;
let mut result = vec![f32::NEG_INFINITY; logits.len()];
for (i, prob) in indexed {
if cumsum < p {
result[i] = logits[i];
cumsum += prob;
}
}
result
}
fn min_p_filter(&self, logits: &[f32], min_p: f32) -> Vec<f32> {
let probs = self.softmax(logits);
let max_prob = probs.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let threshold = max_prob * min_p;
logits
.iter()
.zip(probs.iter())
.map(|(&l, &p)| if p >= threshold { l } else { f32::NEG_INFINITY })
.collect()
}
fn softmax(&self, logits: &[f32]) -> Vec<f32> {
let max = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
let exp: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = exp.iter().sum();
exp.iter().map(|&e| e / sum).collect()
}
fn categorical_sample(&mut self, logits: &[f32]) -> u32 {
let probs = self.softmax(logits);
let r = self.random_f32();
let mut cumsum = 0.0;
for (i, &p) in probs.iter().enumerate() {
cumsum += p;
if r < cumsum {
return i as u32;
}
}
(probs.len() - 1) as u32
}
fn random_f32(&mut self) -> f32 {
if let Some(rng) = &mut self.rng {
rng.f32()
} else {
fastrand::f32()
}
}
#[must_use]
pub fn params(&self) -> &SamplingParams {
&self.params
}
#[must_use]
pub fn is_stop_token(&self, text: &str) -> bool {
self.params
.stop_sequences
.iter()
.any(|stop| text.contains(stop))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greedy_sampling() {
let params = SamplingParams::greedy();
let mut sampler = Sampler::new(params);
let logits = vec![1.0, 5.0, 2.0, 0.5];
assert_eq!(sampler.sample(&logits), 1);
}
#[test]
fn test_deterministic_with_seed() {
let params = SamplingParams::balanced().with_seed(42);
let mut sampler1 = Sampler::new(params.clone());
let mut sampler2 = Sampler::new(params);
let logits = vec![1.0, 1.0, 1.0, 1.0];
assert_eq!(sampler1.sample(&logits), sampler2.sample(&logits));
}
}