memra_sampling/lib.rs
1//! Host-side sampler chain (BASE-2, MEMRA-BUILD-MAP §BASE-2). Ports llama.cpp CPU sampler
2//! semantics (llama-sampler.cpp): repetition/freq/presence penalties -> temperature -> top-k ->
3//! top-p -> min-p -> categorical draw. Greedy (temp<=0) = argmax, the bit-exact reference.
4//!
5//! Runs on the host over the full [n_vocab] f32 logit vector already brought back by the per-step
6//! D2H sync (decode.rs) — at B=2-4 this is single-µs, no GPU kernel needed (the GPU-fused sampler
7//! is a deferred PERF item, only needed once CUDA-graph removes the D2H barrier).
8
9use std::collections::HashMap;
10
11/// Sampler configuration. Defaults = greedy (temp 0). Order of application matches llama.cpp.
12#[derive(Clone, Debug)]
13pub struct SamplerConfig {
14 pub temperature: f32, // <= 0.0 => greedy argmax (penalties/top-k/p ignored)
15 pub top_k: usize, // 0 => disabled (keep all)
16 pub top_p: f32, // 1.0 => disabled
17 pub min_p: f32, // 0.0 => disabled
18 pub penalty_last_n: usize, // window of recent tokens for penalties (0 => disabled)
19 pub penalty_repeat: f32, // 1.0 => disabled (llama default 1.0)
20 pub penalty_freq: f32, // 0.0 => disabled
21 pub penalty_present: f32, // 0.0 => disabled
22 pub seed: u64,
23}
24
25impl Default for SamplerConfig {
26 fn default() -> Self {
27 SamplerConfig {
28 temperature: 0.0,
29 top_k: 0,
30 top_p: 1.0,
31 min_p: 0.0,
32 penalty_last_n: 0,
33 penalty_repeat: 1.0,
34 penalty_freq: 0.0,
35 penalty_present: 0.0,
36 seed: 0,
37 }
38 }
39}
40
41/// SESSION-RESUME SAMPLER IDENTITY (lane/session-resume-sampler-predicate-20260820; receipts
42/// `research/spec-cache-20260818/SESSION-RESUME-PREDICATE.md`).
43///
44/// The canonical form of a request's sampler, for exactly one question: **may this request resume
45/// a parked whole session that some OTHER request's sampler shaped?** The spec pool's resume probe
46/// compared prompts and never samplers — that omission is how a filtered request inherited a draft
47/// graph captured unfiltered (`memra-engine` `SampledGraphKey`, lane/graph-s-key-exactness-
48/// 20260819). Keying the graph closed the exactness hole; it did not make cross-sampler resume
49/// SOUND, and the house posture in that situation is refuse-on-ambiguity with the refusal naming
50/// itself. This type is that predicate.
51///
52/// CANONICALIZATION, and why each rule is safe. Two encodings that name the same program must
53/// compare equal, or the predicate refuses resumes that cost nothing to allow:
54/// - `temperature <= 0.0` is GREEDY — `-1.0` and `0.0` are one program, so `temp_bits` is pinned
55/// to `0.0` in that regime and `greedy` carries the distinction. The greedy/sampled flip is
56/// itself a refusal: the two arms consume a parked `next_pred`/`pending_tok` differently and
57/// engage different captured draft graphs.
58/// - `top_k == 0`, `top_p >= 1.0`, `min_p <= 0.0` are each the OFF sentinel (matching
59/// `is_spec_sampling` and `SampledGraphKey::pure_temp`), canonicalized so `top_p 1.5` and
60/// `top_p 1.0` do not look like a change.
61/// - Penalties are OFF as a group iff `penalty_last_n == 0` or all three coefficients are neutral
62/// — the same `pen_on` predicate `spec.rs` computes. Off canonicalizes to the whole disabled
63/// tuple, so `penalty_last_n 64` with neutral coefficients equals penalties absent.
64///
65/// Float fields compare by BITS after canonicalization (no NaN/`-0.0` surprise), the same
66/// discipline `SampledGraphKey` uses.
67///
68/// `seed` IS carried and DELIBERATELY NOT COMPARED — see [`SamplerIdentity::mismatch`].
69#[derive(Clone, Copy, PartialEq, Eq, Debug)]
70pub struct SamplerIdentity {
71 greedy: bool,
72 temp_bits: u32,
73 /// Carried for the record and for callers that want to log it; NOT part of `mismatch`.
74 seed: u64,
75 top_k: usize,
76 top_p_bits: u32,
77 min_p_bits: u32,
78 penalty_last_n: usize,
79 penalty_repeat_bits: u32,
80 penalty_freq_bits: u32,
81 penalty_present_bits: u32,
82}
83
84impl SamplerIdentity {
85 /// Canonical identity of a sampler configuration.
86 pub fn of(cfg: &SamplerConfig) -> Self {
87 let greedy = cfg.temperature <= 0.0;
88 let pen_on = cfg.penalty_last_n > 0
89 && (cfg.penalty_repeat != 1.0 || cfg.penalty_freq != 0.0 || cfg.penalty_present != 0.0);
90 SamplerIdentity {
91 greedy,
92 temp_bits: if greedy { 0.0f32 } else { cfg.temperature }.to_bits(),
93 seed: cfg.seed,
94 top_k: cfg.top_k,
95 top_p_bits: if cfg.top_p >= 1.0 { 1.0f32 } else { cfg.top_p }.to_bits(),
96 min_p_bits: if cfg.min_p <= 0.0 { 0.0f32 } else { cfg.min_p }.to_bits(),
97 penalty_last_n: if pen_on { cfg.penalty_last_n } else { 0 },
98 penalty_repeat_bits: if pen_on { cfg.penalty_repeat } else { 1.0f32 }.to_bits(),
99 penalty_freq_bits: if pen_on { cfg.penalty_freq } else { 0.0f32 }.to_bits(),
100 penalty_present_bits: if pen_on { cfg.penalty_present } else { 0.0f32 }.to_bits(),
101 }
102 }
103
104 /// The seed this identity was built from (logging/receipts only — never compared).
105 pub fn seed(&self) -> u64 {
106 self.seed
107 }
108
109 /// The FIRST field on which `self` (an incoming request) differs from `parked` (the sampler
110 /// that shaped a parked session), as a stable name for the refusal line — `None` when the two
111 /// samplers are equivalent and the resume is legal. A refusal that does not say why is
112 /// indistinguishable from an unwired mechanism, so the name is the deliverable, not a nicety.
113 ///
114 /// Order is fixed and coarsest-first (`regime` before the field that only exists inside one
115 /// regime), so the reported name is the most informative one rather than an artifact of struct
116 /// layout.
117 ///
118 /// **`seed` IS NOT COMPARED, deliberately.** It is the one sampler field a resume may change,
119 /// for two reasons that are both mechanical:
120 /// - The only parked state that BAKES the seed is the sampled draft graph, and
121 /// `SampledGraphKey` already carries `seed`: a seed change drops the parked graph and
122 /// recaptures. (`memra-engine` `spec.rs`; pinned by `seed_alone_still_rekeys_the_draft_graph`
123 /// in that crate's `sampled_graph_key` tests.)
124 /// - The session's persisted Philox counters (`SpecSession::sctr/uctr`) are counter-based:
125 /// `philox(seed', ctr)` continued from another seed's counter position is an independent
126 /// stream, not a repeated one. Reproducibility is already scoped per `(seed, session)`
127 /// rather than per seed (`memra-server` `worker.rs`, the spec-burst sampling note), so a
128 /// changed seed costs nothing that same-seed resume was not already costing.
129 ///
130 /// Comparing it would refuse essentially ALL sampled traffic: omitting `seed` on a serve
131 /// request draws fresh per-request entropy, so every turn of every seed-omitting conversation
132 /// would carry a "changed" seed. That is a cost with no soundness gain, which is exactly the
133 /// trade this predicate exists to make explicitly rather than by accident.
134 pub fn mismatch(&self, parked: &Self) -> Option<&'static str> {
135 if self.greedy != parked.greedy {
136 return Some("regime");
137 }
138 if self.temp_bits != parked.temp_bits {
139 return Some("temperature");
140 }
141 if self.top_k != parked.top_k {
142 return Some("top_k");
143 }
144 if self.top_p_bits != parked.top_p_bits {
145 return Some("top_p");
146 }
147 if self.min_p_bits != parked.min_p_bits {
148 return Some("min_p");
149 }
150 if self.penalty_last_n != parked.penalty_last_n {
151 return Some("penalty_last_n");
152 }
153 if self.penalty_repeat_bits != parked.penalty_repeat_bits {
154 return Some("penalty_repeat");
155 }
156 if self.penalty_freq_bits != parked.penalty_freq_bits {
157 return Some("penalty_freq");
158 }
159 if self.penalty_present_bits != parked.penalty_present_bits {
160 return Some("penalty_present");
161 }
162 None
163 }
164
165 /// THE PRE-LANE PREDICATE, RESTATED (teeth, not production). The spec pool-resume probe
166 /// applied no sampler test at all — it compared prompts and nothing else — so every sampler
167 /// pair was admitted. Restating it here keeps the refusal tests DECISIVE instead of
168 /// tautological: the same pair that `mismatch` names must be admitted by this, or the test is
169 /// asserting against a mechanism that never existed.
170 ///
171 /// It is also what `MEMRA_SPEC_RESUME_SAMPLER=0` selects at runtime (the rollback door and the
172 /// A/B arm the cost measurement needs), so this function is the single definition of "legacy"
173 /// for both the tests and the server.
174 pub fn legacy_admits(&self, _parked: &Self) -> bool {
175 true
176 }
177}
178
179/// Stateful sampler: owns the RNG + the recent-token history (for penalties).
180pub struct Sampler {
181 cfg: SamplerConfig,
182 rng: SplitMix64,
183 history: Vec<u32>, // recently emitted tokens (for penalty window)
184 // Counts over exactly the active penalty window. Keeping this incrementally makes the host
185 // oracle cheaper too, and lets the serving path upload O(unique ids) sparse penalty state
186 // instead of either the full vocabulary or an O(history^2) device-side dedup walk.
187 penalty_counts: HashMap<u32, u32>,
188}
189
190impl Sampler {
191 pub fn new(cfg: SamplerConfig) -> Self {
192 let rng = SplitMix64::new(cfg.seed);
193 Sampler {
194 cfg,
195 rng,
196 history: Vec::new(),
197 penalty_counts: HashMap::new(),
198 }
199 }
200
201 pub fn is_greedy(&self) -> bool {
202 self.cfg.temperature <= 0.0
203 }
204 /// Sampled spec in its FASTEST regime: pure temperature, no truncation filters, no
205 /// penalties. Filters and penalties are also distribution-exact under the rejection
206 /// verify (spec.rs applies both symmetrically to draft q and target p), so they remain
207 /// spec-ELIGIBLE — see `spec_eligible` in memra-server's worker, the authoritative
208 /// predicate. What they cost is the in-graph draft chain: the captured sampled draft
209 /// samples from the RAW softmax and can hold neither per-row filter stats nor a varying
210 /// penalty history, so `spec.rs` engages `graph_s` only in this pure-temp regime
211 /// (`pure_temp`) and otherwise falls back to the eager draft chain. This predicate names
212 /// that regime; it is NOT an eligibility test.
213 pub fn is_spec_sampling(&self) -> bool {
214 self.cfg.temperature > 0.0
215 && self.cfg.penalty_repeat == 1.0
216 && self.cfg.penalty_freq == 0.0
217 && self.cfg.penalty_present == 0.0
218 && self.cfg.top_k == 0
219 && self.cfg.top_p >= 1.0
220 && self.cfg.min_p <= 0.0
221 }
222 pub fn top_k(&self) -> usize {
223 self.cfg.top_k
224 }
225 pub fn penalty_last_n(&self) -> usize {
226 self.cfg.penalty_last_n
227 }
228 pub fn penalty_repeat(&self) -> f32 {
229 self.cfg.penalty_repeat
230 }
231 pub fn penalty_freq(&self) -> f32 {
232 self.cfg.penalty_freq
233 }
234 pub fn penalty_present(&self) -> f32 {
235 self.cfg.penalty_present
236 }
237 pub fn top_p(&self) -> f32 {
238 self.cfg.top_p
239 }
240 pub fn min_p(&self) -> f32 {
241 self.cfg.min_p
242 }
243 pub fn temperature(&self) -> f32 {
244 self.cfg.temperature
245 }
246 pub fn seed(&self) -> u64 {
247 self.cfg.seed
248 }
249 /// This sampler's canonical [`SamplerIdentity`] — the whole-session resume predicate's input.
250 pub fn identity(&self) -> SamplerIdentity {
251 SamplerIdentity::of(&self.cfg)
252 }
253
254 fn penalties_on(&self) -> bool {
255 self.cfg.penalty_last_n > 0
256 && (self.cfg.penalty_repeat != 1.0
257 || self.cfg.penalty_freq != 0.0
258 || self.cfg.penalty_present != 0.0)
259 }
260
261 /// Sparse `(token_id, count)` rows for the current penalty window. The order cannot affect
262 /// the arithmetic because every entry mutates one distinct logit; avoiding a per-token sort
263 /// is material on long agent histories.
264 pub fn penalty_counts(&self) -> Vec<(u32, u32)> {
265 debug_assert!(self.penalty_counts.values().all(|&count| count > 0));
266 self.penalty_counts
267 .iter()
268 .map(|(&id, &n)| (id, n))
269 .collect()
270 }
271
272 /// Record an emitted token so subsequent penalties see it.
273 pub fn accept(&mut self, token: u32) {
274 if self.penalties_on() {
275 let n = self.cfg.penalty_last_n;
276 if self.history.len() >= n {
277 let expired = self.history[self.history.len() - n];
278 let remove = {
279 let count = self
280 .penalty_counts
281 .get_mut(&expired)
282 .expect("active penalty window lost an accepted token");
283 *count -= 1;
284 *count == 0
285 };
286 if remove {
287 self.penalty_counts.remove(&expired);
288 }
289 }
290 *self.penalty_counts.entry(token).or_insert(0) += 1;
291 }
292 self.history.push(token);
293 }
294
295 /// Sample the next token id from raw logits [n_vocab]. Does NOT mutate logits in place beyond
296 /// a local copy. Returns the chosen token id. (Caller should `accept()` it afterwards.)
297 pub fn sample(&mut self, logits: &[f32]) -> u32 {
298 // Greedy fast path: argmax over RAW logits (penalties don't change the argmax direction
299 // enough to matter for the reference path; llama greedy is also pre-penalty argmax only
300 // when no penalties set — but to stay correct under penalties we still apply them first).
301 if self.is_greedy()
302 && self.cfg.penalty_repeat == 1.0
303 && self.cfg.penalty_freq == 0.0
304 && self.cfg.penalty_present == 0.0
305 {
306 return argmax_u32(logits);
307 }
308
309 // Work on (id, logit) candidates.
310 let mut cand: Vec<(u32, f32)> = logits
311 .iter()
312 .enumerate()
313 .map(|(i, &l)| (i as u32, l))
314 .collect();
315
316 // 1. Penalties (operate on logits, over the last-n history window).
317 // `cand` is DENSE and INDEX-ALIGNED here by construction (built from
318 // `logits.iter().enumerate()` immediately above, nothing has filtered it yet), so the
319 // penalty pass indexes straight into it instead of hashing every candidate. See
320 // `apply_penalties_dense`.
321 self.apply_penalties_dense(&mut cand);
322
323 // Greedy-with-penalties: argmax after penalties, no sampling.
324 if self.is_greedy() {
325 let mut best = cand[0];
326 for &c in &cand[1..] {
327 if c.1 > best.1 {
328 best = c;
329 }
330 }
331 return best.0;
332 }
333
334 // 2. Temperature scale.
335 if self.cfg.temperature > 0.0 && self.cfg.temperature != 1.0 {
336 let inv = 1.0 / self.cfg.temperature;
337 for c in cand.iter_mut() {
338 c.1 *= inv;
339 }
340 }
341
342 // 3. top-k: keep the k highest-logit candidates (partial sort by logit desc).
343 if self.cfg.top_k > 0 && self.cfg.top_k < cand.len() {
344 cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
345 cand.truncate(self.cfg.top_k);
346 }
347
348 // softmax over the surviving candidates (numerically stable).
349 softmax_inplace(&mut cand);
350
351 // 4. top-p (nucleus): smallest set whose cumulative prob >= top_p. Needs desc-by-prob order.
352 if self.cfg.top_p < 1.0 {
353 cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
354 let mut cum = 0.0f32;
355 let mut keep = 0usize;
356 for (i, c) in cand.iter().enumerate() {
357 cum += c.1;
358 keep = i + 1;
359 if cum >= self.cfg.top_p {
360 break;
361 }
362 }
363 cand.truncate(keep.max(1));
364 }
365
366 // 5. min-p: keep candidates with prob >= min_p * max_prob.
367 if self.cfg.min_p > 0.0 {
368 let maxp = cand.iter().map(|c| c.1).fold(0.0f32, f32::max);
369 let thresh = self.cfg.min_p * maxp;
370 cand.retain(|c| c.1 >= thresh);
371 if cand.is_empty() {
372 return argmax_u32(logits);
373 } // safety
374 }
375
376 // renormalize the surviving probs and draw.
377 let sum: f32 = cand.iter().map(|c| c.1).sum();
378 let r = self.rng.next_f32() * sum;
379 let mut acc = 0.0f32;
380 for c in &cand {
381 acc += c.1;
382 if acc >= r {
383 return c.0;
384 }
385 }
386 cand.last().unwrap().0
387 }
388
389 /// llama.cpp penalty over a DENSE, INDEX-ALIGNED candidate slice: `cand[i].0 == i`.
390 ///
391 /// Same arithmetic as `apply_penalties_scan_reference`, on exactly the same elements, in the
392 /// same order — but O(distinct penalized tokens) instead of O(n_vocab) hash lookups.
393 ///
394 /// WHY (lane/glm5-host-audit, 2026-09-01). The scan form did one `HashMap<u32,u32>` SipHash
395 /// probe PER CANDIDATE, i.e. one per vocabulary entry: ~152k probes per token on the Qwen
396 /// class. `penalty_counts` holds at most `PEN_WINDOW_MAX` distinct ids and in practice the
397 /// number of distinct generated tokens so far, so the loop was inverted the expensive way
398 /// round. This matters in production, not in theory: `devsample_meta` refuses the device
399 /// sampler for any penalized config unless `MEMRA_SERVE_DEVPENALTY=1`, the whole fleet runs
400 /// it at 0, and a served model whose VENDOR-RECOMMENDED non-thinking arm carries
401 /// `presence_penalty` therefore lands every token of every request on this function.
402 ///
403 /// BIT-IDENTICAL BY CONSTRUCTION, and gated as such rather than asserted in prose: the set of
404 /// touched entries is identical (`penalty_counts` covers exactly the window, which the
405 /// debug_assert below re-checks), the per-entry arithmetic is copied unchanged, and each
406 /// entry is touched exactly once in both forms, so no float re-association is possible.
407 /// `apply_penalties_scan_reference` is kept as the ORACLE and
408 /// `dense_penalties_match_the_scan_reference_bitwise` compares them over randomized inputs.
409 ///
410 /// THE HOST ORACLE FOR DEVICE PENALTIES (lane/spec-exclusions-20260902): the memra-engine
411 /// `penalize_logits*` kernels (`cu/spec_sample.cu`, `keskar_penalize_rn`) are written to
412 /// produce these exact f32 bits for the same window, and the glm5 spec route's penalized
413 /// verify rows are gated against `penalized_logits` bit for bit
414 /// (`gpu_device_penalties_are_bit_identical_to_the_host_sampler`). Any change to the
415 /// arithmetic here is a change to the spec route's numerics too; keep the two in step.
416 fn apply_penalties_dense(&self, cand: &mut [(u32, f32)]) {
417 let n = self.cfg.penalty_last_n;
418 if n == 0 {
419 return;
420 }
421 if self.cfg.penalty_repeat == 1.0
422 && self.cfg.penalty_freq == 0.0
423 && self.cfg.penalty_present == 0.0
424 {
425 return;
426 }
427 let start = self.history.len().saturating_sub(n);
428 let window = &self.history[start..];
429 if window.is_empty() {
430 return;
431 }
432 debug_assert_eq!(
433 self.penalty_counts
434 .values()
435 .map(|&n| n as usize)
436 .sum::<usize>(),
437 window.len(),
438 "incremental penalty counts must cover the active history window"
439 );
440 for (&id, &cnt) in self.penalty_counts.iter() {
441 let Some(c) = cand.get_mut(id as usize) else {
442 // A count for an id outside the logits row: a vocab/count mismatch, which is a
443 // caller bug rather than something to silently skip. Loud in debug, inert in
444 // release (dropping the penalty is strictly safer than indexing out of bounds).
445 debug_assert!(
446 false,
447 "penalty count for id {id} is outside the {} candidate row",
448 cand.len()
449 );
450 continue;
451 };
452 debug_assert_eq!(
453 c.0, id,
454 "apply_penalties_dense requires an index-aligned candidate slice"
455 );
456 // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
457 if self.cfg.penalty_repeat != 1.0 {
458 if c.1 > 0.0 {
459 c.1 /= self.cfg.penalty_repeat;
460 } else {
461 c.1 *= self.cfg.penalty_repeat;
462 }
463 }
464 c.1 -= cnt as f32 * self.cfg.penalty_freq;
465 c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
466 }
467 }
468
469 /// The penalized logits row this sampler would score the next token from, as bytes: a
470 /// copy of `logits` with the active penalty window applied through the ONE serving
471 /// penalty pass (`apply_penalties_dense`). Identity when penalties are off (window
472 /// empty or every coefficient neutral), exactly as `sample` sees it.
473 ///
474 /// EXISTS AS THE ORACLE SEAM for the device penalty kernels (lane/spec-exclusions-
475 /// 20260902): a spec route that penalizes verify rows on device claims "the same bits
476 /// the plain sampler produces", and that claim needs the plain sampler's bits to compare
477 /// against, not a re-derivation in a test. `sample` only ever returns the chosen id.
478 pub fn penalized_logits(&self, logits: &[f32]) -> Vec<f32> {
479 let mut cand: Vec<(u32, f32)> = logits
480 .iter()
481 .enumerate()
482 .map(|(i, &l)| (i as u32, l))
483 .collect();
484 self.apply_penalties_dense(&mut cand);
485 cand.into_iter().map(|(_, l)| l).collect()
486 }
487
488 /// llama.cpp penalty: for each token in the last-n history, repeat-divide/multiply its logit
489 /// and apply frequency*count + presence. (llama-sampler.cpp penalties.)
490 ///
491 /// THE ORACLE, not the serving path. `apply_penalties_dense` replaced it on the hot path
492 /// 2026-09-01; this form is retained verbatim so the replacement has something to be proven
493 /// bit-identical against, which is worth more than deleting it.
494 #[cfg(test)]
495 fn apply_penalties_scan_reference(&self, cand: &mut [(u32, f32)]) {
496 let n = self.cfg.penalty_last_n;
497 if n == 0 {
498 return;
499 }
500 if self.cfg.penalty_repeat == 1.0
501 && self.cfg.penalty_freq == 0.0
502 && self.cfg.penalty_present == 0.0
503 {
504 return;
505 }
506 let start = self.history.len().saturating_sub(n);
507 let window = &self.history[start..];
508 if window.is_empty() {
509 return;
510 }
511 debug_assert_eq!(
512 self.penalty_counts
513 .values()
514 .map(|&n| n as usize)
515 .sum::<usize>(),
516 window.len(),
517 "incremental penalty counts must cover the active history window"
518 );
519 for c in cand.iter_mut() {
520 if let Some(&cnt) = self.penalty_counts.get(&c.0) {
521 // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
522 if self.cfg.penalty_repeat != 1.0 {
523 if c.1 > 0.0 {
524 c.1 /= self.cfg.penalty_repeat;
525 } else {
526 c.1 *= self.cfg.penalty_repeat;
527 }
528 }
529 c.1 -= cnt as f32 * self.cfg.penalty_freq;
530 c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
531 }
532 }
533 }
534}
535
536fn argmax_u32(logits: &[f32]) -> u32 {
537 let mut best = 0u32;
538 let mut bv = f32::NEG_INFINITY;
539 for (i, &v) in logits.iter().enumerate() {
540 if v > bv {
541 bv = v;
542 best = i as u32;
543 }
544 }
545 best
546}
547
548/// Stable softmax over candidate logits, writing probs back into the logit slot.
549fn softmax_inplace(cand: &mut [(u32, f32)]) {
550 let maxl = cand.iter().map(|c| c.1).fold(f32::NEG_INFINITY, f32::max);
551 let mut sum = 0.0f32;
552 for c in cand.iter_mut() {
553 let e = (c.1 - maxl).exp();
554 c.1 = e;
555 sum += e;
556 }
557 let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
558 for c in cand.iter_mut() {
559 c.1 *= inv;
560 }
561}
562
563/// SplitMix64 — deterministic seedable RNG (so a fixed seed reproduces the token stream for the
564/// validation gate). Not crypto; fine for sampling.
565struct SplitMix64 {
566 state: u64,
567}
568impl SplitMix64 {
569 fn new(seed: u64) -> Self {
570 SplitMix64 {
571 state: seed.wrapping_add(0x9E3779B97F4A7C15),
572 }
573 }
574 fn next_u64(&mut self) -> u64 {
575 self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
576 let mut z = self.state;
577 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
578 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
579 z ^ (z >> 31)
580 }
581 /// uniform f32 in [0,1).
582 fn next_f32(&mut self) -> f32 {
583 // top 24 bits -> [0,1)
584 ((self.next_u64() >> 40) as f32) / (1u32 << 24) as f32
585 }
586}
587
588#[cfg(test)]
589mod tests {
590 use super::*;
591
592 fn sorted_counts(s: &Sampler) -> Vec<(u32, u32)> {
593 let mut counts = s.penalty_counts();
594 counts.sort_unstable_by_key(|&(id, _)| id);
595 counts
596 }
597
598 /// THE GATE for the 2026-09-01 host-cost fix (lane/glm5-host-audit): the dense penalty pass
599 /// must reproduce the O(n_vocab) scan reference BIT FOR BIT, not merely closely.
600 ///
601 /// Compared as raw bit patterns (`to_bits`), because `==` on f32 would call two different
602 /// NaNs equal and would call `-0.0` and `0.0` equal — and `penalty_present` subtraction can
603 /// produce exactly `-0.0`. The inputs deliberately include negative logits (the repeat rule
604 /// branches on sign), repeated tokens (frequency count > 1), a token never generated (must be
605 /// untouched), and the three penalty coefficients both neutral and active.
606 #[test]
607 fn dense_penalties_match_the_scan_reference_bitwise() {
608 // A small deterministic LCG: this gate must not depend on a dev-dependency.
609 let mut state: u64 = 0x9E3779B97F4A7C15;
610 let mut next = || {
611 state = state
612 .wrapping_mul(6364136223846793005)
613 .wrapping_add(1442695040888963407);
614 ((state >> 33) as u32) as f32 / (u32::MAX >> 1) as f32 - 1.0
615 };
616
617 let vocab = 257usize; // prime-ish, and > any window used below
618 let mut cases = 0;
619 for &(repeat, freq, present) in &[
620 (1.0f32, 0.0f32, 0.0f32), // all neutral: both forms must no-op
621 (1.1, 0.0, 0.0), // repeat only (exercises the sign branch)
622 (1.0, 0.5, 0.0), // frequency only (count-scaled)
623 (1.0, 0.0, 1.5), // presence only — the q38 non-thinking vendor arm
624 (1.1, 0.5, 1.5), // all three together
625 (0.8, -0.25, -0.5), // below/negative coefficients are legal API values
626 ] {
627 for &window in &[1usize, 3, 64, 8192] {
628 let cfg = SamplerConfig {
629 temperature: 1.0,
630 penalty_last_n: window,
631 penalty_repeat: repeat,
632 penalty_freq: freq,
633 penalty_present: present,
634 ..SamplerConfig::default()
635 };
636 let mut s = Sampler::new(cfg);
637 // Feed a history with repeats, and never feed id 0 or id 256 so the "untouched
638 // candidate" case is covered at both ends of the row.
639 for step in 0..40u32 {
640 s.accept(1 + (step * 7) % 200);
641 s.accept(1 + (step * 3) % 50); // guarantees counts > 1
642 }
643
644 let base: Vec<(u32, f32)> = (0..vocab).map(|i| (i as u32, next() * 8.0)).collect();
645 let mut dense = base.clone();
646 let mut reference = base.clone();
647 s.apply_penalties_dense(&mut dense);
648 s.apply_penalties_scan_reference(&mut reference);
649
650 assert_eq!(dense.len(), reference.len());
651 for (i, (d, r)) in dense.iter().zip(reference.iter()).enumerate() {
652 assert_eq!(d.0, r.0, "candidate id moved at {i}");
653 assert_eq!(
654 d.1.to_bits(),
655 r.1.to_bits(),
656 "BIT DIVERGENCE at id {i} (repeat={repeat} freq={freq} \
657 present={present} window={window}): dense {} vs reference {}",
658 d.1,
659 r.1
660 );
661 }
662 // Ids never generated must be untouched — otherwise "identical" could be two
663 // equally-wrong passes.
664 assert_eq!(
665 dense[0].1.to_bits(),
666 base[0].1.to_bits(),
667 "id 0 was penalized"
668 );
669 assert_eq!(
670 dense[256].1.to_bits(),
671 base[256].1.to_bits(),
672 "id 256 was penalized"
673 );
674 cases += 1;
675 }
676 }
677 assert_eq!(cases, 24, "the coefficient x window matrix must all run");
678 }
679
680 /// The precondition the dense form trades correctness for speed on: the candidate row it is
681 /// handed is dense and index-aligned. Asserted on the REAL call path (`sample`), so a future
682 /// caller that filters before penalizing cannot quietly pass.
683 #[test]
684 fn the_sampled_path_penalizes_the_token_it_generated() {
685 let cfg = SamplerConfig {
686 temperature: 1.0,
687 penalty_last_n: 64,
688 penalty_present: 100.0, // large enough that the penalized id cannot win
689 seed: 7,
690 ..SamplerConfig::default()
691 };
692 let mut s = Sampler::new(cfg);
693 // Logits that make id 2 the runaway favourite, then penalize exactly id 2.
694 let logits = vec![0.0, 0.0, 20.0, 0.0];
695 s.accept(2);
696 for _ in 0..32 {
697 assert_ne!(
698 s.sample(&logits),
699 2,
700 "presence penalty on the generated id must move the draw off it, which only \
701 happens if the dense pass indexed the right candidate"
702 );
703 }
704 }
705
706 #[test]
707 fn greedy_is_argmax() {
708 let mut s = Sampler::new(SamplerConfig::default()); // temp 0
709 let logits = vec![0.1, 5.0, 2.0, -1.0];
710 assert_eq!(s.sample(&logits), 1);
711 }
712
713 #[test]
714 fn temp_sampling_deterministic_with_seed() {
715 let cfg = SamplerConfig {
716 temperature: 1.0,
717 seed: 42,
718 ..Default::default()
719 };
720 let logits = vec![1.0, 2.0, 3.0, 0.5];
721 let a = Sampler::new(cfg.clone()).sample(&logits);
722 let b = Sampler::new(cfg).sample(&logits);
723 assert_eq!(a, b, "same seed must reproduce the draw");
724 assert!(a < 4);
725 }
726
727 #[test]
728 fn top_k_one_is_argmax() {
729 let cfg = SamplerConfig {
730 temperature: 1.0,
731 top_k: 1,
732 seed: 7,
733 ..Default::default()
734 };
735 let logits = vec![0.1, 5.0, 2.0, -1.0];
736 assert_eq!(
737 Sampler::new(cfg).sample(&logits),
738 1,
739 "top_k=1 collapses to argmax"
740 );
741 }
742
743 #[test]
744 fn min_p_keeps_only_high_prob() {
745 // logit 10 dominates; min_p 0.5 should drop the rest -> always pick id 2.
746 let cfg = SamplerConfig {
747 temperature: 1.0,
748 min_p: 0.5,
749 seed: 3,
750 ..Default::default()
751 };
752 let logits = vec![0.0, 0.0, 10.0, 0.0];
753 for _ in 0..16 {
754 assert_eq!(Sampler::new(cfg.clone()).sample(&logits), 2);
755 }
756 }
757
758 #[test]
759 fn repeat_penalty_suppresses_recent() {
760 // greedy + heavy repeat penalty: id 1 is argmax but recently emitted -> should drop it.
761 let cfg = SamplerConfig {
762 penalty_last_n: 8,
763 penalty_repeat: 100.0,
764 ..Default::default()
765 };
766 let mut s = Sampler::new(cfg);
767 s.accept(1); // 1 was just emitted
768 let logits = vec![4.0, 5.0, 4.5, 1.0]; // raw argmax = 1
769 let got = s.sample(&logits);
770 assert_ne!(
771 got, 1,
772 "recent token must be penalized out of greedy argmax"
773 );
774 assert_eq!(got, 2, "next-highest after penalizing 1");
775 }
776
777 #[test]
778 fn penalty_counts_follow_the_sliding_window() {
779 let cfg = SamplerConfig {
780 penalty_last_n: 4,
781 penalty_repeat: 1.1,
782 penalty_freq: 0.5,
783 penalty_present: 0.25,
784 ..Default::default()
785 };
786 let mut s = Sampler::new(cfg);
787 for tok in [7, 8, 7, 9] {
788 s.accept(tok);
789 }
790 assert_eq!(sorted_counts(&s), vec![(7, 2), (8, 1), (9, 1)]);
791
792 s.accept(8); // active window is now [8, 7, 9, 8]
793 assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 2), (9, 1)]);
794 s.accept(10); // active window is now [7, 9, 8, 10]
795 assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 1), (9, 1), (10, 1)]);
796 s.accept(11); // active window is now [9, 8, 10, 11]; final 7 expires
797 assert_eq!(sorted_counts(&s), vec![(8, 1), (9, 1), (10, 1), (11, 1)]);
798 assert!(!s.penalty_counts().iter().any(|&(id, n)| id == 7 || n == 0));
799 }
800
801 #[test]
802 fn full_context_and_disabled_penalty_counts_are_exact() {
803 let mut full = Sampler::new(SamplerConfig {
804 penalty_last_n: usize::MAX,
805 penalty_present: 1.5,
806 ..Default::default()
807 });
808 for tok in [3, 3, 4, 5, 3] {
809 full.accept(tok);
810 }
811 assert_eq!(sorted_counts(&full), vec![(3, 3), (4, 1), (5, 1)]);
812
813 let mut neutral = Sampler::new(SamplerConfig {
814 penalty_last_n: usize::MAX,
815 ..Default::default()
816 });
817 neutral.accept(3);
818 assert!(neutral.penalty_counts().is_empty());
819 }
820}
821
822/// SESSION-RESUME SAMPLER PREDICATE teeth (lane/session-resume-sampler-predicate-20260820).
823/// CPU-only, no GPU: the predicate is a pure function, so its whole contract is testable here and
824/// a regression cannot hide behind "needs a card".
825///
826/// TEETH BOTH DIRECTIONS is the point. Every refusal test also asserts that `legacy_admits`
827/// ADMITS the same pair — the pre-lane probe compared prompts and never samplers — so the test is
828/// decisive (it fails on the old code) rather than tautological (passing because the pair was
829/// never resumable for some other reason).
830#[cfg(test)]
831mod resume_sampler_predicate_tests {
832 use super::*;
833
834 /// The vendor-default sampled shape the flip makes the majority of traffic.
835 fn vendor() -> SamplerConfig {
836 SamplerConfig {
837 temperature: 0.7,
838 top_k: 20,
839 top_p: 0.95,
840 seed: 20260820,
841 ..Default::default()
842 }
843 }
844
845 /// Today's pure-temp shape — the one that parks a `graph_s`.
846 fn pure_temp() -> SamplerConfig {
847 SamplerConfig {
848 temperature: 0.7,
849 seed: 20260820,
850 ..Default::default()
851 }
852 }
853
854 fn id(cfg: &SamplerConfig) -> SamplerIdentity {
855 SamplerIdentity::of(cfg)
856 }
857
858 // ---- direction 1: a SAME-sampler resume still resumes (no regression) ----
859
860 #[test]
861 fn identical_sampler_resumes() {
862 for cfg in [pure_temp(), vendor(), SamplerConfig::default()] {
863 assert_eq!(
864 id(&cfg).mismatch(&id(&cfg)),
865 None,
866 "a request must resume a session its own sampler shaped: {cfg:?}"
867 );
868 }
869 }
870
871 #[test]
872 fn disabled_sentinels_are_the_same_program() {
873 // top_p >= 1.0, min_p <= 0.0, top_k == 0 all mean OFF; a client that spells OFF
874 // differently on turn 2 must not lose its cache.
875 let a = SamplerConfig {
876 temperature: 0.7,
877 top_p: 1.0,
878 min_p: 0.0,
879 ..Default::default()
880 };
881 let b = SamplerConfig {
882 temperature: 0.7,
883 top_p: 1.5,
884 min_p: -1.0,
885 ..Default::default()
886 };
887 assert_eq!(id(&a).mismatch(&id(&b)), None, "off spelled two ways");
888 }
889
890 #[test]
891 fn greedy_temperature_encodings_are_one_program() {
892 let a = SamplerConfig {
893 temperature: 0.0,
894 ..Default::default()
895 };
896 let b = SamplerConfig {
897 temperature: -1.0,
898 ..Default::default()
899 };
900 assert_eq!(id(&a).mismatch(&id(&b)), None, "temp<=0 is one regime");
901 }
902
903 #[test]
904 fn neutral_penalty_coefficients_equal_penalties_absent() {
905 // penalty_last_n set but every coefficient neutral == `pen_on == false` in spec.rs.
906 let a = SamplerConfig {
907 temperature: 0.7,
908 penalty_last_n: 64,
909 penalty_repeat: 1.0,
910 penalty_freq: 0.0,
911 penalty_present: 0.0,
912 ..Default::default()
913 };
914 let b = SamplerConfig {
915 temperature: 0.7,
916 penalty_last_n: 0,
917 ..Default::default()
918 };
919 assert_eq!(
920 id(&a).mismatch(&id(&b)),
921 None,
922 "an inert penalty window is not a penalty change"
923 );
924 }
925
926 // ---- direction 2: a sampler-DIFFERING resume refuses, and names the field ----
927
928 #[test]
929 fn the_reproduced_collision_pair_refuses_and_names_a_filter() {
930 // The exact pair the predecessor reproduced on a live server: turn 1 pure-temp parks,
931 // turn 2 adds top_p 0.95 / top_k 20 and resumes. Same seed, same temperature.
932 let parked = id(&pure_temp());
933 let incoming = id(&vendor());
934 let field = incoming
935 .mismatch(&parked)
936 .expect("the reproduced collision pair must refuse");
937 assert_eq!(field, "top_k", "coarsest-first order names top_k here");
938 // DECISIVE: the pre-lane probe admitted exactly this pair.
939 assert!(
940 incoming.legacy_admits(&parked),
941 "legacy must admit the collision pair, or this test proves nothing"
942 );
943 }
944
945 #[test]
946 fn every_compared_field_refuses_on_its_own_and_names_itself() {
947 let base = pure_temp();
948 // A penalized base, so the three coefficients can each move ALONE: with penalties off on
949 // the parked side, turning any of them on also moves `penalty_last_n`, and coarsest-first
950 // order would (correctly) name the window instead of the coefficient.
951 let pen_base = SamplerConfig {
952 penalty_last_n: 64,
953 penalty_repeat: 1.1,
954 penalty_freq: 0.5,
955 penalty_present: 0.5,
956 ..base.clone()
957 };
958 // (field, parked, incoming) — exactly one canonical field differs in each row.
959 let cases: [(&str, SamplerConfig, SamplerConfig); 9] = [
960 (
961 "regime",
962 base.clone(),
963 SamplerConfig {
964 temperature: 0.0,
965 ..base.clone()
966 },
967 ),
968 (
969 "temperature",
970 base.clone(),
971 SamplerConfig {
972 temperature: 0.8,
973 ..base.clone()
974 },
975 ),
976 (
977 "top_k",
978 base.clone(),
979 SamplerConfig {
980 top_k: 20,
981 ..base.clone()
982 },
983 ),
984 (
985 "top_p",
986 base.clone(),
987 SamplerConfig {
988 top_p: 0.95,
989 ..base.clone()
990 },
991 ),
992 (
993 "min_p",
994 base.clone(),
995 SamplerConfig {
996 min_p: 0.05,
997 ..base.clone()
998 },
999 ),
1000 (
1001 "penalty_last_n",
1002 pen_base.clone(),
1003 SamplerConfig {
1004 penalty_last_n: 128,
1005 ..pen_base.clone()
1006 },
1007 ),
1008 (
1009 "penalty_repeat",
1010 pen_base.clone(),
1011 SamplerConfig {
1012 penalty_repeat: 1.2,
1013 ..pen_base.clone()
1014 },
1015 ),
1016 (
1017 "penalty_freq",
1018 pen_base.clone(),
1019 SamplerConfig {
1020 penalty_freq: 0.6,
1021 ..pen_base.clone()
1022 },
1023 ),
1024 (
1025 "penalty_present",
1026 pen_base.clone(),
1027 SamplerConfig {
1028 penalty_present: 0.6,
1029 ..pen_base.clone()
1030 },
1031 ),
1032 ];
1033 for (expect, parked_cfg, cfg) in cases {
1034 let parked = id(&parked_cfg);
1035 let incoming = id(&cfg);
1036 assert_eq!(
1037 incoming.mismatch(&parked),
1038 Some(expect),
1039 "changing {expect} alone must refuse and name {expect} ({cfg:?})"
1040 );
1041 assert!(
1042 incoming.legacy_admits(&parked),
1043 "legacy must admit the {expect} change, or the refusal test is tautological"
1044 );
1045 }
1046 // Turning penalties ON from an unpenalized parked session is a `penalty_last_n` refusal —
1047 // the coarsest true statement about that pair, asserted so the order is pinned.
1048 assert_eq!(
1049 id(&pen_base).mismatch(&id(&base)),
1050 Some("penalty_last_n"),
1051 "penalties on vs off is named at the window, not at a coefficient"
1052 );
1053 }
1054
1055 #[test]
1056 fn greedy_to_sampled_and_back_both_refuse_as_regime() {
1057 let g = id(&SamplerConfig::default());
1058 let s = id(&pure_temp());
1059 assert_eq!(s.mismatch(&g), Some("regime"));
1060 assert_eq!(g.mismatch(&s), Some("regime"));
1061 }
1062
1063 // ---- the seed decision, pinned so it cannot change silently ----
1064
1065 #[test]
1066 fn seed_alone_does_not_refuse() {
1067 // DELIBERATE (see SamplerIdentity::mismatch): the draft graph is re-keyed on seed by
1068 // SampledGraphKey and the session's Philox counters are counter-based, so a changed seed
1069 // is sound — and comparing it would refuse every seed-omitting sampled conversation,
1070 // because an omitted seed draws fresh per-request entropy.
1071 let a = pure_temp();
1072 let b = SamplerConfig {
1073 seed: 999,
1074 ..a.clone()
1075 };
1076 assert_eq!(
1077 id(&a).mismatch(&id(&b)),
1078 None,
1079 "seed is carried but not compared"
1080 );
1081 assert_ne!(id(&a).seed(), id(&b).seed(), "the seed is still recorded");
1082 }
1083
1084 #[test]
1085 fn mismatch_is_symmetric_and_identity_is_an_equivalence() {
1086 let a = id(&pure_temp());
1087 let b = id(&vendor());
1088 assert_eq!(a.mismatch(&b).is_some(), b.mismatch(&a).is_some());
1089 assert_eq!(a.mismatch(&a), None);
1090 assert_eq!(b.mismatch(&b), None);
1091 }
1092}