use rlx_ir::Philox4x32;
use rlx_runtime::samplers::{
Dry, DynamicTemperature, MinP, MirostatV1, MirostatV2, RepetitionPenalty, SamplerChain,
SamplerState, Temperature, TopK, TopNSigma, TopP, TypicalP, Xtc,
};
pub use rlx_runtime::samplers::apply_logit_bias;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum MirostatMode {
#[default]
Off,
V1,
V2,
}
pub const DRY_BREAKERS_MAX: usize = 32;
#[derive(Debug, Clone, Copy)]
pub struct SampleOpts {
pub temperature: f32,
pub top_k: usize,
pub top_p: f32,
pub seed: u64,
pub greedy: bool,
pub dynamic_temp: Option<(f32, f32)>,
pub dynamic_temp_exponent: f32,
pub typical_p: f32,
pub top_n_sigma: f32,
pub min_p: f32,
pub xtc_threshold: f32,
pub xtc_prob: f32,
pub dry_multiplier: f32,
pub dry_base: f32,
pub dry_allowed_length: usize,
pub dry_max_ngram: usize,
pub dry_sequence_breakers: [u32; DRY_BREAKERS_MAX],
pub dry_sequence_breakers_len: u8,
pub mirostat: MirostatMode,
pub mirostat_tau: f32,
pub mirostat_eta: f32,
pub mirostat_m: usize,
pub repetition_penalty: f32,
pub frequency_penalty: f32,
pub presence_penalty: f32,
pub repetition_window: usize,
pub min_keep: usize,
}
impl SampleOpts {
pub fn greedy() -> Self {
Self {
temperature: 1.0,
top_k: 0,
top_p: 1.0,
seed: 0,
greedy: true,
dynamic_temp: None,
dynamic_temp_exponent: 1.0,
typical_p: 1.0,
top_n_sigma: 0.0,
min_p: 0.0,
xtc_threshold: 0.0,
xtc_prob: 0.0,
dry_multiplier: 0.0,
dry_base: 1.75,
dry_allowed_length: 2,
dry_max_ngram: 32,
dry_sequence_breakers: [0; DRY_BREAKERS_MAX],
dry_sequence_breakers_len: 0,
mirostat: MirostatMode::Off,
mirostat_tau: 5.0,
mirostat_eta: 0.1,
mirostat_m: 100,
repetition_penalty: 1.0,
frequency_penalty: 0.0,
presence_penalty: 0.0,
repetition_window: 64,
min_keep: 1,
}
}
pub fn temperature(temp: f32, seed: u64) -> Self {
Self {
temperature: temp,
top_k: 0,
top_p: 1.0,
seed,
greedy: false,
..Self::greedy()
}
}
pub fn with_top_k(mut self, k: usize) -> Self {
self.top_k = k;
self
}
pub fn with_top_p(mut self, p: f32) -> Self {
self.top_p = p;
self
}
pub fn with_dynamic_temp(mut self, min: f32, max: f32) -> Self {
self.dynamic_temp = Some((min, max));
self.greedy = false;
self
}
pub fn with_typical_p(mut self, p: f32) -> Self {
self.typical_p = p;
self.greedy = false;
self
}
pub fn with_top_n_sigma(mut self, n: f32) -> Self {
self.top_n_sigma = n;
self.greedy = false;
self
}
pub fn with_min_p(mut self, p: f32) -> Self {
self.min_p = p;
self.greedy = false;
self
}
pub fn with_xtc(mut self, threshold: f32, prob: f32) -> Self {
self.xtc_threshold = threshold;
self.xtc_prob = prob;
self.greedy = false;
self
}
pub fn with_dry(mut self, multiplier: f32, base: f32, allowed_length: usize) -> Self {
self.dry_multiplier = multiplier;
self.dry_base = base;
self.dry_allowed_length = allowed_length;
self.greedy = false;
self
}
pub fn with_mirostat_v1(mut self, tau: f32, eta: f32) -> Self {
self.mirostat = MirostatMode::V1;
self.mirostat_tau = tau;
self.mirostat_eta = eta;
self.greedy = false;
self
}
pub fn with_mirostat_v2(mut self, tau: f32, eta: f32) -> Self {
self.mirostat = MirostatMode::V2;
self.mirostat_tau = tau;
self.mirostat_eta = eta;
self.greedy = false;
self
}
pub fn with_repetition_penalty(mut self, p: f32) -> Self {
self.repetition_penalty = p;
self
}
pub fn with_frequency_presence(mut self, frequency: f32, presence: f32) -> Self {
self.frequency_penalty = frequency;
self.presence_penalty = presence;
self
}
pub fn is_classic(&self) -> bool {
self.dynamic_temp.is_none()
&& self.typical_p >= 1.0
&& self.top_n_sigma <= 0.0
&& self.min_p <= 0.0
&& self.xtc_prob <= 0.0
&& self.dry_multiplier <= 0.0
&& self.mirostat == MirostatMode::Off
&& self.frequency_penalty == 0.0
&& self.presence_penalty == 0.0
&& (self.repetition_penalty - 1.0).abs() < f32::EPSILON
}
pub fn into_chain(&self) -> SamplerChain {
let mut b = SamplerChain::builder();
if (self.repetition_penalty - 1.0).abs() > f32::EPSILON
|| self.frequency_penalty != 0.0
|| self.presence_penalty != 0.0
{
b = b.push(RepetitionPenalty {
penalty: self.repetition_penalty,
frequency: self.frequency_penalty,
presence: self.presence_penalty,
last_n: self.repetition_window,
});
}
if self.dry_multiplier > 0.0 {
b = b.push(Dry {
multiplier: self.dry_multiplier,
base: self.dry_base,
allowed_length: self.dry_allowed_length,
max_ngram: self.dry_max_ngram,
sequence_breakers: self.dry_sequence_breakers
[..self.dry_sequence_breakers_len as usize]
.to_vec(),
});
}
if self.mirostat == MirostatMode::Off {
if let Some((mn, mx)) = self.dynamic_temp {
b = b.push(DynamicTemperature {
min: mn,
max: mx,
exponent: self.dynamic_temp_exponent,
});
} else if self.temperature > 0.0 && (self.temperature - 1.0).abs() > f32::EPSILON {
b = b.push(Temperature {
t: self.temperature,
});
}
}
if self.top_k > 0 {
b = b.push(TopK { k: self.top_k });
}
if self.typical_p < 1.0 && self.typical_p > 0.0 {
b = b.push(TypicalP {
p: self.typical_p,
min_keep: self.min_keep,
});
}
if self.top_p < 1.0 && self.top_p > 0.0 {
b = b.push(TopP {
p: self.top_p,
min_keep: self.min_keep,
});
}
if self.min_p > 0.0 {
b = b.push(MinP {
p: self.min_p,
min_keep: self.min_keep,
});
}
if self.top_n_sigma > 0.0 {
b = b.push(TopNSigma {
n: self.top_n_sigma,
});
}
if self.xtc_prob > 0.0 && self.xtc_threshold > 0.0 {
b = b.push(Xtc {
threshold: self.xtc_threshold,
prob: self.xtc_prob,
min_keep: self.min_keep,
});
}
match self.mirostat {
MirostatMode::Off => {}
MirostatMode::V1 => {
b = b.push(MirostatV1 {
tau: self.mirostat_tau,
eta: self.mirostat_eta,
m: self.mirostat_m,
});
}
MirostatMode::V2 => {
b = b.push(MirostatV2 {
tau: self.mirostat_tau,
eta: self.mirostat_eta,
});
}
}
b.build()
}
}
pub fn sample_token(logits: &[f32], opts: SampleOpts) -> usize {
sample_token_at(logits, opts, 0)
}
pub fn sample_token_at(logits: &[f32], opts: SampleOpts, step: u64) -> usize {
if opts.is_classic() {
return sample_token_inner(logits, opts, step);
}
sample_token_with_history(logits, &opts, &[], step) as usize
}
pub fn sample_token_with_history(
logits: &[f32],
opts: &SampleOpts,
history: &[u32],
step: u64,
) -> u32 {
let mut state = SamplerState::new();
sample_token_stateful(logits, opts, history, step, &mut state)
}
pub fn sample_token_stateful(
logits: &[f32],
opts: &SampleOpts,
history: &[u32],
step: u64,
state: &mut SamplerState,
) -> u32 {
let mut work = logits.to_vec();
let chain = opts.into_chain();
let mut rng = Philox4x32::new(opts.seed.wrapping_add(step));
chain.sample(&mut work, history, state, &mut rng)
}
fn sample_token_inner(logits: &[f32], opts: SampleOpts, step: u64) -> usize {
assert!(!logits.is_empty(), "sample_token: empty logits");
if opts.greedy {
return argmax(logits);
}
let mut work: Vec<f32> = if opts.temperature > 0.0 && opts.temperature != 1.0 {
logits.iter().map(|&l| l / opts.temperature).collect()
} else {
logits.to_vec()
};
if opts.top_k > 0 && opts.top_k < work.len() {
let mut indexed: Vec<(usize, f32)> =
work.iter().enumerate().map(|(i, &v)| (i, v)).collect();
indexed.sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
let cutoff = indexed[opts.top_k - 1].1;
for v in work.iter_mut() {
if *v < cutoff {
*v = f32::NEG_INFINITY;
}
}
}
let max = work.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut probs: Vec<f32> = work.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = probs.iter().sum();
if sum > 0.0 {
for p in probs.iter_mut() {
*p /= sum;
}
} else {
return argmax(logits);
}
if opts.top_p < 1.0 && opts.top_p > 0.0 {
let mut order: Vec<usize> = (0..probs.len()).collect();
order.sort_unstable_by(|&a, &b| {
probs[b]
.partial_cmp(&probs[a])
.unwrap_or(std::cmp::Ordering::Equal)
});
let mut cum = 0.0f32;
let mut keep = vec![false; probs.len()];
for &i in &order {
cum += probs[i];
keep[i] = true;
if cum >= opts.top_p {
break;
}
}
let mut renorm = 0.0f32;
for (i, p) in probs.iter_mut().enumerate() {
if !keep[i] {
*p = 0.0;
} else {
renorm += *p;
}
}
if renorm > 0.0 {
for p in probs.iter_mut() {
*p /= renorm;
}
}
}
let mut rng = Philox4x32::new(opts.seed.wrapping_add(step));
let u = rng.next_f32();
let mut acc = 0.0f32;
for (i, &p) in probs.iter().enumerate() {
acc += p;
if u < acc {
return i;
}
}
probs.len() - 1
}
fn argmax(xs: &[f32]) -> usize {
let mut best = 0usize;
let mut best_v = f32::NEG_INFINITY;
for (i, &v) in xs.iter().enumerate() {
if v > best_v {
best_v = v;
best = i;
}
}
best
}
pub fn apply_repetition_penalty(
logits: &mut [f32],
counts: &std::collections::HashMap<u32, u32>,
penalty: f32,
) {
if penalty <= 1.0 || counts.is_empty() {
return;
}
for (&tok, &count) in counts {
let idx = tok as usize;
if idx >= logits.len() {
continue;
}
let factor = penalty.powi(count as i32);
if logits[idx] > 0.0 {
logits[idx] /= factor;
} else {
logits[idx] *= factor;
}
}
}
pub fn softmax_logits(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let mut p: Vec<f32> = logits.iter().map(|&l| (l - max).exp()).collect();
let sum: f32 = p.iter().sum();
if sum > 0.0 {
for v in p.iter_mut() {
*v /= sum;
}
}
p
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn greedy_matches_argmax() {
let logits = vec![0.1, 0.5, 0.2, -1.0, 0.49];
let t = sample_token(&logits, SampleOpts::greedy());
assert_eq!(t, 1);
}
#[test]
fn top_k_one_equals_greedy() {
let logits = vec![0.1, 0.5, 0.2, -1.0, 0.49];
let opts = SampleOpts::temperature(1.0, 42).with_top_k(1);
assert_eq!(sample_token(&logits, opts), 1);
}
#[test]
fn top_p_full_equals_unrestricted_multinomial() {
let logits = vec![1.0, 2.0, 0.5, 0.0];
let opts = SampleOpts::temperature(1.0, 7).with_top_p(1.0);
let t = sample_token(&logits, opts);
assert!(t < logits.len());
}
#[test]
fn deterministic_for_same_seed() {
let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.01).collect();
let opts = SampleOpts::temperature(0.7, 123).with_top_k(4);
let a = sample_token(&logits, opts);
let b = sample_token(&logits, opts);
assert_eq!(a, b);
}
#[test]
fn top_p_truncates_low_mass() {
let mut logits = vec![-10.0f32; 16];
logits[7] = 10.0;
let opts = SampleOpts::temperature(1.0, 999).with_top_p(0.5);
assert_eq!(sample_token(&logits, opts), 7);
}
#[test]
fn high_temperature_still_returns_valid_id() {
let logits = vec![0.0; 10];
let opts = SampleOpts::temperature(100.0, 1);
let t = sample_token(&logits, opts);
assert!(t < 10);
}
#[test]
fn sample_token_at_varies_rng_by_step() {
let logits: Vec<f32> = (0..64).map(|i| (i as f32) * 0.05).collect();
let opts = SampleOpts::temperature(0.9, 100).with_top_p(1.0);
let a = sample_token_at(&logits, opts, 0);
let b = sample_token_at(&logits, opts, 1);
assert_ne!(a, b);
}
#[test]
fn mirostat_v2_routes_through_chain() {
let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.1).collect();
let opts = SampleOpts::temperature(0.7, 42).with_mirostat_v2(5.0, 0.1);
assert!(!opts.is_classic());
let t = sample_token(&logits, opts);
assert!(t < 32);
}
#[test]
fn dynamic_temp_routes_through_chain() {
let logits: Vec<f32> = (0..32).map(|i| (i as f32) * 0.1).collect();
let opts = SampleOpts::temperature(1.0, 7).with_dynamic_temp(0.5, 1.5);
assert!(!opts.is_classic());
let t = sample_token(&logits, opts);
assert!(t < 32);
}
#[test]
fn classic_path_unchanged_with_new_fields() {
let opts = SampleOpts::greedy();
assert!(opts.is_classic());
let opts = SampleOpts::temperature(0.7, 1)
.with_top_k(40)
.with_top_p(0.9);
assert!(opts.is_classic());
}
#[test]
fn min_p_routes_through_chain_and_truncates() {
let mut logits = vec![-10.0f32; 16];
logits[7] = 10.0;
let opts = SampleOpts::temperature(1.0, 5).with_min_p(0.1);
assert!(!opts.is_classic());
assert_eq!(sample_token(&logits, opts), 7);
}
#[test]
fn logit_bias_steers_greedy() {
let mut logits = vec![0.1, 0.5, 0.2, -1.0];
apply_logit_bias(&mut logits, &[(2, 1.0)]);
assert_eq!(sample_token(&logits, SampleOpts::greedy()), 2);
}
#[test]
fn sample_token_with_history_threads_dry() {
let history = vec![0u32, 1, 0, 1, 0];
let logits = vec![0.0, 0.0];
let opts = SampleOpts::temperature(1.0, 0).with_dry(1.0, 2.0, 2);
let mut state = SamplerState::new();
let t = sample_token_stateful(&logits, &opts, &history, 0, &mut state);
assert!(t < 2);
}
}