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 fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
36 struct Restore(bool);
37 impl Drop for Restore {
38 fn drop(&mut self) {
39 CPU_ONLY.with(|c| c.set(self.0));
40 }
41 }
42 let previous = CPU_ONLY.with(|c| c.replace(true));
43 let _restore = Restore(previous);
44 f()
45}
46
47pub(crate) fn probe_note_cold() {
50 PROBE_COLD.with(|c| c.set(true));
51}
52
53pub(crate) fn probe_was_cold() -> bool {
57 PROBE_COLD.with(|c| c.get())
58}
59
60pub fn set_layer(l: i64) {
62 CUR_LAYER.with(|c| c.set(l));
63}
64
65pub fn cur_layer() -> i64 {
67 CUR_LAYER.with(|c| c.get())
68}
69
70fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
73 static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
74 R.get_or_init(|| {
75 let s = std::env::var("CMF_GPU_LAYERS").ok()?;
76 let mut v = Vec::new();
77 for part in s.split(',') {
78 let part = part.trim();
79 match part.split_once('-') {
80 Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
81 None => {
82 let x: i64 = part.parse().ok()?;
83 v.push((x, x));
84 }
85 }
86 }
87 Some(v)
88 })
89}
90
91fn layer_allowed() -> bool {
92 match layer_ranges() {
93 None => true,
94 Some(ranges) => {
95 let cur = CUR_LAYER.with(|c| c.get());
96 cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
97 }
98 }
99}
100
101pub fn enabled_here() -> bool {
105 !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
106}
107
108#[derive(Clone, Copy)]
120pub enum OpClass {
121 Ffn = 0,
123 Matvec = 1,
125 Matmat = 2,
127 Batch = 3,
129 MatmatWide = 4,
135 MatvecHead = 5,
142 GemmNt = 6,
149}
150
151pub fn matvec_class(rows: usize, cols: usize) -> OpClass {
155 if rows * cols >= 67_108_864 {
156 OpClass::MatvecHead
157 } else {
158 OpClass::Matvec
159 }
160}
161
162pub enum ProbeArm {
164 Gpu,
166 CpuTimed,
168 Cpu,
170}
171
172const PROBE_SAMPLES: u32 = 6;
174
175struct Probe {
176 state: AtomicU8,
178 flip: AtomicU32,
179 gpu_ns: AtomicU64,
180 gpu_n: AtomicU32,
181 cpu_ns: AtomicU64,
182 cpu_n: AtomicU32,
183 gpu_min: AtomicU64,
190 cpu_min: AtomicU64,
191}
192
193impl Probe {
194 const fn new() -> Self {
195 Self {
196 state: AtomicU8::new(0),
197 flip: AtomicU32::new(0),
198 gpu_ns: AtomicU64::new(0),
199 gpu_n: AtomicU32::new(0),
200 cpu_ns: AtomicU64::new(0),
201 cpu_n: AtomicU32::new(0),
202 gpu_min: AtomicU64::new(u64::MAX),
203 cpu_min: AtomicU64::new(u64::MAX),
204 }
205 }
206}
207
208static PROBES: [Probe; 7] = [
209 Probe::new(),
210 Probe::new(),
211 Probe::new(),
212 Probe::new(),
213 Probe::new(),
214 Probe::new(),
215 Probe::new(),
216];
217
218fn probe_on() -> bool {
219 static ON: OnceLock<bool> = OnceLock::new();
220 *ON.get_or_init(|| {
221 std::env::var("CMF_GPU_PROBE")
222 .map(|v| v != "0" && v != "off")
223 .unwrap_or(true)
224 })
225}
226
227pub fn q1_force() -> bool {
232 #[cfg(target_os = "macos")]
233 {
234 backend() == Backend::Metal
235 }
236 #[cfg(not(target_os = "macos"))]
237 {
238 false
239 }
240}
241
242pub fn fused_block_trusted() -> bool {
261 #[cfg(target_os = "macos")]
262 if backend() == Backend::Metal {
263 return true;
264 }
265 wgpu_graph_default()
266}
267
268pub fn probe_arm(c: OpClass) -> ProbeArm {
272 PROBE_COLD.with(|f| f.set(false));
277 if !probe_on() {
278 return ProbeArm::Gpu;
279 }
280 let p = &PROBES[c as usize];
281 match p.state.load(Ordering::Relaxed) {
282 1 => ProbeArm::Gpu,
283 2 => ProbeArm::Cpu,
284 _ => {
285 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
286 ProbeArm::Gpu
287 } else {
288 ProbeArm::CpuTimed
289 }
290 }
291 }
292}
293
294pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
297 let p = &PROBES[c as usize];
298 if p.state.load(Ordering::Relaxed) != 0 {
299 return;
300 }
301 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
302 return; }
304 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
305 if gpu {
306 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
307 p.gpu_n.fetch_add(1, Ordering::Relaxed);
308 p.gpu_min.fetch_min(ns, Ordering::Relaxed);
309 } else {
310 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
311 p.cpu_n.fetch_add(1, Ordering::Relaxed);
312 p.cpu_min.fetch_min(ns, Ordering::Relaxed);
313 }
314 let (gn, cn) = (
315 p.gpu_n.load(Ordering::Relaxed),
316 p.cpu_n.load(Ordering::Relaxed),
317 );
318 if gn >= 2 && cn >= 2 {
319 let g = p.gpu_min.load(Ordering::Relaxed) as f64;
323 let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
324 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
327 return;
328 }
329 let winner = if g <= cp { 1 } else { 2 };
330 if p.state
331 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
332 .is_ok()
333 {
334 tracing::info!(
335 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
336 ["ffn", "matvec", "matmat", "qkv-batch", "matmat-wide", "lm-head", "gemm-nt"]
337 [c as usize],
338 g / 1e6,
339 cp / 1e6,
340 if winner == 1 { "gpu" } else { "cpu" },
341 );
342 }
343 }
344}
345
346pub fn probe_deciding(c: OpClass) -> bool {
349 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
350}
351
352#[allow(unused_variables)]
362pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
363 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
364 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
365 let resident = match backend() {
366 #[cfg(target_os = "macos")]
367 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
368 #[cfg(feature = "gpu")]
369 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
370 Backend::None => false,
371 };
372 if !resident && may_upload {
373 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
374 }
375 resident
376}
377
378#[cfg(test)]
380pub(crate) fn probe_reset() {
381 for p in &PROBES {
382 p.state.store(0, Ordering::Relaxed);
383 p.flip.store(0, Ordering::Relaxed);
384 p.gpu_ns.store(0, Ordering::Relaxed);
385 p.gpu_n.store(0, Ordering::Relaxed);
386 p.cpu_ns.store(0, Ordering::Relaxed);
387 p.cpu_n.store(0, Ordering::Relaxed);
388 }
389}
390
391#[cfg(test)]
392mod probe_tests {
393 use super::*;
394 use std::time::Duration;
395
396 #[test]
399 fn probe_alternates_discards_cold_and_decides() {
400 probe_reset();
401 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
403 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
404
405 probe_note_cold();
409 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
410 for _ in 0..PROBE_SAMPLES {
411 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
412 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
413 }
414 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
415
416 for _ in 0..PROBE_SAMPLES {
418 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
419 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
420 }
421 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
422
423 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
425 CPU_ONLY.with(|c| assert!(!c.get()));
426 cpu_scope(|| {
427 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
428 CPU_ONLY.with(|c| assert!(c.get()));
429 });
430 let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
431 CPU_ONLY.with(|c| assert!(!c.get()));
432 probe_reset();
433 }
434}
435
436pub const GPU_MIN_ROWS: usize = 65_536;
439
440pub fn min_rows() -> usize {
447 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
448 .ok()
449 .and_then(|v| v.parse().ok())
450 {
451 return v;
452 }
453 if discrete() { 4096 } else { GPU_MIN_ROWS }
454}
455
456pub fn discrete() -> bool {
458 match backend() {
459 #[cfg(feature = "gpu")]
460 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
461 #[cfg(target_os = "macos")]
462 Backend::Metal => false, Backend::None => false,
464 }
465}
466
467pub struct MoeJob<'a> {
471 pub gate: (usize, usize, usize, &'a [f32]),
472 pub up: (usize, usize, usize, &'a [f32]),
473 pub down: (usize, usize, usize, &'a [f32]),
474 pub xs_gate: Vec<f32>,
475 pub xs_up: Vec<f32>,
476 pub down_col: &'a [f32],
477 pub w: f32,
478 pub q1: bool,
481 pub q4t: bool,
484 pub q4tp: bool,
488 pub swiglu_limit: f32,
493}
494
495pub struct BatchJob<'a> {
497 pub idx: usize,
498 pub rows: usize,
499 pub cols: usize,
500 pub row_scale: &'a [f32],
501 pub xs: Vec<f32>,
502 pub layout: BatchLayout,
506}
507
508#[derive(Clone, Copy, PartialEq, Eq, Debug)]
511pub enum BatchLayout {
512 Q8,
513 Q1,
514 Q4t,
515 Q4tp,
516}
517
518#[derive(Clone, Copy, PartialEq, Eq)]
519enum Backend {
520 None,
521 #[cfg(target_os = "macos")]
522 Metal,
523 #[cfg(feature = "gpu")]
524 Wgpu,
525}
526
527fn backend() -> Backend {
528 #[cfg(feature = "gpu")]
529 if crate::gpu_wgpu::selected() {
530 return if crate::gpu_wgpu::enabled() {
531 Backend::Wgpu
532 } else {
533 Backend::None
534 };
535 }
536 #[cfg(target_os = "macos")]
537 if crate::gpu_metal::enabled() {
538 return Backend::Metal;
539 }
540 Backend::None
541}
542
543pub fn backend_available() -> bool {
549 #[cfg(target_os = "macos")]
550 {
551 true
553 }
554 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
555 {
556 static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
557 *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
558 }
559 #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
560 {
561 false
562 }
563}
564
565pub fn enabled() -> bool {
566 backend() != Backend::None
567}
568
569pub fn wgpu_active() -> bool {
583 #[cfg(feature = "gpu")]
584 {
585 matches!(backend(), Backend::Wgpu)
586 }
587 #[cfg(not(feature = "gpu"))]
588 {
589 false
590 }
591}
592
593pub fn wgpu_graph_default() -> bool {
594 #[cfg(feature = "gpu")]
595 {
596 matches!(backend(), Backend::Wgpu)
602 && (crate::gpu_wgpu::discrete_active()
603 || (cfg!(target_os = "macos") && crate::gpu_wgpu::adapter_up()))
604 }
605 #[cfg(not(feature = "gpu"))]
606 {
607 false
608 }
609}
610
611#[allow(clippy::too_many_arguments, unused_variables)]
613pub fn q8_matvec_range(
614 model: &Arc<CmfModel>,
615 idx: usize,
616 row0: usize,
617 row_scale: &[f32],
618 xs: &[f32],
619 rows: usize,
620 cols: usize,
621 out: &mut [f32],
622) -> bool {
623 match backend() {
624 #[cfg(target_os = "macos")]
625 Backend::Metal => {
626 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
627 }
628 #[cfg(feature = "gpu")]
629 Backend::Wgpu => {
630 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
631 }
632 Backend::None => false,
633 }
634}
635
636#[allow(clippy::too_many_arguments, unused_variables)]
639pub fn q8_matmat(
640 model: &Arc<CmfModel>,
641 idx: usize,
642 row_scale: &[f32],
643 pre: &[f32],
644 b: usize,
645 rows: usize,
646 cols: usize,
647 out: &mut [f32],
648) -> bool {
649 match backend() {
650 #[cfg(target_os = "macos")]
651 Backend::Metal => {
652 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
653 }
654 #[cfg(feature = "gpu")]
655 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
656 Backend::None => false,
657 }
658}
659
660#[allow(unused_variables)]
663pub fn q1_matvec(
664 model: &Arc<CmfModel>,
665 idx: usize,
666 xs: &[f32],
667 rows: usize,
668 cols: usize,
669 out: &mut [f32],
670) -> bool {
671 match backend() {
672 #[cfg(target_os = "macos")]
673 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
674 #[cfg(feature = "gpu")]
675 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
676 Backend::None => false,
677 }
678}
679
680#[allow(clippy::too_many_arguments)]
684pub fn attn_dropin(
685 model: &Arc<CmfModel>,
686 kv_id: u64,
687 layer: usize,
688 normed: &[f32],
689 wq_idx: usize,
690 wk_idx: usize,
691 wv_idx: usize,
692 wo_idx: usize,
693 q_norm: Option<&[f32]>,
694 k_norm: Option<&[f32]>,
695 invf: &[f32],
696 nh: usize,
697 nkv: usize,
698 hd: usize,
699 rd: usize,
700 hidden: usize,
701 pos: usize,
702 cap: usize,
703 gemma: bool,
704 eps: f32,
705 cpu_k: &[Vec<f32>],
706 cpu_v: &[Vec<f32>],
707 out: &mut [f32],
708) -> bool {
709 match backend() {
710 #[cfg(feature = "gpu")]
711 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
712 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
713 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
714 ),
715 #[allow(unused_variables)]
716 _ => false,
717 }
718}
719
720pub struct GraphW<'a> {
724 pub idx: usize,
725 pub kind: u8,
726 pub row_scale: &'a [f32],
727 pub data: &'a [f32],
728}
729
730pub enum GraphAttn<'a> {
733 Full {
734 wq: GraphW<'a>,
735 wk: GraphW<'a>,
736 wv: GraphW<'a>,
737 wo: GraphW<'a>,
738 q_norm: Option<&'a [f32]>,
739 k_norm: Option<&'a [f32]>,
740 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
742 output_gate: bool,
745 cpu_k: &'a [Vec<f32>],
746 cpu_v: &'a [Vec<f32>],
747 },
748 Gdn {
749 qkv: GraphW<'a>,
750 z: GraphW<'a>,
751 a: GraphW<'a>,
752 b: GraphW<'a>,
753 out: GraphW<'a>,
754 conv1d: &'a [f32],
755 a_log: &'a [f32],
756 dt_bias: &'a [f32],
757 norm: &'a [f32],
758 nv: usize,
759 nk: usize,
760 dk: usize,
761 dv: usize,
762 kk: usize,
763 cpu_state: &'a [f32],
768 },
769}
770
771pub struct GraphLayer<'a> {
773 pub input_norm: &'a [f32],
774 pub attn: GraphAttn<'a>,
775 pub post_norm: &'a [f32],
776 pub ffn: GraphFfn<'a>,
777}
778
779pub enum GraphFfn<'a> {
784 Dense {
785 gate: GraphW<'a>,
786 up: GraphW<'a>,
787 down: GraphW<'a>,
788 },
789 Moe {
790 router: GraphW<'a>,
792 shared_gate: GraphW<'a>,
794 experts: Vec<(usize, usize, usize)>,
798 n_exp: usize,
800 top_k: usize,
801 inter: usize,
802 norm_topk: bool,
803 q4tp: bool,
809 gu_q2: bool,
813 },
814}
815
816#[allow(clippy::too_many_arguments)]
821pub fn forward_token_graph(
822 model: &Arc<CmfModel>,
823 kv_id: u64,
824 layers: &[GraphLayer],
825 o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
828 o1_epoch: u64,
829 invf: &[f32],
830 h: &mut [f32],
831 nh: usize,
832 nkv: usize,
833 hd: usize,
834 rd: usize,
835 hidden: usize,
836 inter: usize,
837 position: usize,
838 cap: usize,
839 gemma: bool,
840 eps: f32,
841 lm_head: Option<(&GraphW, usize)>,
842 final_norm: &[f32],
843 logits: &mut Vec<f32>,
844 loop_norm_at: &[usize],
845 steps: usize,
846 embed: Option<(&GraphW, usize, f32)>,
847 ids_out: Option<&mut Vec<u32>>,
848 layers_run: Option<&mut usize>,
851) -> bool {
852 match backend() {
853 #[cfg(feature = "gpu")]
854 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
855 model,
856 kv_id,
857 layers,
858 o1,
859 o1_epoch,
860 invf,
861 h,
862 nh,
863 nkv,
864 hd,
865 rd,
866 hidden,
867 inter,
868 position,
869 cap,
870 gemma,
871 eps,
872 lm_head,
873 final_norm,
874 logits,
875 loop_norm_at,
876 steps,
877 embed,
878 ids_out,
879 layers_run,
880 ),
881 #[allow(unused_variables)]
882 _ => {
883 let _ = (lm_head, final_norm, logits, loop_norm_at, layers_run);
884 false
885 }
886 }
887}
888
889pub struct SpecTail<'a> {
893 pub lm: GraphW<'a>,
894 pub lm_rows: usize,
895 pub final_norm: &'a [f32],
896 pub logits_out: &'a mut Vec<f32>,
897}
898
899#[allow(clippy::too_many_arguments)]
903pub fn forward_batch_graph(
904 model: &Arc<CmfModel>,
905 kv_id: u64,
906 layers: &[GraphLayer],
907 invf: &[f32],
908 h: &mut [f32],
909 nh: usize,
910 nkv: usize,
911 hd: usize,
912 rd: usize,
913 hidden: usize,
914 inter: usize,
915 positions: &[usize],
916 cap: usize,
917 gemma: bool,
918 eps: f32,
919 k: usize,
920 spec: Option<SpecTail<'_>>,
921) -> bool {
922 match backend() {
923 #[cfg(feature = "gpu")]
924 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
925 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
926 eps, k, spec,
927 ),
928 #[allow(unreachable_patterns)]
929 _ => {
930 let _ = spec;
931 false
932 }
933 }
934}
935
936pub fn gdn_spec_restore(kv_id: u64, slot: usize) -> bool {
939 #[cfg(feature = "gpu")]
940 if backend() == Backend::Wgpu {
941 return crate::gpu_wgpu::gdn_spec_restore(kv_id, slot);
942 }
943 #[allow(unreachable_code)]
944 {
945 let _ = (kv_id, slot);
946 false
947 }
948}
949
950pub fn graph_kv_reset(_kv_id: u64) {
952 #[cfg(feature = "gpu")]
953 if backend() == Backend::Wgpu {
954 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
955 }
956}
957
958pub fn q1t_matvec(
962 model: &Arc<CmfModel>,
963 idx: usize,
964 xs: &[f32],
965 rows: usize,
966 cols: usize,
967 out: &mut [f32],
968) -> bool {
969 match backend() {
970 #[cfg(target_os = "macos")]
971 Backend::Metal => {
972 if metal_q1t_enabled() {
973 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
974 } else {
975 false
976 }
977 }
978 #[cfg(feature = "gpu")]
979 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
980 Backend::None => false,
981 }
982}
983
984#[allow(unused_variables)]
987pub fn q4b_matvec(
988 model: &Arc<CmfModel>,
989 idx: usize,
990 xs: &[f32],
991 rows: usize,
992 cols: usize,
993 out: &mut [f32],
994) -> bool {
995 match backend() {
996 #[cfg(target_os = "macos")]
997 Backend::Metal => false,
998 #[cfg(feature = "gpu")]
999 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
1000 Backend::None => false,
1001 }
1002}
1003
1004pub fn q1t_matmat(
1007 model: &Arc<CmfModel>,
1008 idx: usize,
1009 xs: &[f32],
1010 b: usize,
1011 rows: usize,
1012 cols: usize,
1013 out: &mut [f32],
1014) -> bool {
1015 match backend() {
1016 #[cfg(target_os = "macos")]
1017 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
1021 #[cfg(feature = "gpu")]
1022 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
1023 Backend::None => false,
1024 }
1025}
1026
1027#[cfg(target_os = "macos")]
1031pub(crate) fn metal_q1t_enabled() -> bool {
1032 std::env::var("CMF_METAL_Q1T")
1033 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
1034 .unwrap_or(true)
1035}
1036
1037pub fn q1_matmat(
1039 model: &Arc<CmfModel>,
1040 idx: usize,
1041 xs: &[f32],
1042 b: usize,
1043 rows: usize,
1044 cols: usize,
1045 out: &mut [f32],
1046) -> bool {
1047 match backend() {
1048 #[cfg(feature = "gpu")]
1049 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
1050 #[allow(unused_variables)]
1051 _ => false,
1052 }
1053}
1054
1055static MM_KILL: AtomicBool = AtomicBool::new(false);
1060pub(crate) fn mm_killed() -> bool {
1061 MM_KILL.load(Ordering::Relaxed)
1062}
1063pub(crate) fn mm_kill() {
1064 MM_KILL.store(true, Ordering::Relaxed);
1065}
1066
1067#[allow(unused_variables, clippy::too_many_arguments)]
1072pub fn chunk_attend(
1073 q: &[f32],
1074 k: &[&[f32]],
1075 v: &[&[f32]],
1076 b: usize,
1077 s0: usize,
1078 nh: usize,
1079 nkv: usize,
1080 hd: usize,
1081 scale: f32,
1082 out: &mut [f32],
1083) -> bool {
1084 match backend() {
1085 #[cfg(feature = "gpu")]
1086 Backend::Wgpu => crate::gpu_wgpu::chunk_attend(q, k, v, b, s0, nh, nkv, hd, scale, out),
1087 #[allow(unreachable_patterns)]
1088 _ => false,
1089 }
1090}
1091
1092#[allow(unused_variables, clippy::too_many_arguments)]
1096pub fn q4t_qkv(
1097 model: &Arc<CmfModel>,
1098 wq: usize,
1099 wk: usize,
1100 wv: usize,
1101 xs: &[f32],
1102 b: usize,
1103 cols: usize,
1104 rq: usize,
1105 rk: usize,
1106 rv: usize,
1107 out: &mut [f32],
1108) -> bool {
1109 match backend() {
1110 #[cfg(feature = "gpu")]
1111 Backend::Wgpu => crate::gpu_wgpu::q4t_qkv(model, wq, wk, wv, xs, b, cols, rq, rk, rv, out),
1112 #[allow(unreachable_patterns)]
1113 _ => false,
1114 }
1115}
1116
1117#[allow(unused_variables, clippy::too_many_arguments)]
1119pub fn q4tp_ffn(
1120 model: &Arc<CmfModel>,
1121 w1: usize,
1122 w3: usize,
1123 w2: usize,
1124 xs: &[f32],
1125 b: usize,
1126 hidden: usize,
1127 inter: usize,
1128 out: &mut [f32],
1129) -> bool {
1130 match backend() {
1131 #[cfg(target_os = "macos")]
1132 Backend::Metal => crate::gpu_metal::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1133 #[cfg(feature = "gpu")]
1134 Backend::Wgpu => crate::gpu_wgpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1135 #[allow(unreachable_patterns)]
1136 _ => false,
1137 }
1138}
1139
1140pub fn q4t_ffn(
1141 model: &Arc<CmfModel>,
1142 w1: usize,
1143 w3: usize,
1144 w2: usize,
1145 xs: &[f32],
1146 b: usize,
1147 hidden: usize,
1148 inter: usize,
1149 out: &mut [f32],
1150) -> bool {
1151 match backend() {
1152 #[cfg(target_os = "macos")]
1153 Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1154 #[cfg(feature = "gpu")]
1155 Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1156 #[allow(unreachable_patterns)]
1157 _ => false,
1158 }
1159}
1160
1161pub struct DitBlockArgs<'a> {
1166 pub n: usize,
1167 pub hidden: usize,
1168 pub inter: usize,
1169 pub nh: usize,
1170 pub nkv: usize,
1171 pub hd: usize,
1172 pub eps: f32,
1173 pub rope_cos: &'a [f32],
1174 pub rope_sin: &'a [f32],
1175 pub norm1: &'a [f32],
1176 pub norm2: &'a [f32],
1177 pub ffn_norm1: &'a [f32],
1178 pub ffn_norm2: &'a [f32],
1179 pub norm_q: &'a [f32],
1180 pub norm_k: &'a [f32],
1181 pub s_msa: &'a [f32],
1182 pub gate_msa: &'a [f32],
1183 pub s_mlp: &'a [f32],
1184 pub gate_mlp: &'a [f32],
1185 pub wq: usize,
1186 pub wk: usize,
1187 pub wv: usize,
1188 pub wo: usize,
1189 pub w1: usize,
1190 pub w3: usize,
1191 pub w2: usize,
1192 pub q4tp: bool,
1196 pub resident_in: bool,
1199 pub resident_out: bool,
1203}
1204
1205pub fn dit_chain_supported() -> bool {
1209 #[cfg(feature = "gpu")]
1210 {
1211 return matches!(backend(), Backend::Wgpu) && fused_dit_block_available();
1212 }
1213 #[allow(unreachable_code)]
1214 false
1215}
1216
1217pub fn dit_state_fetch(_x: &mut [f32]) -> bool {
1220 #[cfg(feature = "gpu")]
1221 {
1222 if matches!(backend(), Backend::Wgpu) {
1223 return crate::gpu_wgpu::dit_state_fetch(_x);
1224 }
1225 }
1226 false
1227}
1228
1229#[allow(unused_variables)]
1233#[allow(unused_variables, clippy::too_many_arguments)]
1237pub fn dit_qkv(
1238 model: &Arc<CmfModel>,
1239 wq: usize,
1240 wk: usize,
1241 wv: usize,
1242 xs: &[f32],
1243 b: usize,
1244 hidden: usize,
1245 qrows: usize,
1246 kvrows: usize,
1247 q_out: &mut [f32],
1248 k_out: &mut [f32],
1249 v_out: &mut [f32],
1250) -> bool {
1251 match backend() {
1252 #[cfg(feature = "gpu")]
1253 Backend::Wgpu => crate::gpu_wgpu::q4tp_qkv(
1254 model, wq, wk, wv, xs, b, hidden, qrows, kvrows, q_out, k_out, v_out,
1255 ),
1256 #[allow(unreachable_patterns)]
1257 _ => false,
1258 }
1259}
1260
1261pub fn fused_dit_block_available() -> bool {
1265 #[cfg(target_os = "macos")]
1266 {
1267 matches!(backend(), Backend::Metal) && fused_block_trusted()
1268 }
1269 #[cfg(not(target_os = "macos"))]
1270 {
1271 false
1272 }
1273}
1274
1275pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
1276 dit_block_seg(model, a, &[a.n], x)
1277}
1278
1279pub fn dit_block_seg(
1283 model: &Arc<CmfModel>,
1284 a: &DitBlockArgs,
1285 segs: &[usize],
1286 x: &mut [f32],
1287) -> bool {
1288 match backend() {
1289 #[cfg(target_os = "macos")]
1290 Backend::Metal if segs.len() <= 1 => crate::gpu_metal::dit_block(model, a, x),
1291 #[cfg(feature = "gpu")]
1298 Backend::Wgpu
1299 if match std::env::var("CMF_DIT_FUSED").ok().as_deref() {
1300 Some("0") => false,
1301 Some(_) => true,
1302 None => crate::gpu_wgpu::discrete_active(),
1303 } =>
1304 {
1305 crate::gpu_wgpu::dit_block_seg(model, a, segs, x)
1306 }
1307 #[allow(unreachable_patterns)]
1308 _ => false,
1309 }
1310}
1311
1312pub struct VaeResnetArgs<'a> {
1316 pub groups: usize,
1317 pub ic: usize,
1318 pub oc: usize,
1319 pub h: usize,
1320 pub w: usize,
1321 pub n1w: &'a [f32],
1322 pub n1b: &'a [f32],
1323 pub c1w: &'a [f32],
1324 pub c1b: &'a [f32],
1325 pub c1k: usize,
1326 pub n2w: &'a [f32],
1327 pub n2b: &'a [f32],
1328 pub c2w: &'a [f32],
1329 pub c2b: &'a [f32],
1330 pub c2k: usize,
1331 pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
1332}
1333
1334#[allow(unused_variables)]
1337pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
1338 match backend() {
1339 #[cfg(target_os = "macos")]
1340 Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
1341 _ => false,
1342 }
1343}
1344
1345#[allow(unused_variables, clippy::too_many_arguments)]
1348pub fn vae_upsample_conv(
1349 w: &[f32],
1350 bias: &[f32],
1351 x: &[f32],
1352 ic: usize,
1353 oc: usize,
1354 h: usize,
1355 w_img: usize,
1356 k: usize,
1357 out: &mut [f32],
1358) -> bool {
1359 match backend() {
1360 #[cfg(target_os = "macos")]
1361 Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
1362 #[cfg(feature = "gpu")]
1363 Backend::Wgpu => {
1364 crate::gpu_wgpu::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out)
1365 }
1366 #[allow(unreachable_patterns)]
1367 _ => false,
1368 }
1369}
1370
1371#[allow(unused_variables, clippy::too_many_arguments)]
1374pub fn vae_conv2d(
1375 w: &[f32],
1376 bias: &[f32],
1377 x: &[f32],
1378 ic: usize,
1379 oc: usize,
1380 h: usize,
1381 w_img: usize,
1382 k: usize,
1383 out: &mut [f32],
1384) -> bool {
1385 match backend() {
1386 #[cfg(target_os = "macos")]
1387 Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
1388 #[cfg(feature = "gpu")]
1389 Backend::Wgpu => crate::gpu_wgpu::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
1390 #[allow(unreachable_patterns)]
1391 _ => false,
1392 }
1393}
1394
1395#[allow(unused_variables, clippy::too_many_arguments)]
1399pub fn dit_attention(
1400 qh: &[f32],
1401 kh: &[f32],
1402 vh: &[f32],
1403 nh: usize,
1404 nkv: usize,
1405 n: usize,
1406 hd: usize,
1407 scale: f32,
1408 out: &mut [f32],
1409) -> bool {
1410 match backend() {
1411 #[cfg(target_os = "macos")]
1412 Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
1413 #[cfg(feature = "gpu")]
1414 Backend::Wgpu => crate::gpu_wgpu::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
1415 #[allow(unreachable_patterns)]
1416 _ => false,
1417 }
1418}
1419
1420#[allow(unused_variables)]
1425pub fn q4tp_matmat(
1426 model: &Arc<CmfModel>,
1427 idx: usize,
1428 xs: &[f32],
1429 b: usize,
1430 rows: usize,
1431 cols: usize,
1432 out: &mut [f32],
1433) -> bool {
1434 match backend() {
1435 #[cfg(target_os = "macos")]
1436 Backend::Metal => crate::gpu_metal::q4tp_matmat(model, idx, xs, b, rows, cols, out),
1437 #[cfg(feature = "gpu")]
1438 Backend::Wgpu => crate::gpu_wgpu::q4tp_matmat(model, idx, xs, b, rows, cols, out),
1439 #[allow(unreachable_patterns)]
1440 _ => false,
1441 }
1442}
1443
1444pub fn q2tp_matmat(
1447 model: &Arc<CmfModel>,
1448 idx: usize,
1449 xs: &[f32],
1450 b: usize,
1451 rows: usize,
1452 cols: usize,
1453 out: &mut [f32],
1454) -> bool {
1455 match backend() {
1456 #[cfg(feature = "gpu")]
1457 Backend::Wgpu => crate::gpu_wgpu::q2tp_matmat(model, idx, xs, b, rows, cols, out),
1458 #[allow(unreachable_patterns)]
1459 _ => false,
1460 }
1461}
1462
1463pub fn q4tp_matvec(
1468 model: &Arc<CmfModel>,
1469 idx: usize,
1470 xs: &[f32],
1471 rows: usize,
1472 cols: usize,
1473 out: &mut [f32],
1474) -> bool {
1475 match backend() {
1476 #[cfg(target_os = "macos")]
1477 Backend::Metal => crate::gpu_metal::q4tp_matvec_for_test(model, idx, xs, rows, cols, out),
1478 #[cfg(feature = "gpu")]
1479 Backend::Wgpu => crate::gpu_wgpu::q4tp_matvec(model, idx, xs, rows, cols, out),
1480 #[allow(unreachable_patterns)]
1481 _ => false,
1482 }
1483}
1484
1485pub fn q4t_matmat(
1486 model: &Arc<CmfModel>,
1487 idx: usize,
1488 xs: &[f32],
1489 b: usize,
1490 rows: usize,
1491 cols: usize,
1492 out: &mut [f32],
1493) -> bool {
1494 match backend() {
1495 #[cfg(target_os = "macos")]
1496 Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
1497 #[cfg(feature = "gpu")]
1498 Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
1499 #[allow(unreachable_patterns)]
1500 _ => false,
1501 }
1502}
1503
1504#[cfg(target_os = "macos")]
1506pub use crate::gpu_metal::{
1507 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
1508 kv_mirror_read_last, kv_mirror_take_imp,
1509};
1510
1511#[cfg(target_os = "macos")]
1513pub fn gdn_block(
1514 model: &Arc<CmfModel>,
1515 layers: &[GdnGpuLayer],
1516 states: &mut [&mut [f32]],
1517 cfg: &GdnGpuCfg,
1518 h: &mut [f32],
1519) -> bool {
1520 match backend() {
1521 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
1522 _ => false,
1523 }
1524}
1525
1526#[allow(unused_variables)]
1528pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
1529 match backend() {
1530 #[cfg(target_os = "macos")]
1531 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
1532 #[cfg(feature = "gpu")]
1533 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
1534 Backend::None => false,
1535 }
1536}
1537
1538#[allow(unused_variables)]
1540pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
1541 match backend() {
1542 #[cfg(target_os = "macos")]
1543 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
1544 #[cfg(feature = "gpu")]
1545 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
1546 Backend::None => false,
1547 }
1548}
1549
1550static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
1566static 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)];
1570
1571const GRAPH_RACE_SAMPLES: u32 = 4;
1573
1574pub fn graph_race_begin_generation() {
1577 GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
1578 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1579 return;
1580 }
1581 let (gn, cn) = (
1582 GRAPH_N[1].load(Ordering::Relaxed),
1583 GRAPH_N[0].load(Ordering::Relaxed),
1584 );
1585 if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
1586 let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
1587 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1588 let verdict = if g_avg < c_avg { 1 } else { 2 };
1589 GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
1590 tracing::info!(
1591 "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
1592 g_avg as f64 / 1e6,
1593 c_avg as f64 / 1e6,
1594 if verdict == 1 { "graph" } else { "normal path" }
1595 );
1596 return;
1597 }
1598 let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
1599 GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
1600}
1601
1602pub fn graph_race_use_graph(trusted: bool) -> bool {
1606 if trusted {
1607 return true;
1608 }
1609 match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
1610 1 => true,
1611 2 => false,
1612 _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
1613 }
1614}
1615
1616pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
1621 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1622 return false;
1623 }
1624 let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
1625 let cn = GRAPH_N[0].load(Ordering::Relaxed);
1626 if !first || cn == 0 {
1627 return false;
1628 }
1629 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1630 let ns = dur.as_nanos() as u64;
1631 if ns > 1_000_000_000 && ns > 4 * c_avg {
1632 GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
1633 tracing::info!(
1634 "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
1635 ns as f64 / 1e6,
1636 c_avg as f64 / 1e6
1637 );
1638 return true;
1639 }
1640 false
1641}
1642
1643pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
1647 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1648 return;
1649 }
1650 let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
1651 if tok == 0 {
1652 return;
1653 }
1654 let i = used_graph as usize;
1655 GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
1656 GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
1657}
1658
1659pub(crate) fn fp_bytes(data: &[u8]) -> u64 {
1669 #[inline]
1670 fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
1671 let (chunks, tail) = bytes.split_at(bytes.len() & !7);
1672 for c in chunks.chunks_exact(8) {
1673 h ^= u64::from_le_bytes(c.try_into().unwrap());
1674 h = h.wrapping_mul(0x100_0000_01b3);
1675 }
1676 for &b in tail {
1677 h ^= b as u64;
1678 h = h.wrapping_mul(0x100_0000_01b3);
1679 }
1680 h
1681 }
1682 let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (data.len() as u64);
1683 if data.len() <= 4096 {
1684 return fnv(h, data);
1685 }
1686 let step = (data.len() - 64) / 63;
1687 for i in 0..64 {
1688 h = fnv(h, &data[i * step..i * step + 64]);
1689 }
1690 h
1691}
1692
1693pub(crate) fn fp_f32(data: &[f32]) -> u64 {
1696 let bytes =
1697 unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
1698 fp_bytes(bytes)
1699}
1700
1701#[cfg(test)]
1702mod fp_tests {
1703 use super::fp_bytes;
1704
1705 #[test]
1710 fn fp_bytes_sees_a_change_anywhere_in_a_sampled_slice() {
1711 let n = 1 << 20; let base: Vec<u8> = (0..n).map(|i| (i * 31 + 7) as u8).collect();
1713 let h0 = fp_bytes(&base);
1714 assert_eq!(h0, fp_bytes(&base), "fingerprint must be deterministic");
1715 let mut dense = base.clone();
1718 for b in dense.iter_mut() {
1719 *b = b.wrapping_add(1);
1720 }
1721 assert_ne!(h0, fp_bytes(&dense), "a fully different tensor slipped through");
1722 assert_ne!(h0, fp_bytes(&base[..n - 64]));
1725 let mut small = vec![3u8; 4096];
1728 let hs = fp_bytes(&small);
1729 small[2048] ^= 1;
1730 assert_ne!(hs, fp_bytes(&small), "full hash missed a one-byte change");
1731 for n in [4097usize, 5000, 64 * 64, 1 << 16] {
1733 let v = vec![9u8; n];
1734 let _ = fp_bytes(&v); }
1736 }
1737}
1738
1739pub fn bake_release() {
1743 #[cfg(feature = "gpu")]
1744 crate::gpu_wgpu::bake_release();
1745}
1746
1747pub fn bake_precision_strict(on: bool) {
1751 #[cfg(feature = "gpu")]
1752 crate::gpu_wgpu::bake_precision_strict(on);
1753 #[cfg(not(feature = "gpu"))]
1754 let _ = on;
1755}