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#[derive(Clone, Copy)]
180enum NucleusOrdering {
181 Comparison,
182 PrefixRadix,
183}
184
185/// Stateful sampler: owns the RNG + the recent-token history (for penalties).
186pub struct Sampler {
187 cfg: SamplerConfig,
188 rng: SplitMix64,
189 history: Vec<u32>, // recently emitted tokens (for penalty window)
190 // Counts over exactly the active penalty window. Keeping this incrementally makes the host
191 // oracle cheaper too, and lets the serving path upload O(unique ids) sparse penalty state
192 // instead of either the full vocabulary or an O(history^2) device-side dedup walk.
193 penalty_counts: HashMap<u32, u32>,
194 nucleus_order: NucleusOrderScratch,
195 nucleus_ordering: NucleusOrdering,
196}
197
198impl Sampler {
199 pub fn new(cfg: SamplerConfig) -> Self {
200 Self::with_nucleus_ordering(cfg, NucleusOrdering::Comparison)
201 }
202
203 /// Ordering-only specialization for the qualified GLM plain TP2 serving route.
204 /// Broad synthetic rows regress when radix reaches a tie and repeats the sort;
205 /// other callers and sampling shapes therefore retain the comparison path.
206 /// This does not change any filter, probability arithmetic, or RNG draw.
207 pub fn for_glm5_plain_tp2(cfg: SamplerConfig) -> Self {
208 let neutral_penalties = cfg.penalty_last_n == 0
209 || (cfg.penalty_repeat == 1.0 && cfg.penalty_freq == 0.0 && cfg.penalty_present == 0.0);
210 let ordering = if cfg.temperature == 1.0
211 && cfg.top_p == 0.95
212 && cfg.top_k == 0
213 && cfg.min_p == 0.0
214 && neutral_penalties
215 {
216 NucleusOrdering::PrefixRadix
217 } else {
218 NucleusOrdering::Comparison
219 };
220 Self::with_nucleus_ordering(cfg, ordering)
221 }
222
223 fn with_nucleus_ordering(cfg: SamplerConfig, nucleus_ordering: NucleusOrdering) -> Self {
224 let rng = SplitMix64::new(cfg.seed);
225 Sampler {
226 cfg,
227 rng,
228 history: Vec::new(),
229 penalty_counts: HashMap::new(),
230 nucleus_order: NucleusOrderScratch::default(),
231 nucleus_ordering,
232 }
233 }
234
235 /// Diagnostic counts for the nucleus ordering mechanism, not sampling-policy state.
236 pub fn nucleus_sort_counts(&self) -> (u64, u64) {
237 (
238 self.nucleus_order.radix_calls,
239 self.nucleus_order.comparison_calls,
240 )
241 }
242
243 pub fn is_greedy(&self) -> bool {
244 self.cfg.temperature <= 0.0
245 }
246 /// Sampled spec in its FASTEST regime: pure temperature, no truncation filters, no
247 /// penalties. Filters and penalties are also distribution-exact under the rejection
248 /// verify (spec.rs applies both symmetrically to draft q and target p), so they remain
249 /// spec-ELIGIBLE — see `spec_eligible` in memra-server's worker, the authoritative
250 /// predicate. What they cost is the in-graph draft chain: the captured sampled draft
251 /// samples from the RAW softmax and can hold neither per-row filter stats nor a varying
252 /// penalty history, so `spec.rs` engages `graph_s` only in this pure-temp regime
253 /// (`pure_temp`) and otherwise falls back to the eager draft chain. This predicate names
254 /// that regime; it is NOT an eligibility test.
255 pub fn is_spec_sampling(&self) -> bool {
256 self.cfg.temperature > 0.0
257 && self.cfg.penalty_repeat == 1.0
258 && self.cfg.penalty_freq == 0.0
259 && self.cfg.penalty_present == 0.0
260 && self.cfg.top_k == 0
261 && self.cfg.top_p >= 1.0
262 && self.cfg.min_p <= 0.0
263 }
264 pub fn top_k(&self) -> usize {
265 self.cfg.top_k
266 }
267 pub fn penalty_last_n(&self) -> usize {
268 self.cfg.penalty_last_n
269 }
270 pub fn penalty_repeat(&self) -> f32 {
271 self.cfg.penalty_repeat
272 }
273 pub fn penalty_freq(&self) -> f32 {
274 self.cfg.penalty_freq
275 }
276 pub fn penalty_present(&self) -> f32 {
277 self.cfg.penalty_present
278 }
279 pub fn top_p(&self) -> f32 {
280 self.cfg.top_p
281 }
282 pub fn min_p(&self) -> f32 {
283 self.cfg.min_p
284 }
285 pub fn temperature(&self) -> f32 {
286 self.cfg.temperature
287 }
288 pub fn seed(&self) -> u64 {
289 self.cfg.seed
290 }
291 /// This sampler's canonical [`SamplerIdentity`] — the whole-session resume predicate's input.
292 pub fn identity(&self) -> SamplerIdentity {
293 SamplerIdentity::of(&self.cfg)
294 }
295
296 fn penalties_on(&self) -> bool {
297 self.cfg.penalty_last_n > 0
298 && (self.cfg.penalty_repeat != 1.0
299 || self.cfg.penalty_freq != 0.0
300 || self.cfg.penalty_present != 0.0)
301 }
302
303 /// Sparse `(token_id, count)` rows for the current penalty window. The order cannot affect
304 /// the arithmetic because every entry mutates one distinct logit; avoiding a per-token sort
305 /// is material on long agent histories.
306 pub fn penalty_counts(&self) -> Vec<(u32, u32)> {
307 debug_assert!(self.penalty_counts.values().all(|&count| count > 0));
308 self.penalty_counts
309 .iter()
310 .map(|(&id, &n)| (id, n))
311 .collect()
312 }
313
314 /// Record an emitted token so subsequent penalties see it.
315 pub fn accept(&mut self, token: u32) {
316 if self.penalties_on() {
317 let n = self.cfg.penalty_last_n;
318 if self.history.len() >= n {
319 let expired = self.history[self.history.len() - n];
320 let remove = {
321 let count = self
322 .penalty_counts
323 .get_mut(&expired)
324 .expect("active penalty window lost an accepted token");
325 *count -= 1;
326 *count == 0
327 };
328 if remove {
329 self.penalty_counts.remove(&expired);
330 }
331 }
332 *self.penalty_counts.entry(token).or_insert(0) += 1;
333 }
334 self.history.push(token);
335 }
336
337 /// Sample the next token id from raw logits [n_vocab]. Does NOT mutate logits in place beyond
338 /// a local copy. Returns the chosen token id. (Caller should `accept()` it afterwards.)
339 pub fn sample(&mut self, logits: &[f32]) -> u32 {
340 // Greedy fast path: argmax over RAW logits (penalties don't change the argmax direction
341 // enough to matter for the reference path; llama greedy is also pre-penalty argmax only
342 // when no penalties set — but to stay correct under penalties we still apply them first).
343 if self.is_greedy()
344 && self.cfg.penalty_repeat == 1.0
345 && self.cfg.penalty_freq == 0.0
346 && self.cfg.penalty_present == 0.0
347 {
348 return argmax_u32(logits);
349 }
350
351 // Work on (id, logit) candidates.
352 let mut cand: Vec<(u32, f32)> = logits
353 .iter()
354 .enumerate()
355 .map(|(i, &l)| (i as u32, l))
356 .collect();
357
358 // 1. Penalties (operate on logits, over the last-n history window).
359 // `cand` is DENSE and INDEX-ALIGNED here by construction (built from
360 // `logits.iter().enumerate()` immediately above, nothing has filtered it yet), so the
361 // penalty pass indexes straight into it instead of hashing every candidate. See
362 // `apply_penalties_dense`.
363 self.apply_penalties_dense(&mut cand);
364
365 // Greedy-with-penalties: argmax after penalties, no sampling.
366 if self.is_greedy() {
367 let mut best = cand[0];
368 for &c in &cand[1..] {
369 if c.1 > best.1 {
370 best = c;
371 }
372 }
373 return best.0;
374 }
375
376 // 2. Temperature scale.
377 if self.cfg.temperature > 0.0 && self.cfg.temperature != 1.0 {
378 let inv = 1.0 / self.cfg.temperature;
379 for c in cand.iter_mut() {
380 c.1 *= inv;
381 }
382 }
383
384 // 3. top-k: keep the k highest-logit candidates (partial sort by logit desc).
385 if self.cfg.top_k > 0 && self.cfg.top_k < cand.len() {
386 cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
387 cand.truncate(self.cfg.top_k);
388 }
389
390 // softmax over the surviving candidates (numerically stable).
391 softmax_inplace(&mut cand);
392
393 // 4. top-p (nucleus): smallest set whose cumulative prob >= top_p. Needs desc-by-prob order.
394 if self.cfg.top_p < 1.0 {
395 match self.nucleus_ordering {
396 NucleusOrdering::Comparison => self.nucleus_order.comparison(&mut cand),
397 NucleusOrdering::PrefixRadix => self.nucleus_order.sort(&mut cand, self.cfg.top_p),
398 }
399 let mut cum = 0.0f32;
400 let mut keep = 0usize;
401 for (i, c) in cand.iter().enumerate() {
402 cum += c.1;
403 keep = i + 1;
404 if cum >= self.cfg.top_p {
405 break;
406 }
407 }
408 cand.truncate(keep.max(1));
409 }
410
411 // 5. min-p: keep candidates with prob >= min_p * max_prob.
412 if self.cfg.min_p > 0.0 {
413 let maxp = cand.iter().map(|c| c.1).fold(0.0f32, f32::max);
414 let thresh = self.cfg.min_p * maxp;
415 cand.retain(|c| c.1 >= thresh);
416 if cand.is_empty() {
417 return argmax_u32(logits);
418 } // safety
419 }
420
421 // renormalize the surviving probs and draw.
422 let sum: f32 = cand.iter().map(|c| c.1).sum();
423 let r = self.rng.next_f32() * sum;
424 let mut acc = 0.0f32;
425 for c in &cand {
426 acc += c.1;
427 if acc >= r {
428 return c.0;
429 }
430 }
431 cand.last().unwrap().0
432 }
433
434 /// llama.cpp penalty over a DENSE, INDEX-ALIGNED candidate slice: `cand[i].0 == i`.
435 ///
436 /// Same arithmetic as `apply_penalties_scan_reference`, on exactly the same elements, in the
437 /// same order — but O(distinct penalized tokens) instead of O(n_vocab) hash lookups.
438 ///
439 /// WHY (lane/glm5-host-audit, 2026-09-01). The scan form did one `HashMap<u32,u32>` SipHash
440 /// probe PER CANDIDATE, i.e. one per vocabulary entry: ~152k probes per token on the Qwen
441 /// class. `penalty_counts` holds at most `PEN_WINDOW_MAX` distinct ids and in practice the
442 /// number of distinct generated tokens so far, so the loop was inverted the expensive way
443 /// round. This matters in production, not in theory: `devsample_meta` refuses the device
444 /// sampler for any penalized config unless `MEMRA_SERVE_DEVPENALTY=1`, the whole fleet runs
445 /// it at 0, and a served model whose VENDOR-RECOMMENDED non-thinking arm carries
446 /// `presence_penalty` therefore lands every token of every request on this function.
447 ///
448 /// BIT-IDENTICAL BY CONSTRUCTION, and gated as such rather than asserted in prose: the set of
449 /// touched entries is identical (`penalty_counts` covers exactly the window, which the
450 /// debug_assert below re-checks), the per-entry arithmetic is copied unchanged, and each
451 /// entry is touched exactly once in both forms, so no float re-association is possible.
452 /// `apply_penalties_scan_reference` is kept as the ORACLE and
453 /// `dense_penalties_match_the_scan_reference_bitwise` compares them over randomized inputs.
454 ///
455 /// THE HOST ORACLE FOR DEVICE PENALTIES (lane/spec-exclusions-20260902): the memra-engine
456 /// `penalize_logits*` kernels (`cu/spec_sample.cu`, `keskar_penalize_rn`) are written to
457 /// produce these exact f32 bits for the same window, and the glm5 spec route's penalized
458 /// verify rows are gated against `penalized_logits` bit for bit
459 /// (`gpu_device_penalties_are_bit_identical_to_the_host_sampler`). Any change to the
460 /// arithmetic here is a change to the spec route's numerics too; keep the two in step.
461 fn apply_penalties_dense(&self, cand: &mut [(u32, f32)]) {
462 let n = self.cfg.penalty_last_n;
463 if n == 0 {
464 return;
465 }
466 if self.cfg.penalty_repeat == 1.0
467 && self.cfg.penalty_freq == 0.0
468 && self.cfg.penalty_present == 0.0
469 {
470 return;
471 }
472 let start = self.history.len().saturating_sub(n);
473 let window = &self.history[start..];
474 if window.is_empty() {
475 return;
476 }
477 debug_assert_eq!(
478 self.penalty_counts
479 .values()
480 .map(|&n| n as usize)
481 .sum::<usize>(),
482 window.len(),
483 "incremental penalty counts must cover the active history window"
484 );
485 for (&id, &cnt) in self.penalty_counts.iter() {
486 let Some(c) = cand.get_mut(id as usize) else {
487 // A count for an id outside the logits row: a vocab/count mismatch, which is a
488 // caller bug rather than something to silently skip. Loud in debug, inert in
489 // release (dropping the penalty is strictly safer than indexing out of bounds).
490 debug_assert!(
491 false,
492 "penalty count for id {id} is outside the {} candidate row",
493 cand.len()
494 );
495 continue;
496 };
497 debug_assert_eq!(
498 c.0, id,
499 "apply_penalties_dense requires an index-aligned candidate slice"
500 );
501 // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
502 if self.cfg.penalty_repeat != 1.0 {
503 if c.1 > 0.0 {
504 c.1 /= self.cfg.penalty_repeat;
505 } else {
506 c.1 *= self.cfg.penalty_repeat;
507 }
508 }
509 c.1 -= cnt as f32 * self.cfg.penalty_freq;
510 c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
511 }
512 }
513
514 /// The penalized logits row this sampler would score the next token from, as bytes: a
515 /// copy of `logits` with the active penalty window applied through the ONE serving
516 /// penalty pass (`apply_penalties_dense`). Identity when penalties are off (window
517 /// empty or every coefficient neutral), exactly as `sample` sees it.
518 ///
519 /// EXISTS AS THE ORACLE SEAM for the device penalty kernels (lane/spec-exclusions-
520 /// 20260902): a spec route that penalizes verify rows on device claims "the same bits
521 /// the plain sampler produces", and that claim needs the plain sampler's bits to compare
522 /// against, not a re-derivation in a test. `sample` only ever returns the chosen id.
523 pub fn penalized_logits(&self, logits: &[f32]) -> Vec<f32> {
524 let mut cand: Vec<(u32, f32)> = logits
525 .iter()
526 .enumerate()
527 .map(|(i, &l)| (i as u32, l))
528 .collect();
529 self.apply_penalties_dense(&mut cand);
530 cand.into_iter().map(|(_, l)| l).collect()
531 }
532
533 /// llama.cpp penalty: for each token in the last-n history, repeat-divide/multiply its logit
534 /// and apply frequency*count + presence. (llama-sampler.cpp penalties.)
535 ///
536 /// THE ORACLE, not the serving path. `apply_penalties_dense` replaced it on the hot path
537 /// 2026-09-01; this form is retained verbatim so the replacement has something to be proven
538 /// bit-identical against, which is worth more than deleting it.
539 #[cfg(test)]
540 fn apply_penalties_scan_reference(&self, cand: &mut [(u32, f32)]) {
541 let n = self.cfg.penalty_last_n;
542 if n == 0 {
543 return;
544 }
545 if self.cfg.penalty_repeat == 1.0
546 && self.cfg.penalty_freq == 0.0
547 && self.cfg.penalty_present == 0.0
548 {
549 return;
550 }
551 let start = self.history.len().saturating_sub(n);
552 let window = &self.history[start..];
553 if window.is_empty() {
554 return;
555 }
556 debug_assert_eq!(
557 self.penalty_counts
558 .values()
559 .map(|&n| n as usize)
560 .sum::<usize>(),
561 window.len(),
562 "incremental penalty counts must cover the active history window"
563 );
564 for c in cand.iter_mut() {
565 if let Some(&cnt) = self.penalty_counts.get(&c.0) {
566 // repeat: llama divides if logit>0 else multiplies (penalize toward 0)
567 if self.cfg.penalty_repeat != 1.0 {
568 if c.1 > 0.0 {
569 c.1 /= self.cfg.penalty_repeat;
570 } else {
571 c.1 *= self.cfg.penalty_repeat;
572 }
573 }
574 c.1 -= cnt as f32 * self.cfg.penalty_freq;
575 c.1 -= self.cfg.penalty_present; // presence: applied once if count>0
576 }
577 }
578 }
579}
580
581fn argmax_u32(logits: &[f32]) -> u32 {
582 let mut best = 0u32;
583 let mut bv = f32::NEG_INFINITY;
584 for (i, &v) in logits.iter().enumerate() {
585 if v > bv {
586 bv = v;
587 best = i as u32;
588 }
589 }
590 best
591}
592
593/// Ordering-only prefix radix path. Probability arithmetic and the downstream
594/// f32 cutoff/draw are unchanged. Resolve high-probability buckets first and stop
595/// at the exact nucleus, avoiding passes/copies over discarded vocabulary tails.
596/// The legacy unstable comparator still owns ties that affect retained IDs.
597#[derive(Default)]
598struct NucleusOrderScratch {
599 keys: Vec<u32>,
600 order: Vec<u32>,
601 scratch: Vec<u32>,
602 sorted: Vec<(u32, f32)>,
603 radix_calls: u64,
604 comparison_calls: u64,
605}
606
607enum NucleusVisit {
608 More,
609 Complete,
610 Tied,
611}
612
613fn descending_probability_key(value: f32) -> u32 {
614 let bits = value.to_bits();
615 let ascending = if bits & 0x8000_0000 == 0 {
616 bits ^ 0x8000_0000
617 } else {
618 !bits
619 };
620 // total_cmp distinguishes signed zeros, unlike DSV4's partial_cmp contract.
621 !ascending
622}
623
624impl NucleusOrderScratch {
625 fn comparison(&mut self, cand: &mut [(u32, f32)]) {
626 cand.sort_unstable_by(|a, b| b.1.total_cmp(&a.1));
627 self.comparison_calls += 1;
628 }
629
630 fn sort(&mut self, cand: &mut Vec<(u32, f32)>, top_p: f32) {
631 if cand.len() < 1024
632 || cand.len() > u32::MAX as usize
633 || cand.windows(2).all(|w| w[0].1.total_cmp(&w[1].1).is_ge())
634 {
635 self.comparison(cand);
636 return;
637 }
638 self.keys.resize(cand.len(), 0);
639 self.order.clear();
640 for (i, &(_, probability)) in cand.iter().enumerate() {
641 if !probability.is_finite() || probability < 0.0 {
642 self.comparison(cand);
643 return;
644 }
645 // Zero tails cannot contribute mass. If the positive prefix fails
646 // to reach the cutoff, fall back on the complete untouched input.
647 if probability != 0.0 {
648 self.keys[i] = descending_probability_key(probability);
649 self.order.push(i as u32);
650 }
651 }
652 self.scratch.resize(self.order.len(), 0);
653 self.sorted.clear();
654 let mut cumulative = 0.0f32;
655 let result = self.visit(cand, 0, self.order.len(), 24, top_p, &mut cumulative);
656 if !matches!(result, NucleusVisit::Complete) {
657 self.comparison(cand);
658 return;
659 }
660 // Only discard a tail after the exact original f32 cutoff is reached.
661 // The caller retains its original cutoff/min-p/renormalize/draw code.
662 cand.truncate(self.sorted.len());
663 cand.copy_from_slice(&self.sorted);
664 self.radix_calls += 1;
665 }
666
667 fn visit(
668 &mut self,
669 cand: &[(u32, f32)],
670 lo: usize,
671 hi: usize,
672 shift: i32,
673 top_p: f32,
674 cumulative: &mut f32,
675 ) -> NucleusVisit {
676 if shift < 0 && hi - lo > 1 {
677 return NucleusVisit::Tied;
678 }
679 if hi - lo <= 32 {
680 self.order[lo..hi]
681 .sort_unstable_by(|&a, &b| cand[b as usize].1.total_cmp(&cand[a as usize].1));
682 for i in lo..hi {
683 let candidate = cand[self.order[i] as usize];
684 // Same full key cannot straddle radix buckets. Look ahead in
685 // this leaf before retaining either member of an equal pair.
686 if i + 1 < hi
687 && candidate
688 .1
689 .total_cmp(&cand[self.order[i + 1] as usize].1)
690 .is_eq()
691 {
692 return NucleusVisit::Tied;
693 }
694 self.sorted.push(candidate);
695 *cumulative += candidate.1;
696 if *cumulative >= top_p {
697 return NucleusVisit::Complete;
698 }
699 }
700 return NucleusVisit::More;
701 }
702 let mut counts = [0usize; 256];
703 for &id in &self.order[lo..hi] {
704 counts[((self.keys[id as usize] >> shift) & 255) as usize] += 1;
705 }
706 let mut offsets = [0usize; 256];
707 let mut next = lo;
708 for (offset, &count) in offsets.iter_mut().zip(&counts) {
709 *offset = next;
710 next += count;
711 }
712 for &id in &self.order[lo..hi] {
713 let bucket = ((self.keys[id as usize] >> shift) & 255) as usize;
714 self.scratch[offsets[bucket]] = id;
715 offsets[bucket] += 1;
716 }
717 self.order[lo..hi].copy_from_slice(&self.scratch[lo..hi]);
718 let mut begin = lo;
719 for count in counts {
720 if count != 0 {
721 match self.visit(cand, begin, begin + count, shift - 8, top_p, cumulative) {
722 NucleusVisit::More => {}
723 result => return result,
724 }
725 }
726 begin += count;
727 }
728 NucleusVisit::More
729 }
730}
731
732/// Stable softmax over candidate logits, writing probs back into the logit slot.
733fn softmax_inplace(cand: &mut [(u32, f32)]) {
734 let maxl = cand.iter().map(|c| c.1).fold(f32::NEG_INFINITY, f32::max);
735 let mut sum = 0.0f32;
736 for c in cand.iter_mut() {
737 let e = (c.1 - maxl).exp();
738 c.1 = e;
739 sum += e;
740 }
741 let inv = if sum > 0.0 { 1.0 / sum } else { 0.0 };
742 for c in cand.iter_mut() {
743 c.1 *= inv;
744 }
745}
746
747/// SplitMix64 — deterministic seedable RNG (so a fixed seed reproduces the token stream for the
748/// validation gate). Not crypto; fine for sampling.
749struct SplitMix64 {
750 state: u64,
751}
752impl SplitMix64 {
753 fn new(seed: u64) -> Self {
754 SplitMix64 {
755 state: seed.wrapping_add(0x9E3779B97F4A7C15),
756 }
757 }
758 fn next_u64(&mut self) -> u64 {
759 self.state = self.state.wrapping_add(0x9E3779B97F4A7C15);
760 let mut z = self.state;
761 z = (z ^ (z >> 30)).wrapping_mul(0xBF58476D1CE4E5B9);
762 z = (z ^ (z >> 27)).wrapping_mul(0x94D049BB133111EB);
763 z ^ (z >> 31)
764 }
765 /// uniform f32 in [0,1).
766 fn next_f32(&mut self) -> f32 {
767 // top 24 bits -> [0,1)
768 ((self.next_u64() >> 40) as f32) / (1u32 << 24) as f32
769 }
770}
771
772#[cfg(test)]
773mod tests {
774 use super::*;
775
776 fn sorted_counts(s: &Sampler) -> Vec<(u32, u32)> {
777 let mut counts = s.penalty_counts();
778 counts.sort_unstable_by_key(|&(id, _)| id);
779 counts
780 }
781
782 /// THE GATE for the 2026-09-01 host-cost fix (lane/glm5-host-audit): the dense penalty pass
783 /// must reproduce the O(n_vocab) scan reference BIT FOR BIT, not merely closely.
784 ///
785 /// Compared as raw bit patterns (`to_bits`), because `==` on f32 would call two different
786 /// NaNs equal and would call `-0.0` and `0.0` equal — and `penalty_present` subtraction can
787 /// produce exactly `-0.0`. The inputs deliberately include negative logits (the repeat rule
788 /// branches on sign), repeated tokens (frequency count > 1), a token never generated (must be
789 /// untouched), and the three penalty coefficients both neutral and active.
790 #[test]
791 fn dense_penalties_match_the_scan_reference_bitwise() {
792 // A small deterministic LCG: this gate must not depend on a dev-dependency.
793 let mut state: u64 = 0x9E3779B97F4A7C15;
794 let mut next = || {
795 state = state
796 .wrapping_mul(6364136223846793005)
797 .wrapping_add(1442695040888963407);
798 ((state >> 33) as u32) as f32 / (u32::MAX >> 1) as f32 - 1.0
799 };
800
801 let vocab = 257usize; // prime-ish, and > any window used below
802 let mut cases = 0;
803 for &(repeat, freq, present) in &[
804 (1.0f32, 0.0f32, 0.0f32), // all neutral: both forms must no-op
805 (1.1, 0.0, 0.0), // repeat only (exercises the sign branch)
806 (1.0, 0.5, 0.0), // frequency only (count-scaled)
807 (1.0, 0.0, 1.5), // presence only — the q38 non-thinking vendor arm
808 (1.1, 0.5, 1.5), // all three together
809 (0.8, -0.25, -0.5), // below/negative coefficients are legal API values
810 ] {
811 for &window in &[1usize, 3, 64, 8192] {
812 let cfg = SamplerConfig {
813 temperature: 1.0,
814 penalty_last_n: window,
815 penalty_repeat: repeat,
816 penalty_freq: freq,
817 penalty_present: present,
818 ..SamplerConfig::default()
819 };
820 let mut s = Sampler::new(cfg);
821 // Feed a history with repeats, and never feed id 0 or id 256 so the "untouched
822 // candidate" case is covered at both ends of the row.
823 for step in 0..40u32 {
824 s.accept(1 + (step * 7) % 200);
825 s.accept(1 + (step * 3) % 50); // guarantees counts > 1
826 }
827
828 let base: Vec<(u32, f32)> = (0..vocab).map(|i| (i as u32, next() * 8.0)).collect();
829 let mut dense = base.clone();
830 let mut reference = base.clone();
831 s.apply_penalties_dense(&mut dense);
832 s.apply_penalties_scan_reference(&mut reference);
833
834 assert_eq!(dense.len(), reference.len());
835 for (i, (d, r)) in dense.iter().zip(reference.iter()).enumerate() {
836 assert_eq!(d.0, r.0, "candidate id moved at {i}");
837 assert_eq!(
838 d.1.to_bits(),
839 r.1.to_bits(),
840 "BIT DIVERGENCE at id {i} (repeat={repeat} freq={freq} \
841 present={present} window={window}): dense {} vs reference {}",
842 d.1,
843 r.1
844 );
845 }
846 // Ids never generated must be untouched — otherwise "identical" could be two
847 // equally-wrong passes.
848 assert_eq!(
849 dense[0].1.to_bits(),
850 base[0].1.to_bits(),
851 "id 0 was penalized"
852 );
853 assert_eq!(
854 dense[256].1.to_bits(),
855 base[256].1.to_bits(),
856 "id 256 was penalized"
857 );
858 cases += 1;
859 }
860 }
861 assert_eq!(cases, 24, "the coefficient x window matrix must all run");
862 }
863
864 /// The precondition the dense form trades correctness for speed on: the candidate row it is
865 /// handed is dense and index-aligned. Asserted on the REAL call path (`sample`), so a future
866 /// caller that filters before penalizing cannot quietly pass.
867 #[test]
868 fn the_sampled_path_penalizes_the_token_it_generated() {
869 let cfg = SamplerConfig {
870 temperature: 1.0,
871 penalty_last_n: 64,
872 penalty_present: 100.0, // large enough that the penalized id cannot win
873 seed: 7,
874 ..SamplerConfig::default()
875 };
876 let mut s = Sampler::new(cfg);
877 // Logits that make id 2 the runaway favourite, then penalize exactly id 2.
878 let logits = vec![0.0, 0.0, 20.0, 0.0];
879 s.accept(2);
880 for _ in 0..32 {
881 assert_ne!(
882 s.sample(&logits),
883 2,
884 "presence penalty on the generated id must move the draw off it, which only \
885 happens if the dense pass indexed the right candidate"
886 );
887 }
888 }
889
890 #[test]
891 fn greedy_is_argmax() {
892 let mut s = Sampler::new(SamplerConfig::default()); // temp 0
893 let logits = vec![0.1, 5.0, 2.0, -1.0];
894 assert_eq!(s.sample(&logits), 1);
895 }
896
897 #[test]
898 fn temp_sampling_deterministic_with_seed() {
899 let cfg = SamplerConfig {
900 temperature: 1.0,
901 seed: 42,
902 ..Default::default()
903 };
904 let logits = vec![1.0, 2.0, 3.0, 0.5];
905 let a = Sampler::new(cfg.clone()).sample(&logits);
906 let b = Sampler::new(cfg).sample(&logits);
907 assert_eq!(a, b, "same seed must reproduce the draw");
908 assert!(a < 4);
909 }
910
911 #[test]
912 fn top_k_one_is_argmax() {
913 let cfg = SamplerConfig {
914 temperature: 1.0,
915 top_k: 1,
916 seed: 7,
917 ..Default::default()
918 };
919 let logits = vec![0.1, 5.0, 2.0, -1.0];
920 assert_eq!(
921 Sampler::new(cfg).sample(&logits),
922 1,
923 "top_k=1 collapses to argmax"
924 );
925 }
926
927 #[test]
928 fn min_p_keeps_only_high_prob() {
929 // logit 10 dominates; min_p 0.5 should drop the rest -> always pick id 2.
930 let cfg = SamplerConfig {
931 temperature: 1.0,
932 min_p: 0.5,
933 seed: 3,
934 ..Default::default()
935 };
936 let logits = vec![0.0, 0.0, 10.0, 0.0];
937 for _ in 0..16 {
938 assert_eq!(Sampler::new(cfg.clone()).sample(&logits), 2);
939 }
940 }
941
942 #[test]
943 fn repeat_penalty_suppresses_recent() {
944 // greedy + heavy repeat penalty: id 1 is argmax but recently emitted -> should drop it.
945 let cfg = SamplerConfig {
946 penalty_last_n: 8,
947 penalty_repeat: 100.0,
948 ..Default::default()
949 };
950 let mut s = Sampler::new(cfg);
951 s.accept(1); // 1 was just emitted
952 let logits = vec![4.0, 5.0, 4.5, 1.0]; // raw argmax = 1
953 let got = s.sample(&logits);
954 assert_ne!(
955 got, 1,
956 "recent token must be penalized out of greedy argmax"
957 );
958 assert_eq!(got, 2, "next-highest after penalizing 1");
959 }
960
961 #[test]
962 fn penalty_counts_follow_the_sliding_window() {
963 let cfg = SamplerConfig {
964 penalty_last_n: 4,
965 penalty_repeat: 1.1,
966 penalty_freq: 0.5,
967 penalty_present: 0.25,
968 ..Default::default()
969 };
970 let mut s = Sampler::new(cfg);
971 for tok in [7, 8, 7, 9] {
972 s.accept(tok);
973 }
974 assert_eq!(sorted_counts(&s), vec![(7, 2), (8, 1), (9, 1)]);
975
976 s.accept(8); // active window is now [8, 7, 9, 8]
977 assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 2), (9, 1)]);
978 s.accept(10); // active window is now [7, 9, 8, 10]
979 assert_eq!(sorted_counts(&s), vec![(7, 1), (8, 1), (9, 1), (10, 1)]);
980 s.accept(11); // active window is now [9, 8, 10, 11]; final 7 expires
981 assert_eq!(sorted_counts(&s), vec![(8, 1), (9, 1), (10, 1), (11, 1)]);
982 assert!(!s.penalty_counts().iter().any(|&(id, n)| id == 7 || n == 0));
983 }
984
985 #[test]
986 fn full_context_and_disabled_penalty_counts_are_exact() {
987 let mut full = Sampler::new(SamplerConfig {
988 penalty_last_n: usize::MAX,
989 penalty_present: 1.5,
990 ..Default::default()
991 });
992 for tok in [3, 3, 4, 5, 3] {
993 full.accept(tok);
994 }
995 assert_eq!(sorted_counts(&full), vec![(3, 3), (4, 1), (5, 1)]);
996
997 let mut neutral = Sampler::new(SamplerConfig {
998 penalty_last_n: usize::MAX,
999 ..Default::default()
1000 });
1001 neutral.accept(3);
1002 assert!(neutral.penalty_counts().is_empty());
1003 }
1004}
1005
1006/// SESSION-RESUME SAMPLER PREDICATE teeth (lane/session-resume-sampler-predicate-20260820).
1007/// CPU-only, no GPU: the predicate is a pure function, so its whole contract is testable here and
1008/// a regression cannot hide behind "needs a card".
1009///
1010/// TEETH BOTH DIRECTIONS is the point. Every refusal test also asserts that `legacy_admits`
1011/// ADMITS the same pair — the pre-lane probe compared prompts and never samplers — so the test is
1012/// decisive (it fails on the old code) rather than tautological (passing because the pair was
1013/// never resumable for some other reason).
1014#[cfg(test)]
1015mod resume_sampler_predicate_tests {
1016 use super::*;
1017
1018 /// The vendor-default sampled shape the flip makes the majority of traffic.
1019 fn vendor() -> SamplerConfig {
1020 SamplerConfig {
1021 temperature: 0.7,
1022 top_k: 20,
1023 top_p: 0.95,
1024 seed: 20260820,
1025 ..Default::default()
1026 }
1027 }
1028
1029 /// Today's pure-temp shape — the one that parks a `graph_s`.
1030 fn pure_temp() -> SamplerConfig {
1031 SamplerConfig {
1032 temperature: 0.7,
1033 seed: 20260820,
1034 ..Default::default()
1035 }
1036 }
1037
1038 fn id(cfg: &SamplerConfig) -> SamplerIdentity {
1039 SamplerIdentity::of(cfg)
1040 }
1041
1042 // ---- direction 1: a SAME-sampler resume still resumes (no regression) ----
1043
1044 #[test]
1045 fn identical_sampler_resumes() {
1046 for cfg in [pure_temp(), vendor(), SamplerConfig::default()] {
1047 assert_eq!(
1048 id(&cfg).mismatch(&id(&cfg)),
1049 None,
1050 "a request must resume a session its own sampler shaped: {cfg:?}"
1051 );
1052 }
1053 }
1054
1055 #[test]
1056 fn disabled_sentinels_are_the_same_program() {
1057 // top_p >= 1.0, min_p <= 0.0, top_k == 0 all mean OFF; a client that spells OFF
1058 // differently on turn 2 must not lose its cache.
1059 let a = SamplerConfig {
1060 temperature: 0.7,
1061 top_p: 1.0,
1062 min_p: 0.0,
1063 ..Default::default()
1064 };
1065 let b = SamplerConfig {
1066 temperature: 0.7,
1067 top_p: 1.5,
1068 min_p: -1.0,
1069 ..Default::default()
1070 };
1071 assert_eq!(id(&a).mismatch(&id(&b)), None, "off spelled two ways");
1072 }
1073
1074 #[test]
1075 fn greedy_temperature_encodings_are_one_program() {
1076 let a = SamplerConfig {
1077 temperature: 0.0,
1078 ..Default::default()
1079 };
1080 let b = SamplerConfig {
1081 temperature: -1.0,
1082 ..Default::default()
1083 };
1084 assert_eq!(id(&a).mismatch(&id(&b)), None, "temp<=0 is one regime");
1085 }
1086
1087 #[test]
1088 fn neutral_penalty_coefficients_equal_penalties_absent() {
1089 // penalty_last_n set but every coefficient neutral == `pen_on == false` in spec.rs.
1090 let a = SamplerConfig {
1091 temperature: 0.7,
1092 penalty_last_n: 64,
1093 penalty_repeat: 1.0,
1094 penalty_freq: 0.0,
1095 penalty_present: 0.0,
1096 ..Default::default()
1097 };
1098 let b = SamplerConfig {
1099 temperature: 0.7,
1100 penalty_last_n: 0,
1101 ..Default::default()
1102 };
1103 assert_eq!(
1104 id(&a).mismatch(&id(&b)),
1105 None,
1106 "an inert penalty window is not a penalty change"
1107 );
1108 }
1109
1110 // ---- direction 2: a sampler-DIFFERING resume refuses, and names the field ----
1111
1112 #[test]
1113 fn the_reproduced_collision_pair_refuses_and_names_a_filter() {
1114 // The exact pair the predecessor reproduced on a live server: turn 1 pure-temp parks,
1115 // turn 2 adds top_p 0.95 / top_k 20 and resumes. Same seed, same temperature.
1116 let parked = id(&pure_temp());
1117 let incoming = id(&vendor());
1118 let field = incoming
1119 .mismatch(&parked)
1120 .expect("the reproduced collision pair must refuse");
1121 assert_eq!(field, "top_k", "coarsest-first order names top_k here");
1122 // DECISIVE: the pre-lane probe admitted exactly this pair.
1123 assert!(
1124 incoming.legacy_admits(&parked),
1125 "legacy must admit the collision pair, or this test proves nothing"
1126 );
1127 }
1128
1129 #[test]
1130 fn every_compared_field_refuses_on_its_own_and_names_itself() {
1131 let base = pure_temp();
1132 // A penalized base, so the three coefficients can each move ALONE: with penalties off on
1133 // the parked side, turning any of them on also moves `penalty_last_n`, and coarsest-first
1134 // order would (correctly) name the window instead of the coefficient.
1135 let pen_base = SamplerConfig {
1136 penalty_last_n: 64,
1137 penalty_repeat: 1.1,
1138 penalty_freq: 0.5,
1139 penalty_present: 0.5,
1140 ..base.clone()
1141 };
1142 // (field, parked, incoming) — exactly one canonical field differs in each row.
1143 let cases: [(&str, SamplerConfig, SamplerConfig); 9] = [
1144 (
1145 "regime",
1146 base.clone(),
1147 SamplerConfig {
1148 temperature: 0.0,
1149 ..base.clone()
1150 },
1151 ),
1152 (
1153 "temperature",
1154 base.clone(),
1155 SamplerConfig {
1156 temperature: 0.8,
1157 ..base.clone()
1158 },
1159 ),
1160 (
1161 "top_k",
1162 base.clone(),
1163 SamplerConfig {
1164 top_k: 20,
1165 ..base.clone()
1166 },
1167 ),
1168 (
1169 "top_p",
1170 base.clone(),
1171 SamplerConfig {
1172 top_p: 0.95,
1173 ..base.clone()
1174 },
1175 ),
1176 (
1177 "min_p",
1178 base.clone(),
1179 SamplerConfig {
1180 min_p: 0.05,
1181 ..base.clone()
1182 },
1183 ),
1184 (
1185 "penalty_last_n",
1186 pen_base.clone(),
1187 SamplerConfig {
1188 penalty_last_n: 128,
1189 ..pen_base.clone()
1190 },
1191 ),
1192 (
1193 "penalty_repeat",
1194 pen_base.clone(),
1195 SamplerConfig {
1196 penalty_repeat: 1.2,
1197 ..pen_base.clone()
1198 },
1199 ),
1200 (
1201 "penalty_freq",
1202 pen_base.clone(),
1203 SamplerConfig {
1204 penalty_freq: 0.6,
1205 ..pen_base.clone()
1206 },
1207 ),
1208 (
1209 "penalty_present",
1210 pen_base.clone(),
1211 SamplerConfig {
1212 penalty_present: 0.6,
1213 ..pen_base.clone()
1214 },
1215 ),
1216 ];
1217 for (expect, parked_cfg, cfg) in cases {
1218 let parked = id(&parked_cfg);
1219 let incoming = id(&cfg);
1220 assert_eq!(
1221 incoming.mismatch(&parked),
1222 Some(expect),
1223 "changing {expect} alone must refuse and name {expect} ({cfg:?})"
1224 );
1225 assert!(
1226 incoming.legacy_admits(&parked),
1227 "legacy must admit the {expect} change, or the refusal test is tautological"
1228 );
1229 }
1230 // Turning penalties ON from an unpenalized parked session is a `penalty_last_n` refusal —
1231 // the coarsest true statement about that pair, asserted so the order is pinned.
1232 assert_eq!(
1233 id(&pen_base).mismatch(&id(&base)),
1234 Some("penalty_last_n"),
1235 "penalties on vs off is named at the window, not at a coefficient"
1236 );
1237 }
1238
1239 #[test]
1240 fn greedy_to_sampled_and_back_both_refuse_as_regime() {
1241 let g = id(&SamplerConfig::default());
1242 let s = id(&pure_temp());
1243 assert_eq!(s.mismatch(&g), Some("regime"));
1244 assert_eq!(g.mismatch(&s), Some("regime"));
1245 }
1246
1247 // ---- the seed decision, pinned so it cannot change silently ----
1248
1249 #[test]
1250 fn seed_alone_does_not_refuse() {
1251 // DELIBERATE (see SamplerIdentity::mismatch): the draft graph is re-keyed on seed by
1252 // SampledGraphKey and the session's Philox counters are counter-based, so a changed seed
1253 // is sound — and comparing it would refuse every seed-omitting sampled conversation,
1254 // because an omitted seed draws fresh per-request entropy.
1255 let a = pure_temp();
1256 let b = SamplerConfig {
1257 seed: 999,
1258 ..a.clone()
1259 };
1260 assert_eq!(
1261 id(&a).mismatch(&id(&b)),
1262 None,
1263 "seed is carried but not compared"
1264 );
1265 assert_ne!(id(&a).seed(), id(&b).seed(), "the seed is still recorded");
1266 }
1267
1268 #[test]
1269 fn mismatch_is_symmetric_and_identity_is_an_equivalence() {
1270 let a = id(&pure_temp());
1271 let b = id(&vendor());
1272 assert_eq!(a.mismatch(&b).is_some(), b.mismatch(&a).is_some());
1273 assert_eq!(a.mismatch(&a), None);
1274 assert_eq!(b.mismatch(&b), None);
1275 }
1276}
1277
1278#[cfg(test)]
1279mod nucleus_order_tests;