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