Skip to main content

cortiq_engine/
sampler.rs

1//! Token sampling — temperature, top-p, top-k, min-p, repetition penalty.
2//!
3//! Randomness comes from an explicit SplitMix64 PRNG carried by the
4//! caller: reproducible with a seed, unbiased across the whole CDF
5//! (the v1 `subsec_nanos` source could never pick past ~23% of it).
6
7use serde::{Deserialize, Serialize};
8
9/// SplitMix64 — tiny, fast, statistically solid for sampling.
10#[derive(Debug, Clone)]
11pub struct SplitMix64 {
12    state: u64,
13}
14
15/// Reusable per-pipeline sampling workspace. The epoch table lets the
16/// repetition penalty visit each token id once without allocating a HashSet
17/// or clearing a vocab-sized boolean vector on every decode step.
18#[derive(Debug, Default)]
19pub struct SamplerScratch {
20    seen_epoch: Vec<u32>,
21    epoch: u32,
22    /// Distinct-token set for the presence penalty; reused per token.
23    presence_seen: std::collections::HashSet<u32>,
24    /// The working copy of the logits. At a 129k vocab that is half a
25    /// megabyte allocated, filled and dropped per token; the struct that
26    /// exists to hold scratch may as well hold this one too.
27    probs: Vec<f32>,
28    /// The SECOND whole-vocab copy — top-k's partition buffer. Qwen3.6's
29    /// vocab is 248320, so this was another megabyte allocated, filled
30    /// and dropped per token, on the same hot path and for the same
31    /// reason. Same fix.
32    topk: Vec<f32>,
33    /// The sparse chain's candidate list and its per-grain partials.
34    cand: Vec<(u32, f32)>,
35    cand_parts: Vec<Vec<(u32, f32)>>,
36    sum_parts: Vec<f32>,
37    sparse: Sparse,
38    /// Set by `sparse_distribution_into`: the top-p nucleus closed inside
39    /// the candidates (false = the candidate cap cut it short).
40    sparse_cut: bool,
41}
42
43impl SamplerScratch {
44    fn begin_seen(&mut self, vocab_size: usize) -> u32 {
45        if self.seen_epoch.len() < vocab_size {
46            self.seen_epoch.resize(vocab_size, 0);
47        }
48        self.epoch = self.epoch.wrapping_add(1);
49        if self.epoch == 0 {
50            self.seen_epoch.fill(0);
51            self.epoch = 1;
52        }
53        self.epoch
54    }
55}
56
57impl SplitMix64 {
58    pub fn new(seed: u64) -> Self {
59        Self { state: seed }
60    }
61
62    /// Seed from OS entropy (address-space + time mix) when none given.
63    pub fn from_entropy() -> Self {
64        let t = std::time::SystemTime::now()
65            .duration_since(std::time::UNIX_EPOCH)
66            .unwrap_or_default();
67        let addr = Box::into_raw(Box::new(0u8)) as u64;
68        // SAFETY: pointer came from Box::into_raw just above.
69        unsafe { drop(Box::from_raw(addr as *mut u8)) };
70        Self::new(t.as_nanos() as u64 ^ addr.rotate_left(17) ^ 0x9E3779B97F4A7C15)
71    }
72
73    #[inline]
74    pub fn next_u64(&mut self) -> u64 {
75        self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
76        let mut z = self.state;
77        z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
78        z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
79        z ^ (z >> 31)
80    }
81
82    /// Uniform f32 in [0, 1).
83    #[inline]
84    pub fn next_f32(&mut self) -> f32 {
85        (self.next_u64() >> 40) as f32 / (1u64 << 24) as f32
86    }
87}
88
89/// Sampling configuration.
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct SamplerConfig {
92    pub temperature: f32,
93    pub top_p: f32,
94    pub top_k: u32,
95    pub repetition_penalty: f32,
96    pub min_p: f32,
97    /// Flat additive penalty on every token that has appeared at least
98    /// once (OpenAI-style presence penalty). Qwen3.8's instruct sampling
99    /// asks for 1.5 here — the multiplicative repetition_penalty is a
100    /// different curve and cannot stand in for it.
101    #[serde(default)]
102    pub presence_penalty: f32,
103    /// Fixed seed for reproducible generation (None = entropy).
104    #[serde(default)]
105    pub seed: Option<u64>,
106    /// Token IDs to suppress (force logit to -inf).
107    #[serde(default)]
108    pub suppress_tokens: Vec<u32>,
109    /// How many of the most recent ids the repetition / presence
110    /// penalties look at. 0 = the whole sequence (the historical
111    /// behaviour). A natively bounded model (Embryo-O1 anchor) never
112    /// scans unbounded history: the pipeline substitutes
113    /// [`BOUNDED_PENALTY_WINDOW`] there when this is 0.
114    #[serde(default)]
115    pub penalty_window: usize,
116    /// Stop the answer when its tail starts repeating itself
117    /// ([`tail_repeats`], finish reason `loop`): the loop monitor of a small
118    /// model, switched on by `sampling.loop_stop` in the file.
119    #[serde(default)]
120    pub loop_stop: bool,
121}
122
123/// Does `ids` end in a repeated block? Periods 4..=11 need three copies,
124/// periods 12..=64 two: a phrase said twice in a row at that length is a loop,
125/// not rhetoric.
126pub fn tail_repeats(ids: &[u32]) -> bool {
127    tail_repeat_excess(ids).is_some()
128}
129
130/// How many tail tokens repeat what came before them, when [`tail_repeats`]:
131/// the answer without them says the block once.
132pub fn tail_repeat_excess(ids: &[u32]) -> Option<usize> {
133    let n = ids.len();
134    (4..=64usize).find_map(|p| {
135        let copies = if p < 12 { 3 } else { 2 };
136        (n >= p * copies && (1..copies).all(|c| ids[n - p..] == ids[n - (c + 1) * p..n - c * p]))
137            .then_some(p * (copies - 1))
138    })
139}
140
141/// Penalty window a bounded-state model falls back to when
142/// `penalty_window == 0` (`CMF_PENALTY_WINDOW` overrides).
143pub const BOUNDED_PENALTY_WINDOW: usize = 128;
144
145impl SamplerConfig {
146    /// The slice of `past` the penalties may scan: the last
147    /// `penalty_window` ids, or all of them when the window is 0 and the
148    /// model is not bounded-native.
149    pub fn penalty_past<'a>(&self, past: &'a [u32], bounded_native: bool) -> &'a [u32] {
150        let mut w = self.penalty_window;
151        if w == 0 && bounded_native {
152            w = std::env::var("CMF_PENALTY_WINDOW")
153                .ok()
154                .and_then(|v| v.parse::<usize>().ok())
155                .filter(|&v| v > 0)
156                .unwrap_or(BOUNDED_PENALTY_WINDOW);
157        }
158        if w == 0 {
159            past
160        } else {
161            &past[past.len().saturating_sub(w)..]
162        }
163    }
164}
165
166impl Default for SamplerConfig {
167    fn default() -> Self {
168        Self {
169            temperature: 0.7,
170            top_p: 0.9,
171            top_k: 40,
172            repetition_penalty: 1.1,
173            presence_penalty: 0.0,
174            min_p: 0.05,
175            seed: None,
176            suppress_tokens: Vec::new(),
177            penalty_window: 0,
178            loop_stop: false,
179        }
180    }
181}
182
183/// Keys a file may carry in `header.provenance.sampling`
184/// (`cortiq convert --sampling`). `max_tokens` is the completion budget a
185/// request gets when it names none (`run -n`, the server's `max_tokens`).
186pub const FILE_SAMPLING_KEYS: [&str; 9] = [
187    "temperature",
188    "top_p",
189    "top_k",
190    "min_p",
191    "repetition_penalty",
192    "presence_penalty",
193    "max_tokens",
194    "loop_stop",
195    "history_turns",
196];
197
198/// How many of the last user turns the network sees (`sampling.history_turns`):
199/// a small model copies its earlier answers instead of using them.
200pub fn file_history_turns(provenance: Option<&serde_json::Value>) -> Option<usize> {
201    provenance
202        .and_then(|p| p.get("sampling"))
203        .and_then(|s| s.get("history_turns"))
204        .and_then(|v| v.as_f64())
205        .filter(|&n| n >= 1.0)
206        .map(|n| n as usize)
207}
208
209/// Where the last `n` user turns start in a message list (by role).
210pub fn last_turns_start<'a>(roles: impl DoubleEndedIterator<Item = &'a str> + ExactSizeIterator, n: usize) -> usize {
211    let mut seen = 0;
212    for (i, r) in roles.enumerate().rev() {
213        if r == "user" {
214            seen += 1;
215            if seen == n {
216                return i;
217            }
218        }
219    }
220    0
221}
222
223/// The file's default completion budget (`header.provenance.sampling.
224/// max_tokens`), when it carries one.
225pub fn file_max_tokens(provenance: Option<&serde_json::Value>) -> Option<u32> {
226    provenance
227        .and_then(|p| p.get("sampling"))
228        .and_then(|s| s.get("max_tokens"))
229        .and_then(|v| v.as_u64())
230        .filter(|&n| n > 0)
231        .map(|n| n.min(u32::MAX as u64) as u32)
232}
233
234impl SamplerConfig {
235    /// The engine defaults with the file's own sampling recommendation on
236    /// top: `header.provenance.sampling` (written by `cortiq convert
237    /// --sampling`, e.g. a rewriting model trained and evaluated at
238    /// temperature 1.0 / top-p 0.95 / no top-k / no min-p / no repetition
239    /// penalty). Keys the file does not set keep the engine default;
240    /// explicit flags and request fields still override both.
241    pub fn from_provenance(provenance: Option<&serde_json::Value>) -> Self {
242        let mut c = Self::default();
243        let Some(s) = provenance.and_then(|p| p.get("sampling")) else {
244            return c;
245        };
246        let f = |k: &str| s.get(k).and_then(|v| v.as_f64()).filter(|v| v.is_finite());
247        if let Some(v) = f("temperature") {
248            c.temperature = v as f32;
249        }
250        if let Some(v) = f("top_p") {
251            c.top_p = v as f32;
252        }
253        if let Some(v) = s.get("top_k").and_then(|v| v.as_u64()) {
254            c.top_k = v.min(u32::MAX as u64) as u32;
255        }
256        if let Some(v) = f("min_p") {
257            c.min_p = v as f32;
258        }
259        if let Some(v) = f("repetition_penalty") {
260            c.repetition_penalty = v as f32;
261        }
262        if let Some(v) = f("presence_penalty") {
263            c.presence_penalty = v as f32;
264        }
265        if let Some(v) = f("loop_stop") {
266            c.loop_stop = v != 0.0;
267        }
268        c
269    }
270
271    /// True when the file carries a sampling recommendation.
272    pub fn file_has_defaults(provenance: Option<&serde_json::Value>) -> bool {
273        provenance
274            .and_then(|p| p.get("sampling"))
275            .is_some_and(|s| s.as_object().is_some_and(|o| !o.is_empty()))
276    }
277}
278
279/// Sample next token from logits. Chain order is fixed:
280/// rep-penalty → temperature → softmax → min-p → top-k → top-p → sample.
281pub fn sample(
282    logits: &[f32],
283    config: &SamplerConfig,
284    past_tokens: &[u32],
285    rng: &mut SplitMix64,
286) -> u32 {
287    let mut scratch = SamplerScratch::default();
288    sample_with_scratch(logits, config, past_tokens, rng, &mut scratch)
289}
290
291/// Sampling entry point for hot decode loops with reusable scratch storage.
292pub fn sample_with_scratch(
293    logits: &[f32],
294    config: &SamplerConfig,
295    past_tokens: &[u32],
296    rng: &mut SplitMix64,
297    scratch: &mut SamplerScratch,
298) -> u32 {
299    sample_with_scratch_pool(logits, config, past_tokens, rng, scratch, None)
300}
301
302/// The same chain with the whole-vocab passes spread over the CPU pool.
303///
304/// WHY: at Qwen3.8's 248 320-entry vocab the serial sampler is ~14 passes
305/// over a megabyte plus 248k `exp` and a `select_nth` over a second copy
306/// — measured on the RTX 5090 pod as the gap between `bench --core` and
307/// the production loop (50.9 against 46.8 tok/s with penalty+confidence
308/// alone; the temperature path pays the softmax and the partition on top).
309/// The GPU graph owns the token, so during decode the pool sits idle —
310/// this is free work.
311///
312/// WHAT IS PRESERVED: every value. The parallel passes are elementwise
313/// (each output depends on its own input only), the sums that feed
314/// divisions stay sequential in index order, and top-k's threshold is
315/// the k-th largest VALUE — the same number `select_nth` returned. The
316/// sampled token is bit-identical to the serial chain for the same seed.
317/// Largest vocabulary sampled on the calling thread alone.
318const SAMPLER_SERIAL_MAX: usize = 1 << 16;
319
320pub fn sample_with_scratch_pool(
321    logits: &[f32],
322    config: &SamplerConfig,
323    past_tokens: &[u32],
324    rng: &mut SplitMix64,
325    scratch: &mut SamplerScratch,
326    pool: Option<&crate::pool::Pool>,
327) -> u32 {
328    // Waking the workers costs more than these passes over a small vocabulary.
329    let pool = if logits.len() <= SAMPLER_SERIAL_MAX { None } else { pool };
330    if config.temperature < 1e-6
331        && config.repetition_penalty == 1.0
332        && config.presence_penalty == 0.0
333        && config.suppress_tokens.is_empty()
334    {
335        return argmax(logits);
336    }
337    // Borrowed from the scratch and handed back at the single exit: at a
338    // 129k vocab this copy is half a megabyte allocated, filled and dropped
339    // per token, and the struct that exists to hold scratch may as well
340    // hold it. Every early return goes through `done` so the buffer never
341    // leaks back to the allocator.
342    if config.temperature < 1e-6 {
343        // greedy over the penalized logits, no working copy
344        return argmax_penalized(logits, config, past_tokens, scratch, pool);
345    }
346    if sparse_ok(config) {
347        // The sparse chain: same distribution, a tenth of the passes.
348        let mut sp = std::mem::take(&mut scratch.sparse);
349        let ok = sparse_distribution_into(logits, config, past_tokens, scratch, pool, &mut sp);
350        let t = if ok {
351            draw_sparse(&sp, rng)
352        } else {
353            argmax(logits)
354        };
355        scratch.sparse = sp;
356        return t;
357    }
358    // Top-p with top-k OFF (the humanizer's 0.95 over Gemma's 262k vocab):
359    // the nucleus nearly always closes inside the SPARSE_TOPK_MAX largest
360    // logits, and then top-k at that cap keeps exactly what top-k off keeps
361    // — so the sparse chain serves it. A nucleus that does not close there
362    // (a flat row) falls through to the dense chain. The dense pass over
363    // the whole vocab was ~9 ms of a ~44 ms decode token.
364    if nucleus_sparse_ok(config, logits.len()) {
365        let capped = SamplerConfig {
366            top_k: SPARSE_TOPK_MAX as u32,
367            ..config.clone()
368        };
369        let mut sp = std::mem::take(&mut scratch.sparse);
370        let ok = sparse_distribution_into(logits, &capped, past_tokens, scratch, pool, &mut sp);
371        if ok && scratch.sparse_cut {
372            let t = draw_sparse(&sp, rng);
373            scratch.sparse = sp;
374            return t;
375        }
376        scratch.sparse = sp;
377    }
378    let mut probs = std::mem::take(&mut scratch.probs);
379    let normalized = chain(logits, config, past_tokens, scratch, pool, &mut probs);
380
381    let mut done = |probs: Vec<f32>, tok: u32| -> u32 {
382        scratch.probs = probs;
383        tok
384    };
385
386    if !normalized {
387        // Everything filtered out — fall back to greedy over original logits.
388        let t = argmax(logits);
389        return done(probs, t);
390    }
391    let t = categorical_sample(&probs, rng.next_f32());
392    done(probs, t)
393}
394
395/// Greedy over the PENALIZED logits without the working copy: one pass
396/// that applies the repetition / presence penalty and the suppress list
397/// on the fly (a membership table over the vocab, built from the past
398/// tokens) and keeps the argmax with the same tie rule as `argmax`
399/// (highest index among equal maxima). Bit-identical to
400/// `chain` + `argmax` for temperature 0 — the values compared are the
401/// same expressions — and it is what a greedy decode with penalties pays
402/// per token, and what a speculative round pays per draft and per
403/// verified row (nine such passes a round at k=4).
404pub fn argmax_penalized(
405    logits: &[f32],
406    config: &SamplerConfig,
407    past_tokens: &[u32],
408    scratch: &mut SamplerScratch,
409    pool: Option<&crate::pool::Pool>,
410) -> u32 {
411    let n = logits.len();
412    if config.repetition_penalty == 1.0
413        && config.presence_penalty == 0.0
414        && config.suppress_tokens.is_empty()
415    {
416        return argmax(logits);
417    }
418    // Membership: seen_epoch[i] == epoch for past tokens (each once).
419    let epoch = scratch.begin_seen(n);
420    for &tok in past_tokens {
421        let idx = tok as usize;
422        if idx < n {
423            scratch.seen_epoch[idx] = epoch;
424        }
425    }
426    let rep = config.repetition_penalty;
427    let pres = config.presence_penalty;
428    // Suppressed ids get -inf; a second, rarer set — keep it exact.
429    let suppress = &config.suppress_tokens;
430    let seen = &scratch.seen_epoch;
431    let value = |i: usize| -> f32 {
432        let mut v = logits[i];
433        if suppress.iter().any(|&t| t as usize == i) {
434            return f32::NEG_INFINITY;
435        }
436        if seen[i] == epoch {
437            if rep != 1.0 {
438                if v > 0.0 {
439                    v /= rep;
440                } else {
441                    v *= rep;
442                }
443            }
444            if pres != 0.0 {
445                v -= pres;
446            }
447        }
448        v
449    };
450    // The suppress list is scanned per element above; keep that path
451    // serial and rare. The common (no suppress) case runs the pool.
452    let best_in = |s: usize, e: usize| -> (usize, f32) {
453        let mut bi = s;
454        let mut bv = f32::NEG_INFINITY;
455        for i in s..e {
456            let v = value(i);
457            if v >= bv {
458                bv = v;
459                bi = i;
460            }
461        }
462        (bi, bv)
463    };
464    match pool {
465        Some(p) if n >= PAR_MIN && suppress.is_empty() => {
466            let m = std::sync::Mutex::new(Vec::<(usize, f32)>::new());
467            p.run_rows(n, &|s, e| {
468                let r = best_in(s, e);
469                m.lock().unwrap().push(r);
470            });
471            let mut parts = m.into_inner().unwrap();
472            // Same rule across chunks: the max value, and among equal
473            // maxima the HIGHEST index — chunk order does not matter once
474            // sorted by index.
475            parts.sort_by_key(|(i, _)| *i);
476            let mut bi = 0usize;
477            let mut bv = f32::NEG_INFINITY;
478            for (i, v) in parts {
479                if v >= bv {
480                    bv = v;
481                    bi = i;
482                }
483            }
484            bi as u32
485        }
486        _ => best_in(0, n).0 as u32,
487    }
488}
489
490/// The chain up to the draw, into `probs`: penalties, temperature,
491/// softmax, min-p, top-k, top-p, renormalize. Returns false when the
492/// filters left nothing (the caller's greedy fallback); for a greedy
493/// config it stops after the penalties (`probs` then holds penalized
494/// logits, and argmax over them is the token).
495fn chain(
496    logits: &[f32],
497    config: &SamplerConfig,
498    past_tokens: &[u32],
499    scratch: &mut SamplerScratch,
500    pool: Option<&crate::pool::Pool>,
501    probs: &mut Vec<f32>,
502) -> bool {
503    probs.clear();
504    probs.extend_from_slice(logits);
505    apply_penalties(probs, config, past_tokens, scratch);
506
507    if config.temperature < 1e-6 {
508        return true;
509    }
510    if config.temperature != 1.0 {
511        let t = config.temperature;
512        par_map(pool, probs, &move |p| p / t);
513    }
514
515    softmax_inplace_pool(pool, probs);
516
517    if config.min_p > 0.0 {
518        let max_prob = par_max(pool, probs, 0.0);
519        let threshold = max_prob * config.min_p;
520        par_map(pool, probs, &move |p| if p < threshold { 0.0 } else { p });
521    }
522
523    if config.top_k > 0 && (config.top_k as usize) < probs.len() {
524        apply_top_k_pool(pool, probs, config.top_k as usize);
525    }
526
527    if config.top_p < 1.0 && config.top_p > 0.0 {
528        apply_top_p(probs, config.top_p);
529    }
530
531    let sum: f32 = probs.iter().sum();
532    if sum > 0.0 {
533        par_map(pool, probs, &move |p| p / sum);
534        true
535    } else {
536        false
537    }
538}
539
540/// The chain's first stage — suppress, repetition and presence penalties
541/// — in place on a working copy of the logits. Every penalty only LOWERS
542/// a logit, which is what lets the sparse chain below bound its
543/// candidates.
544fn apply_penalties(
545    probs: &mut [f32],
546    config: &SamplerConfig,
547    past_tokens: &[u32],
548    scratch: &mut SamplerScratch,
549) {
550    for &tok in &config.suppress_tokens {
551        if (tok as usize) < probs.len() {
552            probs[tok as usize] = f32::NEG_INFINITY;
553        }
554    }
555    if config.repetition_penalty != 1.0 {
556        apply_repetition_penalty(probs, past_tokens, config.repetition_penalty, scratch);
557    }
558    if config.presence_penalty != 0.0 {
559        // Once per DISTINCT seen token — presence, not frequency. The
560        // scratch set the repetition penalty uses would serve, but it is
561        // only built on its own branch; a local pass stays correct when
562        // rep-penalty is 1.0 (Qwen3.8's recommended pairing).
563        let mut seen = std::mem::take(&mut scratch.presence_seen);
564        seen.clear();
565        seen.extend(past_tokens.iter().copied());
566        for &tok in &seen {
567            if (tok as usize) < probs.len() {
568                probs[tok as usize] -= config.presence_penalty;
569            }
570        }
571        scratch.presence_seen = seen;
572    }
573}
574
575fn config_penalized(config: &SamplerConfig) -> bool {
576    config.repetition_penalty != 1.0
577        || config.presence_penalty != 0.0
578        || !config.suppress_tokens.is_empty()
579}
580
581/// Largest top-k the sparse chain serves. Past this the dense chain is
582/// the better tool anyway.
583pub const SPARSE_TOPK_MAX: usize = 256;
584
585/// Whether `config` can go through the sparse chain: a real temperature
586/// and a top-k in 1..=256. Qwen's recommended instruct settings
587/// (0.7 / top-p 0.8 / top-k 20 / presence 1.5) do.
588/// Top-p without top-k over a vocabulary much larger than the sparse cap.
589fn nucleus_sparse_ok(config: &SamplerConfig, vocab: usize) -> bool {
590    config.top_k == 0
591        && config.temperature >= 1e-6
592        && config.top_p > 0.0
593        && config.top_p < 1.0
594        && vocab > SPARSE_TOPK_MAX * 4
595        && {
596            // `CMF_NUCLEUS_SPARSE=0` keeps the dense chain (A/B).
597            static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
598            *ON.get_or_init(|| std::env::var("CMF_NUCLEUS_SPARSE").as_deref() != Ok("0"))
599        }
600}
601
602pub fn sparse_ok(config: &SamplerConfig) -> bool {
603    config.temperature >= 1e-6 && config.top_k > 0 && (config.top_k as usize) <= SPARSE_TOPK_MAX
604}
605
606/// A distribution over at most `SPARSE_TOPK_MAX` tokens: `(id, prob)`
607/// sorted by id, probs summing to 1.
608pub type Sparse = Vec<(u32, f32)>;
609
610/// The sampler chain's distribution as a SPARSE list — the same
611/// distribution `chain` builds over the whole vocab, for configs with a
612/// top-k, at a fraction of the cost. The dense chain copies the vocab,
613/// exponentiates it, selects, filters and normalises it — six or seven
614/// passes over 248k floats — and every one of them past the selection
615/// touches only the k survivors. Here: penalties on a copy ONLY when
616/// there are penalties, one pooled pass that selects the top-k penalized
617/// logits, one pooled pass for the vocab-wide softmax denominator (top-p
618/// is defined against the FULL normalisation, so the denominator must
619/// see every token), and the rest over k entries.
620///
621/// Why it is the same distribution: softmax → min-p → top-k → top-p →
622/// renormalise, in the dense order. Softmax is monotone in the logit, so
623/// the top-k SET is the top-k of the penalized logits; min-p drops
624/// tokens below `max_prob·min_p`, i.e. below `exp((l − l_max)/T) <
625/// min_p` — every token outside the top-k that fails it is dropped
626/// either way, and the ones inside are tested exactly as the dense chain
627/// tests them; top-p cuts on the cumulative FULL-normalised probs of the
628/// survivors sorted descending, computed here from the same terms. What
629/// differs is floating-point: the denominator's summation order and the
630/// exp of `(l − l_max)/T` against `l/T − max(l/T)`. Ties at the k-th
631/// place resolve by lower id here where the dense chain keeps them all.
632///
633/// Returns false when the chain filtered everything (the dense chain's
634/// `!normalized`) — the caller falls back to greedy the same way.
635pub fn sparse_distribution_into(
636    logits: &[f32],
637    config: &SamplerConfig,
638    past_tokens: &[u32],
639    scratch: &mut SamplerScratch,
640    pool: Option<&crate::pool::Pool>,
641    out: &mut Sparse,
642) -> bool {
643    debug_assert!(sparse_ok(config));
644    out.clear();
645    let k = (config.top_k as usize).min(logits.len());
646    if k == 0 {
647        return false;
648    }
649    let penalized = config_penalized(config);
650    let mut probs = std::mem::take(&mut scratch.probs);
651    if penalized {
652        probs.clear();
653        probs.extend_from_slice(logits);
654        apply_penalties(&mut probs, config, past_tokens, scratch);
655    }
656    let src: &[f32] = if penalized { &probs } else { logits };
657    let t = if config.temperature > 0.0 {
658        config.temperature
659    } else {
660        1.0
661    };
662    let mut cand = std::mem::take(&mut scratch.cand);
663    par_topk(pool, src, k, &mut cand, &mut scratch.cand_parts);
664    // `cand` is by value descending, ties by id — the top-k AND every tie
665    // at the k-th place, as the dense chain keeps them.
666    let ok = if let Some(&(_, lmax)) = cand.first().filter(|c| c.1.is_finite()) {
667        let sum_all = par_sum_exp(pool, src, lmax, t, &mut scratch.sum_parts);
668        // e_i = exp((l_i − l_max)/T); min-p against e_i < min_p (max_prob
669        // is e = 1 over the same denominator); probs e_i / sum_all.
670        let min_p = config.min_p;
671        let mut cum = 0.0f32;
672        let mut cut = false;
673        scratch.sparse_cut = false;
674        for &(id, l) in cand.iter() {
675            if cut {
676                break;
677            }
678            let e = ((l - lmax) / t).exp();
679            if min_p > 0.0 && e < min_p {
680                continue;
681            }
682            let pr = e / sum_all;
683            if pr <= 0.0 {
684                continue;
685            }
686            out.push((id, pr));
687            cum += pr;
688            if config.top_p < 1.0 && config.top_p > 0.0 && cum >= config.top_p {
689                cut = true;
690            }
691        }
692        scratch.sparse_cut = cut;
693        // renormalise over the survivors and order by id
694        let sum: f32 = out.iter().map(|c| c.1).sum();
695        if sum > 0.0 {
696            for c in out.iter_mut() {
697                c.1 /= sum;
698            }
699            out.sort_unstable_by_key(|c| c.0);
700            true
701        } else {
702            out.clear();
703            false
704        }
705    } else {
706        false
707    };
708    scratch.cand = cand;
709    scratch.probs = probs;
710    ok
711}
712
713/// The k largest of `src` as `(id, value)` sorted by value descending,
714/// ties by id ascending — INCLUDING every value tied with the k-th, which
715/// is what the dense chain's `p < threshold → 0` keeps. Two pooled passes:
716/// a per-grain k-slot selection merged in grain order for the k-th value,
717/// then a gather of everything at or above it. Nothing here depends on
718/// scheduling, so a seed reproduces.
719fn par_topk(
720    pool: Option<&crate::pool::Pool>,
721    src: &[f32],
722    k: usize,
723    out: &mut Vec<(u32, f32)>,
724    parts: &mut Vec<Vec<(u32, f32)>>,
725) {
726    let better = |a: (u32, f32), b: (u32, f32)| a.1 > b.1 || (a.1 == b.1 && a.0 < b.0);
727    // sorted insertion into a fixed k-slot list: the compare against the
728    // current k-th is what nearly every element pays, and nothing else.
729    let scan = |s: usize, e: usize, best: &mut Vec<(u32, f32)>| {
730        best.clear();
731        for i in s..e {
732            let c = (i as u32, src[i]);
733            if best.len() < k {
734                let pos = best
735                    .iter()
736                    .position(|&b| better(c, b))
737                    .unwrap_or(best.len());
738                best.insert(pos, c);
739            } else if better(c, best[k - 1]) {
740                let pos = best.iter().position(|&b| better(c, b)).unwrap_or(k - 1);
741                best.pop();
742                best.insert(pos, c);
743            }
744        }
745    };
746    let by_value_desc = |a: &(u32, f32), b: &(u32, f32)| {
747        b.1.partial_cmp(&a.1)
748            .unwrap_or(std::cmp::Ordering::Equal)
749            .then(a.0.cmp(&b.0))
750    };
751    // A degenerate row (thousands tied at the k-th place) is capped: the
752    // dense chain would keep them all; nobody samples such a row on
753    // purpose.
754    let cap = k * 4 + 64;
755    // gather everything ≥ kth into `slot`, at most `cap` entries
756    let gather = |s: usize, e: usize, kth: f32, slot: &mut Vec<(u32, f32)>| {
757        slot.clear();
758        for i in s..e {
759            let v = src[i];
760            if v >= kth {
761                slot.push((i as u32, v));
762                if slot.len() >= cap {
763                    break;
764                }
765            }
766        }
767    };
768    out.clear();
769    match pool {
770        Some(p) if src.len() >= PAR_MIN => {
771            let n = src.len();
772            let grain = crate::pool::grain_for(n, p.n_workers() + 1);
773            let ng = n.div_ceil(grain);
774            parts.resize_with(ng, Vec::new);
775            let pp = crate::pool::SendMutT::new(parts.as_mut_ptr());
776            p.run_rows(n, &|s, e| {
777                // SAFETY: grain g is written by exactly one range (start =
778                // g·grain) and `parts` outlives the joined dispatch.
779                let slot = unsafe { &mut *pp.at(s / grain) };
780                scan(s, e, slot);
781            });
782            for g in 0..ng {
783                out.extend_from_slice(&parts[g]);
784            }
785            out.sort_unstable_by(by_value_desc);
786            out.truncate(k);
787            let Some(&(_, kth)) = out.last() else {
788                return;
789            };
790            if !kth.is_finite() {
791                return; // -inf ties are the filtered-out set; keep the k
792            }
793            p.run_rows(n, &|s, e| {
794                let slot = unsafe { &mut *pp.at(s / grain) };
795                gather(s, e, kth, slot);
796            });
797            out.clear();
798            for g in 0..ng {
799                out.extend_from_slice(&parts[g]);
800                if out.len() >= cap {
801                    break;
802                }
803            }
804            out.sort_unstable_by(by_value_desc);
805            out.truncate(cap);
806        }
807        _ => {
808            scan(0, src.len(), out);
809            let Some(&(_, kth)) = out.last() else {
810                return;
811            };
812            if !kth.is_finite() {
813                return;
814            }
815            let mut all = std::mem::take(out);
816            gather(0, src.len(), kth, &mut all);
817            all.sort_unstable_by(by_value_desc);
818            all.truncate(cap);
819            *out = all;
820        }
821    }
822}
823
824/// Σ exp((l − lmax)/t) over the vocab, per-grain partials summed in
825/// grain order (deterministic across runs, so a seed reproduces).
826fn par_sum_exp(
827    pool: Option<&crate::pool::Pool>,
828    src: &[f32],
829    lmax: f32,
830    t: f32,
831    parts: &mut Vec<f32>,
832) -> f32 {
833    // A term below e^-40 cannot move a sum that holds the largest term, whose
834    // value is 1, and most of a vocabulary is there: no call to `exp` for them.
835    let floor = lmax - 40.0 * t;
836    let term = |s: usize, e: usize| -> f32 {
837        let mut acc = 0.0f32;
838        for &l in &src[s..e] {
839            if l >= floor {
840                acc += ((l - lmax) / t).exp();
841            }
842        }
843        acc
844    };
845    match pool {
846        Some(p) if src.len() >= PAR_MIN => {
847            let n = src.len();
848            let grain = crate::pool::grain_for(n, p.n_workers() + 1);
849            let ng = n.div_ceil(grain);
850            parts.clear();
851            parts.resize(ng, 0.0);
852            let pp = crate::pool::SendMut::new(parts.as_mut_ptr());
853            p.run_rows(n, &|s, e| {
854                // SAFETY: one writer per grain slot; joined before read.
855                unsafe { *pp.at(s / grain) = term(s, e) };
856            });
857            parts.iter().sum()
858        }
859        _ => term(0, src.len()),
860    }
861}
862
863/// Draw from a sparse distribution: inverse CDF in id order — the same
864/// walk the dense `categorical_sample` makes over the vocab, so a seed
865/// lands on the same token when the survivor set and probs agree.
866pub fn draw_sparse(p: &[(u32, f32)], rng: &mut SplitMix64) -> u32 {
867    let r = rng.next_f32();
868    let mut cum = 0.0f32;
869    for &(id, pr) in p {
870        cum += pr;
871        if r < cum {
872            return id;
873        }
874    }
875    p.iter().rev().find(|c| c.1 > 0.0).map(|c| c.0).unwrap_or(0)
876}
877
878fn sparse_get(p: &[(u32, f32)], id: u32) -> f32 {
879    p.binary_search_by_key(&id, |c| c.0)
880        .map(|i| p[i].1)
881        .unwrap_or(0.0)
882}
883
884/// `spec_accept_or_correct` over sparse distributions: accept the draft
885/// `d` with min(1, p[d]/q[d]); on rejection draw the correction from the
886/// residual max(0, p − q) over p's support (q's support outside p
887/// contributes nothing to the residual). Empty residual → a draw from p.
888pub fn spec_accept_or_correct_sparse(
889    p: &[(u32, f32)],
890    q: &[(u32, f32)],
891    d: u32,
892    rng: &mut SplitMix64,
893    res: &mut Sparse,
894) -> Option<u32> {
895    let (pd, qd) = (sparse_get(p, d), sparse_get(q, d));
896    let r = rng.next_f32();
897    if qd > 0.0 && r * qd < pd {
898        return None;
899    }
900    res.clear();
901    let mut total = 0.0f32;
902    for &(id, pi) in p {
903        let ri = pi - sparse_get(q, id);
904        if ri > 0.0 {
905            res.push((id, ri));
906            total += ri;
907        }
908    }
909    if total <= 0.0 {
910        return Some(draw_sparse(p, rng));
911    }
912    for c in res.iter_mut() {
913        c.1 /= total;
914    }
915    Some(draw_sparse(res, rng))
916}
917
918/// The distribution the sampler would draw from — the whole chain minus
919/// the draw — as a normalized vector over the vocab, in `out`. Greedy
920/// configs (and the filtered-out fallback) come back as a one-hot, so a
921/// caller can treat every configuration uniformly. This is what
922/// speculative SAMPLING needs from both the draft head and the verify:
923/// accept-with-min(1, p/q), correct from max(0, p − q).
924pub fn distribution_into(
925    logits: &[f32],
926    config: &SamplerConfig,
927    past_tokens: &[u32],
928    scratch: &mut SamplerScratch,
929    pool: Option<&crate::pool::Pool>,
930    out: &mut Vec<f32>,
931) {
932    let one_hot = |out: &mut Vec<f32>, t: usize, n: usize| {
933        out.clear();
934        out.resize(n, 0.0);
935        if t < n {
936            out[t] = 1.0;
937        }
938    };
939    if config.temperature < 1e-6
940        && config.repetition_penalty == 1.0
941        && config.presence_penalty == 0.0
942        && config.suppress_tokens.is_empty()
943    {
944        return one_hot(out, argmax(logits) as usize, logits.len());
945    }
946    let mut probs = std::mem::take(&mut scratch.probs);
947    let normalized = chain(logits, config, past_tokens, scratch, pool, &mut probs);
948    if config.temperature < 1e-6 {
949        let t = argmax(&probs) as usize;
950        scratch.probs = probs;
951        return one_hot(out, t, logits.len());
952    }
953    if !normalized {
954        scratch.probs = probs;
955        return one_hot(out, argmax(logits) as usize, logits.len());
956    }
957    out.clear();
958    out.extend_from_slice(&probs);
959    scratch.probs = probs;
960}
961
962/// Draw from a normalized distribution with the caller's RNG.
963pub fn draw(probs: &[f32], rng: &mut SplitMix64) -> u32 {
964    categorical_sample(probs, rng.next_f32())
965}
966
967/// One step of speculative sampling (Leviathan et al. / Chen et al.):
968/// the draft `d` was drawn from `q`; the target distribution at the same
969/// position is `p`. Returns `None` when `d` is accepted (with probability
970/// min(1, p[d]/q[d])) and `Some(c)` when it is rejected, `c` drawn from
971/// the residual max(0, p − q) renormalized — which is exactly what makes
972/// the emitted token stream distributed as `p`, draft or no draft. When
973/// the residual is empty (p ⊆ q, so p == q on the support) the correction
974/// falls back to a draw from `p` itself. `scratch` holds the residual;
975/// the pool spreads the vocab-wide pass.
976pub fn spec_accept_or_correct(
977    p: &[f32],
978    q: &[f32],
979    d: u32,
980    rng: &mut SplitMix64,
981    scratch: &mut Vec<f32>,
982    pool: Option<&crate::pool::Pool>,
983) -> Option<u32> {
984    let di = d as usize;
985    let (pd, qd) = (
986        p.get(di).copied().unwrap_or(0.0),
987        q.get(di).copied().unwrap_or(0.0),
988    );
989    let r = rng.next_f32();
990    // accept iff r < min(1, pd/qd)  ⇔  r·qd < pd (qd > 0 since d was drawn from q)
991    if qd > 0.0 && r * qd < pd {
992        return None;
993    }
994    let n = p.len().min(q.len());
995    scratch.clear();
996    scratch.extend_from_slice(&p[..n]);
997    // residual = max(0, p − q), elementwise over the pool
998    {
999        let qp = q.as_ptr() as usize;
1000        let sm = crate::pool::SendMut::new(scratch.as_mut_ptr());
1001        let body = move |s: usize, e: usize| {
1002            // SAFETY: disjoint ranges; q outlives the (joined) dispatch.
1003            let qs = unsafe { std::slice::from_raw_parts(qp as *const f32, n) };
1004            for i in s..e {
1005                unsafe {
1006                    let x = sm.at(i);
1007                    *x = (*x - qs[i]).max(0.0);
1008                }
1009            }
1010        };
1011        match pool {
1012            Some(pl) if n >= PAR_MIN => pl.run_rows(n, &body),
1013            _ => body(0, n),
1014        }
1015    }
1016    let sum: f32 = scratch.iter().sum();
1017    if sum > 0.0 {
1018        let inv = 1.0 / sum;
1019        par_map(pool, scratch, &move |v| v * inv);
1020        Some(categorical_sample(scratch, rng.next_f32()))
1021    } else {
1022        Some(categorical_sample(&p[..n], rng.next_f32()))
1023    }
1024}
1025
1026/// Greedy: index of the maximum value.
1027///
1028/// Four running maxima instead of one: the scalar `max_by` carried a loop
1029/// dependency through the comparison, which at a 129k vocab is a tenth of a
1030/// millisecond of pure serial work per token.
1031///
1032/// Ties resolve to the HIGHEST index — not an arbitrary choice, it is what
1033/// `Iterator::max_by` does (it keeps the last of several equal maxima) and
1034/// therefore what this has always returned. `explain`'s preview compares
1035/// its own argmax against what greedy emits, and that test is what catches
1036/// the flip.
1037pub fn argmax(values: &[f32]) -> u32 {
1038    if values.is_empty() {
1039        return 0;
1040    }
1041    let n = values.len();
1042    let mut best = [(0usize, f32::NEG_INFINITY); 4];
1043    for (l, b) in best.iter_mut().enumerate() {
1044        b.0 = l.min(n - 1);
1045    }
1046    let mut i = 0;
1047    while i + 4 <= n {
1048        for l in 0..4 {
1049            let v = values[i + l];
1050            if v >= best[l].1 {
1051                best[l] = (i + l, v);
1052            }
1053        }
1054        i += 4;
1055    }
1056    let mut bi = best[0].0;
1057    let mut bv = best[0].1;
1058    for b in &best[1..] {
1059        if b.1 > bv || (b.1 == bv && b.0 > bi) {
1060            bi = b.0;
1061            bv = b.1;
1062        }
1063    }
1064    while i < n {
1065        if values[i] >= bv {
1066            bv = values[i];
1067            bi = i;
1068        }
1069        i += 1;
1070    }
1071    bi as u32
1072}
1073
1074fn softmax_inplace(logits: &mut [f32]) {
1075    let max_val = logits.iter().cloned().fold(f32::NEG_INFINITY, f32::max);
1076    let mut sum = 0.0f32;
1077    for v in logits.iter_mut() {
1078        *v = (*v - max_val).exp();
1079        sum += *v;
1080    }
1081    if sum > 0.0 {
1082        for v in logits.iter_mut() {
1083            *v /= sum;
1084        }
1085    }
1086}
1087
1088/// Below this length the pool's dispatch costs more than the pass.
1089const PAR_MIN: usize = 1 << 14;
1090
1091/// Elementwise `buf[i] = f(buf[i])` over the pool (serial without one, or
1092/// for short buffers). Each output depends on its own input alone, so the
1093/// chunking cannot change a single bit.
1094fn par_map(pool: Option<&crate::pool::Pool>, buf: &mut [f32], f: &(dyn Fn(f32) -> f32 + Sync)) {
1095    match pool {
1096        Some(p) if buf.len() >= PAR_MIN => {
1097            let out = crate::pool::SendMut::new(buf.as_mut_ptr());
1098            p.run_rows(buf.len(), &move |s, e| {
1099                for i in s..e {
1100                    // SAFETY: ranges from run_rows are disjoint and the
1101                    // buffer outlives the (joined) dispatch.
1102                    unsafe {
1103                        let q = out.at(i);
1104                        *q = f(*q);
1105                    }
1106                }
1107            });
1108        }
1109        _ => {
1110            for v in buf.iter_mut() {
1111                *v = f(*v);
1112            }
1113        }
1114    }
1115}
1116
1117/// `fold(init, f32::max)` over the pool. Max is order-free on non-NaN
1118/// input, so per-chunk maxima combined give the serial fold's answer.
1119fn par_max(pool: Option<&crate::pool::Pool>, buf: &[f32], init: f32) -> f32 {
1120    match pool {
1121        Some(p) if buf.len() >= PAR_MIN => {
1122            let m = std::sync::Mutex::new(init);
1123            p.run_rows(buf.len(), &|s, e| {
1124                let local = buf[s..e].iter().cloned().fold(init, f32::max);
1125                let mut g = m.lock().unwrap();
1126                *g = g.max(local);
1127            });
1128            m.into_inner().unwrap()
1129        }
1130        _ => buf.iter().cloned().fold(init, f32::max),
1131    }
1132}
1133
1134/// `softmax_inplace` with the exp and the normalisation spread over the
1135/// pool. The max is order-free, the exp is elementwise, and the SUM stays
1136/// a sequential index-order fold — exactly the serial loop's accumulation
1137/// — so the probabilities are bit-identical.
1138fn softmax_inplace_pool(pool: Option<&crate::pool::Pool>, logits: &mut [f32]) {
1139    if pool.is_none() || logits.len() < PAR_MIN {
1140        return softmax_inplace(logits);
1141    }
1142    let max_val = par_max(pool, logits, f32::NEG_INFINITY);
1143    par_map(pool, logits, &move |v| (v - max_val).exp());
1144    let sum: f32 = logits.iter().sum();
1145    if sum > 0.0 {
1146        par_map(pool, logits, &move |v| v / sum);
1147    }
1148}
1149
1150/// The k-th largest value of `probs` (k ≥ 1, k ≤ len), by the same
1151/// descending `partial_cmp` order `select_nth_unstable_by` used — one
1152/// streaming pass with a k-slot min-heap instead of a whole-vocab copy
1153/// and partition. Values, not indices, so ties give the same threshold.
1154fn kth_largest(probs: &[f32], k: usize) -> f32 {
1155    use std::cmp::Ordering;
1156    // Min-heap on the k largest seen so far: `heap[0]` is the smallest of
1157    // them, i.e. the running k-th largest.
1158    let mut heap: Vec<f32> = Vec::with_capacity(k);
1159    let desc = |a: f32, b: f32| b.partial_cmp(&a).unwrap_or(Ordering::Equal);
1160    let sift_down = |h: &mut [f32], mut i: usize| {
1161        let n = h.len();
1162        loop {
1163            let (l, r) = (2 * i + 1, 2 * i + 2);
1164            let mut m = i;
1165            // child "smaller" in the descending order = later in it
1166            if l < n && desc(h[l], h[m]) == Ordering::Greater {
1167                m = l;
1168            }
1169            if r < n && desc(h[r], h[m]) == Ordering::Greater {
1170                m = r;
1171            }
1172            if m == i {
1173                break;
1174            }
1175            h.swap(i, m);
1176            i = m;
1177        }
1178    };
1179    let sift_up = |h: &mut [f32], mut i: usize| {
1180        while i > 0 {
1181            let parent = (i - 1) / 2;
1182            if desc(h[i], h[parent]) == Ordering::Greater {
1183                h.swap(i, parent);
1184                i = parent;
1185            } else {
1186                break;
1187            }
1188        }
1189    };
1190    for &v in probs {
1191        if heap.len() < k {
1192            heap.push(v);
1193            let n = heap.len();
1194            sift_up(&mut heap, n - 1);
1195        } else if desc(v, heap[0]) == Ordering::Less {
1196            // v is larger than the current k-th largest: replace it.
1197            heap[0] = v;
1198            sift_down(&mut heap, 0);
1199        }
1200    }
1201    heap[0]
1202}
1203
1204/// `apply_top_k` without the second vocab-sized copy: the threshold is
1205/// the k-th largest value from one streaming pass, the zeroing is an
1206/// elementwise pass over the pool. Same kept set, same values.
1207fn apply_top_k_pool(pool: Option<&crate::pool::Pool>, probs: &mut [f32], k: usize) {
1208    if k == 0 || k >= probs.len() {
1209        return;
1210    }
1211    let threshold = kth_largest(probs, k);
1212    par_map(pool, probs, &move |p| if p < threshold { 0.0 } else { p });
1213}
1214
1215/// Top-1 probability of `id` under a softmax at temperature `temp` — the
1216/// per-token confidence — with the exp pass over the pool and the sum
1217/// sequential in index order (bit-identical to the serial fold). Uses
1218/// the scratch's partition buffer, idle now that top-k streams.
1219pub fn top1_prob_pool(
1220    pool: Option<&crate::pool::Pool>,
1221    scratch: &mut SamplerScratch,
1222    logits: &[f32],
1223    id: u32,
1224    temp: f32,
1225) -> f32 {
1226    let pool = if logits.len() <= SAMPLER_SERIAL_MAX { None } else { pool };
1227    let t = if temp > 1e-3 { temp } else { 1.0 };
1228    let max = par_max(pool, logits, f32::NEG_INFINITY);
1229    let sum = par_sum_exp(pool, logits, max, t, &mut scratch.sum_parts);
1230    if sum > 0.0 {
1231        (((logits[id as usize] - max) / t).exp()) / sum
1232    } else {
1233        0.0
1234    }
1235}
1236
1237fn apply_repetition_penalty(
1238    logits: &mut [f32],
1239    past_tokens: &[u32],
1240    penalty: f32,
1241    scratch: &mut SamplerScratch,
1242) {
1243    let epoch = scratch.begin_seen(logits.len());
1244    for &tok in past_tokens {
1245        let idx = tok as usize;
1246        if idx < logits.len() && scratch.seen_epoch[idx] != epoch {
1247            scratch.seen_epoch[idx] = epoch;
1248            if logits[idx] > 0.0 {
1249                logits[idx] /= penalty;
1250            } else {
1251                logits[idx] *= penalty;
1252            }
1253        }
1254    }
1255}
1256
1257/// Keep the k highest-probability tokens (plus exact ties at the
1258/// threshold), zero the rest. Selection, not a full vocab sort — the
1259/// old double `sort_by` over ~150k probs was ~1ms of pure per-token
1260/// overhead (roadmap §3 P0).
1261fn apply_top_k(probs: &mut [f32], k: usize, sel: &mut Vec<f32>) {
1262    if k == 0 || k >= probs.len() {
1263        return;
1264    }
1265    // `select_nth_unstable` permutes, so the partition needs its own
1266    // buffer — but not a FRESH one each token.
1267    sel.clear();
1268    sel.extend_from_slice(probs);
1269    // k-th largest = (k-1)-th index in a descending partition.
1270    let (_, kth, _) = sel.select_nth_unstable_by(k - 1, |a, b| {
1271        b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
1272    });
1273    let threshold = *kth;
1274    for p in probs.iter_mut() {
1275        if *p < threshold {
1276            *p = 0.0;
1277        }
1278    }
1279}
1280
1281/// Nucleus: keep the smallest prefix of tokens whose cumulative
1282/// probability reaches top_p. Only surviving (non-zero) candidates are
1283/// sorted — after top-k that is ≤ k elements, not the whole vocab; the
1284/// kept set is marked in-place instead of a per-token HashSet.
1285fn apply_top_p(probs: &mut [f32], top_p: f32) {
1286    if apply_top_p_head(probs, top_p) {
1287        return;
1288    }
1289    apply_top_p_full(probs, top_p);
1290}
1291
1292/// Top-p from the head of the distribution, without sorting the vocab:
1293/// partition out the `TOP_P_HEAD` largest probabilities, sort only those,
1294/// and find the cutoff there. Keeps exactly what `apply_top_p_full` keeps
1295/// — every probability above the cutoff value, and of those EQUAL to it
1296/// the lowest indices first (the stable sort's order) — and declines
1297/// (false, `probs` untouched) when the nucleus does not close inside the
1298/// head or a tie at the cutoff reaches past it. A 262k vocab with no top-k
1299/// (Gemma-4 humanizer: top-p 0.95, top-k off) paid a full 262k sort per
1300/// token on the host: ~40 ms of a ~70 ms token.
1301const TOP_P_HEAD: usize = 1024;
1302
1303fn apply_top_p_head(probs: &mut [f32], top_p: f32) -> bool {
1304    let n = probs.len();
1305    if n <= TOP_P_HEAD * 4 {
1306        return false;
1307    }
1308    let mut head: Vec<(usize, f32)> = probs
1309        .iter()
1310        .copied()
1311        .enumerate()
1312        .filter(|&(_, p)| p > 0.0)
1313        .collect();
1314    if head.len() <= TOP_P_HEAD {
1315        return false;
1316    }
1317    let by_desc = |a: &(usize, f32), b: &(usize, f32)| {
1318        b.1.partial_cmp(&a.1)
1319            .unwrap_or(std::cmp::Ordering::Equal)
1320            .then(a.0.cmp(&b.0))
1321    };
1322    head.select_nth_unstable_by(TOP_P_HEAD - 1, by_desc);
1323    head.truncate(TOP_P_HEAD);
1324    head.sort_by(by_desc);
1325    let mut cumsum = 0.0f32;
1326    let mut cut = None;
1327    for (i, &(_, prob)) in head.iter().enumerate() {
1328        cumsum += prob;
1329        if cumsum >= top_p {
1330            cut = Some(i);
1331            break;
1332        }
1333    }
1334    let Some(cut) = cut else {
1335        return false;
1336    };
1337    let v = head[cut].1;
1338    // How many of the kept entries carry the cutoff value; the full sort
1339    // keeps exactly that many of the value's lowest indices.
1340    let keep_eq = head[..=cut].iter().filter(|&&(_, p)| p == v).count();
1341    // A tie group that spills past the head could hide lower indices the
1342    // partition left out: the full sort decides then.
1343    if head[TOP_P_HEAD - 1].1 == v {
1344        return false;
1345    }
1346    let mut eq_seen = 0usize;
1347    for p in probs.iter_mut() {
1348        if *p > v {
1349            continue;
1350        }
1351        if *p == v && eq_seen < keep_eq {
1352            eq_seen += 1;
1353            continue;
1354        }
1355        *p = 0.0;
1356    }
1357    true
1358}
1359
1360fn apply_top_p_full(probs: &mut [f32], top_p: f32) {
1361    let mut indexed: Vec<(usize, f32)> = probs
1362        .iter()
1363        .copied()
1364        .enumerate()
1365        .filter(|&(_, p)| p > 0.0)
1366        .collect();
1367    indexed.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
1368
1369    let mut cumsum = 0.0f32;
1370    let mut cutoff_idx = indexed.len();
1371    for (i, &(_, prob)) in indexed.iter().enumerate() {
1372        cumsum += prob;
1373        if cumsum >= top_p {
1374            cutoff_idx = i + 1;
1375            break;
1376        }
1377    }
1378
1379    // Zero the dropped tail directly — indices, not membership tests.
1380    for &(i, _) in &indexed[cutoff_idx..] {
1381        probs[i] = 0.0;
1382    }
1383}
1384
1385/// Inverse-CDF sampling with an externally supplied uniform r ∈ [0, 1).
1386fn categorical_sample(probs: &[f32], r: f32) -> u32 {
1387    let mut cumsum = 0.0f32;
1388    for (i, &p) in probs.iter().enumerate() {
1389        cumsum += p;
1390        if r < cumsum {
1391            return i as u32;
1392        }
1393    }
1394    probs.iter().rposition(|&p| p > 0.0).unwrap_or(0) as u32
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399    /// The four-lane argmax against the obvious scalar one, including the
1400    /// tie rule: `max_by` keeps the LAST of several equal maxima, and a
1401    /// lane split that quietly picked the first would move greedy output on
1402    /// any model with two equally-likely tokens.
1403    #[test]
1404    fn argmax_lanes_match_the_scalar_one_ties_and_all() {
1405        // The reference IS the old implementation, `max_by` and all — the
1406        // point is that nothing observable changed, tie rule included.
1407        let scalar = |v: &[f32]| -> u32 {
1408            v.iter()
1409                .enumerate()
1410                .max_by(|(_, a), (_, b)| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal))
1411                .map(|(i, _)| i as u32)
1412                .unwrap_or(0)
1413        };
1414        for n in 0..40usize {
1415            for seed in 0..8u64 {
1416                let mut r = super::SplitMix64::new(seed * 7 + n as u64);
1417                // Quantized to few distinct values on purpose: ties are the
1418                // case the lanes can get wrong and random floats never hit.
1419                let v: Vec<f32> = (0..n).map(|_| ((r.next_u64() % 5) as f32) - 2.0).collect();
1420                assert_eq!(super::argmax(&v), scalar(&v), "n={n} seed={seed} {v:?}");
1421            }
1422        }
1423        let flat = vec![f32::NEG_INFINITY; 13];
1424        assert_eq!(super::argmax(&flat), scalar(&flat));
1425    }
1426
1427    use super::*;
1428
1429    #[test]
1430    fn test_argmax() {
1431        let logits = vec![0.1, 0.5, 0.3, 0.9, 0.2];
1432        assert_eq!(argmax(&logits), 3);
1433    }
1434
1435    #[test]
1436    fn test_greedy_sampling() {
1437        let logits = vec![1.0, 5.0, 2.0, 3.0];
1438        let config = SamplerConfig {
1439            temperature: 0.0,
1440            ..Default::default()
1441        };
1442        let mut rng = SplitMix64::new(1);
1443        assert_eq!(sample(&logits, &config, &[], &mut rng), 1);
1444    }
1445
1446    /// `argmax_penalized` must equal the copy-and-penalize chain's argmax
1447    /// — same values, same tie rule — with and without the pool.
1448    #[test]
1449    fn argmax_penalized_matches_chain_argmax() {
1450        let pool = crate::pool::Pool::new(3);
1451        let n = 40_000usize;
1452        for seed in 0..8u64 {
1453            let mut r = SplitMix64::new(seed + 3);
1454            // coarse values so ties happen
1455            let logits: Vec<f32> = (0..n)
1456                .map(|_| ((r.next_u64() % 41) as f32 - 20.0) / 4.0)
1457                .collect();
1458            let past: Vec<u32> = (0..2000)
1459                .map(|_| (r.next_u64() % n as u64) as u32)
1460                .collect();
1461            for cfg in [
1462                SamplerConfig {
1463                    temperature: 0.0,
1464                    repetition_penalty: 1.1,
1465                    ..Default::default()
1466                },
1467                SamplerConfig {
1468                    temperature: 0.0,
1469                    repetition_penalty: 1.0,
1470                    presence_penalty: 1.5,
1471                    ..Default::default()
1472                },
1473                SamplerConfig {
1474                    temperature: 0.0,
1475                    repetition_penalty: 1.3,
1476                    presence_penalty: 0.7,
1477                    suppress_tokens: vec![5, 77, 3000],
1478                    ..Default::default()
1479                },
1480            ] {
1481                let mut s1 = SamplerScratch::default();
1482                let mut probs = Vec::new();
1483                chain(&logits, &cfg, &past, &mut s1, None, &mut probs);
1484                let want = argmax(&probs);
1485                let mut s2 = SamplerScratch::default();
1486                let got_serial = argmax_penalized(&logits, &cfg, &past, &mut s2, None);
1487                let got_pool = argmax_penalized(&logits, &cfg, &past, &mut s2, Some(&pool));
1488                assert_eq!(want, got_serial, "serial, seed {seed} cfg {cfg:?}");
1489                assert_eq!(want, got_pool, "pool, seed {seed} cfg {cfg:?}");
1490            }
1491        }
1492    }
1493
1494    /// The pool chain must sample the SAME token as the serial one on the
1495    /// same seed — that is the whole contract of the parallel passes.
1496    #[test]
1497    fn pool_chain_matches_serial_bit_for_bit() {
1498        let pool = crate::pool::Pool::new(3);
1499        let n = 40_000usize; // above PAR_MIN so the pool arm is exercised
1500        for seed in 0..6u64 {
1501            let mut r = SplitMix64::new(seed + 11);
1502            let logits: Vec<f32> = (0..n)
1503                .map(|i| ((r.next_u64() % 2001) as f32 - 1000.0) / 90.0 + (i % 7) as f32 * 0.01)
1504                .collect();
1505            let past: Vec<u32> = (0..500).map(|_| (r.next_u64() % n as u64) as u32).collect();
1506            for cfg in [
1507                SamplerConfig::default(),
1508                SamplerConfig {
1509                    temperature: 0.7,
1510                    top_p: 0.8,
1511                    top_k: 20,
1512                    min_p: 0.0,
1513                    presence_penalty: 1.5,
1514                    repetition_penalty: 1.0,
1515                    ..Default::default()
1516                },
1517                SamplerConfig {
1518                    temperature: 1.0,
1519                    top_p: 0.95,
1520                    top_k: 20,
1521                    min_p: 0.0,
1522                    ..Default::default()
1523                },
1524                SamplerConfig {
1525                    temperature: 1.3,
1526                    top_p: 1.0,
1527                    top_k: 0,
1528                    min_p: 0.02,
1529                    ..Default::default()
1530                },
1531            ] {
1532                let mut s1 = SamplerScratch::default();
1533                let mut s2 = SamplerScratch::default();
1534                for step in 0..5u64 {
1535                    let mut r1 = SplitMix64::new(seed * 100 + step);
1536                    let mut r2 = r1.clone();
1537                    let a = sample_with_scratch(&logits, &cfg, &past, &mut r1, &mut s1);
1538                    let b = sample_with_scratch_pool(
1539                        &logits,
1540                        &cfg,
1541                        &past,
1542                        &mut r2,
1543                        &mut s2,
1544                        Some(&pool),
1545                    );
1546                    assert_eq!(a, b, "seed {seed} step {step} cfg {cfg:?}");
1547                    // and the working copies agree value for value
1548                    assert_eq!(s1.probs, s2.probs, "probs differ seed {seed} step {step}");
1549                }
1550            }
1551        }
1552    }
1553
1554    /// Speculative sampling must reproduce the TARGET distribution however
1555    /// good or bad the draft is: draw d ~ q, accept with min(1, p/q), else
1556    /// correct from max(0, p − q). Empirical law over many trials against
1557    /// p itself, for a sharp draft, a flat draft and a wrong draft.
1558    #[test]
1559    fn spec_accept_or_correct_reproduces_the_target() {
1560        let n = 40usize;
1561        let mk = |seed: u64, sharp: f32| -> Vec<f32> {
1562            let mut r = SplitMix64::new(seed);
1563            let mut v: Vec<f32> = (0..n)
1564                .map(|_| ((r.next_u64() % 1000) as f32 / 1000.0).powf(sharp))
1565                .collect();
1566            // a few exact zeros, like a top-k'd distribution
1567            for i in 0..n {
1568                if (i * 7 + seed as usize) % 5 == 0 {
1569                    v[i] = 0.0;
1570                }
1571            }
1572            let s: f32 = v.iter().sum();
1573            v.iter().map(|x| x / s).collect()
1574        };
1575        let p = mk(3, 3.0);
1576        for (qi, q) in [mk(3, 3.0), mk(11, 1.0), mk(29, 6.0)]
1577            .into_iter()
1578            .enumerate()
1579        {
1580            let mut rng = SplitMix64::new(77 + qi as u64);
1581            let mut counts = vec![0u64; n];
1582            let mut scratch = Vec::new();
1583            let trials = 400_000u64;
1584            for _ in 0..trials {
1585                let d = categorical_sample(&q, rng.next_f32());
1586                let t = match spec_accept_or_correct(&p, &q, d, &mut rng, &mut scratch, None) {
1587                    None => d,
1588                    Some(c) => c,
1589                };
1590                counts[t as usize] += 1;
1591            }
1592            let l1: f64 = (0..n)
1593                .map(|i| (counts[i] as f64 / trials as f64 - p[i] as f64).abs())
1594                .sum();
1595            eprintln!("spec q#{qi}: L1(empirical, p) = {l1:.4}");
1596            assert!(
1597                l1 < 0.01,
1598                "q#{qi}: empirical distribution drifted from p, L1 {l1}"
1599            );
1600            // and nothing outside p's support was ever emitted
1601            for i in 0..n {
1602                if p[i] == 0.0 {
1603                    assert_eq!(counts[i], 0, "q#{qi}: token {i} outside p emitted");
1604                }
1605            }
1606        }
1607    }
1608
1609    /// The sparse chain is the dense chain: same survivor set, same
1610    /// probabilities (to fp), serial and pooled, with and without
1611    /// penalties, min-p, top-p — and a seed draws the same token.
1612    #[test]
1613    fn sparse_chain_matches_the_dense_chain() {
1614        let pool = crate::pool::Pool::new(3);
1615        let n = 40_000usize; // above PAR_MIN: the pooled arms run
1616        for seed in 0..5u64 {
1617            let mut r = SplitMix64::new(100 + seed);
1618            let logits: Vec<f32> = (0..n)
1619                .map(|_| ((r.next_u64() % 3000) as f32 - 1500.0) / 120.0)
1620                .collect();
1621            let past: Vec<u32> = (0..400).map(|_| (r.next_u64() % n as u64) as u32).collect();
1622            for cfg in [
1623                SamplerConfig {
1624                    temperature: 0.7,
1625                    top_p: 0.8,
1626                    top_k: 20,
1627                    min_p: 0.0,
1628                    presence_penalty: 1.5,
1629                    repetition_penalty: 1.0,
1630                    ..Default::default()
1631                },
1632                SamplerConfig {
1633                    temperature: 1.0,
1634                    top_p: 0.95,
1635                    top_k: 40,
1636                    min_p: 0.05,
1637                    presence_penalty: 0.0,
1638                    repetition_penalty: 1.1,
1639                    ..Default::default()
1640                },
1641                SamplerConfig {
1642                    temperature: 0.6,
1643                    top_p: 1.0,
1644                    top_k: 3,
1645                    min_p: 0.0,
1646                    presence_penalty: 0.0,
1647                    repetition_penalty: 1.0,
1648                    suppress_tokens: vec![5, 6, 7],
1649                    ..Default::default()
1650                },
1651            ] {
1652                assert!(sparse_ok(&cfg));
1653                let mut sd = SamplerScratch::default();
1654                let mut dense = Vec::new();
1655                distribution_into(&logits, &cfg, &past, &mut sd, None, &mut dense);
1656                for pl in [None, Some(&pool)] {
1657                    let mut ss = SamplerScratch::default();
1658                    let mut sp = Vec::new();
1659                    let ok = sparse_distribution_into(&logits, &cfg, &past, &mut ss, pl, &mut sp);
1660                    assert!(ok, "seed {seed} cfg {cfg:?}");
1661                    let dense_nz: Vec<(u32, f32)> = dense
1662                        .iter()
1663                        .enumerate()
1664                        .filter(|&(_, &v)| v > 0.0)
1665                        .map(|(i, &v)| (i as u32, v))
1666                        .collect();
1667                    assert_eq!(
1668                        dense_nz.len(),
1669                        sp.len(),
1670                        "seed {seed} pool {} cfg {cfg:?}: support {:?} vs {:?}",
1671                        pl.is_some(),
1672                        dense_nz,
1673                        sp
1674                    );
1675                    for (a, b) in dense_nz.iter().zip(sp.iter()) {
1676                        assert_eq!(a.0, b.0, "seed {seed} cfg {cfg:?}: ids differ");
1677                        assert!(
1678                            (a.1 - b.1).abs() <= 2e-5 * a.1.max(1e-3),
1679                            "seed {seed} cfg {cfg:?}: prob {} vs {}",
1680                            a.1,
1681                            b.1
1682                        );
1683                    }
1684                    // the seed lands on the same token (both walk the ids
1685                    // in order); allow a boundary rounding miss or two
1686                    let mut agree = 0usize;
1687                    let trials = 400usize;
1688                    for k in 0..trials as u64 {
1689                        let mut r1 = SplitMix64::new(500 + k);
1690                        let mut r2 = SplitMix64::new(500 + k);
1691                        let a = categorical_sample(&dense, r1.next_f32());
1692                        let b = draw_sparse(&sp, &mut r2);
1693                        agree += (a == b) as usize;
1694                    }
1695                    assert!(
1696                        agree >= trials - 2,
1697                        "seed {seed} cfg {cfg:?}: agree {agree}/{trials}"
1698                    );
1699                    // and the public entry uses it
1700                    let mut r1 = SplitMix64::new(9);
1701                    let mut r2 = SplitMix64::new(9);
1702                    let a = categorical_sample(&dense, r1.next_f32());
1703                    let mut s3 = SamplerScratch::default();
1704                    let b = sample_with_scratch_pool(&logits, &cfg, &past, &mut r2, &mut s3, pl);
1705                    assert_eq!(a, b, "seed {seed} cfg {cfg:?}: entry draw");
1706                }
1707            }
1708        }
1709    }
1710
1711    /// The sparse accept/correct emits the target distribution, like its
1712    /// dense twin — the same 400k-trial law test over sparse p and q.
1713    #[test]
1714    fn spec_accept_or_correct_sparse_reproduces_the_target() {
1715        let n = 40usize;
1716        let mk = |seed: u64, sharp: f32| -> Vec<(u32, f32)> {
1717            let mut r = SplitMix64::new(seed);
1718            let mut v: Vec<f32> = (0..n)
1719                .map(|_| ((r.next_u64() % 1000) as f32 / 1000.0).powf(sharp))
1720                .collect();
1721            for i in 0..n {
1722                if (i * 7 + seed as usize) % 5 == 0 {
1723                    v[i] = 0.0;
1724                }
1725            }
1726            let s: f32 = v.iter().sum();
1727            v.iter()
1728                .enumerate()
1729                .filter(|&(_, &x)| x > 0.0)
1730                .map(|(i, &x)| (i as u32, x / s))
1731                .collect()
1732        };
1733        let p = mk(3, 3.0);
1734        for (qi, q) in [mk(3, 3.0), mk(11, 1.0), mk(29, 6.0)]
1735            .into_iter()
1736            .enumerate()
1737        {
1738            let mut rng = SplitMix64::new(77 + qi as u64);
1739            let mut counts = vec![0u64; n];
1740            let mut res = Vec::new();
1741            let trials = 400_000u64;
1742            for _ in 0..trials {
1743                let d = draw_sparse(&q, &mut rng);
1744                let t = match spec_accept_or_correct_sparse(&p, &q, d, &mut rng, &mut res) {
1745                    None => d,
1746                    Some(c) => c,
1747                };
1748                counts[t as usize] += 1;
1749            }
1750            let l1: f64 = (0..n)
1751                .map(|i| (counts[i] as f64 / trials as f64 - sparse_get(&p, i as u32) as f64).abs())
1752                .sum();
1753            eprintln!("sparse spec q#{qi}: L1(empirical, p) = {l1:.4}");
1754            assert!(l1 < 0.01, "q#{qi}: drifted, L1 {l1}");
1755            for i in 0..n {
1756                if sparse_get(&p, i as u32) == 0.0 {
1757                    assert_eq!(counts[i], 0, "q#{qi}: token {i} outside p emitted");
1758                }
1759            }
1760        }
1761    }
1762
1763    /// `distribution_into` is the sampler's own chain: drawing from it
1764    /// with the same uniform lands on the same token as `sample_*`.
1765    #[test]
1766    fn distribution_matches_the_sampler_draw() {
1767        let n = 20_000usize;
1768        let mut r = SplitMix64::new(9);
1769        let logits: Vec<f32> = (0..n)
1770            .map(|_| ((r.next_u64() % 3000) as f32 - 1500.0) / 120.0)
1771            .collect();
1772        let past: Vec<u32> = (0..300).map(|_| (r.next_u64() % n as u64) as u32).collect();
1773        for cfg in [
1774            SamplerConfig::default(),
1775            SamplerConfig {
1776                temperature: 0.7,
1777                top_p: 0.8,
1778                top_k: 20,
1779                min_p: 0.0,
1780                presence_penalty: 1.5,
1781                repetition_penalty: 1.0,
1782                ..Default::default()
1783            },
1784            SamplerConfig {
1785                temperature: 0.0,
1786                repetition_penalty: 1.1,
1787                ..Default::default()
1788            },
1789            SamplerConfig {
1790                temperature: 0.0,
1791                repetition_penalty: 1.0,
1792                presence_penalty: 0.0,
1793                ..Default::default()
1794            },
1795        ] {
1796            let mut s1 = SamplerScratch::default();
1797            let mut s2 = SamplerScratch::default();
1798            for step in 0..4u64 {
1799                let mut r1 = SplitMix64::new(step + 1);
1800                let mut r2 = r1.clone();
1801                let a = sample_with_scratch(&logits, &cfg, &past, &mut r1, &mut s1);
1802                let mut dist = Vec::new();
1803                distribution_into(&logits, &cfg, &past, &mut s2, None, &mut dist);
1804                assert!(
1805                    (dist.iter().sum::<f32>() - 1.0).abs() < 1e-3,
1806                    "not normalized: {}",
1807                    dist.iter().sum::<f32>()
1808                );
1809                let b = if cfg.temperature < 1e-6 {
1810                    argmax(&dist)
1811                } else {
1812                    draw(&dist, &mut r2)
1813                };
1814                assert_eq!(a, b, "cfg {cfg:?} step {step}");
1815            }
1816        }
1817    }
1818
1819    /// The streaming k-th largest against `select_nth_unstable_by` — the
1820    /// value the old partition returned, ties and zeros included.
1821    #[test]
1822    fn kth_largest_equals_select_nth() {
1823        for seed in 0..20u64 {
1824            let mut r = SplitMix64::new(seed);
1825            let n = 50 + (r.next_u64() % 3000) as usize;
1826            let v: Vec<f32> = (0..n)
1827                .map(|_| {
1828                    if r.next_u64() % 3 == 0 {
1829                        0.0
1830                    } else {
1831                        (r.next_u64() % 97) as f32 / 97.0
1832                    }
1833                })
1834                .collect();
1835            for k in [1usize, 2, 5, 20, 40, n / 2, n - 1] {
1836                let mut sel = v.clone();
1837                let (_, kth, _) = sel.select_nth_unstable_by(k - 1, |a, b| {
1838                    b.partial_cmp(a).unwrap_or(std::cmp::Ordering::Equal)
1839                });
1840                assert_eq!(kth_largest(&v, k), *kth, "seed {seed} k {k}");
1841            }
1842        }
1843    }
1844
1845    /// The pooled confidence equals the serial formula, bit for bit.
1846    #[test]
1847    fn top1_prob_pool_matches_serial() {
1848        let pool = crate::pool::Pool::new(2);
1849        let n = 30_000usize;
1850        let mut r = SplitMix64::new(5);
1851        let logits: Vec<f32> = (0..n)
1852            .map(|_| ((r.next_u64() % 1000) as f32) / 37.0)
1853            .collect();
1854        let serial = |id: u32, temp: f32| -> f32 {
1855            let t = if temp > 1e-3 { temp } else { 1.0 };
1856            let max = logits.iter().fold(f32::NEG_INFINITY, |m, &v| m.max(v));
1857            let sum: f32 = logits.iter().map(|&v| ((v - max) / t).exp()).sum();
1858            (((logits[id as usize] - max) / t).exp()) / sum
1859        };
1860        let mut sc = SamplerScratch::default();
1861        for (id, t) in [(3u32, 1.0f32), (777, 0.7), (29_999, 2.0), (12, 0.0)] {
1862            let a = serial(id, t);
1863            let b = top1_prob_pool(Some(&pool), &mut sc, &logits, id, t);
1864            assert_eq!(a.to_bits(), b.to_bits(), "id {id} t {t}: {a} vs {b}");
1865        }
1866    }
1867
1868    #[test]
1869    fn test_softmax() {
1870        let mut logits = vec![1.0, 2.0, 3.0];
1871        softmax_inplace(&mut logits);
1872        let sum: f32 = logits.iter().sum();
1873        assert!((sum - 1.0).abs() < 1e-5);
1874        assert!(logits[2] > logits[1] && logits[1] > logits[0]);
1875    }
1876
1877    #[test]
1878    fn test_repetition_penalty() {
1879        let mut logits = vec![1.0, 2.0, 3.0, 4.0];
1880        let mut scratch = SamplerScratch::default();
1881        apply_repetition_penalty(&mut logits, &[1, 3], 2.0, &mut scratch);
1882        assert_eq!(logits, vec![1.0, 1.0, 3.0, 2.0]);
1883    }
1884
1885    #[test]
1886    fn repetition_penalty_applies_once_per_unique_token() {
1887        let mut logits = vec![1.0, 4.0, -6.0];
1888        let mut scratch = SamplerScratch::default();
1889        apply_repetition_penalty(&mut logits, &[1, 1, 2, 1, 2], 2.0, &mut scratch);
1890        assert_eq!(logits, vec![1.0, 2.0, -12.0]);
1891    }
1892
1893    /// Top-p with top-k off through the capped sparse chain: when the
1894    /// nucleus closes inside the cap, the support and probabilities are the
1895    /// dense chain's (top-k 0), as for the sparse chain proper.
1896    #[test]
1897    fn nucleus_sparse_matches_the_dense_top_p() {
1898        let n = 50_000usize;
1899        let mut closed = 0;
1900        for seed in 0..8u64 {
1901            let mut r = SplitMix64::new(500 + seed);
1902            let logits: Vec<f32> = (0..n)
1903                .map(|i| {
1904                    let base = ((r.next_u64() % 3000) as f32 - 1500.0) / 300.0;
1905                    if i % 997 == 0 { base + 14.0 } else { base }
1906                })
1907                .collect();
1908            let cfg = SamplerConfig {
1909                temperature: 1.0,
1910                top_p: 0.95,
1911                top_k: 0,
1912                min_p: 0.0,
1913                repetition_penalty: 1.0,
1914                ..Default::default()
1915            };
1916            assert!(nucleus_sparse_ok(&cfg, n));
1917            let mut sd = SamplerScratch::default();
1918            let mut dense = Vec::new();
1919            distribution_into(&logits, &cfg, &[], &mut sd, None, &mut dense);
1920            let capped = SamplerConfig { top_k: SPARSE_TOPK_MAX as u32, ..cfg.clone() };
1921            let mut ss = SamplerScratch::default();
1922            let mut sp = Vec::new();
1923            let ok = sparse_distribution_into(&logits, &capped, &[], &mut ss, None, &mut sp);
1924            if !(ok && ss.sparse_cut) {
1925                continue;
1926            }
1927            closed += 1;
1928            let dense_nz: Vec<(u32, f32)> = dense
1929                .iter()
1930                .enumerate()
1931                .filter(|&(_, &v)| v > 0.0)
1932                .map(|(i, &v)| (i as u32, v))
1933                .collect();
1934            assert_eq!(dense_nz.len(), sp.len(), "seed {seed}");
1935            for (a, b) in dense_nz.iter().zip(sp.iter()) {
1936                assert_eq!(a.0, b.0, "seed {seed}: ids differ");
1937                assert!((a.1 - b.1).abs() <= 2e-5 * a.1.max(1e-3), "seed {seed}: {} vs {}", a.1, b.1);
1938            }
1939        }
1940        assert!(closed >= 4, "the nucleus closed inside the cap only {closed} times");
1941    }
1942
1943    /// The head-only top-p keeps exactly what the full sort keeps, ties at
1944    /// the cutoff included (few distinct values on purpose).
1945    #[test]
1946    fn top_p_head_matches_the_full_sort() {
1947        for seed in 0..40u64 {
1948            let mut r = SplitMix64::new(seed);
1949            let n = 20_000usize;
1950            let flat = seed % 3 == 0;
1951            let raw: Vec<f32> = (0..n)
1952                .map(|i| {
1953                    let u = r.next_f32();
1954                    if flat {
1955                        ((r.next_u64() % 7) as f32) * 0.5
1956                    } else if i % 97 == 0 {
1957                        8.0 + 4.0 * u
1958                    } else {
1959                        u * 3.0
1960                    }
1961                })
1962                .collect();
1963            let mut probs = raw.clone();
1964            softmax_inplace_pool(None, &mut probs);
1965            for &tp in &[0.5f32, 0.9, 0.95, 0.999] {
1966                let mut a = probs.clone();
1967                let mut b = probs.clone();
1968                apply_top_p(&mut a, tp);
1969                apply_top_p_full(&mut b, tp);
1970                assert_eq!(a, b, "seed {seed} top_p {tp}");
1971            }
1972        }
1973    }
1974
1975    #[test]
1976    fn file_sampling_defaults_override_only_what_they_set() {
1977        let none = SamplerConfig::from_provenance(None);
1978        assert_eq!(none.top_k, SamplerConfig::default().top_k);
1979        let p = serde_json::json!({"sampling": {
1980            "temperature": 1.0, "top_p": 0.95, "top_k": 0, "min_p": 0.0,
1981            "repetition_penalty": 1.0
1982        }});
1983        let c = SamplerConfig::from_provenance(Some(&p));
1984        assert_eq!(
1985            (c.temperature, c.top_p, c.top_k, c.min_p, c.repetition_penalty),
1986            (1.0, 0.95, 0, 0.0, 1.0)
1987        );
1988        assert_eq!(c.presence_penalty, SamplerConfig::default().presence_penalty);
1989        assert!(SamplerConfig::file_has_defaults(Some(&p)));
1990        assert!(!SamplerConfig::file_has_defaults(Some(&serde_json::json!({}))));
1991    }
1992
1993    #[test]
1994    fn top_k_keeps_exactly_k() {
1995        let mut probs = vec![0.1, 0.4, 0.05, 0.3, 0.15];
1996        apply_top_k(&mut probs, 2, &mut Vec::new());
1997        let kept = probs.iter().filter(|&&p| p > 0.0).count();
1998        assert_eq!(kept, 2, "top-k must keep exactly k (was k+1 in v1)");
1999        assert!(probs[1] > 0.0 && probs[3] > 0.0);
2000    }
2001
2002    #[test]
2003    fn rng_reaches_full_cdf() {
2004        // v1 bug: r < 0.233 always, so the CDF tail was unreachable.
2005        // With uniform probs the LAST index must be sampled sometimes.
2006        let probs = vec![0.25f32; 4];
2007        let mut rng = SplitMix64::new(42);
2008        let mut hits = [0usize; 4];
2009        for _ in 0..4000 {
2010            let i = categorical_sample(&probs, rng.next_f32()) as usize;
2011            hits[i] += 1;
2012        }
2013        for (i, &h) in hits.iter().enumerate() {
2014            assert!(h > 700, "index {i} sampled only {h}/4000 — biased RNG");
2015        }
2016    }
2017
2018    #[test]
2019    fn same_seed_same_sequence() {
2020        let logits: Vec<f32> = (0..32).map(|i| (i as f32 * 0.37).sin()).collect();
2021        let config = SamplerConfig {
2022            temperature: 1.0,
2023            seed: Some(7),
2024            ..Default::default()
2025        };
2026        let run = |seed: u64| -> Vec<u32> {
2027            let mut rng = SplitMix64::new(seed);
2028            (0..16)
2029                .map(|_| sample(&logits, &config, &[], &mut rng))
2030                .collect()
2031        };
2032        assert_eq!(run(7), run(7), "same seed must reproduce");
2033        assert_ne!(run(7), run(8), "different seed must differ");
2034    }
2035}
2036
2037#[cfg(test)]
2038mod loop_monitor_tests {
2039    use super::tail_repeats;
2040
2041    #[test]
2042    fn tail_repeats_needs_three_short_or_two_long_copies() {
2043        let short = [1u32, 2, 3, 4];
2044        let mut ids: Vec<u32> = short.iter().chain(&short).copied().collect();
2045        assert!(!tail_repeats(&ids), "two copies of a short block are rhetoric");
2046        ids.extend_from_slice(&short);
2047        assert!(tail_repeats(&ids));
2048        let long: Vec<u32> = (0..12).collect();
2049        let twice: Vec<u32> = long.iter().chain(&long).copied().collect();
2050        assert!(tail_repeats(&twice));
2051        let varied: Vec<u32> = (0..200).collect();
2052        assert!(!tail_repeats(&varied));
2053        assert_eq!(super::tail_repeat_excess(&twice), Some(12));
2054        let roles = ["system", "user", "assistant", "user", "assistant", "user"];
2055        assert_eq!(super::last_turns_start(roles.iter().copied(), 1), 5);
2056        assert_eq!(super::last_turns_start(roles.iter().copied(), 2), 3);
2057        assert_eq!(super::last_turns_start(roles.iter().copied(), 9), 0);
2058    }
2059}