Skip to main content

ferrox_core/
kernel_registry.rs

1//! Sealed kernel-lookup registry: makes a *missing* kernel loud instead
2//! of silently slow.
3//!
4//! ## Why this exists
5//!
6//! The worst bug class this engine has shipped is not a wrong kernel but
7//! an absent one, silently replaced by a correct-but-slow fallback. Two
8//! real examples:
9//!
10//! - IQ4_XS batched prefill ran on the **CPU** because
11//!   [`crate::weight_matrix::WeightMatrix`]'s `metal_kind_supported`
12//!   predicate and `apply_gpu_batch`'s per-kind dispatch table disagreed
13//!   about one kind. The only symptom was a benchmark row 13.7x behind.
14//! - Gemma-4-E2B is slower on Metal than on CPU. Output is correct, so
15//!   nothing fails; the model simply never reaches a batched path.
16//!
17//! Both are *lookups that missed*. Neither produced a diagnostic.
18//!
19//! ## The shape
20//!
21//! Every dispatch decision that asks "is there a kernel for this
22//! (backend, op, quant kind)?" records the answer here.
23//!
24//! - **Build phase.** While the model is constructed, each weight matrix
25//!   is probed eagerly ([`crate::weight_matrix::WeightMatrix::probe_kernels`]):
26//!   the same predicates the hot path will consult are evaluated once per
27//!   weight and recorded. This is what turns "why is this row slow" into
28//!   a startup line.
29//! - **[`seal`].** Called once the model is loaded. It summarises the
30//!   build picture, warns about quantized weights that will run off the
31//!   selected accelerator, and switches on post-seal checking.
32//! - **Run phase.** After sealing, a dispatch-site lookup that misses a
33//!   *combination the build probe never saw* is by definition an
34//!   unpredicted slow path. It warns once, loudly, naming the call site
35//!   ([`#[track_caller]`](std::panic::Location)) and the quant kind, and
36//!   is a hard error under `FERROX_STRICT_KERNELS=1` so CI and benchmarks
37//!   can run closed.
38//!
39//! ## Cost
40//!
41//! This sits next to dispatch, so it follows the `OnceLock` discipline
42//! used by `metal_dense_enabled` / `min_task_macs`: the environment is
43//! read exactly once per process, never per dispatch.
44//!
45//! On the hot path the added cost is **zero instructions on a hit** —
46//! hits are only ever recorded by the build probe, never by a dispatch
47//! site. A dispatch site records only when it is *about to take a
48//! fallback*, i.e. only when it is already paying orders of magnitude
49//! more than the bookkeeping costs.
50//!
51//! And that bookkeeping never takes an exclusive lock in steady state: a
52//! repeat lookup is a shared `RwLock` read plus one relaxed
53//! `fetch_add`. The write lock is taken once per distinct call site, to
54//! create the row and decide whether to warn. A dispatch path must not
55//! be able to serialize rayon workers behind a mutex.
56//!
57//! `FERROX_KERNEL_REGISTRY=0` reduces every entry point to one relaxed
58//! atomic load and a return.
59//!
60//! ## What it must not do
61//!
62//! Observe only. Nothing in this module may change a dispatch decision;
63//! the predicates it calls are the same ones the dispatch takes, read a
64//! second time, and their results are recorded rather than acted on.
65
66use crate::weight_matrix::QuantKind;
67use std::collections::{HashMap, HashSet};
68use std::panic::Location;
69use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
70use std::sync::{OnceLock, RwLock};
71
72/// Which execution backend a lookup was resolved against.
73#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
74pub enum Backend {
75    Cpu,
76    Metal,
77    Cuda,
78}
79
80impl Backend {
81    pub fn as_str(self) -> &'static str {
82        match self {
83            Backend::Cpu => "cpu",
84            Backend::Metal => "metal",
85            Backend::Cuda => "cuda",
86        }
87    }
88
89    /// True for the accelerator backends. A miss here means work the
90    /// user asked to run on a GPU is running somewhere else.
91    pub fn is_accelerator(self) -> bool {
92        !matches!(self, Backend::Cpu)
93    }
94}
95
96impl std::fmt::Display for Backend {
97    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
98        f.write_str(self.as_str())
99    }
100}
101
102/// Canonical op names. Free-form `&'static str` is accepted everywhere,
103/// but sticking to these keeps the report groupable.
104pub mod op {
105    /// One activation against a whole weight matrix (decode).
106    pub const MATVEC: &str = "matvec";
107    /// Several independent matvecs sharing one activation (fused q/k/v).
108    pub const MATVEC_MULTI: &str = "matvec_multi";
109    /// A real batched GEMM over `batch` activations (prefill).
110    pub const GEMM_PREFILL: &str = "gemm_prefill";
111    /// Whole gate/up/down SwiGLU fused into one dispatch.
112    pub const FFN_SWIGLU: &str = "ffn_swiglu";
113    /// An engine-level capability rather than a per-tensor kernel: does
114    /// this engine have a batched prefill path at all?
115    pub const ENGINE_PREFILL_BATCH: &str = "engine.prefill_batch";
116}
117
118/// The identity of one lookup. Deduplicated on this; a repeated lookup
119/// bumps [`Entry::count`] instead of adding a row.
120#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
121pub struct Key {
122    pub backend: Backend,
123    /// See [`op`].
124    pub op: &'static str,
125    /// Tensor role (`"attn_q"`, `"ffn_down"`, `"moe_router"`, ...) or
126    /// `"(dispatch)"` when recorded from the hot path, where the role is
127    /// not known.
128    pub role: &'static str,
129    /// `None` for a non-quantized (F32 / MXFP4-pair) matrix.
130    pub kind: Option<QuantKind>,
131    pub file: &'static str,
132    pub line: u32,
133}
134
135impl Key {
136    /// The part of the identity that decides whether a kernel exists.
137    /// Call site and tensor role are diagnostics, not part of the
138    /// question being asked.
139    fn shape(&self) -> (Backend, &'static str, Option<QuantKind>) {
140        (self.backend, self.op, self.kind)
141    }
142
143    fn kind_name(&self) -> &'static str {
144        self.kind.map_or("f32", QuantKind::name)
145    }
146}
147
148impl std::fmt::Display for Key {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        write!(
151            f,
152            "{} {} {} ({}) at {}:{}",
153            self.backend,
154            self.op,
155            self.kind_name(),
156            self.role,
157            self.file,
158            self.line
159        )
160    }
161}
162
163/// How bad a miss is. Recorded at the call site, which is the only
164/// place that knows — inferring it at report time from the kind or the
165/// backend is how a signal turns into noise nobody reads.
166#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
167pub enum Severity {
168    /// The fallback *is* the intended path. An MoE router is a lone
169    /// small F32 matvec that costs more to ship to a GPU than to compute
170    /// on the host; that is a decision, not an omission.
171    ByDesign,
172    /// No kernel exists, so the work runs somewhere slower than the
173    /// backend the user selected, with correct output and no symptom.
174    /// This is the class the registry exists to surface.
175    SlowPath,
176}
177
178/// What the lookup resolved to.
179#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
180pub enum Outcome {
181    /// A real kernel exists and will be used.
182    Hit,
183    /// No kernel; the caller takes `fallback` instead. The string is the
184    /// fallback's name, so the report reads as a sentence.
185    Miss {
186        fallback: &'static str,
187        severity: Severity,
188    },
189}
190
191impl Outcome {
192    /// A miss that is a silent slow path.
193    pub fn slow_path(fallback: &'static str) -> Self {
194        Outcome::Miss {
195            fallback,
196            severity: Severity::SlowPath,
197        }
198    }
199
200    /// A miss whose fallback is the deliberate, documented choice.
201    pub fn by_design(fallback: &'static str) -> Self {
202        Outcome::Miss {
203            fallback,
204            severity: Severity::ByDesign,
205        }
206    }
207
208    pub fn is_miss(self) -> bool {
209        matches!(self, Outcome::Miss { .. })
210    }
211
212    pub fn is_slow_path(self) -> bool {
213        matches!(
214            self,
215            Outcome::Miss {
216                severity: Severity::SlowPath,
217                ..
218            }
219        )
220    }
221
222    pub fn fallback(self) -> Option<&'static str> {
223        match self {
224            Outcome::Miss { fallback, .. } => Some(fallback),
225            Outcome::Hit => None,
226        }
227    }
228}
229
230/// Whether the lookup happened while the model was being built or after
231/// [`seal`].
232#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
233pub enum Phase {
234    Build,
235    Run,
236}
237
238/// A lookup being reported, minus the call site (which `#[track_caller]`
239/// supplies).
240#[derive(Clone, Copy, Debug)]
241pub struct Lookup {
242    pub backend: Backend,
243    pub op: &'static str,
244    pub role: &'static str,
245    pub kind: Option<QuantKind>,
246}
247
248impl Lookup {
249    pub fn new(backend: Backend, op: &'static str, kind: Option<QuantKind>) -> Self {
250        Lookup {
251            backend,
252            op,
253            role: "(dispatch)",
254            kind,
255        }
256    }
257
258    pub fn with_role(mut self, role: &'static str) -> Self {
259        self.role = role;
260        self
261    }
262}
263
264/// One deduplicated row of the registry.
265#[derive(Clone, Copy, Debug)]
266pub struct Entry {
267    pub key: Key,
268    pub outcome: Outcome,
269    pub phase: Phase,
270    /// How many lookups collapsed into this row.
271    pub count: u64,
272}
273
274impl std::fmt::Display for Entry {
275    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
276        match self.outcome {
277            Outcome::Hit => write!(f, "hit   {}  x{}", self.key, self.count),
278            Outcome::Miss {
279                fallback,
280                severity: Severity::ByDesign,
281            } => write!(f, "host  {} -> {}  x{}", self.key, fallback, self.count),
282            Outcome::Miss {
283                fallback,
284                severity: Severity::SlowPath,
285            } => write!(f, "MISS  {} -> {}  x{}", self.key, fallback, self.count),
286        }
287    }
288}
289
290/// The picture at [`seal`] time.
291#[derive(Clone, Debug, Default)]
292pub struct SealReport {
293    /// Every build-phase row, sorted for stable printing.
294    pub entries: Vec<Entry>,
295    /// Build-phase rows whose outcome was a miss, of either severity.
296    pub misses: Vec<Entry>,
297    /// The subset of `misses` that is a real silent slow path
298    /// ([`Severity::SlowPath`]) on an *accelerator* backend the process
299    /// actually selected — work the user asked to run on a GPU that will
300    /// not. A CPU-backend miss is informational: there is nothing to
301    /// fall off.
302    pub violations: Vec<Entry>,
303}
304
305impl SealReport {
306    /// One line per row, for `FERROX_KERNEL_REGISTRY=1`.
307    pub fn render(&self) -> String {
308        let mut s = String::new();
309        for e in &self.entries {
310            s.push_str("ferrox kernels: ");
311            s.push_str(&e.to_string());
312            s.push('\n');
313        }
314        s
315    }
316
317    /// The loud one-paragraph summary printed whenever a violation
318    /// exists, registry verbose or not.
319    pub fn render_violations(&self) -> String {
320        let mut s = String::new();
321        for e in &self.violations {
322            // Engine-level capabilities are counted once, not per
323            // weight, so do not label their count "weights".
324            let unit = if e.key.op.starts_with("engine.") {
325                String::new()
326            } else {
327                format!(", {} weights", e.count)
328            };
329            let kind = match e.key.kind {
330                Some(k) => format!(" {}", k.name()),
331                None => String::new(),
332            };
333            s.push_str(&format!(
334                "ferrox: NO KERNEL for {} {}{} ({}) -> falls back to {} [{}:{}{}]\n",
335                e.key.backend,
336                e.key.op,
337                kind,
338                e.key.role,
339                e.outcome.fallback().unwrap_or("(hit)"),
340                e.key.file,
341                e.key.line,
342                unit,
343            ));
344        }
345        s
346    }
347}
348
349/// Raised by [`seal_or_error`] under `FERROX_STRICT_KERNELS=1`.
350#[derive(Clone, Debug)]
351pub struct StrictKernelError {
352    pub report: SealReport,
353}
354
355impl std::fmt::Display for StrictKernelError {
356    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
357        write!(
358            f,
359            "FERROX_STRICT_KERNELS=1 and {} kernel lookup(s) missed:\n{}",
360            self.report.violations.len(),
361            self.report.render_violations()
362        )
363    }
364}
365
366impl std::error::Error for StrictKernelError {}
367
368/// One stored row. `count` is atomic so a repeat lookup -- the only
369/// thing a dispatch site ever does after the first call -- needs a
370/// *shared* read lock and a relaxed increment, never exclusive access.
371/// A dispatch path must not be able to serialize rayon workers on a
372/// mutex, whatever else it does.
373struct Row {
374    outcome: Outcome,
375    phase: Phase,
376    count: AtomicU64,
377}
378
379#[derive(Default)]
380struct State {
381    rows: HashMap<Key, Row>,
382    /// Lookup *shapes* seen during the build phase. A post-seal miss on
383    /// a shape in here was predicted at load time and already reported;
384    /// one that is not is the thing this registry exists to find.
385    known: HashSet<(Backend, &'static str, Option<QuantKind>)>,
386    /// Post-seal unpredicted misses, in discovery order.
387    surprises: Vec<Entry>,
388}
389
390impl State {
391    fn snapshot(&self, build_only: bool) -> Vec<Entry> {
392        let mut entries: Vec<Entry> = self
393            .rows
394            .iter()
395            .filter(|(_, row)| !build_only || row.phase == Phase::Build)
396            .map(|(key, row)| Entry {
397                key: *key,
398                outcome: row.outcome,
399                phase: row.phase,
400                count: row.count.load(Ordering::Relaxed),
401            })
402            .collect();
403        entries.sort_by_key(|e| {
404            (
405                e.key.backend,
406                e.key.op,
407                e.key.kind.map(|k| k.name()).unwrap_or("f32"),
408                e.key.role,
409                e.key.line,
410            )
411        });
412        entries
413    }
414}
415
416/// A registry instance. There is one [`global`] instance; tests build
417/// their own so they never race the process-wide one.
418pub struct Registry {
419    inner: RwLock<State>,
420    sealed: AtomicBool,
421}
422
423impl Default for Registry {
424    fn default() -> Self {
425        Self::new()
426    }
427}
428
429impl Registry {
430    pub fn new() -> Self {
431        Registry {
432            inner: RwLock::new(State::default()),
433            sealed: AtomicBool::new(false),
434        }
435    }
436
437    fn read(&self) -> std::sync::RwLockReadGuard<'_, State> {
438        self.inner.read().unwrap_or_else(|e| e.into_inner())
439    }
440
441    fn write(&self) -> std::sync::RwLockWriteGuard<'_, State> {
442        self.inner.write().unwrap_or_else(|e| e.into_inner())
443    }
444
445    /// Shared-lock increment for a row that already exists. Returns
446    /// false when the key is new and the caller must take the write
447    /// lock. This is the whole steady-state cost of the registry on a
448    /// dispatch path.
449    fn bump_existing(&self, key: &Key) -> bool {
450        match self.read().rows.get(key) {
451            Some(row) => {
452                row.count.fetch_add(1, Ordering::Relaxed);
453                true
454            }
455            None => false,
456        }
457    }
458
459    pub fn is_sealed(&self) -> bool {
460        self.sealed.load(Ordering::Relaxed)
461    }
462
463    /// Records a build-phase lookup. Always accepted, even after
464    /// [`Self::seal`] — a process that loads a second model (draft +
465    /// target for speculative decoding) probes it too, and those
466    /// lookups are predictions, not surprises.
467    pub fn record_build_at(&self, loc: &'static Location<'static>, l: Lookup, outcome: Outcome) {
468        let key = Key {
469            backend: l.backend,
470            op: l.op,
471            role: l.role,
472            kind: l.kind,
473            file: loc.file(),
474            line: loc.line(),
475        };
476        if self.bump_existing(&key) {
477            return;
478        }
479        let mut st = self.write();
480        st.known.insert(key.shape());
481        st.rows
482            .entry(key)
483            .or_insert_with(|| Row {
484                outcome,
485                phase: Phase::Build,
486                count: AtomicU64::new(0),
487            })
488            .count
489            .fetch_add(1, Ordering::Relaxed);
490    }
491
492    /// Records a dispatch-site lookup. Before sealing this is just
493    /// bookkeeping; after sealing, a miss on a shape the build probe
494    /// never predicted warns once and is collected into
495    /// [`Self::surprises`].
496    pub fn record_at(&self, loc: &'static Location<'static>, l: Lookup, outcome: Outcome) {
497        let key = Key {
498            backend: l.backend,
499            op: l.op,
500            role: l.role,
501            kind: l.kind,
502            file: loc.file(),
503            line: loc.line(),
504        };
505        // Steady state: the key already exists, so the warn-or-not
506        // decision was settled when it was created. A shared read lock
507        // and one relaxed add, and nothing here blocks anything.
508        if self.bump_existing(&key) {
509            return;
510        }
511        let sealed = self.is_sealed();
512        let mut st = self.write();
513        // Re-check under the write lock: another thread may have raced
514        // us to this key, in which case it already made the decision.
515        if let Some(row) = st.rows.get(&key) {
516            row.count.fetch_add(1, Ordering::Relaxed);
517            return;
518        }
519        let phase = if sealed { Phase::Run } else { Phase::Build };
520        st.rows.insert(
521            key,
522            Row {
523                outcome,
524                phase,
525                count: AtomicU64::new(1),
526            },
527        );
528        if !sealed {
529            st.known.insert(key.shape());
530            return;
531        }
532        if !outcome.is_slow_path() || st.known.contains(&key.shape()) {
533            return;
534        }
535        st.surprises.push(Entry {
536            key,
537            outcome,
538            phase: Phase::Run,
539            count: 1,
540        });
541        drop(st);
542        let fallback = outcome
543            .fallback()
544            .unwrap_or("(unknown)" /* unreachable: guarded by is_slow_path */);
545        eprintln!(
546            "ferrox: SILENT SLOW PATH — kernel lookup missed after the model was sealed.\n\
547             ferrox:   {} {} for {} has no kernel; falling back to {}.\n\
548             ferrox:   call site {}:{} (role {}).\n\
549             ferrox:   this was not predicted at load time, so no startup diagnostic covered it.\n\
550             ferrox:   set FERROX_STRICT_KERNELS=1 to make this a hard error.",
551            key.backend,
552            key.op,
553            key.kind_name(),
554            fallback,
555            key.file,
556            key.line,
557            key.role,
558        );
559    }
560
561    /// Freezes the build picture and switches on post-seal checking.
562    /// Idempotent: re-sealing recomputes the report over everything
563    /// recorded so far.
564    pub fn seal(&self) -> SealReport {
565        self.sealed.store(true, Ordering::Relaxed);
566        let entries = self.read().snapshot(true);
567        let misses: Vec<Entry> = entries
568            .iter()
569            .copied()
570            .filter(|e| e.outcome.is_miss())
571            .collect();
572        let violations: Vec<Entry> = misses
573            .iter()
574            .copied()
575            .filter(|e| e.key.backend.is_accelerator() && e.outcome.is_slow_path())
576            .collect();
577        SealReport {
578            entries,
579            misses,
580            violations,
581        }
582    }
583
584    /// Post-seal misses that the build probe did not predict.
585    pub fn surprises(&self) -> Vec<Entry> {
586        self.read().surprises.clone()
587    }
588
589    /// Every row, build and run, sorted like [`SealReport::entries`].
590    pub fn entries(&self) -> Vec<Entry> {
591        self.read().snapshot(false)
592    }
593}
594
595static GLOBAL: OnceLock<Registry> = OnceLock::new();
596
597/// The process-wide registry.
598pub fn global() -> &'static Registry {
599    GLOBAL.get_or_init(Registry::new)
600}
601
602/// Whether the registry records at all.
603///
604/// Default **on**: a warning nobody enabled is the entire point. Read
605/// once per process (`FERROX_KERNEL_REGISTRY=0|false|off` disables), so
606/// no dispatch ever touches the environment.
607pub fn enabled() -> bool {
608    static V: OnceLock<bool> = OnceLock::new();
609    *V.get_or_init(|| {
610        !matches!(
611            std::env::var("FERROX_KERNEL_REGISTRY").ok().as_deref(),
612            Some("0") | Some("false") | Some("off")
613        )
614    })
615}
616
617/// Whether the full build-phase table is printed at [`seal`].
618pub fn verbose() -> bool {
619    static V: OnceLock<bool> = OnceLock::new();
620    *V.get_or_init(|| {
621        matches!(
622            std::env::var("FERROX_KERNEL_REGISTRY").ok().as_deref(),
623            Some("1") | Some("true") | Some("on") | Some("verbose")
624        )
625    })
626}
627
628/// Whether a missing kernel is a hard error. Set this in CI and in
629/// benchmark harnesses so a slow path cannot be published as a number.
630pub fn strict() -> bool {
631    static V: OnceLock<bool> = OnceLock::new();
632    *V.get_or_init(|| {
633        matches!(
634            std::env::var("FERROX_STRICT_KERNELS").ok().as_deref(),
635            Some("1") | Some("true") | Some("on")
636        )
637    })
638}
639
640/// Record an eager, load-time lookup against the global registry.
641#[track_caller]
642pub fn record_build(l: Lookup, outcome: Outcome) {
643    if !enabled() {
644        return;
645    }
646    global().record_build_at(Location::caller(), l, outcome);
647}
648
649/// Record a dispatch-site lookup that found a kernel. Dispatch sites do
650/// not normally call this — hits cost nothing precisely because they are
651/// not recorded on the hot path — but it exists for completeness.
652#[track_caller]
653pub fn hit(l: Lookup) {
654    if !enabled() {
655        return;
656    }
657    global().record_at(Location::caller(), l, Outcome::Hit);
658}
659
660/// Record a dispatch-site lookup that missed and is taking a slower
661/// `fallback` than the selected backend.
662///
663/// Call this on the fallback branch only. By construction the branch is
664/// already about to do far more work than a hash and a lock.
665#[track_caller]
666pub fn miss(l: Lookup, fallback: &'static str) {
667    if !enabled() {
668        return;
669    }
670    global().record_at(Location::caller(), l, Outcome::slow_path(fallback));
671}
672
673/// Record a dispatch-site lookup that missed, where the fallback is the
674/// deliberate choice rather than a gap. Never warns; recorded so the
675/// report is a complete picture instead of a filtered one.
676#[track_caller]
677pub fn miss_by_design(l: Lookup, fallback: &'static str) {
678    if !enabled() {
679        return;
680    }
681    global().record_at(Location::caller(), l, Outcome::by_design(fallback));
682}
683
684/// Seal the global registry: print what the build probe found, warn
685/// loudly about quantized weights that will run off the selected
686/// accelerator, and switch on post-seal checking.
687pub fn seal() -> SealReport {
688    let report = global().seal();
689    if !enabled() {
690        return report;
691    }
692    if verbose() {
693        eprint!("{}", report.render());
694    }
695    if !report.violations.is_empty() && !strict() {
696        eprint!("{}", report.render_violations());
697        eprintln!(
698            "ferrox: {} kernel lookup(s) above will run on a slower path than the \
699             selected backend. Set FERROX_STRICT_KERNELS=1 to refuse to run instead.",
700            report.violations.len()
701        );
702    }
703    report
704}
705
706/// [`seal`], but returns `Err` under `FERROX_STRICT_KERNELS=1` when a
707/// quantized weight has no kernel on the selected accelerator.
708pub fn seal_or_error() -> Result<SealReport, StrictKernelError> {
709    let report = seal();
710    if strict() && !report.violations.is_empty() {
711        return Err(StrictKernelError { report });
712    }
713    Ok(report)
714}
715
716#[cfg(test)]
717mod tests {
718    use super::*;
719
720    fn lookup(backend: Backend, kind: Option<QuantKind>) -> Lookup {
721        Lookup::new(backend, op::GEMM_PREFILL, kind).with_role("ffn_down")
722    }
723
724    #[test]
725    fn a_build_hit_is_recorded_once_per_shape_with_a_count() {
726        let r = Registry::new();
727        let loc = Location::caller();
728        for _ in 0..5 {
729            r.record_build_at(
730                loc,
731                lookup(Backend::Metal, Some(QuantKind::Q4K)),
732                Outcome::Hit,
733            );
734        }
735        let report = r.seal();
736        assert_eq!(report.entries.len(), 1);
737        assert_eq!(report.entries[0].count, 5);
738        assert!(report.misses.is_empty());
739        assert!(report.violations.is_empty());
740    }
741
742    /// A quantized weight with no accelerator kernel is exactly the
743    /// IQ4_XS bug: correct output, silent CPU fallback, no diagnostic.
744    /// Seal must call it a violation.
745    #[test]
746    fn a_quantized_weight_with_no_accelerator_kernel_is_a_violation() {
747        let r = Registry::new();
748        r.record_build_at(
749            Location::caller(),
750            lookup(Backend::Metal, Some(QuantKind::IQ4XS)),
751            Outcome::slow_path("CPU apply_batch"),
752        );
753        let report = r.seal();
754        assert_eq!(report.violations.len(), 1);
755        assert_eq!(report.violations[0].key.kind, Some(QuantKind::IQ4XS));
756        assert!(report.render_violations().contains("IQ4_XS"));
757    }
758
759    /// An F32 matrix has no quantized kernel by construction and its
760    /// host GEMV is a deliberate decision. It is a miss, but not a
761    /// violation — otherwise every MoE router would fail the gate and
762    /// the signal would be worthless.
763    #[test]
764    fn an_f32_miss_is_reported_but_is_not_a_violation() {
765        let r = Registry::new();
766        r.record_build_at(
767            Location::caller(),
768            lookup(Backend::Metal, None),
769            Outcome::by_design("host GEMV"),
770        );
771        let report = r.seal();
772        assert_eq!(report.misses.len(), 1);
773        assert!(report.violations.is_empty());
774    }
775
776    /// A CPU-only process has no accelerator to fall off, so its misses
777    /// are informational.
778    #[test]
779    fn a_cpu_backend_miss_is_not_a_violation() {
780        let r = Registry::new();
781        r.record_build_at(
782            Location::caller(),
783            lookup(Backend::Cpu, Some(QuantKind::IQ2XXS)),
784            Outcome::slow_path("f32 dequant-dot"),
785        );
786        assert!(r.seal().violations.is_empty());
787    }
788
789    #[test]
790    fn a_post_seal_miss_on_a_predicted_shape_is_not_a_surprise() {
791        let r = Registry::new();
792        let loc = Location::caller();
793        r.record_build_at(
794            loc,
795            lookup(Backend::Metal, Some(QuantKind::IQ4XS)),
796            Outcome::slow_path("CPU apply_batch"),
797        );
798        r.seal();
799        r.record_at(
800            loc,
801            lookup(Backend::Metal, Some(QuantKind::IQ4XS)),
802            Outcome::slow_path("CPU apply_batch"),
803        );
804        assert!(r.surprises().is_empty(), "seal already reported this shape");
805    }
806
807    /// The registry's whole purpose: a lookup that misses on a shape the
808    /// build probe never saw is an unpredicted slow path.
809    #[test]
810    fn a_post_seal_miss_on_an_unpredicted_shape_is_a_surprise_reported_once() {
811        let r = Registry::new();
812        let loc = Location::caller();
813        r.record_build_at(
814            loc,
815            lookup(Backend::Metal, Some(QuantKind::Q4K)),
816            Outcome::Hit,
817        );
818        r.seal();
819        for _ in 0..3 {
820            r.record_at(
821                loc,
822                lookup(Backend::Metal, Some(QuantKind::Q2K)),
823                Outcome::slow_path("CPU apply_batch"),
824            );
825        }
826        let surprises = r.surprises();
827        assert_eq!(surprises.len(), 1, "warned once, not once per dispatch");
828        assert_eq!(surprises[0].key.kind, Some(QuantKind::Q2K));
829        assert_eq!(surprises[0].phase, Phase::Run);
830    }
831
832    #[test]
833    fn a_post_seal_hit_is_never_a_surprise() {
834        let r = Registry::new();
835        r.seal();
836        r.record_at(
837            Location::caller(),
838            lookup(Backend::Metal, Some(QuantKind::Q4K)),
839            Outcome::Hit,
840        );
841        assert!(r.surprises().is_empty());
842    }
843
844    /// Probing a second model after the first was sealed must not
845    /// manufacture surprises (speculative decoding loads two).
846    #[test]
847    fn build_records_after_seal_extend_the_predicted_set() {
848        let r = Registry::new();
849        let loc = Location::caller();
850        r.seal();
851        r.record_build_at(
852            loc,
853            lookup(Backend::Metal, Some(QuantKind::Q6K)),
854            Outcome::slow_path("CPU apply_batch"),
855        );
856        r.record_at(
857            loc,
858            lookup(Backend::Metal, Some(QuantKind::Q6K)),
859            Outcome::slow_path("CPU apply_batch"),
860        );
861        assert!(r.surprises().is_empty());
862    }
863
864    /// The dispatch path runs on rayon workers, so the first-sighting
865    /// write must be raced-into by many threads and still warn exactly
866    /// once, with every lookup counted.
867    #[test]
868    fn concurrent_dispatch_misses_warn_once_and_count_all() {
869        let r = std::sync::Arc::new(Registry::new());
870        let loc = Location::caller();
871        r.seal();
872        std::thread::scope(|s| {
873            for _ in 0..8 {
874                let r = std::sync::Arc::clone(&r);
875                s.spawn(move || {
876                    for _ in 0..250 {
877                        r.record_at(
878                            loc,
879                            lookup(Backend::Metal, Some(QuantKind::IQ1S)),
880                            Outcome::slow_path("CPU apply_batch"),
881                        );
882                    }
883                });
884            }
885        });
886        assert_eq!(r.surprises().len(), 1, "warned once across 8 threads");
887        let counted: u64 = r
888            .entries()
889            .iter()
890            .filter(|e| e.key.kind == Some(QuantKind::IQ1S))
891            .map(|e| e.count)
892            .sum();
893        assert_eq!(counted, 2000, "every lookup counted exactly once");
894    }
895
896    #[test]
897    fn the_report_names_the_call_site_and_the_quant_kind() {
898        let r = Registry::new();
899        r.record_build_at(
900            Location::caller(),
901            lookup(Backend::Metal, Some(QuantKind::Q5K)),
902            Outcome::slow_path("CPU apply_batch"),
903        );
904        let rendered = r.seal().render();
905        assert!(rendered.contains("Q5_K"), "{rendered}");
906        assert!(rendered.contains("kernel_registry.rs"), "{rendered}");
907        assert!(rendered.contains("ffn_down"), "{rendered}");
908        assert!(rendered.contains("CPU apply_batch"), "{rendered}");
909    }
910}