1use 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#[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 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
102pub mod op {
105 pub const MATVEC: &str = "matvec";
107 pub const MATVEC_MULTI: &str = "matvec_multi";
109 pub const GEMM_PREFILL: &str = "gemm_prefill";
111 pub const FFN_SWIGLU: &str = "ffn_swiglu";
113 pub const ENGINE_PREFILL_BATCH: &str = "engine.prefill_batch";
116}
117
118#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
121pub struct Key {
122 pub backend: Backend,
123 pub op: &'static str,
125 pub role: &'static str,
129 pub kind: Option<QuantKind>,
131 pub file: &'static str,
132 pub line: u32,
133}
134
135impl Key {
136 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#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
167pub enum Severity {
168 ByDesign,
172 SlowPath,
176}
177
178#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
180pub enum Outcome {
181 Hit,
183 Miss {
186 fallback: &'static str,
187 severity: Severity,
188 },
189}
190
191impl Outcome {
192 pub fn slow_path(fallback: &'static str) -> Self {
194 Outcome::Miss {
195 fallback,
196 severity: Severity::SlowPath,
197 }
198 }
199
200 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#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
233pub enum Phase {
234 Build,
235 Run,
236}
237
238#[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#[derive(Clone, Copy, Debug)]
266pub struct Entry {
267 pub key: Key,
268 pub outcome: Outcome,
269 pub phase: Phase,
270 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#[derive(Clone, Debug, Default)]
292pub struct SealReport {
293 pub entries: Vec<Entry>,
295 pub misses: Vec<Entry>,
297 pub violations: Vec<Entry>,
303}
304
305impl SealReport {
306 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 pub fn render_violations(&self) -> String {
320 let mut s = String::new();
321 for e in &self.violations {
322 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#[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
368struct Row {
374 outcome: Outcome,
375 phase: Phase,
376 count: AtomicU64,
377}
378
379#[derive(Default)]
380struct State {
381 rows: HashMap<Key, Row>,
382 known: HashSet<(Backend, &'static str, Option<QuantKind>)>,
386 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
416pub 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 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 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 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 if self.bump_existing(&key) {
509 return;
510 }
511 let sealed = self.is_sealed();
512 let mut st = self.write();
513 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)" );
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 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 pub fn surprises(&self) -> Vec<Entry> {
586 self.read().surprises.clone()
587 }
588
589 pub fn entries(&self) -> Vec<Entry> {
591 self.read().snapshot(false)
592 }
593}
594
595static GLOBAL: OnceLock<Registry> = OnceLock::new();
596
597pub fn global() -> &'static Registry {
599 GLOBAL.get_or_init(Registry::new)
600}
601
602pub 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
617pub 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
628pub 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#[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#[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#[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#[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
684pub 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
706pub 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 #[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 #[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 #[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 #[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 #[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 #[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}