use super::SamplingParams;
use crate::sampler_order::ChainStep;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) enum StepEffect {
Inert,
KeepsTheMaximum,
MovesTheArgmax,
}
pub(crate) fn step_effect(step: ChainStep, params: &SamplingParams) -> StepEffect {
let SamplingParams {
temperature,
top_p,
min_p,
top_k,
typical_p,
top_n_sigma,
xtc_probability: _,
xtc_threshold: _,
dry,
repetition_penalty,
penalty_last_n,
presence_penalty,
frequency_penalty,
sampler_order: _,
} = params;
match step {
ChainStep::Penalties => {
let neutral =
*repetition_penalty == 1.0 && *presence_penalty == 0.0 && *frequency_penalty == 0.0;
if *penalty_last_n == 0 || neutral {
StepEffect::Inert
} else {
StepEffect::MovesTheArgmax
}
}
ChainStep::Dry => {
if dry.is_enabled() {
StepEffect::MovesTheArgmax
} else {
StepEffect::Inert
}
}
ChainStep::TopNSigma => {
if *top_n_sigma <= 0.0 {
StepEffect::Inert
} else {
StepEffect::KeepsTheMaximum
}
}
ChainStep::TopK => {
if *top_k == 0 {
StepEffect::Inert
} else {
StepEffect::KeepsTheMaximum
}
}
ChainStep::TypP => {
if *typical_p >= 1.0 {
StepEffect::Inert
} else {
StepEffect::MovesTheArgmax
}
}
ChainStep::TopP => {
if *top_p >= 1.0 {
StepEffect::Inert
} else {
StepEffect::KeepsTheMaximum
}
}
ChainStep::MinP => {
if *min_p <= 0.0 {
StepEffect::Inert
} else {
StepEffect::KeepsTheMaximum
}
}
ChainStep::Xtc => {
if params.xtc_can_fire() {
StepEffect::MovesTheArgmax
} else {
StepEffect::Inert
}
}
ChainStep::Temperature => {
debug_assert!(!temperature.is_nan(), "a NaN temperature is not a chain");
StepEffect::KeepsTheMaximum
}
}
}
impl SamplingParams {
pub fn chain_keeps_the_argmax(&self) -> bool {
self.sampler_order.steps().iter().all(|&step| {
step == ChainStep::Penalties || step_effect(step, self) != StepEffect::MovesTheArgmax
})
}
pub fn greedy_equals_raw_argmax(&self) -> bool {
self.sampler_order
.steps()
.iter()
.all(|&step| step_effect(step, self) != StepEffect::MovesTheArgmax)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dry::{DryBreakers, DryParams};
use crate::penalty_window::PenaltyWindow;
use crate::sampler_order::SamplerOrder;
use crate::sampling::Sampler;
fn cli_defaults() -> SamplingParams {
SamplingParams {
temperature: 0.0,
top_p: 0.95,
min_p: 0.05,
top_k: 40,
repetition_penalty: 1.1,
penalty_last_n: 64,
..SamplingParams::default()
}
}
#[test]
fn the_default_repetition_penalty_forbids_a_raw_argmax_fold() {
let defaults = cli_defaults();
assert!(
!defaults.greedy_equals_raw_argmax(),
"a device argmax over raw logits skips the repetition penalty"
);
assert!(defaults.chain_keeps_the_argmax());
assert!(SamplingParams {
repetition_penalty: 1.0,
..defaults.clone()
}
.greedy_equals_raw_argmax());
assert!(SamplingParams {
penalty_last_n: 0,
..defaults.clone()
}
.greedy_equals_raw_argmax());
for moved in [
SamplingParams {
repetition_penalty: 1.0,
presence_penalty: 0.5,
..defaults.clone()
},
SamplingParams {
repetition_penalty: 1.0,
frequency_penalty: 0.5,
..defaults.clone()
},
] {
assert!(
!moved.greedy_equals_raw_argmax(),
"{moved:?} moves logits before the argmax"
);
}
assert!(SamplingParams {
sampler_order: SamplerOrder::from_names(["top_k", "top_p", "min_p", "temperature"])
.expect("a chain without penalties"),
..defaults
}
.greedy_equals_raw_argmax());
}
#[test]
fn a_penalty_that_moves_the_argmax_is_one_the_fold_must_not_skip() {
let logits = vec![4.0f32, 3.8, 1.0];
let history = || PenaltyWindow::new(&[], &[0]);
let params = SamplingParams {
repetition_penalty: 1.1,
..cli_defaults()
};
let chosen = Sampler::new(7).sample(&logits, ¶ms, history());
assert_eq!(chosen, 1, "the penalty demotes the raw argmax");
assert_ne!(
chosen,
super::super::argmax(&logits),
"raw argmax and the chain's answer differ, so a fold is wrong here"
);
assert!(!params.greedy_equals_raw_argmax());
let neutral = SamplingParams {
repetition_penalty: 1.0,
..params
};
assert!(neutral.greedy_equals_raw_argmax());
assert_eq!(
Sampler::new(7).sample(&logits, &neutral, history()),
super::super::argmax(&logits)
);
}
fn live_exemplar(step: ChainStep) -> SamplingParams {
let base = SamplingParams::default();
match step {
ChainStep::Penalties => SamplingParams {
repetition_penalty: 1.1,
..base
},
ChainStep::Dry => SamplingParams {
dry: DryParams::new(6.0, 1.1, 2, -1, 1024, DryBreakers::none()),
..base
},
ChainStep::TopNSigma => SamplingParams {
top_n_sigma: 1.0,
..base
},
ChainStep::TopK => SamplingParams { top_k: 40, ..base },
ChainStep::TypP => SamplingParams {
typical_p: 0.5,
..base
},
ChainStep::TopP => SamplingParams { top_p: 0.9, ..base },
ChainStep::MinP => SamplingParams {
min_p: 0.05,
..base
},
ChainStep::Xtc => SamplingParams {
xtc_probability: 1.0,
xtc_threshold: 0.1,
..base
},
ChainStep::Temperature => SamplingParams {
temperature: 0.8,
..base
},
}
}
#[test]
fn every_chain_step_is_classified_and_neutral_at_the_struct_defaults() {
let neutral = SamplingParams::default();
let steps = ChainStep::all();
assert_eq!(steps.len(), 9, "the chain gained or lost a step");
for step in steps {
let expected_at_rest = if step == ChainStep::Temperature {
StepEffect::KeepsTheMaximum
} else {
StepEffect::Inert
};
assert_eq!(
step_effect(step, &neutral),
expected_at_rest,
"{step:?} is not neutral at the struct's defaults"
);
assert_ne!(
step_effect(step, &live_exemplar(step)),
StepEffect::Inert,
"{step:?}'s exemplar does not switch it on, so its \
classification is never exercised"
);
}
assert!(neutral.greedy_equals_raw_argmax());
assert!(neutral.chain_keeps_the_argmax());
}
#[test]
fn a_step_that_moves_the_argmax_is_a_step_that_refuses_the_fold() {
for step in ChainStep::all() {
let live = live_exemplar(step);
let moves = step_effect(step, &live) == StepEffect::MovesTheArgmax;
assert_eq!(
!live.greedy_equals_raw_argmax(),
moves,
"{step:?}: the classification and the fold gate disagree"
);
let expected_after_penalties = if step == ChainStep::Penalties {
true
} else {
!moves
};
assert_eq!(
live.chain_keeps_the_argmax(),
expected_after_penalties,
"{step:?}: the post-penalty predicate excuses the wrong step"
);
}
}
#[test]
fn the_inert_verdict_matches_what_the_penalty_step_actually_does() {
use crate::sampling::penalties::apply_history_penalties;
let logits = vec![4.0f32, 3.8, -1.0];
for &rep in &[1.0f32, 1.1] {
for &pres in &[0.0f32, 0.5] {
for &freq in &[0.0f32, 0.5] {
for &last_n in &[0usize, 64] {
let params = SamplingParams {
repetition_penalty: rep,
presence_penalty: pres,
frequency_penalty: freq,
penalty_last_n: last_n,
..SamplingParams::default()
};
let mut scores = logits.clone();
apply_history_penalties(
&mut scores,
¶ms,
PenaltyWindow::new(&[], &[0, 1]),
);
let untouched = scores == logits;
let inert = step_effect(ChainStep::Penalties, ¶ms) == StepEffect::Inert;
assert_eq!(
inert,
untouched,
"rep={rep} pres={pres} freq={freq} last_n={last_n}: the \
classification says inert={inert} and the penalty step \
{}",
if untouched {
"changed nothing"
} else {
"changed the scores"
}
);
}
}
}
}
}
}