use ferrum_types::{Result, SamplingParams, TokenId};
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha12Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
pub const SAMPLING_RNG_ALGORITHM_ID: &str = "chacha12-rand-core-pcg32-u64-v1";
#[derive(Clone, Debug)]
pub struct SamplingRng {
inner: ChaCha12Rng,
}
impl SamplingRng {
pub fn seeded(seed: u64) -> Self {
Self {
inner: ChaCha12Rng::seed_from_u64(seed),
}
}
pub fn from_seed_bytes(seed: [u8; 32]) -> Self {
Self {
inner: ChaCha12Rng::from_seed(seed),
}
}
pub fn from_entropy() -> Self {
let mut entropy = rand::rng();
Self {
inner: ChaCha12Rng::from_rng(&mut entropy),
}
}
pub const fn algorithm_id() -> &'static str {
SAMPLING_RNG_ALGORITHM_ID
}
}
impl RngCore for SamplingRng {
fn next_u32(&mut self) -> u32 {
self.inner.next_u32()
}
fn next_u64(&mut self) -> u64 {
self.inner.next_u64()
}
fn fill_bytes(&mut self, dest: &mut [u8]) {
self.inner.fill_bytes(dest);
}
}
#[derive(Debug)]
pub struct SamplingContext<'a> {
pub step: usize,
pub sampling_params: &'a SamplingParams,
pub logits: &'a mut [f32],
pub previous_tokens: &'a [TokenId],
pub token_frequencies: &'a HashMap<TokenId, usize>,
pub vocab_size: usize,
pub metadata: HashMap<String, f32>,
}
impl<'a> SamplingContext<'a> {
pub fn new(
step: usize,
sampling_params: &'a SamplingParams,
logits: &'a mut [f32],
previous_tokens: &'a [TokenId],
token_frequencies: &'a HashMap<TokenId, usize>,
vocab_size: usize,
) -> Self {
Self {
step,
sampling_params,
logits,
previous_tokens,
token_frequencies,
vocab_size,
metadata: HashMap::new(),
}
}
pub fn get_logit(&self, token_id: TokenId) -> Option<f32> {
if usize::from(token_id) < self.logits.len() {
Some(self.logits[usize::from(token_id)])
} else {
None
}
}
pub fn set_logit(&mut self, token_id: TokenId, value: f32) -> bool {
if usize::from(token_id) < self.logits.len() {
self.logits[usize::from(token_id)] = value;
true
} else {
false
}
}
pub fn mask_tokens(&mut self, token_ids: &[TokenId]) {
for &token_id in token_ids {
if usize::from(token_id) < self.logits.len() {
self.logits[usize::from(token_id)] = f32::NEG_INFINITY;
}
}
}
}
pub trait LogitsProcessor: Send + Sync {
fn process(&self, ctx: &mut SamplingContext) -> Result<()>;
fn name(&self) -> &str;
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::Normal
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub enum ProcessorPriority {
High = 3,
Normal = 2,
Low = 1,
}
pub trait Sampler: Send + Sync {
fn sample(&self, logits: &[f32], rng: &mut dyn RngCore) -> Result<TokenId>;
fn sample_with_context(&self, ctx: &SamplingContext, rng: &mut dyn RngCore) -> Result<TokenId> {
self.sample(ctx.logits, rng)
}
fn name(&self) -> &str;
fn is_deterministic(&self) -> bool;
}
pub trait MultiSampler: Sampler {
fn sample_multiple(
&self,
logits: &[f32],
num_samples: usize,
rng: &mut dyn RngCore,
) -> Result<Vec<TokenId>>;
fn sample_with_probabilities(
&self,
logits: &[f32],
rng: &mut dyn RngCore,
) -> Result<(TokenId, Vec<f32>)>;
}
pub struct LogitsProcessorChain {
processors: Vec<Box<dyn LogitsProcessor>>,
}
impl LogitsProcessorChain {
pub fn new() -> Self {
Self {
processors: Vec::new(),
}
}
pub fn add_processor(mut self, processor: Box<dyn LogitsProcessor>) -> Self {
self.processors.push(processor);
self.processors
.sort_by(|a, b| b.priority().cmp(&a.priority()));
self
}
pub fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
for processor in &self.processors {
processor.process(ctx)?;
}
Ok(())
}
pub fn processor_names(&self) -> Vec<&str> {
self.processors.iter().map(|p| p.name()).collect()
}
pub fn is_empty(&self) -> bool {
self.processors.is_empty()
}
}
impl fmt::Debug for LogitsProcessorChain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_list()
.entries(self.processors.iter().map(|processor| processor.name()))
.finish()
}
}
impl Default for LogitsProcessorChain {
fn default() -> Self {
Self::new()
}
}
pub struct TemperatureProcessor {
pub temperature: f32,
}
impl TemperatureProcessor {
pub fn new(temperature: f32) -> Self {
Self { temperature }
}
}
impl LogitsProcessor for TemperatureProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.temperature > 0.0 && self.temperature != 1.0 {
for logit in ctx.logits.iter_mut() {
*logit /= self.temperature;
}
}
Ok(())
}
fn name(&self) -> &str {
"temperature"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::Normal
}
}
pub struct TopKProcessor {
pub k: usize,
}
impl TopKProcessor {
pub fn new(k: usize) -> Self {
Self { k }
}
}
impl LogitsProcessor for TopKProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.k > 0 && self.k < ctx.logits.len() {
let mut indices: Vec<usize> = (0..ctx.logits.len()).collect();
indices.sort_by(|&a, &b| {
ctx.logits[b]
.partial_cmp(&ctx.logits[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let threshold = ctx.logits[indices[self.k - 1]];
for logit in ctx.logits.iter_mut() {
if *logit < threshold {
*logit = f32::NEG_INFINITY;
}
}
}
Ok(())
}
fn name(&self) -> &str {
"top_k"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::Low
}
}
pub struct TopPProcessor {
pub p: f32,
}
impl TopPProcessor {
pub fn new(p: f32) -> Self {
Self { p }
}
}
impl LogitsProcessor for TopPProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.p < 1.0 && self.p > 0.0 {
let mut candidates = ctx
.logits
.iter()
.copied()
.enumerate()
.filter(|(_, logit)| logit.is_finite())
.collect::<Vec<_>>();
if candidates.is_empty() {
return Ok(());
}
candidates.sort_by(|(left_idx, left), (right_idx, right)| {
right.total_cmp(left).then_with(|| left_idx.cmp(right_idx))
});
let max_logit = candidates[0].1;
let sum = candidates
.iter()
.map(|(_, logit)| (*logit - max_logit).exp())
.sum::<f32>();
let mut cum_prob = 0.0;
let mut cutoff_idx = candidates.len();
for (i, (_, logit)) in candidates.iter().enumerate() {
cum_prob += (*logit - max_logit).exp() / sum;
if cum_prob >= self.p {
cutoff_idx = i + 1;
break;
}
}
for (idx, _) in candidates.into_iter().skip(cutoff_idx) {
ctx.logits[idx] = f32::NEG_INFINITY;
}
}
Ok(())
}
fn name(&self) -> &str {
"top_p"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::Low
}
}
pub struct MinPProcessor {
pub min_p: f32,
}
impl MinPProcessor {
pub fn new(min_p: f32) -> Self {
Self { min_p }
}
}
impl LogitsProcessor for MinPProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.min_p > 0.0 && self.min_p <= 1.0 {
let max_logit = ctx.logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let threshold = max_logit + self.min_p.ln();
for logit in ctx.logits.iter_mut() {
if *logit < threshold {
*logit = f32::NEG_INFINITY;
}
}
}
Ok(())
}
fn name(&self) -> &str {
"min_p"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::Low
}
}
pub struct RepetitionPenaltyProcessor {
pub penalty: f32,
}
impl RepetitionPenaltyProcessor {
pub fn new(penalty: f32) -> Self {
Self { penalty }
}
}
impl LogitsProcessor for RepetitionPenaltyProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.penalty != 1.0 {
for &token_id in ctx.token_frequencies.keys() {
if usize::from(token_id) >= ctx.logits.len() {
continue;
}
let idx = usize::from(token_id);
let current_logit = ctx.logits[idx];
if current_logit > 0.0 {
ctx.logits[idx] = current_logit / self.penalty;
} else {
ctx.logits[idx] = current_logit * self.penalty;
}
}
}
Ok(())
}
fn name(&self) -> &str {
"repetition_penalty"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::High }
}
pub struct PresenceFrequencyPenaltyProcessor {
pub presence_penalty: f32,
pub frequency_penalty: f32,
}
impl PresenceFrequencyPenaltyProcessor {
pub fn new(presence_penalty: f32, frequency_penalty: f32) -> Self {
Self {
presence_penalty,
frequency_penalty,
}
}
}
impl LogitsProcessor for PresenceFrequencyPenaltyProcessor {
fn process(&self, ctx: &mut SamplingContext) -> Result<()> {
if self.presence_penalty == 0.0 && self.frequency_penalty == 0.0 {
return Ok(());
}
for (&token_id, &count) in ctx.token_frequencies {
let idx = usize::from(token_id);
if idx >= ctx.logits.len() || count == 0 {
continue;
}
ctx.logits[idx] -= self.presence_penalty + self.frequency_penalty * count as f32;
}
Ok(())
}
fn name(&self) -> &str {
"presence_frequency_penalty"
}
fn priority(&self) -> ProcessorPriority {
ProcessorPriority::High
}
}
pub struct GreedySampler;
impl Sampler for GreedySampler {
fn sample(&self, logits: &[f32], _rng: &mut dyn RngCore) -> Result<TokenId> {
let max_idx = logits
.iter()
.enumerate()
.filter(|(_, logit)| logit.is_finite())
.reduce(|best, candidate| match candidate.1.total_cmp(best.1) {
std::cmp::Ordering::Greater => candidate,
std::cmp::Ordering::Equal if candidate.0 < best.0 => candidate,
_ => best,
})
.map(|(idx, _)| idx)
.ok_or_else(|| {
ferrum_types::FerrumError::backend("No finite logits available for sampling")
})?;
Ok(TokenId::new(max_idx as u32))
}
fn name(&self) -> &str {
"greedy"
}
fn is_deterministic(&self) -> bool {
true
}
}
#[cfg(test)]
mod greedy_sampler_tests {
use super::{GreedySampler, Sampler, SamplingRng};
#[test]
fn ties_choose_the_lowest_token_id() {
let mut rng = SamplingRng::seeded(1);
let token = GreedySampler
.sample(&[-1.0, 4.0, 4.0, f32::NAN], &mut rng)
.unwrap();
assert_eq!(token.get(), 1);
}
#[test]
fn non_finite_logits_are_never_selected() {
let mut rng = SamplingRng::seeded(1);
let token = GreedySampler
.sample(&[f32::NAN, f32::INFINITY, -2.0], &mut rng)
.unwrap();
assert_eq!(token.get(), 2);
assert!(GreedySampler
.sample(&[f32::NAN, f32::INFINITY], &mut rng)
.is_err());
}
}
pub struct MultinomialSampler;
impl Sampler for MultinomialSampler {
fn sample(&self, logits: &[f32], rng: &mut dyn RngCore) -> Result<TokenId> {
let max_logit = logits.iter().fold(f32::NEG_INFINITY, |a, &b| a.max(b));
let mut probs: Vec<f32> = logits
.iter()
.map(|&logit| {
if logit.is_finite() && logit > f32::NEG_INFINITY {
(logit - max_logit).exp()
} else {
0.0
}
})
.collect();
let sum: f32 = probs.iter().sum();
if sum <= 0.0 {
return Err(ferrum_types::FerrumError::backend(
"No valid tokens for sampling",
));
}
for prob in probs.iter_mut() {
*prob /= sum;
}
let threshold = rng.next_u32() as f32 / u32::MAX as f32;
let mut cumulative = 0.0;
for (idx, prob) in probs.iter().enumerate() {
cumulative += prob;
if cumulative >= threshold {
return Ok(TokenId::new(idx as u32));
}
}
Ok(TokenId::new((probs.len() - 1) as u32))
}
fn name(&self) -> &str {
"multinomial"
}
fn is_deterministic(&self) -> bool {
false
}
}
pub struct SamplingConfigBuilder {
processors: Vec<Box<dyn LogitsProcessor>>,
sampler: Option<Box<dyn Sampler>>,
}
impl SamplingConfigBuilder {
pub fn new() -> Self {
Self {
processors: Vec::new(),
sampler: None,
}
}
pub fn with_temperature(mut self, temperature: f32) -> Self {
if temperature > 0.0 && temperature != 1.0 {
self.processors
.push(Box::new(TemperatureProcessor::new(temperature)));
}
self
}
pub fn with_top_k(mut self, k: usize) -> Self {
if k > 0 {
self.processors.push(Box::new(TopKProcessor::new(k)));
}
self
}
pub fn with_top_p(mut self, p: f32) -> Self {
if p > 0.0 && p < 1.0 {
self.processors.push(Box::new(TopPProcessor::new(p)));
}
self
}
pub fn with_min_p(mut self, min_p: f32) -> Self {
if min_p > 0.0 && min_p <= 1.0 {
self.processors.push(Box::new(MinPProcessor::new(min_p)));
}
self
}
pub fn with_repetition_penalty(mut self, penalty: f32) -> Self {
if penalty != 1.0 {
self.processors
.push(Box::new(RepetitionPenaltyProcessor::new(penalty)));
}
self
}
pub fn with_presence_frequency_penalty(
mut self,
presence_penalty: f32,
frequency_penalty: f32,
) -> Self {
if presence_penalty != 0.0 || frequency_penalty != 0.0 {
self.processors
.push(Box::new(PresenceFrequencyPenaltyProcessor::new(
presence_penalty,
frequency_penalty,
)));
}
self
}
pub fn with_sampler(mut self, sampler: Box<dyn Sampler>) -> Self {
self.sampler = Some(sampler);
self
}
pub fn build(self) -> SamplingConfig {
let mut chain = LogitsProcessorChain::new();
for processor in self.processors {
chain = chain.add_processor(processor);
}
let sampler = self.sampler.unwrap_or_else(|| Box::new(MultinomialSampler));
SamplingConfig {
processor_chain: chain,
sampler,
}
}
}
impl Default for SamplingConfigBuilder {
fn default() -> Self {
Self::new()
}
}
pub struct SamplingConfig {
pub processor_chain: LogitsProcessorChain,
pub sampler: Box<dyn Sampler>,
}
impl fmt::Debug for SamplingConfig {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SamplingConfig")
.field("processor_chain", &self.processor_chain)
.field("sampler", &self.sampler.name())
.finish()
}
}
impl SamplingConfig {
pub fn from_params(params: &SamplingParams) -> Self {
let mut builder = SamplingConfigBuilder::new()
.with_temperature(params.temperature)
.with_repetition_penalty(params.repetition_penalty)
.with_presence_frequency_penalty(params.presence_penalty, params.frequency_penalty);
if let Some(min_p) = params.min_p {
builder = builder.with_min_p(min_p);
}
if let Some(top_k) = params.top_k {
builder = builder.with_top_k(top_k);
}
if params.top_p < 1.0 {
builder = builder.with_top_p(params.top_p);
}
let sampler: Box<dyn Sampler> = if params.temperature == 0.0 {
Box::new(GreedySampler)
} else {
Box::new(MultinomialSampler)
};
builder.with_sampler(sampler).build()
}
pub fn supports_raw_greedy_speculation(&self) -> bool {
self.sampler.is_deterministic() && self.processor_chain.is_empty()
}
pub fn sample(&self, mut ctx: SamplingContext, rng: &mut dyn RngCore) -> Result<TokenId> {
self.processor_chain.process(&mut ctx)?;
self.sampler.sample_with_context(&ctx, rng)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SamplingStats {
pub total_samples: u64,
pub avg_sample_time_us: f64,
pub token_distribution: HashMap<TokenId, u64>,
pub effective_temperature: f32,
pub processor_times: HashMap<String, f64>,
}