use crate::model::qwen35_config::{GenerateConfig, TopLogprob};
#[cfg_attr(not(feature = "metal-gpu"), allow(dead_code))]
pub(crate) fn argmax_f32_first_wins(logits: &[f32]) -> u32 {
let mut best_idx = 0u32;
let mut best_val = f32::NEG_INFINITY;
for (i, &v) in logits.iter().enumerate() {
if v > best_val {
best_val = v;
best_idx = i as u32;
}
}
best_idx
}
#[cfg(test)]
mod argmax_f32_first_wins_tests {
use super::argmax_f32_first_wins;
#[test]
fn tie_break_is_first_wins() {
let logits = [0.0_f32, 1.0, 1.0, 0.5];
assert_eq!(
argmax_f32_first_wins(&logits),
1,
"tied maximum logits must resolve to the FIRST index, not the last"
);
}
#[test]
fn no_tie_returns_true_max() {
let logits = [0.1_f32, -2.0, 5.5, -0.001];
assert_eq!(argmax_f32_first_wins(&logits), 2);
}
#[test]
fn all_negative_logits_still_find_true_max() {
let logits = [-5.0_f32, -1.0, -3.0];
assert_eq!(argmax_f32_first_wins(&logits), 1);
}
#[test]
fn nan_in_nonmax_position_is_skipped() {
let logits = [1.0_f32, f32::NAN, 2.0];
assert_eq!(argmax_f32_first_wins(&logits), 2);
}
#[test]
fn all_nan_fails_closed_to_zero() {
let logits = [f32::NAN, f32::NAN, f32::NAN];
assert_eq!(argmax_f32_first_wins(&logits), 0);
}
#[test]
fn empty_slice_fails_closed_to_zero() {
let logits: [f32; 0] = [];
assert_eq!(argmax_f32_first_wins(&logits), 0);
}
#[test]
fn all_neg_infinity_fails_closed_to_zero() {
let logits = [f32::NEG_INFINITY; 4];
assert_eq!(argmax_f32_first_wins(&logits), 0);
}
}
#[derive(Debug, Clone)]
pub struct SamplingConfig {
pub temperature: f32,
pub top_k: usize,
pub top_p: f32,
pub repetition_penalty: f32,
}
impl Default for SamplingConfig {
fn default() -> Self {
Self {
temperature: 0.7,
top_k: 50,
top_p: 0.9,
repetition_penalty: 1.1,
}
}
}
impl SamplingConfig {
pub fn greedy() -> Self {
Self {
temperature: 0.0,
top_k: 1,
top_p: 1.0,
repetition_penalty: 1.0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Candidate {
pub token_id: u32,
pub logit: f32,
}
pub struct CandidateSet {
candidates: Vec<Candidate>,
}
impl CandidateSet {
pub fn from_full_logits(logits: &[f32]) -> Self {
let candidates = logits
.iter()
.enumerate()
.map(|(i, &l)| Candidate {
token_id: i as u32,
logit: l,
})
.collect();
Self { candidates }
}
pub fn from_candidates(candidates: Vec<Candidate>) -> Self {
Self { candidates }
}
pub fn apply_repetition_penalty(&mut self, previous_ids: &[u32], penalty: f32) {
if penalty == 1.0 {
return;
}
for c in &mut self.candidates {
if previous_ids.contains(&c.token_id) {
c.logit = penalized_logit(c.logit, penalty);
}
}
}
pub fn argmax(&self) -> u32 {
let mut best_id = self.candidates.first().map(|c| c.token_id).unwrap_or(0);
let mut best_val = f32::NEG_INFINITY;
for c in &self.candidates {
if c.logit > best_val {
best_val = c.logit;
best_id = c.token_id;
}
}
best_id
}
pub fn apply_temperature(&mut self, temperature: f32) {
if !temperature.is_finite() || temperature <= 0.0 || temperature == 1.0 {
return;
}
if temperature_degenerate(temperature) {
let best = self.argmax();
for c in &mut self.candidates {
c.logit = if c.token_id == best {
0.0
} else {
f32::NEG_INFINITY
};
}
return;
}
let inv = 1.0 / temperature;
for c in &mut self.candidates {
c.logit *= inv;
}
}
pub fn retain_top_k(&mut self, k: usize) {
if k == 0 || k >= self.candidates.len() {
return;
}
self.candidates
.select_nth_unstable_by(k - 1, candidate_order);
self.candidates.truncate(k);
}
pub fn sample_top_p(&mut self, top_p: f32, r: f32) -> u32 {
let mut probs = Vec::new();
self.sample_top_p_with_scratch(top_p, r, &mut probs)
}
fn sample_top_p_with_scratch(&mut self, top_p: f32, r: f32, probs: &mut Vec<f32>) -> u32 {
if self.candidates.is_empty() {
return 0;
}
let top_p = if top_p.is_nan() {
1.0
} else {
top_p.clamp(0.0, 1.0)
};
self.candidates.sort_by(candidate_order);
let max_logit = self.candidates[0].logit;
if !max_logit.is_finite() {
return self.candidates[0].token_id;
}
probs.clear();
probs.extend(self.candidates.iter().map(|c| (c.logit - max_logit).exp()));
let sum: f32 = probs.iter().sum();
if !sum.is_finite() || sum <= 0.0 {
return self.candidates[0].token_id;
}
for p in probs.iter_mut() {
*p /= sum;
}
if top_p < 1.0 {
let mut cumsum = 0.0f32;
let mut cutoff = probs.len();
for (i, &p) in probs.iter().enumerate() {
cumsum += p;
if cumsum >= top_p {
cutoff = i + 1;
break;
}
}
probs.truncate(cutoff);
self.candidates.truncate(cutoff);
let sum: f32 = probs.iter().sum();
for p in probs.iter_mut() {
*p /= sum;
}
}
let mut cumsum = 0.0f32;
for (c, &p) in self.candidates.iter().zip(probs.iter()) {
cumsum += p;
if r < cumsum {
return c.token_id;
}
}
self.candidates.last().map(|c| c.token_id).unwrap_or(0)
}
}
pub(crate) fn xorshift64_next(state: &mut u64) -> u64 {
let mut x = *state;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
*state = x;
x
}
pub(crate) fn uniform_f32_from_u64(x: u64) -> f32 {
(x >> 40) as f32 / (1u64 << 24) as f32
}
struct Rng {
state: u64,
}
impl Rng {
fn new(seed: u64) -> Self {
Self {
state: if seed == 0 { 0x853c49e6748fea9b } else { seed },
}
}
fn next_u64(&mut self) -> u64 {
xorshift64_next(&mut self.state)
}
fn next_f32(&mut self) -> f32 {
uniform_f32_from_u64(self.next_u64())
}
}
pub struct Sampler {
config: SamplingConfig,
rng: Rng,
recent_tokens: Vec<u32>,
penalty_seen: std::collections::HashSet<u32>,
candidate_scratch: Vec<Candidate>,
prob_scratch: Vec<f32>,
logit_scratch: Vec<f32>,
}
impl Sampler {
pub fn new(config: SamplingConfig) -> Self {
let seed = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos() as u64)
.unwrap_or(0x853c_49e6_748f_ea9b);
Self {
config,
rng: Rng::new(seed),
recent_tokens: Vec::new(),
penalty_seen: std::collections::HashSet::new(),
candidate_scratch: Vec::new(),
prob_scratch: Vec::new(),
logit_scratch: Vec::new(),
}
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.rng = Rng::new(seed);
self
}
pub fn seed_history(&mut self, prompt_ids: &[u32]) {
self.recent_tokens.extend_from_slice(prompt_ids);
}
pub fn sample(&mut self, logits: &[f32]) -> u32 {
if temperature_degenerate(self.config.temperature) || self.config.top_k == 1 {
let raw_best = argmax_f32(logits);
let penalty = self.config.repetition_penalty;
let penalty_inactive = penalty == 1.0 || !penalty.is_finite() || penalty <= 0.0;
if penalty_inactive || (penalty > 1.0 && !self.recent_tokens.contains(&raw_best)) {
self.push_token(raw_best);
return raw_best;
}
self.logit_scratch.clear();
self.logit_scratch.extend_from_slice(logits);
self.apply_penalty_to_logit_scratch(penalty);
let token = argmax_f32(&self.logit_scratch);
self.push_token(token);
return token;
}
self.logit_scratch.clear();
self.logit_scratch.extend_from_slice(logits);
if self.config.repetition_penalty != 1.0 {
let penalty = self.config.repetition_penalty;
self.apply_penalty_to_logit_scratch(penalty);
}
let inv_temp = if self.config.temperature != 1.0 {
1.0 / self.config.temperature
} else {
1.0
};
select_top_k(
&self.logit_scratch,
self.config.top_k,
inv_temp,
&mut self.candidate_scratch,
);
let mut cs = CandidateSet {
candidates: std::mem::take(&mut self.candidate_scratch),
};
let r = self.rng.next_f32();
let token = cs.sample_top_p_with_scratch(self.config.top_p, r, &mut self.prob_scratch);
self.candidate_scratch = cs.candidates; self.push_token(token);
token
}
fn push_token(&mut self, token: u32) {
self.recent_tokens.push(token);
}
fn apply_penalty_to_logit_scratch(&mut self, penalty: f32) {
self.penalty_seen.clear();
for &tok in &self.recent_tokens {
let idx = tok as usize;
if idx < self.logit_scratch.len() && self.penalty_seen.insert(tok) {
self.logit_scratch[idx] = penalized_logit(self.logit_scratch[idx], penalty);
}
}
}
pub fn reset(&mut self) {
self.recent_tokens.clear();
}
}
thread_local! {
static FULL_LOGIT_SCRATCH: std::cell::RefCell<FullLogitScratch> =
std::cell::RefCell::new(FullLogitScratch::new());
}
struct FullLogitScratch {
logit_scratch: Vec<f32>,
candidate_scratch: Vec<Candidate>,
prob_scratch: Vec<f32>,
penalty_seen: std::collections::HashSet<u32>,
}
impl FullLogitScratch {
fn new() -> Self {
Self {
logit_scratch: Vec::new(),
candidate_scratch: Vec::new(),
prob_scratch: Vec::new(),
penalty_seen: std::collections::HashSet::new(),
}
}
}
pub(crate) fn sample_full_logits(
logits: &[f32],
cfg: &GenerateConfig,
previous_ids: &[u32],
rng_state: &mut u64,
) -> u32 {
FULL_LOGIT_SCRATCH.with(|cell| {
let mut scratch = cell.borrow_mut();
let FullLogitScratch {
logit_scratch,
candidate_scratch,
prob_scratch,
penalty_seen,
} = &mut *scratch;
logit_scratch.clear();
logit_scratch.extend_from_slice(logits);
if cfg.repetition_penalty != 1.0 {
penalty_seen.clear();
for &tok in previous_ids {
let idx = tok as usize;
if idx < logit_scratch.len() && penalty_seen.insert(tok) {
logit_scratch[idx] =
penalized_logit(logit_scratch[idx], cfg.repetition_penalty);
}
}
}
if temperature_degenerate(cfg.temperature) {
return argmax_f32(logit_scratch);
}
let inv_temp = if cfg.temperature != 1.0 {
1.0 / cfg.temperature
} else {
1.0
};
let (has_nan, max_logit) = scan_nan_or_nonfinite_max(logit_scratch);
if has_nan || !max_logit.is_finite() {
return argmax_f32(logit_scratch);
}
select_top_k(logit_scratch, cfg.top_k, inv_temp, candidate_scratch);
let mut cs = CandidateSet {
candidates: std::mem::take(candidate_scratch),
};
let r = uniform_f32_from_u64(xorshift64_next(rng_state));
let token = cs.sample_top_p_with_scratch(cfg.top_p, r, prob_scratch);
*candidate_scratch = cs.candidates; token
})
}
const MAX_PLAUSIBLE_ABS_LOGIT: f32 = 1.0e4;
#[inline]
pub(crate) fn temperature_degenerate(temperature: f32) -> bool {
if !temperature.is_finite() || temperature <= 0.0 {
return true;
}
let inv_temp = 1.0 / temperature;
!inv_temp.is_finite() || inv_temp > f32::MAX / MAX_PLAUSIBLE_ABS_LOGIT
}
#[inline(always)]
pub(crate) fn penalized_logit(logit: f32, penalty: f32) -> f32 {
if penalty == 1.0 || !penalty.is_finite() || penalty <= 0.0 {
return logit;
}
if logit > 0.0 {
logit / penalty
} else {
logit * penalty
}
}
const LOGPROB_NEG_SENTINEL: f32 = -1.0e9;
pub(crate) fn compute_step_logprobs(
logits: &[f32],
token_id: u32,
temperature: f32,
top_n: usize,
) -> (f32, Vec<TopLogprob>) {
let vocab_size = logits.len();
let scale = if temperature_degenerate(temperature) {
1.0
} else {
1.0 / temperature
};
let mut max_scaled = f32::NEG_INFINITY;
for &v in logits {
let scaled = v * scale;
if scaled > max_scaled {
max_scaled = scaled;
}
}
if !max_scaled.is_finite() {
let top = if top_n > 0 {
vec![TopLogprob {
token_id,
logprob: LOGPROB_NEG_SENTINEL,
}]
} else {
Vec::new()
};
return (LOGPROB_NEG_SENTINEL, top);
}
let mut sum = 0.0f32;
for &v in logits {
sum += (v * scale - max_scaled).exp();
}
let log_sum = sum.ln();
let logprob_of = |idx: usize| -> f32 {
let lp = (logits[idx] * scale - max_scaled) - log_sum;
if lp.is_finite() {
lp
} else {
LOGPROB_NEG_SENTINEL
}
};
let token_logprob = if (token_id as usize) < vocab_size {
logprob_of(token_id as usize)
} else {
LOGPROB_NEG_SENTINEL
};
let k = top_n.min(vocab_size);
let top = if k == 0 {
Vec::new()
} else {
let mut candidates: Vec<Candidate> = (0..vocab_size)
.map(|i| Candidate {
token_id: i as u32,
logit: logits[i] * scale,
})
.collect();
candidates.select_nth_unstable_by(k - 1, candidate_order);
candidates.truncate(k);
candidates.sort_unstable_by(candidate_order);
candidates
.into_iter()
.map(|c| TopLogprob {
token_id: c.token_id,
logprob: logprob_of(c.token_id as usize),
})
.collect()
};
(token_logprob, top)
}
fn scan_nan_or_nonfinite_max(logits: &[f32]) -> (bool, f32) {
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
return unsafe { scan_nan_or_nonfinite_max_neon(logits) };
}
}
scan_nan_or_nonfinite_max_scalar(logits)
}
fn scan_nan_or_nonfinite_max_scalar(logits: &[f32]) -> (bool, f32) {
let mut max_logit = f32::NEG_INFINITY;
let mut has_nan = false;
for &v in logits {
has_nan |= v.is_nan();
max_logit = max_logit.max(v);
}
(has_nan, max_logit)
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn scan_nan_or_nonfinite_max_neon(logits: &[f32]) -> (bool, f32) {
use std::arch::aarch64::*;
let len = logits.len();
let mut i = 0usize;
let mut max_acc = vdupq_n_f32(f32::NEG_INFINITY);
let mut nan_acc = vdupq_n_u32(0);
while i + 4 <= len {
let v = vld1q_f32(logits.as_ptr().add(i));
max_acc = vmaxnmq_f32(max_acc, v);
let is_non_nan_lane = vceqq_f32(v, v);
nan_acc = vorrq_u32(nan_acc, vmvnq_u32(is_non_nan_lane));
i += 4;
}
let mut max_logit = vmaxnmvq_f32(max_acc);
let mut has_nan = vmaxvq_u32(nan_acc) != 0;
while i < len {
let v = *logits.get_unchecked(i);
has_nan |= v.is_nan();
max_logit = max_logit.max(v);
i += 1;
}
(has_nan, max_logit)
}
fn argmax_f32(logits: &[f32]) -> u32 {
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
return unsafe { argmax_f32_neon(logits) };
}
}
argmax_f32_scalar(logits)
}
fn argmax_f32_scalar(logits: &[f32]) -> u32 {
let mut best_idx = 0u32;
let mut best_val = f32::NEG_INFINITY;
for (i, &v) in logits.iter().enumerate() {
if v > best_val {
best_val = v;
best_idx = i as u32;
}
}
best_idx
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn argmax_f32_neon(logits: &[f32]) -> u32 {
use std::arch::aarch64::*;
let len = logits.len();
let mut i = 0usize;
let mut best_v = vdupq_n_f32(f32::NEG_INFINITY);
let mut best_i = vdupq_n_u32(0);
let idx_init = [0u32, 1, 2, 3];
let mut idx_v = vld1q_u32(idx_init.as_ptr());
let idx_step = vdupq_n_u32(4);
while i + 4 <= len {
let v = vld1q_f32(logits.as_ptr().add(i));
let mask = vcgtq_f32(v, best_v); best_v = vbslq_f32(mask, v, best_v);
best_i = vbslq_u32(mask, idx_v, best_i);
idx_v = vaddq_u32(idx_v, idx_step);
i += 4;
}
let mut best_idx = 0u32;
let mut best_val = f32::NEG_INFINITY;
macro_rules! reduce_lane {
($lane:literal) => {{
let lane_val = vgetq_lane_f32::<$lane>(best_v);
let lane_idx = vgetq_lane_u32::<$lane>(best_i);
if lane_val > best_val || (lane_val == best_val && lane_idx < best_idx) {
best_val = lane_val;
best_idx = lane_idx;
}
}};
}
reduce_lane!(0);
reduce_lane!(1);
reduce_lane!(2);
reduce_lane!(3);
while i < len {
let v = *logits.get_unchecked(i);
if v > best_val {
best_val = v;
best_idx = i as u32;
}
i += 1;
}
best_idx
}
#[inline(always)]
fn heap_less(a: &Candidate, b: &Candidate) -> bool {
match (a.logit.is_nan(), b.logit.is_nan()) {
(true, _) => true,
(_, true) => false,
_ => a.logit < b.logit || (a.logit == b.logit && a.token_id > b.token_id),
}
}
fn heap_sift_down(heap: &mut [Candidate], mut pos: usize) {
let n = heap.len();
loop {
let left = 2 * pos + 1;
let right = 2 * pos + 2;
let mut smallest = pos;
if left < n && heap_less(&heap[left], &heap[smallest]) {
smallest = left;
}
if right < n && heap_less(&heap[right], &heap[smallest]) {
smallest = right;
}
if smallest == pos {
break;
}
heap.swap(pos, smallest);
pos = smallest;
}
}
fn heap_build(heap: &mut [Candidate]) {
if heap.len() <= 1 {
return;
}
let mut i = heap.len() / 2;
while i > 0 {
i -= 1;
heap_sift_down(heap, i);
}
}
fn select_top_k(logits: &[f32], k: usize, inv_temp: f32, out: &mut Vec<Candidate>) {
#[cfg(target_arch = "aarch64")]
{
if std::arch::is_aarch64_feature_detected!("neon") {
unsafe { select_top_k_neon(logits, k, inv_temp, out) };
return;
}
}
select_top_k_scalar(logits, k, inv_temp, out);
}
fn select_top_k_scalar(logits: &[f32], k: usize, inv_temp: f32, out: &mut Vec<Candidate>) {
out.clear();
if logits.is_empty() {
return;
}
let k = if k == 0 {
logits.len()
} else {
k.min(logits.len())
};
out.extend(logits.iter().take(k).enumerate().map(|(i, &raw)| {
let scaled = raw * inv_temp;
Candidate {
token_id: i as u32,
logit: if scaled.is_nan() {
f32::NEG_INFINITY
} else {
scaled
},
}
}));
heap_build(out);
for (i, &raw) in logits.iter().enumerate().skip(k) {
let logit = raw * inv_temp;
let cand = Candidate {
token_id: i as u32,
logit,
};
if heap_less(&out[0], &cand) {
out[0] = cand;
heap_sift_down(out, 0);
}
}
}
#[cfg(target_arch = "aarch64")]
#[target_feature(enable = "neon")]
unsafe fn select_top_k_neon(logits: &[f32], k: usize, inv_temp: f32, out: &mut Vec<Candidate>) {
use std::arch::aarch64::*;
out.clear();
if logits.is_empty() {
return;
}
let k = if k == 0 {
logits.len()
} else {
k.min(logits.len())
};
out.extend(logits.iter().take(k).enumerate().map(|(i, &raw)| {
let scaled = raw * inv_temp;
Candidate {
token_id: i as u32,
logit: if scaled.is_nan() {
f32::NEG_INFINITY
} else {
scaled
},
}
}));
heap_build(out);
let n = logits.len();
let mut i = k;
let inv_v = vdupq_n_f32(inv_temp);
while i + 4 <= n {
let thresh = out[0].logit;
let thresh_v = vdupq_n_f32(thresh);
let raw_v = vld1q_f32(logits.as_ptr().add(i));
let scaled_v = vmulq_f32(raw_v, inv_v);
let mask = vcgtq_f32(scaled_v, thresh_v);
let any = vgetq_lane_u32::<0>(mask)
| vgetq_lane_u32::<1>(mask)
| vgetq_lane_u32::<2>(mask)
| vgetq_lane_u32::<3>(mask);
if any != 0 {
for j in 0..4usize {
let logit = *logits.get_unchecked(i + j) * inv_temp;
let cand = Candidate {
token_id: (i + j) as u32,
logit,
};
if heap_less(&out[0], &cand) {
out[0] = cand;
heap_sift_down(out, 0);
}
}
}
i += 4;
}
while i < n {
let logit = *logits.get_unchecked(i) * inv_temp;
let cand = Candidate {
token_id: i as u32,
logit,
};
if heap_less(&out[0], &cand) {
out[0] = cand;
heap_sift_down(out, 0);
}
i += 1;
}
}
#[inline(always)]
fn candidate_order(a: &Candidate, b: &Candidate) -> std::cmp::Ordering {
use std::cmp::Ordering;
match (a.logit.is_nan(), b.logit.is_nan()) {
(true, true) => a.token_id.cmp(&b.token_id),
(true, false) => Ordering::Greater,
(false, true) => Ordering::Less,
(false, false) => b
.logit
.partial_cmp(&a.logit)
.unwrap_or(Ordering::Equal)
.then_with(|| a.token_id.cmp(&b.token_id)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_greedy_picks_argmax() {
let config = SamplingConfig::greedy();
let mut sampler = Sampler::new(config);
let logits = vec![0.1, 0.5, 0.3, 0.9, 0.2];
assert_eq!(sampler.sample(&logits), 3);
}
#[test]
fn test_temperature_zero_is_greedy() {
let config = SamplingConfig {
temperature: 0.0,
..Default::default()
};
let mut sampler = Sampler::new(config);
let logits = vec![1.0, 5.0, 2.0];
assert_eq!(sampler.sample(&logits), 1);
}
#[test]
fn test_nonfinite_temperature_falls_back_to_argmax() {
let logits = vec![0.0, 100.0, 99.0];
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let config = SamplingConfig {
temperature: bad,
top_k: 2,
top_p: 1.0,
repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(7);
assert_eq!(
sampler.sample(&logits),
1,
"temperature {bad} must fall back to argmax, not collapse to token 0"
);
}
}
#[test]
fn test_tiny_temperature_falls_back_to_argmax() {
let logits = vec![10.0, 11.0, 9.0];
for tiny in [1e-45_f32, 1e-40, 1e-39] {
let config = SamplingConfig {
temperature: tiny,
top_k: 2,
top_p: 1.0,
repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(7);
assert_eq!(
sampler.sample(&logits),
1,
"tiny temperature {tiny} must fall back to argmax, not collapse to token 0"
);
}
}
#[test]
fn test_temperature_degenerate_predicate() {
for bad in [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
0.0,
-1.0,
1e-45,
1e-40,
1e-39,
f32::MIN_POSITIVE,
1e-37,
1e-35,
] {
assert!(
temperature_degenerate(bad),
"temperature {bad} should be degenerate"
);
}
for ok in [1.0, 0.5, 2.0, 0.1, 1e-3, 1e-30] {
assert!(
!temperature_degenerate(ok),
"temperature {ok} should be valid (no scaling overflow)"
);
}
}
#[test]
fn test_residual_band_temperature_falls_back_to_argmax() {
let logits = vec![10.0, 11.0, 9.0];
for band in [f32::MIN_POSITIVE, 1e-37_f32, 1e-36] {
let config = SamplingConfig {
temperature: band,
top_k: 2,
top_p: 1.0,
repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(7);
assert_eq!(
sampler.sample(&logits),
1,
"residual-band temperature {band} must fall back to argmax, not collapse"
);
}
}
#[test]
fn test_apply_temperature_nonfinite_is_noop() {
for bad in [
f32::NAN,
f32::INFINITY,
f32::NEG_INFINITY,
0.0_f32,
-2.0_f32,
] {
let mut cs = CandidateSet::from_full_logits(&[1.0, 5.0, 3.0]);
cs.apply_temperature(bad);
let logits: Vec<f32> = cs.candidates.iter().map(|c| c.logit).collect();
assert_eq!(
logits,
vec![1.0, 5.0, 3.0],
"temperature {bad} must be a no-op, leaving logits finite and unscaled"
);
assert_eq!(cs.argmax(), 1);
}
}
#[test]
fn apply_temperature_tiny_positive_temp_stays_greedy() {
let mut cs = CandidateSet::from_full_logits(&[10.0, 11.0]);
cs.apply_temperature(1e-45);
assert_eq!(
cs.sample_top_p(1.0, 0.0),
1,
"tiny positive temperature must resolve to the argmax (greedy), not token 0"
);
}
#[test]
fn apply_temperature_tiny_temp_is_hard_greedy_for_close_logits() {
let mut cs = CandidateSet::from_full_logits(&[0.0, f32::from_bits(1)]);
cs.apply_temperature(1e-45);
assert_eq!(
cs.sample_top_p(1.0, 0.75),
1,
"degenerate temperature must be hard-greedy even for adjacent logits"
);
}
#[test]
fn test_top_k_limits_candidates() {
let config = SamplingConfig {
temperature: 1.0,
top_k: 2,
top_p: 1.0,
repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(123);
let logits = vec![0.0, 10.0, 0.0, 9.0, 0.0];
let mut counts = [0u32; 5];
for _ in 0..100 {
let tok = sampler.sample(&logits);
counts[tok as usize] += 1;
}
assert_eq!(counts[0], 0);
assert_eq!(counts[2], 0);
assert_eq!(counts[4], 0);
assert!(counts[1] > 0);
assert!(counts[3] > 0);
}
#[test]
fn test_repetition_penalty_reduces_probability() {
let config = SamplingConfig {
temperature: 0.0, top_k: 0,
top_p: 1.0,
repetition_penalty: 100.0, };
let mut sampler = Sampler::new(config);
let logits = vec![0.0, 5.0, 4.9];
let first = sampler.sample(&logits);
assert_eq!(first, 1);
let second = sampler.sample(&logits);
assert_eq!(second, 2);
}
#[test]
fn test_greedy_sub_one_penalty_boosts_recent_token() {
let config = SamplingConfig {
temperature: 0.0, top_k: 0,
top_p: 1.0,
repetition_penalty: 0.5, };
let mut sampler = Sampler::new(config);
assert_eq!(sampler.sample(&[0.0, 10.0]), 1);
assert_eq!(sampler.sample(&[5.0, 4.0]), 1);
}
#[test]
fn test_greedy_sub_one_penalty_recent_argmax_unchanged() {
let config = SamplingConfig {
temperature: 0.0, top_k: 0,
top_p: 1.0,
repetition_penalty: 0.5, };
let mut sampler = Sampler::new(config);
assert_eq!(sampler.sample(&[0.0, 10.0]), 1);
assert_eq!(sampler.sample(&[5.0, 6.0]), 1);
}
#[test]
fn test_top_p_nucleus_sampling() {
let config = SamplingConfig {
temperature: 1.0,
top_k: 0,
top_p: 0.5,
repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(456);
let logits = vec![10.0, 1.0, 1.0, 1.0, 1.0];
let mut counts = [0u32; 5];
for _ in 0..100 {
counts[sampler.sample(&logits) as usize] += 1;
}
assert!(counts[0] > 90);
}
#[test]
fn test_sampler_reset() {
let config = SamplingConfig::greedy();
let mut sampler = Sampler::new(config);
sampler.sample(&[1.0, 2.0, 3.0]);
assert!(!sampler.recent_tokens.is_empty());
sampler.reset();
assert!(sampler.recent_tokens.is_empty());
}
fn check_argmax_parity(logits: &[f32]) {
let scalar = argmax_f32_scalar(logits);
let dispatch = argmax_f32(logits);
assert_eq!(
scalar,
dispatch,
"argmax_f32 dispatch differs from scalar for len={}",
logits.len()
);
}
#[test]
fn test_argmax_full_vocab() {
let n = 248_320usize;
let logits: Vec<f32> = (0..n)
.map(|i| {
let h = (i as u64)
.wrapping_mul(6364136223846793005u64)
.wrapping_add(1442695040888963407u64);
(h as f32 / u64::MAX as f32) * 20.0 - 10.0
})
.collect();
check_argmax_parity(&logits);
}
#[test]
fn test_argmax_lower_id_tie_wins() {
let mut logits = vec![0.0f32; 10];
logits[2] = 5.0;
logits[5] = 5.0;
assert_eq!(argmax_f32_scalar(&logits), 2, "scalar tie");
assert_eq!(argmax_f32(&logits), 2, "dispatch tie");
}
#[test]
fn test_argmax_nan_loses_to_real() {
let logits = vec![f32::NAN, 1.0, 2.0, 9.0, 0.5];
assert_eq!(argmax_f32_scalar(&logits), 3, "scalar nan");
assert_eq!(argmax_f32(&logits), 3, "dispatch nan");
}
#[test]
fn test_argmax_all_nan_returns_0() {
let logits = vec![f32::NAN, f32::NAN, f32::NAN];
assert_eq!(argmax_f32_scalar(&logits), 0, "scalar all-nan");
assert_eq!(argmax_f32(&logits), 0, "dispatch all-nan");
}
#[test]
fn test_argmax_all_neg_inf_returns_0() {
let logits = vec![f32::NEG_INFINITY; 8];
assert_eq!(argmax_f32_scalar(&logits), 0, "scalar neg-inf");
assert_eq!(argmax_f32(&logits), 0, "dispatch neg-inf");
}
#[test]
fn test_candidateset_argmax_all_masked_returns_first_in_set_token() {
let cs = CandidateSet::from_candidates(vec![
Candidate {
token_id: 7,
logit: f32::NEG_INFINITY,
},
Candidate {
token_id: 9,
logit: f32::NEG_INFINITY,
},
]);
assert_eq!(
cs.argmax(),
7,
"all-masked compact set must return first in-set token id"
);
}
#[test]
fn test_argmax_partial_chunk_lengths() {
for tail in 1usize..=7 {
let mut logits = vec![0.0f32; 8 + tail];
logits[8 + tail - 1] = 99.0; check_argmax_parity(&logits);
assert_eq!(
argmax_f32(&logits),
(8 + tail - 1) as u32,
"tail len={tail}"
);
}
}
#[test]
fn test_candidate_order_higher_logit_wins() {
use std::cmp::Ordering;
let a = Candidate {
token_id: 10,
logit: 5.0,
};
let b = Candidate {
token_id: 20,
logit: 3.0,
};
assert_eq!(
candidate_order(&a, &b),
Ordering::Less,
"higher logit must sort first"
);
}
#[test]
fn test_candidate_order_tie_lower_token_id_wins() {
use std::cmp::Ordering;
let a = Candidate {
token_id: 5,
logit: 2.0,
};
let b = Candidate {
token_id: 9,
logit: 2.0,
};
assert_eq!(
candidate_order(&a, &b),
Ordering::Less,
"equal logit: lower token_id is first"
);
assert_eq!(candidate_order(&b, &a), Ordering::Greater);
}
#[test]
fn test_candidate_order_nan_loses() {
use std::cmp::Ordering;
let nan = Candidate {
token_id: 0,
logit: f32::NAN,
};
let real = Candidate {
token_id: 99,
logit: -1000.0,
};
assert_eq!(
candidate_order(&nan, &real),
Ordering::Greater,
"NaN must sort last"
);
assert_eq!(candidate_order(&real, &nan), Ordering::Less);
}
#[test]
fn test_retain_top_k_tie_breaking() {
let mut cs = CandidateSet {
candidates: vec![
Candidate {
token_id: 7,
logit: 1.0,
},
Candidate {
token_id: 2,
logit: 1.0,
},
Candidate {
token_id: 5,
logit: 1.0,
},
],
};
cs.retain_top_k(2);
cs.candidates.sort_by(candidate_order);
assert_eq!(cs.candidates[0].token_id, 2);
assert_eq!(cs.candidates[1].token_id, 5);
}
#[test]
fn test_candidate_order_nan_nan_antisymmetric() {
use std::cmp::Ordering;
let a = Candidate {
token_id: 3,
logit: f32::NAN,
};
let b = Candidate {
token_id: 7,
logit: f32::NAN,
};
assert_eq!(candidate_order(&a, &b), Ordering::Less);
assert_eq!(candidate_order(&b, &a), Ordering::Greater);
assert_eq!(candidate_order(&a, &a), Ordering::Equal);
let mut nans = [
Candidate {
token_id: 9,
logit: f32::NAN,
},
Candidate {
token_id: 1,
logit: f32::NAN,
},
Candidate {
token_id: 4,
logit: f32::NAN,
},
];
nans.sort_by(candidate_order);
assert_eq!(
nans.iter().map(|c| c.token_id).collect::<Vec<_>>(),
vec![1, 4, 9],
"all-NaN set must sort by ascending token_id"
);
}
#[test]
fn test_sample_top_p_tail_nan_returns_argmax() {
let mut cs = CandidateSet::from_candidates(vec![
Candidate {
token_id: 0,
logit: 100.0,
},
Candidate {
token_id: 1,
logit: f32::NAN,
},
Candidate {
token_id: 2,
logit: 50.0,
},
]);
let token = cs.sample_top_p(1.0, 0.5);
assert_eq!(
token, 0,
"tail-NaN poisons the softmax sum; must fall back to argmax (token 0), not last()"
);
}
#[test]
fn test_sample_top_p_invalid_top_p_normalized() {
let make = || {
CandidateSet::from_candidates(vec![
Candidate {
token_id: 0,
logit: 3.0,
},
Candidate {
token_id: 1,
logit: 2.0,
},
Candidate {
token_id: 2,
logit: 1.0,
},
Candidate {
token_id: 3,
logit: 0.0,
},
])
};
for &r in &[0.0f32, 0.25, 0.5, 0.75, 0.999] {
let baseline = make().sample_top_p(1.0, r);
assert_eq!(
make().sample_top_p(f32::NAN, r),
baseline,
"NaN top_p must behave as top_p == 1.0 at r={r}"
);
assert_eq!(
make().sample_top_p(1.5, r),
baseline,
">1 top_p must behave as top_p == 1.0 at r={r}"
);
assert_eq!(
make().sample_top_p(f32::INFINITY, r),
baseline,
"+Inf top_p must behave as top_p == 1.0 at r={r}"
);
assert_eq!(
make().sample_top_p(-0.5, r),
0,
"negative top_p must collapse to greedy argmax at r={r}"
);
}
}
fn check_top_k_parity(logits: &[f32], k: usize) {
let mut scalar_out = Vec::new();
select_top_k_scalar(logits, k, 1.0, &mut scalar_out);
scalar_out.sort_by(candidate_order);
let mut dispatch_out = Vec::new();
select_top_k(logits, k, 1.0, &mut dispatch_out);
dispatch_out.sort_by(candidate_order);
assert_eq!(
scalar_out.len(),
dispatch_out.len(),
"k={k}: length mismatch"
);
for (i, (s, d)) in scalar_out.iter().zip(dispatch_out.iter()).enumerate() {
assert_eq!(
s.token_id, d.token_id,
"k={k}: position {i} token_id mismatch (scalar={} dispatch={})",
s.token_id, d.token_id
);
}
}
#[test]
fn test_select_top_k_basic() {
let logits = vec![1.0f32, 5.0, 3.0, 9.0, 2.0, 7.0];
let mut out = Vec::new();
select_top_k_scalar(&logits, 3, 1.0, &mut out);
out.sort_by(candidate_order);
let ids: Vec<u32> = out.iter().map(|c| c.token_id).collect();
assert_eq!(ids, vec![3, 5, 1], "top-3 from [1,5,3,9,2,7]");
}
#[test]
fn test_select_top_k_tie_breaking() {
let logits = vec![5.0f32, 5.0, 1.0];
let mut out = Vec::new();
select_top_k_scalar(&logits, 1, 1.0, &mut out);
assert_eq!(out[0].token_id, 0, "tie: lower id must win");
}
#[test]
fn test_select_top_k_nan_excluded() {
let logits = vec![f32::NAN, 2.0, 9.0, 3.0];
let mut out = Vec::new();
select_top_k_scalar(&logits, 2, 1.0, &mut out);
out.sort_by(candidate_order);
let ids: Vec<u32> = out.iter().map(|c| c.token_id).collect();
assert_eq!(ids, vec![2, 3], "NaN must not appear in top-2");
}
#[test]
fn test_select_top_k_full_vocab_parity() {
let n = 248_320usize;
let logits: Vec<f32> = (0..n)
.map(|i| {
let h = (i as u64)
.wrapping_mul(6364136223846793005u64)
.wrapping_add(1442695040888963407u64);
(h as f32 / u64::MAX as f32) * 20.0 - 10.0
})
.collect();
check_top_k_parity(&logits, 50);
}
#[test]
fn test_select_top_k_dispatch_tie_breaking() {
let logits = vec![5.0f32, 5.0, 5.0, 1.0, 1.0];
let mut out = Vec::new();
select_top_k(&logits, 2, 1.0, &mut out);
out.sort_by(candidate_order);
let ids: Vec<u32> = out.iter().map(|c| c.token_id).collect();
assert_eq!(
ids,
vec![0, 1],
"dispatch: tie must break by lower token_id"
);
}
#[test]
fn test_select_top_k_dispatch_nan_excluded() {
let logits = vec![f32::NAN, 4.0, 9.0, f32::NAN, 7.0];
let mut out = Vec::new();
select_top_k(&logits, 2, 1.0, &mut out);
out.sort_by(candidate_order);
let ids: Vec<u32> = out.iter().map(|c| c.token_id).collect();
assert_eq!(ids, vec![2, 4], "dispatch: NaN must not appear in top-2");
}
#[test]
fn test_select_top_k_dispatch_nan_in_seed() {
let mut logits = vec![
f32::NAN,
2.0,
3.0,
f32::NAN,
5.0,
6.0,
7.0,
f32::NAN,
9.0,
10.0,
];
logits.extend((1..=990u32).map(|x| x as f32));
let mut scalar_out = Vec::new();
select_top_k_scalar(&logits, 10, 1.0, &mut scalar_out);
scalar_out.sort_by(candidate_order);
let mut dispatch_out = Vec::new();
select_top_k(&logits, 10, 1.0, &mut dispatch_out);
dispatch_out.sort_by(candidate_order);
assert_eq!(scalar_out.len(), dispatch_out.len());
for (i, (s, d)) in scalar_out.iter().zip(dispatch_out.iter()).enumerate() {
assert_eq!(
s.token_id, d.token_id,
"nan_in_seed: position {i} token_id mismatch (scalar={} dispatch={})",
s.token_id, d.token_id
);
}
}
#[test]
fn test_select_top_k_applies_inv_temp_to_candidate_logits() {
let logits = vec![1.0f32, 2.0, 3.0, 4.0];
let mut out = Vec::new();
select_top_k(&logits, 2, 0.5, &mut out);
out.sort_by(candidate_order);
assert_eq!(out[0].token_id, 3);
assert_eq!(out[1].token_id, 2);
assert!(
(out[0].logit - 2.0).abs() < 1e-6,
"logit[3] must be 4.0*0.5=2.0"
);
assert!(
(out[1].logit - 1.5).abs() < 1e-6,
"logit[2] must be 3.0*0.5=1.5"
);
}
#[test]
fn test_select_top_k_zero_keeps_all() {
let logits = vec![1.0f32, 5.0, 3.0, 9.0, 2.0];
let mut scalar_out = Vec::new();
select_top_k_scalar(&logits, 0, 1.0, &mut scalar_out);
assert_eq!(scalar_out.len(), 5, "scalar: k=0 must keep all candidates");
let mut dispatch_out = Vec::new();
select_top_k(&logits, 0, 1.0, &mut dispatch_out);
assert_eq!(
dispatch_out.len(),
5,
"dispatch: k=0 must keep all candidates"
);
}
#[test]
fn test_top_k_zero_is_no_filtering_not_token_zero() {
let config = SamplingConfig {
temperature: 1.0,
top_k: 0, top_p: 1.0, repetition_penalty: 1.0,
};
let mut sampler = Sampler::new(config).with_seed(42);
let logits = vec![1.0f32, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0];
let mut counts = [0u32; 8];
for _ in 0..400 {
counts[sampler.sample(&logits) as usize] += 1;
}
let nonzero = counts.iter().filter(|&&c| c > 0).count();
assert!(
nonzero >= 5,
"top_k=0 must sample across the vocab, got counts={counts:?}"
);
}
#[test]
fn test_sample_top_p_handles_pos_inf_logit() {
let mut cs = CandidateSet {
candidates: vec![
Candidate {
token_id: 0,
logit: 1.0,
},
Candidate {
token_id: 1,
logit: f32::INFINITY,
},
Candidate {
token_id: 2,
logit: 2.0,
},
],
};
let mut scratch = Vec::new();
let tok = cs.sample_top_p_with_scratch(1.0, 0.999, &mut scratch);
assert_eq!(tok, 1, "+INF logit token must be selected");
}
#[test]
fn test_sample_top_p_all_neg_inf_falls_back_to_first_sorted_candidate() {
let mut cs = CandidateSet {
candidates: vec![
Candidate {
token_id: 3,
logit: f32::NEG_INFINITY,
},
Candidate {
token_id: 1,
logit: f32::NEG_INFINITY,
},
Candidate {
token_id: 2,
logit: f32::NEG_INFINITY,
},
],
};
let mut scratch = Vec::new();
let tok = cs.sample_top_p_with_scratch(0.9, 0.5, &mut scratch);
assert_eq!(
tok, 1,
"an all-NEG_INFINITY CandidateSet must deterministically fall back \
to the first sorted candidate's token id, not NaN-derived garbage"
);
}
#[test]
fn test_penalized_logit_invalid_penalty_is_noop() {
for &bad in &[0.0f32, -1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
assert_eq!(
penalized_logit(5.0, bad),
5.0,
"positive logit unchanged for penalty={bad}"
);
assert_eq!(
penalized_logit(-5.0, bad),
-5.0,
"negative logit unchanged for penalty={bad}"
);
}
assert!(
(penalized_logit(2.0, 2.0) - 1.0).abs() < 1e-6,
"2.0/2.0 == 1.0"
);
assert!(
(penalized_logit(-2.0, 2.0) - -4.0).abs() < 1e-6,
"-2.0*2.0 == -4.0"
);
}
#[test]
fn test_repetition_penalty_applied_once_per_token() {
let mut s = Sampler::new(SamplingConfig {
temperature: 0.0,
top_k: 1,
top_p: 1.0,
repetition_penalty: 2.0,
})
.with_seed(1);
assert_eq!(s.sample(&[0.0, 10.0, 0.0]), 1);
assert_eq!(s.sample(&[0.0, 10.0, 0.0]), 1);
assert_eq!(
s.sample(&[0.0, 10.0, 3.0]),
1,
"duplicate history id must be penalized once, not per occurrence"
);
}
#[test]
fn uniform_f32_from_u64_is_always_in_unit_interval() {
for x in [
0u64,
1,
u64::MAX,
0xFFFF_FFFF_FFFF_FFFF,
1u64 << 40,
(1u64 << 40) - 1,
0x8000_0000_0000_0000,
] {
let f = uniform_f32_from_u64(x);
assert!(
(0.0..1.0).contains(&f),
"uniform_f32_from_u64({x:#018x}) = {f} is not in [0, 1)"
);
}
}
#[test]
fn test_seed_history_penalizes_prompt_token_on_first_sample() {
let config = SamplingConfig {
temperature: 0.0, top_k: 1,
top_p: 1.0,
repetition_penalty: 2.0,
};
let mut sampler = Sampler::new(config).with_seed(1);
sampler.seed_history(&[1u32]);
let token = sampler.sample(&[6.0, 10.0]);
assert_eq!(
token, 0,
"token 1 was seeded in history; penalty 2.0 reduces its adjusted logit \
below token 0; without seed_history, token 1 wins (mutation: omit seed_history)"
);
}
#[test]
fn test_uncapped_history_penalizes_tokens_beyond_64() {
let config = SamplingConfig {
temperature: 0.0, top_k: 1,
top_p: 1.0,
repetition_penalty: 2.0,
};
let mut sampler = Sampler::new(config).with_seed(1);
sampler.push_token(1);
for t in 2u32..66 {
sampler.push_token(t); }
let token = sampler.sample(&[6.0, 10.0]);
assert_eq!(
token, 0,
"token 1 at position 0 in a 65-entry history must still be penalized; \
the old 64-cap silently dropped it (mutation: restore max_recent truncation)"
);
}
#[test]
fn test_penalty_applied_exactly_once_for_repeated_history_token() {
let config = SamplingConfig {
temperature: 0.0, top_k: 1,
top_p: 1.0,
repetition_penalty: 2.0,
};
let mut sampler = Sampler::new(config).with_seed(1);
sampler.seed_history(&[1, 1, 1, 1]);
let token = sampler.sample(&[4.9, 10.0]);
assert_eq!(
token, 1,
"token 1 repeated 4× must be penalized once (5.0 > 4.9, token 1 wins); \
per-occurrence compounding yields 0.625 and wrongly selects token 0 \
(mutation: replace HashSet dedup with per-occurrence penalty)"
);
}
fn reference_ln_softmax(logits: &[f32]) -> Vec<f32> {
let max = logits.iter().copied().fold(f32::NEG_INFINITY, f32::max);
let sum: f32 = logits.iter().map(|&v| (v - max).exp()).sum();
let log_sum = sum.ln();
logits.iter().map(|&v| (v - max) - log_sum).collect()
}
#[test]
fn test_compute_step_logprobs_matches_hand_computed_softmax() {
let logits = [1.0f32, 2.0, 3.0];
let reference = reference_ln_softmax(&logits);
let (logprob, top) = compute_step_logprobs(&logits, 2, 1.0, 0);
assert!(
(logprob - reference[2]).abs() < 1e-4,
"token 2 logprob {logprob} should match reference {}",
reference[2]
);
assert!(top.is_empty(), "top_n=0 must return no alternatives");
for (idx, &want) in reference.iter().enumerate() {
let (lp, _) = compute_step_logprobs(&logits, idx as u32, 1.0, 0);
assert!(
(lp - want).abs() < 1e-4,
"token {idx} logprob {lp} should match reference {want}"
);
}
}
#[test]
fn test_compute_step_logprobs_top_n_sorted_descending_by_probability() {
let logits = [1.0f32, 2.0, 3.0];
let reference = reference_ln_softmax(&logits);
let (_, top) = compute_step_logprobs(&logits, 2, 1.0, 2);
assert_eq!(top.len(), 2, "top_logprobs=2 must return exactly 2 entries");
assert_eq!(top[0].token_id, 2, "highest-logit token must be first");
assert_eq!(
top[1].token_id, 1,
"second-highest-logit token must be second"
);
assert!((top[0].logprob - reference[2]).abs() < 1e-4);
assert!((top[1].logprob - reference[1]).abs() < 1e-4);
assert!(
top[0].logprob > top[1].logprob,
"entries must be sorted descending by logprob"
);
}
#[test]
fn test_compute_step_logprobs_top_n_clamped_to_vocab_size() {
let logits = [1.0f32, 2.0, 3.0];
let (_, top) = compute_step_logprobs(&logits, 0, 1.0, 20);
assert_eq!(top.len(), 3);
}
#[test]
fn test_compute_step_logprobs_degenerate_temperature_reports_unscaled_softmax() {
let logits = [1.0f32, 2.0, 3.0];
let reference = reference_ln_softmax(&logits);
let (logprob, top) = compute_step_logprobs(&logits, 2, 0.0, 1);
assert!(
(logprob - reference[2]).abs() < 1e-4,
"degenerate temperature must report the T=1.0 softmax logprob \
({}), not one-hot 0.0; mutation would collapse this to 0.0",
reference[2]
);
assert_eq!(top.len(), 1);
assert_eq!(top[0].token_id, 2);
}
#[test]
fn test_compute_step_logprobs_all_nonfinite_logits_falls_back_to_sentinel() {
let logits = [f32::NAN, f32::NAN, f32::NAN];
let (logprob, top) = compute_step_logprobs(&logits, 1, 1.0, 3);
assert_eq!(logprob, LOGPROB_NEG_SENTINEL);
assert_eq!(
top,
vec![TopLogprob {
token_id: 1,
logprob: LOGPROB_NEG_SENTINEL
}],
"the requested token_id must still be reported (as the sentinel), \
not dropped or replaced by an arbitrary index"
);
}
#[test]
fn test_compute_step_logprobs_out_of_vocab_token_id_falls_back_to_sentinel() {
let logits = [1.0f32, 2.0, 3.0];
let reference = reference_ln_softmax(&logits);
let (logprob, top) = compute_step_logprobs(&logits, 99, 1.0, 1);
assert_eq!(logprob, LOGPROB_NEG_SENTINEL);
assert_eq!(top.len(), 1);
assert_eq!(top[0].token_id, 2);
assert!((top[0].logprob - reference[2]).abs() < 1e-4);
}
fn old_full_vocab_sample(
logits: &[f32],
temperature: f32,
top_k: usize,
top_p: f32,
repetition_penalty: f32,
previous_ids: &[u32],
rng_state: &mut u64,
) -> u32 {
let vocab_size = logits.len();
let mut adjusted = logits.to_vec();
if repetition_penalty != 1.0 {
let mut seen = std::collections::HashSet::with_capacity(previous_ids.len());
for &id in previous_ids {
let idx = id as usize;
if idx < vocab_size && seen.insert(id) {
adjusted[idx] = penalized_logit(adjusted[idx], repetition_penalty);
}
}
}
if temperature_degenerate(temperature) {
return argmax_f32(&adjusted);
}
if temperature != 1.0 {
let inv_temp = 1.0 / temperature;
for v in &mut adjusted {
*v *= inv_temp;
}
}
let mut indices: Vec<usize> = (0..vocab_size).collect();
if top_k > 0 && top_k < vocab_size {
indices.select_nth_unstable_by(top_k - 1, |&a, &b| {
adjusted[b]
.partial_cmp(&adjusted[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.cmp(&b))
});
indices.truncate(top_k);
}
indices.sort_unstable_by(|&a, &b| {
adjusted[b]
.partial_cmp(&adjusted[a])
.unwrap_or(std::cmp::Ordering::Equal)
.then_with(|| a.cmp(&b))
});
let max_logit = indices
.iter()
.map(|&i| adjusted[i])
.fold(f32::NEG_INFINITY, f32::max);
if !max_logit.is_finite() {
return argmax_f32(&adjusted);
}
let mut probs: Vec<(usize, f32)> = indices
.iter()
.map(|&i| (i, (adjusted[i] - max_logit).exp()))
.collect();
let sum: f32 = probs.iter().map(|(_, p)| p).sum();
if !sum.is_finite() || sum <= 0.0 {
return argmax_f32(&adjusted);
}
for (_, p) in &mut probs {
*p /= sum;
}
if top_p < 1.0 {
let mut cumsum = 0.0f32;
let mut cutoff = probs.len();
for (i, (_, p)) in probs.iter().enumerate() {
cumsum += p;
if cumsum >= top_p {
cutoff = i + 1;
break;
}
}
probs.truncate(cutoff);
let new_sum: f32 = probs.iter().map(|(_, p)| p).sum();
for (_, p) in probs.iter_mut() {
*p /= new_sum;
}
}
let r = uniform_f32_from_u64(xorshift64_next(rng_state));
let mut cumsum = 0.0f32;
for &(idx, p) in &probs {
cumsum += p;
if r < cumsum {
return idx as u32;
}
}
probs.last().map(|&(idx, _)| idx as u32).unwrap_or(0)
}
#[test]
#[ignore = "perf microbench, not a correctness check; run explicitly with --ignored"]
fn microbench_sample_full_logits_default_issue_config() {
const VOCAB_SIZE: usize = 248_320; const ITERS: usize = 300;
let logits: Vec<f32> = (0..VOCAB_SIZE as u64)
.map(|i| {
let h = i
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
(h as f32 / u64::MAX as f32) * 20.0 - 10.0
})
.collect();
let previous_ids: Vec<u32> = (0..64u32).map(|i| (i * 4099) % VOCAB_SIZE as u32).collect();
let cfg = SamplingConfig {
temperature: 0.7,
top_k: 40,
top_p: 0.9,
repetition_penalty: 1.1,
};
let gen_cfg = GenerateConfig {
temperature: cfg.temperature,
top_k: cfg.top_k,
top_p: cfg.top_p,
repetition_penalty: cfg.repetition_penalty,
..Default::default()
};
let mut rng_before = 0xDEAD_BEEFu64;
let before_start = std::time::Instant::now();
for _ in 0..ITERS {
std::hint::black_box(old_full_vocab_sample(
&logits,
cfg.temperature,
cfg.top_k,
cfg.top_p,
cfg.repetition_penalty,
&previous_ids,
&mut rng_before,
));
}
let before_elapsed = before_start.elapsed();
let mut rng_after = 0xDEAD_BEEFu64;
let after_start = std::time::Instant::now();
for _ in 0..ITERS {
std::hint::black_box(sample_full_logits(
&logits,
&gen_cfg,
&previous_ids,
&mut rng_after,
));
}
let after_elapsed = after_start.elapsed();
let before_us_per_call = before_elapsed.as_secs_f64() * 1e6 / ITERS as f64;
let after_us_per_call = after_elapsed.as_secs_f64() * 1e6 / ITERS as f64;
eprintln!(
"microbench sample_full_logits @ vocab={VOCAB_SIZE}, iters={ITERS}, cfg=(temp=0.7,top_k=40,top_p=0.9,rep_penalty=1.1):\n\
before (old_full_vocab_sample): {before_elapsed:?} total, {before_us_per_call:.1} us/call, {:.1} tok/s\n\
after (sample_full_logits): {after_elapsed:?} total, {after_us_per_call:.1} us/call, {:.1} tok/s",
1e6 / before_us_per_call,
1e6 / after_us_per_call,
);
assert!(
after_elapsed < before_elapsed,
"optimized sample_full_logits ({after_elapsed:?}) must be faster than \
the old full-vocab-allocating algorithm ({before_elapsed:?}) at the \
issue's default config and vocab size"
);
}
}