mod greedy_equivalence;
mod params;
mod penalties;
mod recommended;
mod rng;
pub use params::SamplingParams;
pub use recommended::{RecommendedSampling, RequestedSampling};
pub use rng::{LogitMask, Sampler};
use crate::penalty_window::PenaltyWindow;
use crate::sampler_chain::Candidates;
use crate::sampler_order::ChainStep;
use penalties::apply_history_penalties;
#[cfg(test)]
pub(crate) fn spread_logits(vocab: usize) -> Vec<f32> {
(0..vocab)
.map(|i| ((i as f32 * 12.9898).sin() * 43_758.547).fract() * 8.0 - 3.0)
.collect()
}
fn greedy_choice(
scores: Vec<f32>,
params: &SamplingParams,
history: PenaltyWindow<'_>,
xtc_roll: Option<f32>,
) -> usize {
if params.chain_keeps_the_argmax() {
return argmax(&scores);
}
argmax(&filtered_distribution(scores, params, history, xtc_roll))
}
pub fn sampling_distribution(
logits: &[f32],
params: &SamplingParams,
history: PenaltyWindow<'_>,
xtc_roll: Option<f32>,
) -> Vec<f32> {
let mut scores = logits.to_vec();
apply_history_penalties(&mut scores, params, history);
if params.temperature <= 0.0 {
let vocab = scores.len();
let chosen = greedy_choice(scores, params, history, xtc_roll);
let mut probs = vec![0.0f32; vocab];
if let Some(p) = probs.get_mut(chosen) {
*p = 1.0;
}
return probs;
}
filtered_distribution(scores, params, history, xtc_roll)
}
fn filtered_distribution(
scores: Vec<f32>,
params: &SamplingParams,
history: PenaltyWindow<'_>,
xtc_roll: Option<f32>,
) -> Vec<f32> {
let vocab = scores.len();
let mut candidates = Candidates::new(&scores);
for &step in params.sampler_order.steps() {
match step {
ChainStep::Penalties => {}
ChainStep::Dry => candidates.dry(¶ms.dry.penalties(history)),
ChainStep::TopNSigma => candidates.top_n_sigma(params.top_n_sigma),
ChainStep::TopK => candidates.top_k(params.top_k),
ChainStep::TypP => candidates.typical_p(params.typical_p),
ChainStep::TopP => candidates.top_p(params.top_p),
ChainStep::MinP => candidates.min_p(params.min_p),
ChainStep::Xtc => {
if let Some(chance) = xtc_roll {
candidates.xtc(params.xtc_probability, params.xtc_threshold, chance);
}
}
ChainStep::Temperature => candidates.temperature(params.temperature),
}
}
candidates.into_distribution(vocab)
}
fn argmax(logits: &[f32]) -> usize {
logits
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dry::{DryBreakers, DryParams};
use crate::sampler_order::SamplerOrder;
fn the_chain_ferrox_used_to_hardcode(
logits: &[f32],
params: &SamplingParams,
history: PenaltyWindow<'_>,
) -> Vec<f32> {
let mut scores = logits.to_vec();
apply_history_penalties(&mut scores, params, history);
let vocab = scores.len();
let mut candidates = Candidates::new(&scores);
candidates.top_k(params.top_k);
candidates.top_p(params.top_p);
candidates.min_p(params.min_p);
candidates.temperature(params.temperature);
candidates.into_distribution(vocab)
}
#[test]
fn the_default_order_is_the_chain_ferrox_already_ran() {
let logits = spread_logits(64);
let prompt = [3usize, 9, 17, 9];
let generated = [9usize, 40, 3];
for (temperature, top_k, top_p, min_p) in [
(0.8f32, 5usize, 0.9f32, 0.05f32),
(4.0, 3, 0.85, 0.2),
(0.2, 8, 0.95, 0.1),
(1.0, 40, 0.5, 0.02),
(0.8, 40, 0.95, 0.05),
] {
let params = SamplingParams {
temperature,
top_k,
top_p,
min_p,
repetition_penalty: 1.1,
presence_penalty: 0.3,
frequency_penalty: 0.4,
..SamplingParams::default()
};
let window = || PenaltyWindow::new(&prompt, &generated);
let expected = the_chain_ferrox_used_to_hardcode(&logits, ¶ms, window());
let actual = sampling_distribution(&logits, ¶ms, window(), None);
for (i, (a, e)) in actual.iter().zip(expected.iter()).enumerate() {
assert_eq!(
a.to_bits(),
e.to_bits(),
"token {i} at temp {temperature}, top_k {top_k}, top_p {top_p}, \
min_p {min_p}: the default order sampled {a} where the chain ferrox \
already ran gives {e}"
);
}
}
}
#[test]
fn spelling_out_the_default_chain_draws_the_same_tokens() {
let logits = spread_logits(48);
let base = SamplingParams {
temperature: 0.8,
top_k: 40,
top_p: 0.95,
min_p: 0.05,
repetition_penalty: 1.1,
..SamplingParams::default()
};
let spelled = SamplingParams {
sampler_order: "penalties;dry;top_n_sigma;top_k;typ_p;top_p;min_p;xtc;temperature"
.parse::<SamplerOrder>()
.expect("the default chain must parse"),
..base.clone()
};
let draw = |params: &SamplingParams| {
let mut sampler = Sampler::new(0xFE0);
let mut generated: Vec<usize> = Vec::new();
for _ in 0..64 {
let next = sampler.sample(&logits, params, PenaltyWindow::new(&[7], &generated));
generated.push(next);
}
generated
};
assert_eq!(draw(&base), draw(&spelled));
}
#[test]
fn running_the_temperature_first_keeps_a_different_candidate_set() {
let logits = vec![6.0f32, 4.0, 2.0, 0.0, -2.0, -4.0];
let params = |order: &str| SamplingParams {
temperature: 8.0,
top_p: 0.9,
top_k: 0,
min_p: 0.0,
sampler_order: order.parse().expect("chain"),
..SamplingParams::default()
};
let support = |order: &str| -> Vec<bool> {
sampling_distribution(&logits, ¶ms(order), PenaltyWindow::new(&[], &[]), None)
.iter()
.map(|&p| p > 0.0)
.collect()
};
let default = support("penalties;top_k;top_p;min_p;temperature");
let temperature_first = support("penalties;temperature;top_k;top_p;min_p");
assert_ne!(
default, temperature_first,
"reordering the chain must change which candidates survive, \
or the flag is decorative"
);
assert!(
temperature_first.iter().filter(|&&k| k).count()
> default.iter().filter(|&&k| k).count(),
"temp 8.0 flattens the distribution, so a later top-p keeps more: \
default={default:?} temperature_first={temperature_first:?}"
);
}
#[test]
fn a_sampler_absent_from_the_chain_does_not_filter() {
let logits = vec![4.0f32, 3.0, 2.0, 1.0];
let survivors = |params: &SamplingParams, roll: Option<f32>| {
sampling_distribution(&logits, params, PenaltyWindow::new(&[], &[]), roll)
.iter()
.filter(|&&p| p > 0.0)
.count()
};
let cases: Vec<(SamplingParams, &str, Option<f32>)> = vec![
(
SamplingParams {
temperature: 1.0,
min_p: 0.2,
..SamplingParams::default()
},
"penalties;top_k;top_p;temperature",
None,
),
(
SamplingParams {
temperature: 1.0,
typical_p: 0.5,
..SamplingParams::default()
},
"penalties;top_k;top_p;min_p;temperature",
None,
),
(
SamplingParams {
temperature: 1.0,
top_n_sigma: 0.5,
..SamplingParams::default()
},
"penalties;top_k;top_p;min_p;temperature",
None,
),
(
SamplingParams {
temperature: 1.0,
xtc_probability: 1.0,
xtc_threshold: 0.05,
..SamplingParams::default()
},
"penalties;top_k;top_p;min_p;temperature",
Some(0.0),
),
];
for (with, chain_without, roll) in cases {
let filtered = survivors(&with, roll);
assert!(
filtered < 4,
"the knob must bite when its step IS in the chain, \
or the second half proves nothing: {with:?}"
);
let without = SamplingParams {
sampler_order: chain_without.parse().expect("chain"),
..with.clone()
};
assert_eq!(
survivors(&without, roll),
4,
"the knob is set but its step is not in `{chain_without}`, \
so nothing should truncate: {without:?}"
);
}
}
#[test]
fn a_chain_without_penalties_does_not_penalise_on_either_path() {
let logits = vec![4.0f32, 3.9];
let history = || PenaltyWindow::new(&[0], &[]);
let greedy = SamplingParams {
temperature: 0.0,
repetition_penalty: 1.1,
..SamplingParams::default()
};
let mut sampler = Sampler::new(1);
assert_eq!(
sampler.sample(&logits, &greedy, history()),
1,
"the default chain penalises the prompt token"
);
let unpenalised = SamplingParams {
sampler_order: "top_k;top_p;min_p;temperature".parse().expect("chain"),
..greedy.clone()
};
assert!(!unpenalised.sampler_order.has_penalties());
assert_eq!(
sampler.sample(&logits, &unpenalised, history()),
0,
"`penalties` is not in the chain, so the argmax must stand"
);
let sampled = SamplingParams {
temperature: 1.0,
..unpenalised
};
let with = SamplingParams {
sampler_order: SamplerOrder::default(),
..sampled.clone()
};
assert_ne!(
sampling_distribution(&logits, &sampled, history(), None),
sampling_distribution(&logits, &with, history(), None)
);
}
#[test]
fn a_prompt_token_is_penalised_before_it_is_ever_generated() {
let params = SamplingParams {
temperature: 0.0,
repetition_penalty: 1.1,
..SamplingParams::default()
};
let logits = vec![4.0f32, 3.9];
let mut sampler = Sampler::new(1);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
0,
"with nothing behind it the argmax wins"
);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[0], &[])),
1,
"token 0 is in the prompt, so llama.cpp penalises it here"
);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[9, 0], &[8])),
1
);
}
#[test]
fn a_prompt_token_leaves_the_window_once_the_generation_outgrows_it() {
let params = SamplingParams {
temperature: 0.0,
repetition_penalty: 1.1,
penalty_last_n: 2,
..SamplingParams::default()
};
let logits = vec![4.0f32, 3.9];
let mut sampler = Sampler::new(1);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[0], &[5])),
1
);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[0], &[5, 6])),
0
);
}
#[test]
fn temperature_does_not_change_which_candidates_top_p_keeps() {
let logits = vec![3.0f32, 2.0, 1.0, 0.0];
let at = |temperature: f32| -> Vec<bool> {
let params = SamplingParams {
temperature,
top_p: 0.9,
top_k: 0,
..SamplingParams::default()
};
sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &[]), None)
.iter()
.map(|&p| p > 0.0)
.collect()
};
let cold = at(0.5);
let hot = at(4.0);
assert_eq!(
cold, hot,
"the surviving set must not depend on the temperature: \
cold={cold:?} hot={hot:?}"
);
assert!(
cold.iter().any(|&k| !k),
"top_p = 0.9 must drop at least one of these four candidates"
);
}
#[test]
fn min_p_truncates_at_ln_p_below_the_top_logit() {
let logits = vec![4.0f32, 3.0, 2.0, 1.0];
let params = SamplingParams {
temperature: 1.0,
min_p: 0.2,
..SamplingParams::default()
};
let probs = sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &[]), None);
assert!(probs[0] > 0.0 && probs[1] > 0.0);
assert_eq!(probs[2], 0.0, "2.0 is below 4 + ln(0.2) = 2.3905");
assert_eq!(probs[3], 0.0);
assert!((probs.iter().sum::<f32>() - 1.0).abs() < 1e-6);
assert!((probs[0] - 0.731_059).abs() < 1e-5, "got {}", probs[0]);
let off = SamplingParams {
min_p: 0.0,
..params.clone()
};
let unfiltered = sampling_distribution(&logits, &off, PenaltyWindow::new(&[], &[]), None);
assert!(unfiltered.iter().all(|&p| p > 0.0));
}
#[test]
fn temperature_does_not_change_which_candidates_min_p_keeps() {
let logits = vec![3.0f32, 2.0, 1.0, 0.0];
let survivors = |temperature: f32| -> Vec<bool> {
let params = SamplingParams {
temperature,
min_p: 0.2,
..SamplingParams::default()
};
sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &[]), None)
.iter()
.map(|&p| p > 0.0)
.collect()
};
let cold = survivors(0.5);
let warm = survivors(1.0);
let hot = survivors(2.0);
assert_eq!(cold, warm, "cold={cold:?} warm={warm:?}");
assert_eq!(warm, hot, "warm={warm:?} hot={hot:?}");
assert_eq!(warm, vec![true, true, false, false]);
}
#[test]
fn top_p_and_min_p_both_apply() {
let logits = vec![3.0f32, 2.0, 1.0, 0.0];
let params = SamplingParams {
temperature: 1.0,
top_p: 0.95,
min_p: 0.2,
..SamplingParams::default()
};
let probs = sampling_distribution(&logits, ¶ms, PenaltyWindow::new(&[], &[]), None);
assert_eq!(
probs.iter().map(|&p| p > 0.0).collect::<Vec<_>>(),
vec![true, true, false, false]
);
let top_p_only = SamplingParams {
min_p: 0.0,
..params.clone()
};
assert_eq!(
sampling_distribution(&logits, &top_p_only, PenaltyWindow::new(&[], &[]), None)
.iter()
.filter(|&&p| p > 0.0)
.count(),
3
);
}
#[test]
fn dry_moves_the_chosen_token_on_both_the_greedy_and_the_sampled_path() {
let logits = vec![0.0f32, 0.0, 5.0, 0.0];
let history = || PenaltyWindow::new(&[], &[0, 1, 2, 0, 1]);
let dry = DryParams::new(6.0, 1.1, 2, -1, 1024, DryBreakers::none());
let greedy = SamplingParams {
temperature: 0.0,
dry: dry.clone(),
..SamplingParams::default()
};
let mut sampler = Sampler::new(5);
assert_eq!(
sampler.sample(&logits, &SamplingParams::default(), history()),
2,
"without DRY token 2 is the argmax"
);
assert_ne!(
sampler.sample(&logits, &greedy, history()),
2,
"DRY subtracts 4.0 from token 2's logit of 5.0, so it loses"
);
let sampled = SamplingParams {
temperature: 1.0,
..greedy.clone()
};
let with = sampling_distribution(&logits, &sampled, history(), None);
let without = sampling_distribution(
&logits,
&SamplingParams {
dry: DryParams::off(),
..sampled
},
history(),
None,
);
assert!(with[2] < without[2], "with={with:?} without={without:?}");
}
#[test]
fn xtc_changes_the_greedy_choice_because_it_removes_the_top() {
let logits = vec![3.0f32, 2.9, -10.0];
let params = SamplingParams {
temperature: 0.0,
xtc_probability: 1.0,
xtc_threshold: 0.05,
..SamplingParams::default()
};
assert!(!params.chain_keeps_the_argmax());
let mut sampler = Sampler::new(11);
assert_eq!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
1,
"XTC drops token 0, so the greedy answer is token 1"
);
assert_eq!(
sampler.sample(
&logits,
&SamplingParams::default(),
PenaltyWindow::new(&[], &[])
),
0
);
}
#[test]
fn typical_p_can_drop_the_argmax_so_greedy_must_run_the_chain() {
let params = SamplingParams {
temperature: 0.0,
typical_p: 0.5,
..SamplingParams::default()
};
assert!(!params.chain_keeps_the_argmax());
assert!(SamplingParams {
typical_p: 1.0,
..params.clone()
}
.chain_keeps_the_argmax());
let logits: Vec<f32> = [0.4f32, 0.2, 0.2, 0.2].iter().map(|p| p.ln()).collect();
let mut sampler = Sampler::new(3);
assert_ne!(
sampler.sample(&logits, ¶ms, PenaltyWindow::new(&[], &[])),
0,
"typical-p drops the most likely token here"
);
}
#[test]
fn greedy_is_published_as_a_point_mass_not_a_special_case() {
let logits = vec![0.1, 0.9, 0.3, -0.2];
let probs = sampling_distribution(
&logits,
&SamplingParams::default(),
PenaltyWindow::new(&[], &[]),
None,
);
assert_eq!(probs, vec![0.0, 1.0, 0.0, 0.0]);
let penalized = sampling_distribution(
&logits,
&SamplingParams {
repetition_penalty: 100.0,
..SamplingParams::default()
},
PenaltyWindow::new(&[], &[1]),
None,
);
assert_eq!(penalized[1], 0.0);
assert_eq!(penalized.iter().sum::<f32>(), 1.0);
}
}