1use cortiq_core::CmfModel;
14use std::cell::Cell;
15use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
16use std::sync::{Arc, OnceLock};
17
18thread_local! {
19 static CUR_LAYER: Cell<i64> = const { Cell::new(-1) };
23 static CPU_ONLY: Cell<bool> = const { Cell::new(false) };
28 static PROBE_COLD: Cell<bool> = const { Cell::new(false) };
32}
33
34pub struct CpuScopeGuard(bool);
40
41impl Drop for CpuScopeGuard {
42 fn drop(&mut self) {
43 CPU_ONLY.with(|c| c.set(self.0));
44 }
45}
46
47pub fn enter_cpu_scope() -> CpuScopeGuard {
48 let previous = CPU_ONLY.with(|c| c.replace(true));
49 CpuScopeGuard(previous)
50}
51
52pub fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
54 let _restore = enter_cpu_scope();
55 f()
56}
57
58pub fn probe_set_device(label: &str) {
63 let _ = DEVICE_LABEL.set(label.to_string());
64}
65
66fn device_label() -> &'static str {
67 DEVICE_LABEL.get().map(String::as_str).unwrap_or("unknown")
68}
69
70static DEVICE_LABEL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
71
72static CACHE_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
81
82pub fn set_cache_dir(dir: std::path::PathBuf) {
84 let _ = CACHE_DIR.set(dir);
85}
86
87pub fn cache_dir_pub() -> std::path::PathBuf {
89 cache_dir()
90}
91
92fn cache_dir() -> std::path::PathBuf {
93 if let Some(d) = CACHE_DIR.get() {
94 return d.clone();
95 }
96 match std::env::var_os("TMPDIR") {
97 Some(t) => std::path::PathBuf::from(t),
98 None => std::env::temp_dir(),
99 }
100}
101
102fn probe_cache_path() -> Option<std::path::PathBuf> {
105 match std::env::var("CMF_PROBE_CACHE") {
106 Ok(v) if v == "0" => None,
107 Ok(v) => Some(std::path::PathBuf::from(v)),
108 Err(_) => Some(cache_dir().join("cortiq-gpu-probe.tsv")),
109 }
110}
111
112fn probe_cache_key_named(class: &str) -> String {
116 format!(
117 "{}\t{}\t{}",
118 env!("CARGO_PKG_VERSION"),
119 device_label(),
120 class
121 )
122}
123
124const CLASS_NAMES: [&str; 7] = [
125 "ffn",
126 "matvec",
127 "matmat",
128 "qkv-batch",
129 "matmat-wide",
130 "lm-head",
131 "gemm-nt",
132];
133
134fn probe_cache_load() {
143 static ONCE: std::sync::Once = std::sync::Once::new();
144 ONCE.call_once(|| {
145 let Some(path) = probe_cache_path() else {
146 return;
147 };
148 if cfg!(test) && std::env::var("CMF_PROBE_CACHE").is_err() {
153 return;
154 }
155 let Ok(text) = std::fs::read_to_string(&path) else {
156 return;
157 };
158 probe_cache_adopt(&text);
159 });
160}
161
162fn probe_cache_adopt(text: &str) {
166 for line in text.lines() {
167 let Some((key, verdict)) = line.rsplit_once('\t') else {
168 continue;
169 };
170 let winner = match verdict.trim() {
171 "gpu" => 1u8,
172 "cpu" => 2u8,
173 _ => continue,
174 };
175 for (i, name) in CLASS_NAMES.iter().enumerate() {
176 if probe_cache_key_named(name) == key {
177 let _ = PROBES[i].state.compare_exchange(
178 0,
179 winner,
180 Ordering::Relaxed,
181 Ordering::Relaxed,
182 );
183 tracing::debug!("gpu probe [{name}]: remembered → {verdict}");
184 }
185 }
186 }
187}
188
189fn probe_cache_store(c: OpClass, winner: u8) {
192 let Some(path) = probe_cache_path() else {
193 return;
194 };
195 let line = format!(
196 "{}\t{}\n",
197 probe_cache_key_named(CLASS_NAMES[c as usize]),
198 if winner == 1 { "gpu" } else { "cpu" }
199 );
200 use std::io::Write;
201 if let Ok(mut f) = std::fs::OpenOptions::new()
202 .create(true)
203 .append(true)
204 .open(&path)
205 {
206 let _ = f.write_all(line.as_bytes());
207 }
208}
209
210pub fn cold_epoch() -> u64 {
216 COLD_EPOCH.load(std::sync::atomic::Ordering::Relaxed)
217}
218static COLD_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
219
220pub(crate) fn probe_note_cold() {
221 COLD_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
222 PROBE_COLD.with(|c| c.set(true));
223}
224
225pub(crate) fn probe_was_cold() -> bool {
229 PROBE_COLD.with(|c| c.get())
230}
231
232pub fn set_layer(l: i64) {
234 CUR_LAYER.with(|c| c.set(l));
235}
236
237pub fn cur_layer() -> i64 {
239 CUR_LAYER.with(|c| c.get())
240}
241
242pub fn automatic_layer_prefix(
245 model: &Arc<CmfModel>,
246 num_layers: usize,
247 physical_layers: usize,
248) -> Option<usize> {
249 match backend() {
250 #[cfg(feature = "gpu")]
251 Backend::Wgpu => {
252 crate::gpu_wgpu::automatic_layer_prefix(model, num_layers, physical_layers)
253 }
254 _ => None,
255 }
256}
257
258fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
261 static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
262 R.get_or_init(|| {
263 let s = std::env::var("CMF_GPU_LAYERS").ok()?;
264 let mut v = Vec::new();
265 for part in s.split(',') {
266 let part = part.trim();
267 match part.split_once('-') {
268 Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
269 None => {
270 let x: i64 = part.parse().ok()?;
271 v.push((x, x));
272 }
273 }
274 }
275 Some(v)
276 })
277}
278
279fn layer_allowed() -> bool {
280 match layer_ranges() {
281 None => true,
282 Some(ranges) => {
283 let cur = CUR_LAYER.with(|c| c.get());
284 cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
285 }
286 }
287}
288
289pub fn enabled_here() -> bool {
293 !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
294}
295
296#[derive(Clone, Copy)]
308pub enum OpClass {
309 Ffn = 0,
311 Matvec = 1,
313 Matmat = 2,
315 Batch = 3,
317 MatmatWide = 4,
323 MatvecHead = 5,
330 GemmNt = 6,
337}
338
339pub fn matvec_class(rows: usize, cols: usize) -> OpClass {
343 if rows * cols >= 67_108_864 {
344 OpClass::MatvecHead
345 } else {
346 OpClass::Matvec
347 }
348}
349
350pub enum ProbeArm {
352 Gpu,
354 CpuTimed,
356 Cpu,
358}
359
360const PROBE_SAMPLES: u32 = 6;
362
363const PROBE_DECLINE_LIMIT: u32 = 16;
367
368const PROBE_WARMUP: u32 = 1;
370
371struct Probe {
372 state: AtomicU8,
374 flip: AtomicU32,
375 gpu_ns: AtomicU64,
376 gpu_n: AtomicU32,
377 declines: AtomicU32,
387 gpu_burn: AtomicU32,
400 cpu_ns: AtomicU64,
401 cpu_n: AtomicU32,
402 gpu_min: AtomicU64,
409 cpu_min: AtomicU64,
410}
411
412impl Probe {
413 const fn new() -> Self {
414 Self {
415 state: AtomicU8::new(0),
416 flip: AtomicU32::new(0),
417 gpu_ns: AtomicU64::new(0),
418 gpu_n: AtomicU32::new(0),
419 declines: AtomicU32::new(0),
420 gpu_burn: AtomicU32::new(PROBE_WARMUP),
421 cpu_ns: AtomicU64::new(0),
422 cpu_n: AtomicU32::new(0),
423 gpu_min: AtomicU64::new(u64::MAX),
424 cpu_min: AtomicU64::new(u64::MAX),
425 }
426 }
427}
428
429static PROBES: [Probe; 7] = [
430 Probe::new(),
431 Probe::new(),
432 Probe::new(),
433 Probe::new(),
434 Probe::new(),
435 Probe::new(),
436 Probe::new(),
437];
438
439static TRUST_GPU: AtomicBool = AtomicBool::new(false);
446
447pub fn trust_gpu() -> GpuTrust {
449 let was = TRUST_GPU.swap(true, Ordering::Relaxed);
450 GpuTrust(was)
451}
452
453pub struct GpuTrust(bool);
454
455impl Drop for GpuTrust {
456 fn drop(&mut self) {
457 TRUST_GPU.store(self.0, Ordering::Relaxed);
458 }
459}
460
461fn probe_on_for(c: OpClass) -> bool {
462 if TRUST_GPU.load(Ordering::Relaxed) && matches!(c, OpClass::MatmatWide | OpClass::Ffn) {
468 return false;
469 }
470 probe_on()
471}
472
473fn probe_on() -> bool {
474 static ON: OnceLock<bool> = OnceLock::new();
475 *ON.get_or_init(|| {
476 std::env::var("CMF_GPU_PROBE")
477 .map(|v| v != "0" && v != "off")
478 .unwrap_or(true)
479 })
480}
481
482pub fn q1_force() -> bool {
487 #[cfg(target_os = "macos")]
488 {
489 backend() == Backend::Metal
490 }
491 #[cfg(not(target_os = "macos"))]
492 {
493 false
494 }
495}
496
497pub fn fused_block_trusted() -> bool {
516 #[cfg(target_os = "macos")]
517 if backend() == Backend::Metal {
518 return true;
519 }
520 wgpu_graph_default()
521}
522
523pub fn weight_is_resident(model: &Arc<CmfModel>, idx: usize) -> bool {
535 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
536 {
537 return crate::gpu_wgpu::weight_is_resident(model, idx);
538 }
539 #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
540 {
541 let _ = (model, idx);
542 true
543 }
544}
545
546pub fn probe_arm_cold_prefers_gpu(c: OpClass, weights_resident: bool) -> ProbeArm {
547 if !weights_resident && probe_deciding(c) {
548 return ProbeArm::Gpu;
549 }
550 probe_arm(c)
551}
552
553pub fn probe_arm(c: OpClass) -> ProbeArm {
554 PROBE_COLD.with(|f| f.set(false));
559 if !probe_on_for(c) {
560 return ProbeArm::Gpu;
561 }
562 probe_cache_load();
563 let p = &PROBES[c as usize];
564 match p.state.load(Ordering::Relaxed) {
565 1 => ProbeArm::Gpu,
566 2 => ProbeArm::Cpu,
567 _ => {
568 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
569 ProbeArm::Gpu
570 } else {
571 ProbeArm::CpuTimed
572 }
573 }
574 }
575}
576
577pub fn probe_note_decline(c: OpClass) {
581 let p = &PROBES[c as usize];
582 if p.state.load(Ordering::Relaxed) != 0 {
583 return;
584 }
585 let n = p.declines.fetch_add(1, Ordering::Relaxed) + 1;
586 if n >= PROBE_DECLINE_LIMIT
587 && p.state
588 .compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed)
589 .is_ok()
590 {
591 tracing::info!(
592 "gpu probe [{}]: device declined {n} times → cpu",
593 CLASS_NAMES[c as usize]
594 );
595 }
596}
597
598pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
601 probe_record_into(
602 &PROBES[c as usize],
603 CLASS_NAMES[c as usize],
604 Some(c),
605 gpu,
606 dur,
607 )
608}
609
610fn probe_record_into(
613 p: &Probe,
614 class_name: &str,
615 cache: Option<OpClass>,
616 gpu: bool,
617 dur: std::time::Duration,
618) {
619 if p.state.load(Ordering::Relaxed) != 0 {
620 return;
621 }
622 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
623 return; }
625 if gpu {
626 let left = p.gpu_burn.load(Ordering::Relaxed);
630 if left > 0 {
631 p.gpu_burn.store(left - 1, Ordering::Relaxed);
632 return; }
634 }
635 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
636 if gpu {
637 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
638 p.gpu_n.fetch_add(1, Ordering::Relaxed);
639 p.gpu_min.fetch_min(ns, Ordering::Relaxed);
640 } else {
641 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
642 p.cpu_n.fetch_add(1, Ordering::Relaxed);
643 p.cpu_min.fetch_min(ns, Ordering::Relaxed);
644 }
645 let (gn, cn) = (
646 p.gpu_n.load(Ordering::Relaxed),
647 p.cpu_n.load(Ordering::Relaxed),
648 );
649 if gn >= 2 && cn >= 2 {
650 let g = p.gpu_min.load(Ordering::Relaxed) as f64;
654 let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
655 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 2.0 && cp < g * 2.0 {
665 return;
666 }
667 let winner = if g <= cp { 1 } else { 2 };
668 if p.state
669 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
670 .is_ok()
671 {
672 tracing::info!(
673 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
674 class_name,
675 g / 1e6,
676 cp / 1e6,
677 if winner == 1 { "gpu" } else { "cpu" },
678 );
679 if let Some(c) = cache {
680 probe_cache_store(c, winner);
681 }
682 }
683 }
684}
685
686pub fn probe_deciding(c: OpClass) -> bool {
689 probe_on_for(c) && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
690}
691
692#[allow(unused_variables)]
702pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
703 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
704 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
705 let resident = match backend() {
706 #[cfg(target_os = "macos")]
707 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
708 #[cfg(feature = "gpu")]
709 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
710 Backend::None => false,
711 };
712 if !resident && may_upload {
713 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
714 }
715 resident
716}
717
718#[cfg(test)]
720pub(crate) fn probe_reset() {
721 for p in &PROBES {
722 p.state.store(0, Ordering::Relaxed);
723 p.flip.store(0, Ordering::Relaxed);
724 p.gpu_ns.store(0, Ordering::Relaxed);
725 p.gpu_n.store(0, Ordering::Relaxed);
726 p.cpu_ns.store(0, Ordering::Relaxed);
727 p.cpu_n.store(0, Ordering::Relaxed);
728 }
729}
730
731#[cfg(test)]
732mod probe_tests {
733 use super::*;
734 use std::time::Duration;
735
736 #[test]
739 fn probe_alternates_discards_cold_and_decides() {
740 probe_reset();
741 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
743 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
744
745 probe_note_cold();
749 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
750 for _ in 0..PROBE_SAMPLES {
751 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
752 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
753 }
754 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
755
756 for _ in 0..PROBE_SAMPLES {
758 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
759 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
760 }
761 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
762
763 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
765 CPU_ONLY.with(|c| assert!(!c.get()));
766 cpu_scope(|| {
767 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
768 CPU_ONLY.with(|c| assert!(c.get()));
769 });
770 let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
771 CPU_ONLY.with(|c| assert!(!c.get()));
772 probe_reset();
773 }
774
775 #[test]
776 fn a_remembered_verdict_is_adopted_and_a_stranger_is_not() {
777 let mine = probe_cache_key_named("gemm-nt");
789 let state = || {
790 PROBES[OpClass::GemmNt as usize]
791 .state
792 .load(Ordering::Relaxed)
793 };
794
795 probe_cache_adopt("SomeOtherGPU/Vulkan\tgemm-nt\tgpu\n");
797 assert_eq!(state(), 0);
798 let older = mine.replacen(env!("CARGO_PKG_VERSION"), "0.0.0-old", 1);
800 assert_ne!(older, mine);
801 probe_cache_adopt(&format!("{older}\tgpu\n"));
802 assert_eq!(state(), 0);
803 probe_cache_adopt(&format!("{mine}\tcpu\n"));
805 assert_eq!(state(), 2);
806
807 PROBES[OpClass::GemmNt as usize]
808 .state
809 .store(0, Ordering::Relaxed);
810 }
811}
812
813pub const GPU_MIN_ROWS: usize = 65_536;
816
817pub fn min_rows() -> usize {
824 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
825 .ok()
826 .and_then(|v| v.parse().ok())
827 {
828 return v;
829 }
830 if discrete() { 4096 } else { GPU_MIN_ROWS }
831}
832
833pub fn discrete() -> bool {
835 match backend() {
836 #[cfg(feature = "gpu")]
837 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
838 #[cfg(target_os = "macos")]
839 Backend::Metal => false, Backend::None => false,
841 }
842}
843
844pub struct MoeJob<'a> {
848 pub gate: (usize, usize, usize, &'a [f32]),
849 pub up: (usize, usize, usize, &'a [f32]),
850 pub down: (usize, usize, usize, &'a [f32]),
851 pub xs_gate: Vec<f32>,
852 pub xs_up: Vec<f32>,
853 pub down_col: &'a [f32],
854 pub w: f32,
855 pub q1: bool,
858 pub q4t: bool,
861 pub q4tp: bool,
865 pub gu_q2: bool,
869 pub swiglu_limit: f32,
874}
875
876pub struct BatchJob<'a> {
878 pub idx: usize,
879 pub rows: usize,
880 pub cols: usize,
881 pub row_scale: &'a [f32],
882 pub xs: Vec<f32>,
883 pub layout: BatchLayout,
887}
888
889#[derive(Clone, Copy, PartialEq, Eq, Debug)]
892pub enum BatchLayout {
893 Q8,
894 Q1,
895 Q4t,
896 Q4tp,
897}
898
899#[derive(Clone, Copy, PartialEq, Eq)]
900enum Backend {
901 None,
902 #[cfg(target_os = "macos")]
903 Metal,
904 #[cfg(feature = "gpu")]
905 Wgpu,
906}
907
908fn backend() -> Backend {
909 #[cfg(feature = "gpu")]
910 if crate::gpu_wgpu::selected() {
911 return if crate::gpu_wgpu::enabled() {
912 Backend::Wgpu
913 } else {
914 Backend::None
915 };
916 }
917 #[cfg(target_os = "macos")]
918 if crate::gpu_metal::enabled() {
919 return Backend::Metal;
920 }
921 Backend::None
922}
923
924pub fn backend_available() -> bool {
930 #[cfg(target_os = "macos")]
931 {
932 true
934 }
935 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
936 {
937 static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
938 *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
939 }
940 #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
941 {
942 false
943 }
944}
945
946static GPU_PAUSED: AtomicBool = AtomicBool::new(false);
952
953pub fn pause_gpu() -> GpuPause {
955 GPU_PAUSED.store(true, Ordering::Relaxed);
956 GpuPause(())
957}
958
959pub struct GpuPause(());
960
961impl Drop for GpuPause {
962 fn drop(&mut self) {
963 GPU_PAUSED.store(false, Ordering::Relaxed);
964 }
965}
966
967pub fn enabled() -> bool {
968 !GPU_PAUSED.load(Ordering::Relaxed) && backend() != Backend::None
969}
970
971pub fn wgpu_active() -> bool {
985 #[cfg(feature = "gpu")]
986 {
987 matches!(backend(), Backend::Wgpu)
988 }
989 #[cfg(not(feature = "gpu"))]
990 {
991 false
992 }
993}
994
995pub fn default_device() -> usize {
1002 static D: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1003 *D.get_or_init(|| {
1004 std::env::var("CMF_GPU_ADAPTER")
1005 .ok()
1006 .and_then(|v| v.trim().parse::<usize>().ok())
1007 .unwrap_or(0)
1008 })
1009}
1010
1011thread_local! {
1012 static CUR_DEV: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
1013}
1014
1015pub fn current_device() -> usize {
1017 CUR_DEV.with(|c| c.get()).unwrap_or_else(default_device)
1018}
1019
1020pub fn set_current_device(i: usize) {
1024 CUR_DEV.with(|c| c.set(Some(i)));
1025}
1026
1027pub fn with_device<R>(dev: usize, f: impl FnOnce() -> R) -> R {
1029 let prev = CUR_DEV.with(|c| c.replace(Some(dev)));
1030 let r = f();
1031 CUR_DEV.with(|c| c.set(prev));
1032 r
1033}
1034
1035pub fn device_count() -> usize {
1038 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1039 {
1040 return crate::gpu_wgpu::adapter_count();
1041 }
1042 #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1043 {
1044 usize::from(backend_available())
1045 }
1046}
1047
1048pub fn vram_budget() -> u64 {
1052 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1053 {
1054 return crate::gpu_wgpu::device_vram_budget();
1055 }
1056 #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1057 {
1058 if backend_available() { u64::MAX } else { 0 }
1059 }
1060}
1061
1062pub fn upload_bytes() -> u64 {
1066 #[cfg(feature = "gpu")]
1067 {
1068 return crate::gpu_wgpu::UPLOAD_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1069 }
1070 #[cfg(not(feature = "gpu"))]
1071 0
1072}
1073
1074#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1086pub enum GraphPhase {
1087 Prefill,
1088 Decode,
1089}
1090
1091pub fn wgpu_graph_on(phase: GraphPhase) -> bool {
1099 match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
1100 Some("0") => false,
1101 Some("prefill") => phase == GraphPhase::Prefill,
1102 Some(_) => true,
1103 None => {
1104 if wgpu_graph_default() {
1105 return true;
1106 }
1107 let _ = phase;
1112 false
1113 }
1114 }
1115}
1116
1117pub fn wgpu_graph_default() -> bool {
1118 #[cfg(feature = "gpu")]
1119 {
1120 matches!(backend(), Backend::Wgpu)
1126 && (crate::gpu_wgpu::discrete_active()
1127 || (cfg!(target_os = "macos") && crate::gpu_wgpu::adapter_up()))
1128 }
1129 #[cfg(not(feature = "gpu"))]
1130 {
1131 false
1132 }
1133}
1134
1135#[allow(clippy::too_many_arguments, unused_variables)]
1137pub fn q8_matvec_range(
1138 model: &Arc<CmfModel>,
1139 idx: usize,
1140 row0: usize,
1141 row_scale: &[f32],
1142 xs: &[f32],
1143 rows: usize,
1144 cols: usize,
1145 out: &mut [f32],
1146) -> bool {
1147 match backend() {
1148 #[cfg(target_os = "macos")]
1149 Backend::Metal => {
1150 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1151 }
1152 #[cfg(feature = "gpu")]
1153 Backend::Wgpu => {
1154 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1155 }
1156 Backend::None => false,
1157 }
1158}
1159
1160#[allow(clippy::too_many_arguments, unused_variables)]
1163#[allow(clippy::too_many_arguments)]
1167pub fn q8_matmat_2f(
1168 model: &Arc<CmfModel>,
1169 idx: usize,
1170 row_scale: &[f32],
1171 col_field: &[f32],
1172 xs: &[f32],
1173 b: usize,
1174 rows: usize,
1175 cols: usize,
1176 out: &mut [f32],
1177) -> bool {
1178 #[allow(unreachable_patterns)]
1179 match backend() {
1180 #[cfg(feature = "gpu")]
1181 Backend::Wgpu => {
1182 crate::gpu_wgpu::q8_matmat_2f(model, idx, row_scale, col_field, xs, b, rows, cols, out)
1183 }
1184 _ => false,
1185 }
1186}
1187
1188pub fn q8_matmat(
1189 model: &Arc<CmfModel>,
1190 idx: usize,
1191 row_scale: &[f32],
1192 pre: &[f32],
1193 b: usize,
1194 rows: usize,
1195 cols: usize,
1196 out: &mut [f32],
1197) -> bool {
1198 match backend() {
1199 #[cfg(target_os = "macos")]
1200 Backend::Metal => {
1201 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
1202 }
1203 #[cfg(feature = "gpu")]
1204 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
1205 Backend::None => false,
1206 }
1207}
1208
1209#[allow(unused_variables)]
1212pub fn q1_matvec(
1213 model: &Arc<CmfModel>,
1214 idx: usize,
1215 xs: &[f32],
1216 rows: usize,
1217 cols: usize,
1218 out: &mut [f32],
1219) -> bool {
1220 match backend() {
1221 #[cfg(target_os = "macos")]
1222 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
1223 #[cfg(feature = "gpu")]
1224 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
1225 Backend::None => false,
1226 }
1227}
1228
1229#[allow(clippy::too_many_arguments)]
1233pub fn attn_dropin(
1234 model: &Arc<CmfModel>,
1235 kv_id: u64,
1236 layer: usize,
1237 normed: &[f32],
1238 wq_idx: usize,
1239 wk_idx: usize,
1240 wv_idx: usize,
1241 wo_idx: usize,
1242 q_norm: Option<&[f32]>,
1243 k_norm: Option<&[f32]>,
1244 invf: &[f32],
1245 nh: usize,
1246 nkv: usize,
1247 hd: usize,
1248 rd: usize,
1249 hidden: usize,
1250 pos: usize,
1251 cap: usize,
1252 gemma: bool,
1253 eps: f32,
1254 cpu_k: &[Vec<f32>],
1255 cpu_v: &[Vec<f32>],
1256 out: &mut [f32],
1257) -> bool {
1258 match backend() {
1259 #[cfg(feature = "gpu")]
1260 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
1261 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
1262 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
1263 ),
1264 #[allow(unused_variables)]
1265 _ => false,
1266 }
1267}
1268
1269pub struct GraphW<'a> {
1273 pub idx: usize,
1274 pub kind: u8,
1275 pub row_scale: &'a [f32],
1276 pub data: &'a [f32],
1277}
1278
1279pub enum GraphAttn<'a> {
1282 Full {
1283 wq: GraphW<'a>,
1284 wk: GraphW<'a>,
1285 wv: GraphW<'a>,
1286 wo: GraphW<'a>,
1287 q_norm: Option<&'a [f32]>,
1288 k_norm: Option<&'a [f32]>,
1289 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1291 output_gate: bool,
1294 cpu_k: &'a [Vec<f32>],
1295 cpu_v: &'a [Vec<f32>],
1296 },
1297 Gdn {
1298 qkv: GraphW<'a>,
1299 z: GraphW<'a>,
1300 a: GraphW<'a>,
1301 b: GraphW<'a>,
1302 out: GraphW<'a>,
1303 conv1d: &'a [f32],
1304 a_log: &'a [f32],
1305 dt_bias: &'a [f32],
1306 norm: &'a [f32],
1307 nv: usize,
1308 nk: usize,
1309 dk: usize,
1310 dv: usize,
1311 kk: usize,
1312 cpu_state: &'a [f32],
1317 },
1318 ShortConv {
1325 inp: GraphW<'a>,
1327 out: GraphW<'a>,
1329 taps: &'a [f32],
1332 kernel: usize,
1333 cpu_state: &'a [f32],
1337 },
1338}
1339
1340pub struct GraphLayer<'a> {
1342 pub input_norm: &'a [f32],
1343 pub attn: GraphAttn<'a>,
1344 pub post_norm: &'a [f32],
1345 pub ffn: GraphFfn<'a>,
1346}
1347
1348pub enum GraphFfn<'a> {
1353 Dense {
1354 gate: GraphW<'a>,
1355 up: GraphW<'a>,
1356 down: GraphW<'a>,
1357 },
1358 Moe {
1359 router: GraphW<'a>,
1361 shared_gate: GraphW<'a>,
1363 experts: Vec<(usize, usize, usize)>,
1367 n_exp: usize,
1369 top_k: usize,
1370 inter: usize,
1371 norm_topk: bool,
1372 q4tp: bool,
1378 gu_q2: bool,
1382 sigmoid: bool,
1386 bias: Option<&'a [f32]>,
1389 has_shared: bool,
1393 },
1394}
1395
1396#[allow(clippy::too_many_arguments)]
1401pub fn forward_token_graph(
1402 model: &Arc<CmfModel>,
1403 kv_id: u64,
1404 layers: &[GraphLayer],
1405 o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
1408 o1_epoch: u64,
1409 invf: &[f32],
1410 h: &mut [f32],
1411 nh: usize,
1412 nkv: usize,
1413 hd: usize,
1414 attn_scale: f32,
1415 rd: usize,
1416 hidden: usize,
1417 inter: usize,
1418 position: usize,
1419 cap: usize,
1420 gemma: bool,
1421 eps: f32,
1422 lm_head: Option<(&GraphW, usize)>,
1423 final_norm: &[f32],
1424 logits: &mut Vec<f32>,
1425 loop_norm_at: &[usize],
1426 steps: usize,
1427 embed: Option<(&GraphW, usize, f32)>,
1428 ids_out: Option<&mut Vec<u32>>,
1429 layers_run: Option<&mut usize>,
1432 layer_base: usize,
1436 hidden_too: bool,
1438) -> bool {
1439 match backend() {
1440 #[cfg(feature = "gpu")]
1441 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
1442 model,
1443 kv_id,
1444 layers,
1445 o1,
1446 o1_epoch,
1447 invf,
1448 h,
1449 nh,
1450 nkv,
1451 hd,
1452 attn_scale,
1453 rd,
1454 hidden,
1455 inter,
1456 position,
1457 cap,
1458 gemma,
1459 eps,
1460 lm_head,
1461 final_norm,
1462 logits,
1463 loop_norm_at,
1464 steps,
1465 embed,
1466 ids_out,
1467 layers_run,
1468 layer_base,
1469 hidden_too,
1470 ),
1471 #[allow(unused_variables)]
1472 _ => {
1473 let _ = (
1474 attn_scale,
1475 lm_head,
1476 final_norm,
1477 logits,
1478 loop_norm_at,
1479 layers_run,
1480 layer_base,
1481 hidden_too,
1482 );
1483 false
1484 }
1485 }
1486}
1487
1488pub struct SpecTail<'a> {
1492 pub lm: GraphW<'a>,
1493 pub lm_rows: usize,
1494 pub final_norm: &'a [f32],
1495 pub logits_out: &'a mut Vec<f32>,
1496}
1497
1498#[allow(clippy::too_many_arguments)]
1502pub fn forward_batch_graph(
1503 model: &Arc<CmfModel>,
1504 kv_id: u64,
1505 layers: &[GraphLayer],
1506 invf: &[f32],
1507 h: &mut [f32],
1508 nh: usize,
1509 nkv: usize,
1510 hd: usize,
1511 rd: usize,
1512 hidden: usize,
1513 inter: usize,
1514 positions: &[usize],
1515 cap: usize,
1516 gemma: bool,
1517 eps: f32,
1518 attn_scale: f32,
1519 k: usize,
1520 spec: Option<SpecTail<'_>>,
1521) -> bool {
1522 match backend() {
1523 #[cfg(feature = "gpu")]
1524 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
1525 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
1526 eps, attn_scale, k, spec,
1527 ),
1528 #[allow(unreachable_patterns)]
1529 _ => {
1530 let _ = spec;
1531 false
1532 }
1533 }
1534}
1535
1536pub fn gdn_spec_restore(kv_id: u64, slot: usize) -> bool {
1539 #[cfg(feature = "gpu")]
1540 if backend() == Backend::Wgpu {
1541 return crate::gpu_wgpu::gdn_spec_restore(kv_id, slot);
1542 }
1543 #[allow(unreachable_code)]
1544 {
1545 let _ = (kv_id, slot);
1546 false
1547 }
1548}
1549
1550pub fn graph_kv_reset(_kv_id: u64) {
1552 #[cfg(feature = "gpu")]
1553 if backend() == Backend::Wgpu {
1554 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
1555 }
1556}
1557
1558pub fn q1t_matvec(
1562 model: &Arc<CmfModel>,
1563 idx: usize,
1564 xs: &[f32],
1565 rows: usize,
1566 cols: usize,
1567 out: &mut [f32],
1568) -> bool {
1569 match backend() {
1570 #[cfg(target_os = "macos")]
1571 Backend::Metal => {
1572 if metal_q1t_enabled() {
1573 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
1574 } else {
1575 false
1576 }
1577 }
1578 #[cfg(feature = "gpu")]
1579 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
1580 Backend::None => false,
1581 }
1582}
1583
1584#[allow(unused_variables)]
1587pub fn q4b_matvec(
1588 model: &Arc<CmfModel>,
1589 idx: usize,
1590 xs: &[f32],
1591 rows: usize,
1592 cols: usize,
1593 out: &mut [f32],
1594) -> bool {
1595 match backend() {
1596 #[cfg(target_os = "macos")]
1597 Backend::Metal => false,
1598 #[cfg(feature = "gpu")]
1599 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
1600 Backend::None => false,
1601 }
1602}
1603
1604pub fn q1t_matmat(
1607 model: &Arc<CmfModel>,
1608 idx: usize,
1609 xs: &[f32],
1610 b: usize,
1611 rows: usize,
1612 cols: usize,
1613 out: &mut [f32],
1614) -> bool {
1615 match backend() {
1616 #[cfg(target_os = "macos")]
1617 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
1621 #[cfg(feature = "gpu")]
1622 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
1623 Backend::None => false,
1624 }
1625}
1626
1627#[cfg(target_os = "macos")]
1631pub(crate) fn metal_q1t_enabled() -> bool {
1632 std::env::var("CMF_METAL_Q1T")
1633 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
1634 .unwrap_or(true)
1635}
1636
1637pub fn q1_matmat(
1639 model: &Arc<CmfModel>,
1640 idx: usize,
1641 xs: &[f32],
1642 b: usize,
1643 rows: usize,
1644 cols: usize,
1645 out: &mut [f32],
1646) -> bool {
1647 match backend() {
1648 #[cfg(feature = "gpu")]
1649 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
1650 #[allow(unused_variables)]
1651 _ => false,
1652 }
1653}
1654
1655static MM_KILL: AtomicBool = AtomicBool::new(false);
1660pub(crate) fn mm_killed() -> bool {
1661 MM_KILL.load(Ordering::Relaxed)
1662}
1663pub(crate) fn mm_kill() {
1664 MM_KILL.store(true, Ordering::Relaxed);
1665}
1666
1667static MM_STRIKES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
1674const MM_STRIKES_TO_KILL: u32 = 3;
1675static MM_ARMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
1682
1683pub fn mm_kill_arm(on: bool) {
1686 MM_ARMED.store(on, Ordering::Relaxed);
1687 if on {
1688 MM_STRIKES.store(0, Ordering::Relaxed);
1689 }
1690}
1691
1692pub(crate) fn mm_budget_check(
1699 what: &str,
1700 el: std::time::Duration,
1701 budget: std::time::Duration,
1702 exempt: bool,
1703) {
1704 if el <= budget {
1705 MM_STRIKES.store(0, Ordering::Relaxed);
1706 return;
1707 }
1708 if exempt || !MM_ARMED.load(Ordering::Relaxed) {
1709 return;
1710 }
1711 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1712 let on = *ON.get_or_init(|| std::env::var("CMF_MM_KILL").as_deref() != Ok("0"));
1713 let n = MM_STRIKES.fetch_add(1, Ordering::Relaxed) + 1;
1714 if !on {
1715 tracing::info!(
1716 "gpu {what} took {el:?} (budget {budget:?}) — over budget, CMF_MM_KILL=0 keeps the device"
1717 );
1718 return;
1719 }
1720 if n >= MM_STRIKES_TO_KILL {
1721 tracing::warn!(
1722 "gpu {what} took {el:?} (budget {budget:?}), {n} in a row — \
1723 device contended, CPU for the rest of the process (CMF_MM_KILL=0 to override)"
1724 );
1725 mm_kill();
1726 } else {
1727 tracing::info!(
1728 "gpu {what} took {el:?} (budget {budget:?}) — strike {n} of {MM_STRIKES_TO_KILL}"
1729 );
1730 }
1731}
1732
1733#[allow(unused_variables, clippy::too_many_arguments)]
1738pub fn chunk_attend(
1739 q: &[f32],
1740 k: &[&[f32]],
1741 v: &[&[f32]],
1742 b: usize,
1743 s0: usize,
1744 nh: usize,
1745 nkv: usize,
1746 hd: usize,
1747 scale: f32,
1748 out: &mut [f32],
1749) -> bool {
1750 match backend() {
1751 #[cfg(feature = "gpu")]
1752 Backend::Wgpu => crate::gpu_wgpu::chunk_attend(q, k, v, b, s0, nh, nkv, hd, scale, out),
1753 #[allow(unreachable_patterns)]
1754 _ => false,
1755 }
1756}
1757
1758#[allow(unused_variables, clippy::too_many_arguments)]
1762pub fn q4t_qkv(
1763 model: &Arc<CmfModel>,
1764 wq: usize,
1765 wk: usize,
1766 wv: usize,
1767 xs: &[f32],
1768 b: usize,
1769 cols: usize,
1770 rq: usize,
1771 rk: usize,
1772 rv: usize,
1773 out: &mut [f32],
1774) -> bool {
1775 match backend() {
1776 #[cfg(feature = "gpu")]
1777 Backend::Wgpu => crate::gpu_wgpu::q4t_qkv(model, wq, wk, wv, xs, b, cols, rq, rk, rv, out),
1778 #[allow(unreachable_patterns)]
1779 _ => false,
1780 }
1781}
1782
1783#[allow(unused_variables, clippy::too_many_arguments)]
1785#[allow(clippy::too_many_arguments, unused_variables)]
1789pub fn q4tp_ffn_packed(
1790 model: &Arc<CmfModel>,
1791 w1: usize,
1792 w2: usize,
1793 xs: &[f32],
1794 b: usize,
1795 hidden: usize,
1796 inter: usize,
1797 bias: Option<&[f32]>,
1798 out: &mut [f32],
1799) -> bool {
1800 match backend() {
1801 #[cfg(feature = "gpu")]
1802 Backend::Wgpu => {
1803 crate::gpu_wgpu::ffn_packed(model, w1, w2, xs, b, hidden, inter, bias, out)
1804 }
1805 #[allow(unreachable_patterns)]
1806 _ => false,
1807 }
1808}
1809
1810pub fn q4tp_ffn(
1811 model: &Arc<CmfModel>,
1812 w1: usize,
1813 w3: usize,
1814 w2: usize,
1815 xs: &[f32],
1816 b: usize,
1817 hidden: usize,
1818 inter: usize,
1819 out: &mut [f32],
1820) -> bool {
1821 match backend() {
1822 #[cfg(target_os = "macos")]
1823 Backend::Metal => crate::gpu_metal::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1824 #[cfg(feature = "gpu")]
1825 Backend::Wgpu => crate::gpu_wgpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1826 #[allow(unreachable_patterns)]
1827 _ => false,
1828 }
1829}
1830
1831pub fn q4t_ffn(
1832 model: &Arc<CmfModel>,
1833 w1: usize,
1834 w3: usize,
1835 w2: usize,
1836 xs: &[f32],
1837 b: usize,
1838 hidden: usize,
1839 inter: usize,
1840 out: &mut [f32],
1841) -> bool {
1842 match backend() {
1843 #[cfg(target_os = "macos")]
1844 Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1845 #[cfg(feature = "gpu")]
1846 Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1847 #[allow(unreachable_patterns)]
1848 _ => false,
1849 }
1850}
1851
1852pub struct DitBlockArgs<'a> {
1857 pub n: usize,
1858 pub hidden: usize,
1859 pub inter: usize,
1860 pub nh: usize,
1861 pub nkv: usize,
1862 pub hd: usize,
1863 pub eps: f32,
1864 pub rope_cos: &'a [f32],
1865 pub rope_sin: &'a [f32],
1866 pub norm1: &'a [f32],
1867 pub norm2: &'a [f32],
1868 pub ffn_norm1: &'a [f32],
1869 pub ffn_norm2: &'a [f32],
1870 pub norm_q: &'a [f32],
1871 pub norm_k: &'a [f32],
1872 pub s_msa: &'a [f32],
1873 pub gate_msa: &'a [f32],
1874 pub s_mlp: &'a [f32],
1875 pub gate_mlp: &'a [f32],
1876 pub wq: usize,
1877 pub wk: usize,
1878 pub wv: usize,
1879 pub wo: usize,
1880 pub w1: usize,
1881 pub w3: usize,
1882 pub w2: usize,
1883 pub q4tp: bool,
1887 pub resident_in: bool,
1890 pub resident_out: bool,
1894}
1895
1896pub fn dit_chain_supported() -> bool {
1900 #[cfg(feature = "gpu")]
1901 {
1902 return matches!(backend(), Backend::Wgpu) && fused_dit_block_available();
1903 }
1904 #[allow(unreachable_code)]
1905 false
1906}
1907
1908pub fn dit_state_fetch(_x: &mut [f32]) -> bool {
1911 #[cfg(feature = "gpu")]
1912 {
1913 if matches!(backend(), Backend::Wgpu) {
1914 return crate::gpu_wgpu::dit_state_fetch(_x);
1915 }
1916 }
1917 false
1918}
1919
1920#[allow(unused_variables)]
1924#[allow(unused_variables, clippy::too_many_arguments)]
1928pub fn dit_qkv(
1929 model: &Arc<CmfModel>,
1930 wq: usize,
1931 wk: usize,
1932 wv: usize,
1933 xs: &[f32],
1934 b: usize,
1935 hidden: usize,
1936 qrows: usize,
1937 kvrows: usize,
1938 q_out: &mut [f32],
1939 k_out: &mut [f32],
1940 v_out: &mut [f32],
1941) -> bool {
1942 match backend() {
1943 #[cfg(feature = "gpu")]
1944 Backend::Wgpu => crate::gpu_wgpu::q4tp_qkv(
1945 model, wq, wk, wv, xs, b, hidden, qrows, kvrows, q_out, k_out, v_out,
1946 ),
1947 #[allow(unreachable_patterns)]
1948 _ => false,
1949 }
1950}
1951
1952pub fn fused_dit_block_available() -> bool {
1956 #[cfg(target_os = "macos")]
1957 {
1958 matches!(backend(), Backend::Metal) && fused_block_trusted()
1959 }
1960 #[cfg(not(target_os = "macos"))]
1961 {
1962 false
1963 }
1964}
1965
1966pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
1967 dit_block_seg(model, a, &[a.n], x)
1968}
1969
1970pub fn dit_block_seg(
1974 model: &Arc<CmfModel>,
1975 a: &DitBlockArgs,
1976 segs: &[usize],
1977 x: &mut [f32],
1978) -> bool {
1979 match backend() {
1980 #[cfg(target_os = "macos")]
1981 Backend::Metal if segs.len() <= 1 => crate::gpu_metal::dit_block(model, a, x),
1982 #[cfg(feature = "gpu")]
1989 Backend::Wgpu
1990 if match std::env::var("CMF_DIT_FUSED").ok().as_deref() {
1991 Some("0") => false,
1992 Some(_) => true,
1993 None => crate::gpu_wgpu::discrete_active(),
1994 } =>
1995 {
1996 crate::gpu_wgpu::dit_block_seg(model, a, segs, x)
1997 }
1998 #[allow(unreachable_patterns)]
1999 _ => false,
2000 }
2001}
2002
2003pub struct VaeResnetArgs<'a> {
2007 pub groups: usize,
2008 pub ic: usize,
2009 pub oc: usize,
2010 pub h: usize,
2011 pub w: usize,
2012 pub n1w: &'a [f32],
2013 pub n1b: &'a [f32],
2014 pub c1w: &'a [f32],
2015 pub c1b: &'a [f32],
2016 pub c1k: usize,
2017 pub n2w: &'a [f32],
2018 pub n2b: &'a [f32],
2019 pub c2w: &'a [f32],
2020 pub c2b: &'a [f32],
2021 pub c2k: usize,
2022 pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
2023}
2024
2025#[allow(unused_variables)]
2028pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
2029 match backend() {
2030 #[cfg(target_os = "macos")]
2031 Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
2032 _ => false,
2033 }
2034}
2035
2036#[allow(unused_variables, clippy::too_many_arguments)]
2039pub fn vae_upsample_conv(
2040 w: &[f32],
2041 bias: &[f32],
2042 x: &[f32],
2043 ic: usize,
2044 oc: usize,
2045 h: usize,
2046 w_img: usize,
2047 k: usize,
2048 out: &mut [f32],
2049) -> bool {
2050 match backend() {
2051 #[cfg(target_os = "macos")]
2052 Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
2053 #[cfg(feature = "gpu")]
2054 Backend::Wgpu => crate::gpu_wgpu::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
2055 #[allow(unreachable_patterns)]
2056 _ => false,
2057 }
2058}
2059
2060#[allow(unused_variables, clippy::too_many_arguments)]
2063pub fn vae_conv2d(
2064 w: &[f32],
2065 bias: &[f32],
2066 x: &[f32],
2067 ic: usize,
2068 oc: usize,
2069 h: usize,
2070 w_img: usize,
2071 k: usize,
2072 out: &mut [f32],
2073) -> bool {
2074 match backend() {
2075 #[cfg(target_os = "macos")]
2076 Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
2077 #[cfg(feature = "gpu")]
2078 Backend::Wgpu => crate::gpu_wgpu::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
2079 #[allow(unreachable_patterns)]
2080 _ => false,
2081 }
2082}
2083
2084#[allow(unused_variables, clippy::too_many_arguments)]
2088#[allow(unused_variables)]
2092#[allow(clippy::too_many_arguments)]
2093#[allow(clippy::too_many_arguments, unused_variables)]
2096pub fn dit_qkv_attention(
2097 model: &Arc<CmfModel>,
2098 qkv_idx: usize,
2099 xn: &[f32],
2100 n: usize,
2101 hidden: usize,
2102 nh: usize,
2103 hd: usize,
2104 scale: f32,
2105 nr: (&[f32], &[f32], &[f32], f32),
2106 out: &mut [f32],
2107) -> bool {
2108 match backend() {
2109 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2110 Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attention(
2111 model, qkv_idx, xn, n, hidden, nh, hd, scale, nr, out,
2112 ),
2113 #[allow(unreachable_patterns)]
2114 _ => false,
2115 }
2116}
2117
2118#[allow(clippy::too_many_arguments)]
2121pub fn dit_qkv_attn_out(
2122 model: &Arc<CmfModel>,
2123 qkv_idx: usize,
2124 out_idx: usize,
2125 xn: &[f32],
2126 n: usize,
2127 hidden: usize,
2128 nh: usize,
2129 hd: usize,
2130 scale: f32,
2131 nr: (&[f32], &[f32], &[f32], f32),
2132 proj: &mut [f32],
2133) -> bool {
2134 match backend() {
2135 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2136 Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attn_out(
2137 model, qkv_idx, out_idx, xn, n, hidden, nh, hd, scale, nr, proj,
2138 ),
2139 #[allow(unreachable_patterns)]
2140 _ => false,
2141 }
2142}
2143
2144#[allow(clippy::too_many_arguments)]
2146pub fn vae_qkv_attn_out(
2147 model: &Arc<CmfModel>,
2148 qkv_idx: usize,
2149 out_idx: usize,
2150 xn: &[f32],
2151 n: usize,
2152 dim: usize,
2153 nh: usize,
2154 hd: usize,
2155 scale: f32,
2156 angles: &[f32],
2157 eps: f32,
2158 qkv_bias: &[f32],
2159 proj: &mut [f32],
2160) -> bool {
2161 match backend() {
2162 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2163 Backend::Wgpu => crate::gpu_wgpu::vae_qkv_attn_out(
2164 model, qkv_idx, out_idx, xn, n, dim, nh, hd, scale, angles, eps, qkv_bias, proj,
2165 ),
2166 #[allow(unreachable_patterns)]
2167 _ => false,
2168 }
2169}
2170
2171#[allow(clippy::too_many_arguments)]
2172pub fn vae_attention_packed(
2173 qkv: &[f32],
2174 nh: usize,
2175 n: usize,
2176 hd: usize,
2177 scale: f32,
2178 angles: &[f32],
2179 eps: f32,
2180 out: &mut [f32],
2181) -> bool {
2182 vae_attention_packed_layout(qkv, nh, n, hd, scale, angles, eps, out, 1)
2183}
2184
2185#[allow(clippy::too_many_arguments)]
2186pub fn vae_attention_packed_layout(
2187 qkv: &[f32],
2188 nh: usize,
2189 n: usize,
2190 hd: usize,
2191 scale: f32,
2192 angles: &[f32],
2193 eps: f32,
2194 out: &mut [f32],
2195 layout: u32,
2196) -> bool {
2197 match backend() {
2198 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2199 Backend::Wgpu => crate::gpu_wgpu::vae_attention_packed_layout(
2200 qkv, nh, n, hd, scale, angles, eps, out, layout,
2201 ),
2202 #[allow(unreachable_patterns)]
2203 _ => false,
2204 }
2205}
2206
2207#[allow(clippy::too_many_arguments)]
2208pub fn dit_split_only(
2209 qkv: &[f32],
2210 nh: usize,
2211 n: usize,
2212 hd: usize,
2213 layout: u32,
2214 norm: Option<(&[f32], f32)>,
2215 out_q: &mut [f32],
2216) -> bool {
2217 match backend() {
2218 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2219 Backend::Wgpu => crate::gpu_wgpu::dit_split_only(qkv, nh, n, hd, layout, norm, out_q),
2220 #[allow(unreachable_patterns)]
2221 _ => false,
2222 }
2223}
2224
2225pub fn gemm_nt_f32_transient(
2233 x: &[f32],
2234 w: &[f32],
2235 y: &mut [f32],
2236 n: usize,
2237 k: usize,
2238 m: usize,
2239) -> bool {
2240 match backend() {
2241 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2242 Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32_transient(x, w, y, n, k, m),
2243 #[allow(unreachable_patterns)]
2244 _ => false,
2245 }
2246}
2247
2248pub fn gemm_nt_f32(x: &[f32], w: &[f32], y: &mut [f32], n: usize, k: usize, m: usize) -> bool {
2249 match backend() {
2250 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2251 Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32(x, w, y, n, k, m),
2252 #[allow(unreachable_patterns)]
2253 _ => false,
2254 }
2255}
2256
2257#[allow(clippy::too_many_arguments)]
2260pub fn music3_ffn(
2261 model: &std::sync::Arc<CmfModel>,
2262 idx_in: usize,
2263 idx_out: usize,
2264 h: &[f32],
2265 bias_in: &[f32],
2266 n: usize,
2267 hs: usize,
2268 inter: usize,
2269 out: &mut [f32],
2270) -> bool {
2271 match backend() {
2272 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2273 Backend::Wgpu => {
2274 crate::gpu_wgpu::music3_ffn(model, idx_in, idx_out, h, bias_in, n, hs, inter, out)
2275 }
2276 #[allow(unreachable_patterns)]
2277 _ => false,
2278 }
2279}
2280
2281#[allow(clippy::too_many_arguments)]
2285pub fn conv1d_gemm(
2286 x: &[f32],
2287 w: &[f32],
2288 ic: usize,
2289 oc: usize,
2290 n: usize,
2291 k: usize,
2292 pad: usize,
2293 dil: usize,
2294 out_n: usize,
2295 yt: &mut [f32],
2296) -> bool {
2297 match backend() {
2298 #[cfg(target_os = "macos")]
2299 Backend::Metal => crate::gpu_metal::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
2300 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2301 Backend::Wgpu => crate::gpu_wgpu::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
2302 #[allow(unreachable_patterns)]
2303 _ => false,
2304 }
2305}
2306
2307#[allow(clippy::too_many_arguments)]
2309pub fn vae_conv2d_coop(
2310 w: &[f32],
2311 bias: Option<&[f32]>,
2312 x: &[f32],
2313 ic: usize,
2314 oc: usize,
2315 h: usize,
2316 wi: usize,
2317 k: usize,
2318 out: &mut [f32],
2319) -> bool {
2320 match backend() {
2321 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2322 Backend::Wgpu => crate::gpu_wgpu::vae_conv2d_coop(w, bias, x, ic, oc, h, wi, k, out),
2323 #[allow(unreachable_patterns)]
2324 _ => false,
2325 }
2326}
2327
2328pub fn dit_attention_packed(
2329 qkv: &[f32],
2330 nh: usize,
2331 n: usize,
2332 hd: usize,
2333 scale: f32,
2334 nr: Option<(&[f32], &[f32], &[f32], f32)>,
2337 out: &mut [f32],
2338) -> bool {
2339 match backend() {
2340 #[cfg(feature = "gpu")]
2347 Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed(qkv, nh, n, hd, scale, nr, out),
2348 #[allow(unreachable_patterns)]
2349 _ => false,
2350 }
2351}
2352
2353pub fn dit_attention_packed_available() -> bool {
2361 #[allow(unreachable_patterns)]
2362 match backend() {
2363 #[cfg(feature = "gpu")]
2364 Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed_ready(),
2365 _ => false,
2366 }
2367}
2368
2369pub fn dit_attention(
2370 qh: &[f32],
2371 kh: &[f32],
2372 vh: &[f32],
2373 nh: usize,
2374 nkv: usize,
2375 n: usize,
2376 hd: usize,
2377 scale: f32,
2378 out: &mut [f32],
2379) -> bool {
2380 match backend() {
2381 #[cfg(target_os = "macos")]
2382 Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
2383 #[cfg(feature = "gpu")]
2384 Backend::Wgpu => crate::gpu_wgpu::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
2385 #[allow(unreachable_patterns)]
2386 _ => false,
2387 }
2388}
2389
2390#[allow(unused_variables)]
2395pub fn q4tp_matmat(
2396 model: &Arc<CmfModel>,
2397 idx: usize,
2398 xs: &[f32],
2399 b: usize,
2400 rows: usize,
2401 cols: usize,
2402 out: &mut [f32],
2403) -> bool {
2404 match backend() {
2405 #[cfg(target_os = "macos")]
2406 Backend::Metal => crate::gpu_metal::q4tp_matmat(model, idx, xs, b, rows, cols, out),
2407 #[cfg(feature = "gpu")]
2408 Backend::Wgpu => crate::gpu_wgpu::q4tp_matmat(model, idx, xs, b, rows, cols, out),
2409 #[allow(unreachable_patterns)]
2410 _ => false,
2411 }
2412}
2413
2414pub fn q2tp_matmat(
2417 model: &Arc<CmfModel>,
2418 idx: usize,
2419 xs: &[f32],
2420 b: usize,
2421 rows: usize,
2422 cols: usize,
2423 out: &mut [f32],
2424) -> bool {
2425 match backend() {
2426 #[cfg(feature = "gpu")]
2427 Backend::Wgpu => crate::gpu_wgpu::q2tp_matmat(model, idx, xs, b, rows, cols, out),
2428 #[allow(unreachable_patterns)]
2429 _ => false,
2430 }
2431}
2432
2433pub fn q4tp_matvec(
2438 model: &Arc<CmfModel>,
2439 idx: usize,
2440 xs: &[f32],
2441 rows: usize,
2442 cols: usize,
2443 out: &mut [f32],
2444) -> bool {
2445 match backend() {
2446 #[cfg(target_os = "macos")]
2447 Backend::Metal => crate::gpu_metal::q4tp_matvec_for_test(model, idx, xs, rows, cols, out),
2448 #[cfg(feature = "gpu")]
2449 Backend::Wgpu => crate::gpu_wgpu::q4tp_matvec(model, idx, xs, rows, cols, out),
2450 #[allow(unreachable_patterns)]
2451 _ => false,
2452 }
2453}
2454
2455pub fn q4t_matvec(
2461 model: &Arc<CmfModel>,
2462 idx: usize,
2463 xs: &[f32],
2464 rows: usize,
2465 cols: usize,
2466 out: &mut [f32],
2467) -> bool {
2468 match backend() {
2469 #[cfg(target_os = "macos")]
2470 Backend::Metal => crate::gpu_metal::q4t_matvec_for_test(model, idx, xs, rows, cols, out),
2471 #[allow(unreachable_patterns)]
2472 _ => false,
2473 }
2474}
2475
2476pub fn q4t_matmat(
2477 model: &Arc<CmfModel>,
2478 idx: usize,
2479 xs: &[f32],
2480 b: usize,
2481 rows: usize,
2482 cols: usize,
2483 out: &mut [f32],
2484) -> bool {
2485 match backend() {
2486 #[cfg(target_os = "macos")]
2487 Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
2488 #[cfg(feature = "gpu")]
2489 Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
2490 #[allow(unreachable_patterns)]
2491 _ => false,
2492 }
2493}
2494
2495#[cfg(target_os = "macos")]
2497pub use crate::gpu_metal::{
2498 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GpuMoe, GraphDims, MetalFfn,
2499 O1AttnParams, TokenGraph, kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp,
2500};
2501
2502#[cfg(target_os = "macos")]
2504pub fn gdn_block(
2505 model: &Arc<CmfModel>,
2506 layers: &[GdnGpuLayer],
2507 states: &mut [&mut [f32]],
2508 cfg: &GdnGpuCfg,
2509 h: &mut [f32],
2510) -> bool {
2511 match backend() {
2512 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
2513 _ => false,
2514 }
2515}
2516
2517#[allow(unused_variables)]
2519pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
2520 match backend() {
2521 #[cfg(target_os = "macos")]
2522 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
2523 #[cfg(feature = "gpu")]
2524 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
2525 Backend::None => false,
2526 }
2527}
2528
2529#[allow(unused_variables)]
2531pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
2532 match backend() {
2533 #[cfg(target_os = "macos")]
2534 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
2535 #[cfg(feature = "gpu")]
2536 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
2537 Backend::None => false,
2538 }
2539}
2540
2541static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
2557static GRAPH_RACE_ARM_GRAPH: AtomicU8 = AtomicU8::new(0); static GRAPH_RACE_TOK: AtomicU32 = AtomicU32::new(0); static GRAPH_NS: [AtomicU64; 2] = [AtomicU64::new(0), AtomicU64::new(0)]; static GRAPH_N: [AtomicU32; 2] = [AtomicU32::new(0), AtomicU32::new(0)];
2561
2562const GRAPH_RACE_SAMPLES: u32 = 4;
2564
2565static GRAPH_UNSUPPORTED: AtomicBool = AtomicBool::new(false);
2575
2576pub fn graph_mark_unsupported() {
2581 if !GRAPH_UNSUPPORTED.swap(true, Ordering::Relaxed) {
2582 tracing::info!("wgpu token graph: unsupported for this model — not retrying");
2583 }
2584}
2585
2586pub fn graph_unsupported() -> bool {
2587 GRAPH_UNSUPPORTED.load(Ordering::Relaxed)
2588}
2589
2590pub fn graph_unsupported_reset() {
2592 GRAPH_UNSUPPORTED.store(false, Ordering::Relaxed);
2593}
2594
2595pub fn graph_race_begin_generation() {
2596 #[cfg(feature = "gpu")]
2601 {
2602 static FLUSHED: std::sync::Once = std::sync::Once::new();
2614 static FIRST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
2615 if FIRST.swap(false, Ordering::Relaxed) {
2616 } else {
2618 FLUSHED.call_once(crate::gpu_wgpu::pipeline_cache_flush);
2619 }
2620 }
2621 GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
2622 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
2623 return;
2624 }
2625 let (gn, cn) = (
2626 GRAPH_N[1].load(Ordering::Relaxed),
2627 GRAPH_N[0].load(Ordering::Relaxed),
2628 );
2629 if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
2630 let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
2631 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
2632 let verdict = if g_avg < c_avg { 1 } else { 2 };
2633 GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
2634 tracing::info!(
2635 "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
2636 g_avg as f64 / 1e6,
2637 c_avg as f64 / 1e6,
2638 if verdict == 1 { "graph" } else { "normal path" }
2639 );
2640 return;
2641 }
2642 let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
2643 GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
2644}
2645
2646pub fn graph_race_use_graph(trusted: bool) -> bool {
2650 if trusted {
2651 return true;
2652 }
2653 match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
2654 1 => true,
2655 2 => false,
2656 _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
2657 }
2658}
2659
2660pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
2665 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
2666 return false;
2667 }
2668 let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
2669 let cn = GRAPH_N[0].load(Ordering::Relaxed);
2670 if !first || cn == 0 {
2671 return false;
2672 }
2673 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
2674 let ns = dur.as_nanos() as u64;
2675 if ns > 1_000_000_000 && ns > 4 * c_avg {
2676 GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
2677 tracing::info!(
2678 "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
2679 ns as f64 / 1e6,
2680 c_avg as f64 / 1e6
2681 );
2682 return true;
2683 }
2684 false
2685}
2686
2687pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
2691 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
2692 return;
2693 }
2694 let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
2695 if tok == 0 {
2696 return;
2697 }
2698 let i = used_graph as usize;
2699 GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
2700 GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
2701}
2702
2703pub(crate) fn fp_bytes(data: &[u8]) -> u64 {
2713 #[inline]
2714 fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
2715 let (chunks, tail) = bytes.split_at(bytes.len() & !7);
2716 for c in chunks.chunks_exact(8) {
2717 h ^= u64::from_le_bytes(c.try_into().unwrap());
2718 h = h.wrapping_mul(0x100_0000_01b3);
2719 }
2720 for &b in tail {
2721 h ^= b as u64;
2722 h = h.wrapping_mul(0x100_0000_01b3);
2723 }
2724 h
2725 }
2726 let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (data.len() as u64);
2727 if data.len() <= 4096 {
2728 return fnv(h, data);
2729 }
2730 let step = (data.len() - 64) / 63;
2731 for i in 0..64 {
2732 h = fnv(h, &data[i * step..i * step + 64]);
2733 }
2734 h
2735}
2736
2737pub(crate) fn fp_f32(data: &[f32]) -> u64 {
2740 let bytes = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
2741 fp_bytes(bytes)
2742}
2743
2744#[cfg(test)]
2745mod fp_tests {
2746 use super::fp_bytes;
2747
2748 #[test]
2753 fn fp_bytes_sees_a_change_anywhere_in_a_sampled_slice() {
2754 let n = 1 << 20; let base: Vec<u8> = (0..n).map(|i| (i * 31 + 7) as u8).collect();
2756 let h0 = fp_bytes(&base);
2757 assert_eq!(h0, fp_bytes(&base), "fingerprint must be deterministic");
2758 let mut dense = base.clone();
2761 for b in dense.iter_mut() {
2762 *b = b.wrapping_add(1);
2763 }
2764 assert_ne!(
2765 h0,
2766 fp_bytes(&dense),
2767 "a fully different tensor slipped through"
2768 );
2769 assert_ne!(h0, fp_bytes(&base[..n - 64]));
2772 let mut small = vec![3u8; 4096];
2775 let hs = fp_bytes(&small);
2776 small[2048] ^= 1;
2777 assert_ne!(hs, fp_bytes(&small), "full hash missed a one-byte change");
2778 for n in [4097usize, 5000, 64 * 64, 1 << 16] {
2780 let v = vec![9u8; n];
2781 let _ = fp_bytes(&v); }
2783 }
2784}
2785
2786pub fn bake_release() {
2790 #[cfg(feature = "gpu")]
2791 crate::gpu_wgpu::bake_release();
2792}
2793
2794pub fn bake_precision_strict(on: bool) {
2798 #[cfg(feature = "gpu")]
2799 crate::gpu_wgpu::bake_precision_strict(on);
2800 #[cfg(not(feature = "gpu"))]
2801 let _ = on;
2802}
2803
2804pub fn hostprof_encode_done(t0: std::time::Instant) {
2810 use std::sync::atomic::{AtomicU64, Ordering};
2811 static ENC: AtomicU64 = AtomicU64::new(0);
2812 static N: AtomicU64 = AtomicU64::new(0);
2813 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
2814 return;
2815 }
2816 ENC.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2817 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2818 if n % 100 == 0 {
2819 eprintln!(
2820 "hostprof: encode {:.2} ms/token over {n} tokens",
2821 ENC.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2822 );
2823 }
2824}
2825
2826pub fn hostprof_total(t0: std::time::Instant) {
2827 use std::sync::atomic::{AtomicU64, Ordering};
2828 static TOT: AtomicU64 = AtomicU64::new(0);
2829 static N: AtomicU64 = AtomicU64::new(0);
2830 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
2831 return;
2832 }
2833 TOT.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
2834 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2835 if n % 100 == 0 {
2836 eprintln!(
2837 "hostprof: total {:.2} ms/token over {n} tokens",
2838 TOT.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2839 );
2840 }
2841}
2842
2843pub fn stageprof(stage: u32, dt: std::time::Duration) {
2847 use std::sync::atomic::{AtomicU64, Ordering};
2848 static NS: [AtomicU64; 4] = [
2849 AtomicU64::new(0),
2850 AtomicU64::new(0),
2851 AtomicU64::new(0),
2852 AtomicU64::new(0),
2853 ];
2854 static N: AtomicU64 = AtomicU64::new(0);
2855 if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
2856 return;
2857 }
2858 NS[stage as usize % 4].fetch_add(dt.as_nanos() as u64, Ordering::Relaxed);
2859 if stage == 1 {
2860 let n = N.fetch_add(1, Ordering::Relaxed) + 1;
2861 if n % 200 == 0 {
2862 eprintln!(
2863 "stageprof: planning {:.2} ms/tok | gdn-item {:.2} ms/tok | attn-item {:.2} ms/tok ({n} tok)",
2864 NS[1].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2865 NS[2].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
2866 NS[3].load(Ordering::Relaxed) as f64 / n as f64 / 1e6
2867 );
2868 }
2869 }
2870}
2871
2872pub fn weight_bytes_dispatched() -> u64 {
2875 let mut total = 0u64;
2876 #[cfg(target_os = "macos")]
2877 {
2878 total += crate::gpu_metal::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
2879 }
2880 #[cfg(feature = "gpu")]
2881 {
2882 total += crate::gpu_wgpu::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
2883 }
2884 total
2885}
2886
2887pub fn weight_bytes_by() -> [u64; 6] {
2890 #[cfg(target_os = "macos")]
2891 {
2892 let mut o = [0u64; 6];
2893 for (i, a) in crate::gpu_metal::WEIGHT_BYTES_BY.iter().enumerate() {
2894 o[i] = a.load(std::sync::atomic::Ordering::Relaxed);
2895 }
2896 return o;
2897 }
2898 #[allow(unreachable_code)]
2899 [0; 6]
2900}
2901
2902#[cfg(test)]
2903mod probe_warmup_tests {
2904 use super::*;
2905 use std::time::Duration;
2906
2907 fn ms(v: f64) -> Duration {
2908 Duration::from_nanos((v * 1e6) as u64)
2909 }
2910
2911 #[test]
2916 fn one_cold_first_sample_does_not_lose_the_class() {
2917 let p = Probe::new();
2918 probe_record_into(&p, "gemm-nt", None, true, ms(117.01));
2920 probe_record_into(&p, "gemm-nt", None, true, ms(1.1));
2921 probe_record_into(&p, "gemm-nt", None, true, ms(1.0));
2922 probe_record_into(&p, "gemm-nt", None, false, ms(3.19));
2923 probe_record_into(&p, "gemm-nt", None, false, ms(3.20));
2924 assert_eq!(
2925 p.state.load(Ordering::Relaxed),
2926 1,
2927 "the device is 3x faster once warm and must win"
2928 );
2929 }
2930
2931 #[test]
2935 fn the_warmup_is_spent_once_and_never_underflows() {
2936 let p = Probe::new();
2937 for _ in 0..8 {
2938 probe_record_into(&p, "matmat", None, true, ms(10.0));
2939 }
2940 assert_eq!(p.gpu_burn.load(Ordering::Relaxed), 0, "spent, not wrapped");
2941 assert_eq!(
2942 p.gpu_n.load(Ordering::Relaxed),
2943 7,
2944 "one sample burned, the rest counted"
2945 );
2946 }
2947
2948 #[test]
2954 fn a_class_whose_device_always_declines_settles_on_the_host() {
2955 let c = OpClass::MatmatWide;
2959 let p = &PROBES[c as usize];
2960 p.state.store(0, Ordering::Relaxed);
2961 p.declines.store(0, Ordering::Relaxed);
2962 for _ in 0..(PROBE_DECLINE_LIMIT - 1) {
2963 probe_note_decline(c);
2964 }
2965 assert_eq!(
2966 p.state.load(Ordering::Relaxed),
2967 0,
2968 "one short of the limit is still a question, not an answer"
2969 );
2970 probe_note_decline(c);
2971 assert_eq!(p.state.load(Ordering::Relaxed), 2, "settled on the host");
2972 assert!(matches!(probe_arm(c), ProbeArm::Cpu));
2973 p.state.store(0, Ordering::Relaxed);
2974 p.declines.store(0, Ordering::Relaxed);
2975 }
2976
2977 #[test]
2980 fn a_slow_device_still_loses_after_the_warmup() {
2981 let p = Probe::new();
2982 for _ in 0..4 {
2983 probe_record_into(&p, "matvec", None, true, ms(40.0));
2984 }
2985 for _ in 0..4 {
2986 probe_record_into(&p, "matvec", None, false, ms(2.0));
2987 }
2988 assert_eq!(p.state.load(Ordering::Relaxed), 2, "host wins on merit");
2989 }
2990}