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}
136
137pub enum ProbeArm {
139 Gpu,
141 CpuTimed,
143 Cpu,
145}
146
147const PROBE_SAMPLES: u32 = 6;
149
150struct Probe {
151 state: AtomicU8,
153 flip: AtomicU32,
154 gpu_ns: AtomicU64,
155 gpu_n: AtomicU32,
156 cpu_ns: AtomicU64,
157 cpu_n: AtomicU32,
158 gpu_min: AtomicU64,
165 cpu_min: AtomicU64,
166}
167
168impl Probe {
169 const fn new() -> Self {
170 Self {
171 state: AtomicU8::new(0),
172 flip: AtomicU32::new(0),
173 gpu_ns: AtomicU64::new(0),
174 gpu_n: AtomicU32::new(0),
175 cpu_ns: AtomicU64::new(0),
176 cpu_n: AtomicU32::new(0),
177 gpu_min: AtomicU64::new(u64::MAX),
178 cpu_min: AtomicU64::new(u64::MAX),
179 }
180 }
181}
182
183static PROBES: [Probe; 5] = [
184 Probe::new(),
185 Probe::new(),
186 Probe::new(),
187 Probe::new(),
188 Probe::new(),
189];
190
191fn probe_on() -> bool {
192 static ON: OnceLock<bool> = OnceLock::new();
193 *ON.get_or_init(|| {
194 std::env::var("CMF_GPU_PROBE")
195 .map(|v| v != "0" && v != "off")
196 .unwrap_or(true)
197 })
198}
199
200pub fn q1_force() -> bool {
205 #[cfg(target_os = "macos")]
206 {
207 backend() == Backend::Metal
208 }
209 #[cfg(not(target_os = "macos"))]
210 {
211 false
212 }
213}
214
215pub fn fused_block_trusted() -> bool {
228 #[cfg(target_os = "macos")]
229 {
230 backend() == Backend::Metal
231 }
232 #[cfg(not(target_os = "macos"))]
233 {
234 false
235 }
236}
237
238pub fn probe_arm(c: OpClass) -> ProbeArm {
242 PROBE_COLD.with(|f| f.set(false));
247 if !probe_on() {
248 return ProbeArm::Gpu;
249 }
250 let p = &PROBES[c as usize];
251 match p.state.load(Ordering::Relaxed) {
252 1 => ProbeArm::Gpu,
253 2 => ProbeArm::Cpu,
254 _ => {
255 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
256 ProbeArm::Gpu
257 } else {
258 ProbeArm::CpuTimed
259 }
260 }
261 }
262}
263
264pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
267 let p = &PROBES[c as usize];
268 if p.state.load(Ordering::Relaxed) != 0 {
269 return;
270 }
271 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
272 return; }
274 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
275 if gpu {
276 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
277 p.gpu_n.fetch_add(1, Ordering::Relaxed);
278 p.gpu_min.fetch_min(ns, Ordering::Relaxed);
279 } else {
280 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
281 p.cpu_n.fetch_add(1, Ordering::Relaxed);
282 p.cpu_min.fetch_min(ns, Ordering::Relaxed);
283 }
284 let (gn, cn) = (
285 p.gpu_n.load(Ordering::Relaxed),
286 p.cpu_n.load(Ordering::Relaxed),
287 );
288 if gn >= 2 && cn >= 2 {
289 let g = p.gpu_min.load(Ordering::Relaxed) as f64;
293 let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
294 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
297 return;
298 }
299 let winner = if g <= cp { 1 } else { 2 };
300 if p.state
301 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
302 .is_ok()
303 {
304 tracing::info!(
305 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
306 ["ffn", "matvec", "matmat", "qkv-batch", "matmat-wide"][c as usize],
307 g / 1e6,
308 cp / 1e6,
309 if winner == 1 { "gpu" } else { "cpu" },
310 );
311 }
312 }
313}
314
315pub fn probe_deciding(c: OpClass) -> bool {
318 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
319}
320
321#[allow(unused_variables)]
331pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
332 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
333 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
334 let resident = match backend() {
335 #[cfg(target_os = "macos")]
336 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
337 #[cfg(feature = "gpu")]
338 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
339 Backend::None => false,
340 };
341 if !resident && may_upload {
342 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
343 }
344 resident
345}
346
347#[cfg(test)]
349pub(crate) fn probe_reset() {
350 for p in &PROBES {
351 p.state.store(0, Ordering::Relaxed);
352 p.flip.store(0, Ordering::Relaxed);
353 p.gpu_ns.store(0, Ordering::Relaxed);
354 p.gpu_n.store(0, Ordering::Relaxed);
355 p.cpu_ns.store(0, Ordering::Relaxed);
356 p.cpu_n.store(0, Ordering::Relaxed);
357 }
358}
359
360#[cfg(test)]
361mod probe_tests {
362 use super::*;
363 use std::time::Duration;
364
365 #[test]
368 fn probe_alternates_discards_cold_and_decides() {
369 probe_reset();
370 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
372 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
373
374 probe_note_cold();
378 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
379 for _ in 0..PROBE_SAMPLES {
380 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
381 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
382 }
383 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
384
385 for _ in 0..PROBE_SAMPLES {
387 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
388 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
389 }
390 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
391
392 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
394 CPU_ONLY.with(|c| assert!(!c.get()));
395 cpu_scope(|| {
396 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
397 CPU_ONLY.with(|c| assert!(c.get()));
398 });
399 let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
400 CPU_ONLY.with(|c| assert!(!c.get()));
401 probe_reset();
402 }
403}
404
405pub const GPU_MIN_ROWS: usize = 65_536;
408
409pub fn min_rows() -> usize {
416 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
417 .ok()
418 .and_then(|v| v.parse().ok())
419 {
420 return v;
421 }
422 if discrete() { 4096 } else { GPU_MIN_ROWS }
423}
424
425pub fn discrete() -> bool {
427 match backend() {
428 #[cfg(feature = "gpu")]
429 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
430 #[cfg(target_os = "macos")]
431 Backend::Metal => false, Backend::None => false,
433 }
434}
435
436pub struct MoeJob<'a> {
440 pub gate: (usize, usize, usize, &'a [f32]),
441 pub up: (usize, usize, usize, &'a [f32]),
442 pub down: (usize, usize, usize, &'a [f32]),
443 pub xs_gate: Vec<f32>,
444 pub xs_up: Vec<f32>,
445 pub down_col: &'a [f32],
446 pub w: f32,
447 pub q1: bool,
450 pub q4t: bool,
453}
454
455pub struct BatchJob<'a> {
457 pub idx: usize,
458 pub rows: usize,
459 pub cols: usize,
460 pub row_scale: &'a [f32],
461 pub xs: Vec<f32>,
462 pub q1: bool,
464}
465
466#[derive(Clone, Copy, PartialEq, Eq)]
467enum Backend {
468 None,
469 #[cfg(target_os = "macos")]
470 Metal,
471 #[cfg(feature = "gpu")]
472 Wgpu,
473}
474
475fn backend() -> Backend {
476 #[cfg(feature = "gpu")]
477 if crate::gpu_wgpu::selected() {
478 return if crate::gpu_wgpu::enabled() {
479 Backend::Wgpu
480 } else {
481 Backend::None
482 };
483 }
484 #[cfg(target_os = "macos")]
485 if crate::gpu_metal::enabled() {
486 return Backend::Metal;
487 }
488 Backend::None
489}
490
491pub fn backend_available() -> bool {
497 #[cfg(target_os = "macos")]
498 {
499 true
501 }
502 #[cfg(all(feature = "gpu", not(target_os = "macos")))]
503 {
504 static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
505 *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
506 }
507 #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
508 {
509 false
510 }
511}
512
513pub fn enabled() -> bool {
514 backend() != Backend::None
515}
516
517pub fn wgpu_active() -> bool {
531 #[cfg(feature = "gpu")]
532 {
533 matches!(backend(), Backend::Wgpu)
534 }
535 #[cfg(not(feature = "gpu"))]
536 {
537 false
538 }
539}
540
541pub fn wgpu_graph_default() -> bool {
542 #[cfg(feature = "gpu")]
543 {
544 matches!(backend(), Backend::Wgpu) && crate::gpu_wgpu::discrete_active()
545 }
546 #[cfg(not(feature = "gpu"))]
547 {
548 false
549 }
550}
551
552#[allow(clippy::too_many_arguments, unused_variables)]
554pub fn q8_matvec_range(
555 model: &Arc<CmfModel>,
556 idx: usize,
557 row0: usize,
558 row_scale: &[f32],
559 xs: &[f32],
560 rows: usize,
561 cols: usize,
562 out: &mut [f32],
563) -> bool {
564 match backend() {
565 #[cfg(target_os = "macos")]
566 Backend::Metal => {
567 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
568 }
569 #[cfg(feature = "gpu")]
570 Backend::Wgpu => {
571 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
572 }
573 Backend::None => false,
574 }
575}
576
577#[allow(clippy::too_many_arguments, unused_variables)]
580pub fn q8_matmat(
581 model: &Arc<CmfModel>,
582 idx: usize,
583 row_scale: &[f32],
584 pre: &[f32],
585 b: usize,
586 rows: usize,
587 cols: usize,
588 out: &mut [f32],
589) -> bool {
590 match backend() {
591 #[cfg(target_os = "macos")]
592 Backend::Metal => {
593 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
594 }
595 #[cfg(feature = "gpu")]
596 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
597 Backend::None => false,
598 }
599}
600
601#[allow(unused_variables)]
604pub fn q1_matvec(
605 model: &Arc<CmfModel>,
606 idx: usize,
607 xs: &[f32],
608 rows: usize,
609 cols: usize,
610 out: &mut [f32],
611) -> bool {
612 match backend() {
613 #[cfg(target_os = "macos")]
614 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
615 #[cfg(feature = "gpu")]
616 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
617 Backend::None => false,
618 }
619}
620
621#[allow(clippy::too_many_arguments)]
625pub fn attn_dropin(
626 model: &Arc<CmfModel>,
627 kv_id: u64,
628 layer: usize,
629 normed: &[f32],
630 wq_idx: usize,
631 wk_idx: usize,
632 wv_idx: usize,
633 wo_idx: usize,
634 q_norm: Option<&[f32]>,
635 k_norm: Option<&[f32]>,
636 invf: &[f32],
637 nh: usize,
638 nkv: usize,
639 hd: usize,
640 rd: usize,
641 hidden: usize,
642 pos: usize,
643 cap: usize,
644 gemma: bool,
645 eps: f32,
646 cpu_k: &[Vec<f32>],
647 cpu_v: &[Vec<f32>],
648 out: &mut [f32],
649) -> bool {
650 match backend() {
651 #[cfg(feature = "gpu")]
652 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
653 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
654 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
655 ),
656 #[allow(unused_variables)]
657 _ => false,
658 }
659}
660
661pub struct GraphW<'a> {
665 pub idx: usize,
666 pub kind: u8,
667 pub row_scale: &'a [f32],
668 pub data: &'a [f32],
669}
670
671pub enum GraphAttn<'a> {
674 Full {
675 wq: GraphW<'a>,
676 wk: GraphW<'a>,
677 wv: GraphW<'a>,
678 wo: GraphW<'a>,
679 q_norm: Option<&'a [f32]>,
680 k_norm: Option<&'a [f32]>,
681 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
683 output_gate: bool,
686 cpu_k: &'a [Vec<f32>],
687 cpu_v: &'a [Vec<f32>],
688 },
689 Gdn {
690 qkv: GraphW<'a>,
691 z: GraphW<'a>,
692 a: GraphW<'a>,
693 b: GraphW<'a>,
694 out: GraphW<'a>,
695 conv1d: &'a [f32],
696 a_log: &'a [f32],
697 dt_bias: &'a [f32],
698 norm: &'a [f32],
699 nv: usize,
700 nk: usize,
701 dk: usize,
702 dv: usize,
703 kk: usize,
704 },
705}
706
707pub struct GraphLayer<'a> {
709 pub input_norm: &'a [f32],
710 pub attn: GraphAttn<'a>,
711 pub post_norm: &'a [f32],
712 pub ffn: GraphFfn<'a>,
713}
714
715pub enum GraphFfn<'a> {
720 Dense {
721 gate: GraphW<'a>,
722 up: GraphW<'a>,
723 down: GraphW<'a>,
724 },
725 Moe {
726 router: GraphW<'a>,
728 shared_gate: GraphW<'a>,
730 experts: Vec<(usize, usize, usize)>,
734 n_exp: usize,
736 top_k: usize,
737 inter: usize,
738 norm_topk: bool,
739 },
740}
741
742#[allow(clippy::too_many_arguments)]
747pub fn forward_token_graph(
748 model: &Arc<CmfModel>,
749 kv_id: u64,
750 layers: &[GraphLayer],
751 invf: &[f32],
752 h: &mut [f32],
753 nh: usize,
754 nkv: usize,
755 hd: usize,
756 rd: usize,
757 hidden: usize,
758 inter: usize,
759 position: usize,
760 cap: usize,
761 gemma: bool,
762 eps: f32,
763 lm_head: Option<(&GraphW, usize)>,
764 final_norm: &[f32],
765 logits: &mut Vec<f32>,
766 loop_norm_at: &[usize],
767) -> bool {
768 match backend() {
769 #[cfg(feature = "gpu")]
770 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
771 model,
772 kv_id,
773 layers,
774 invf,
775 h,
776 nh,
777 nkv,
778 hd,
779 rd,
780 hidden,
781 inter,
782 position,
783 cap,
784 gemma,
785 eps,
786 lm_head,
787 final_norm,
788 logits,
789 loop_norm_at,
790 ),
791 #[allow(unused_variables)]
792 _ => {
793 let _ = (lm_head, final_norm, logits, loop_norm_at);
794 false
795 }
796 }
797}
798
799#[allow(clippy::too_many_arguments)]
803pub fn forward_batch_graph(
804 model: &Arc<CmfModel>,
805 kv_id: u64,
806 layers: &[GraphLayer],
807 invf: &[f32],
808 h: &mut [f32],
809 nh: usize,
810 nkv: usize,
811 hd: usize,
812 rd: usize,
813 hidden: usize,
814 inter: usize,
815 positions: &[usize],
816 cap: usize,
817 gemma: bool,
818 eps: f32,
819 k: usize,
820) -> bool {
821 match backend() {
822 #[cfg(feature = "gpu")]
823 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
824 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
825 eps, k,
826 ),
827 _ => false,
828 }
829}
830
831pub fn graph_kv_reset(_kv_id: u64) {
833 #[cfg(feature = "gpu")]
834 if backend() == Backend::Wgpu {
835 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
836 }
837}
838
839pub fn q1t_matvec(
843 model: &Arc<CmfModel>,
844 idx: usize,
845 xs: &[f32],
846 rows: usize,
847 cols: usize,
848 out: &mut [f32],
849) -> bool {
850 match backend() {
851 #[cfg(target_os = "macos")]
852 Backend::Metal => {
853 if metal_q1t_enabled() {
854 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
855 } else {
856 false
857 }
858 }
859 #[cfg(feature = "gpu")]
860 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
861 Backend::None => false,
862 }
863}
864
865#[allow(unused_variables)]
868pub fn q4b_matvec(
869 model: &Arc<CmfModel>,
870 idx: usize,
871 xs: &[f32],
872 rows: usize,
873 cols: usize,
874 out: &mut [f32],
875) -> bool {
876 match backend() {
877 #[cfg(target_os = "macos")]
878 Backend::Metal => false,
879 #[cfg(feature = "gpu")]
880 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
881 Backend::None => false,
882 }
883}
884
885pub fn q1t_matmat(
888 model: &Arc<CmfModel>,
889 idx: usize,
890 xs: &[f32],
891 b: usize,
892 rows: usize,
893 cols: usize,
894 out: &mut [f32],
895) -> bool {
896 match backend() {
897 #[cfg(target_os = "macos")]
898 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
902 #[cfg(feature = "gpu")]
903 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
904 Backend::None => false,
905 }
906}
907
908#[cfg(target_os = "macos")]
912pub(crate) fn metal_q1t_enabled() -> bool {
913 std::env::var("CMF_METAL_Q1T")
914 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
915 .unwrap_or(true)
916}
917
918pub fn q1_matmat(
920 model: &Arc<CmfModel>,
921 idx: usize,
922 xs: &[f32],
923 b: usize,
924 rows: usize,
925 cols: usize,
926 out: &mut [f32],
927) -> bool {
928 match backend() {
929 #[cfg(feature = "gpu")]
930 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
931 #[allow(unused_variables)]
932 _ => false,
933 }
934}
935
936static MM_KILL: AtomicBool = AtomicBool::new(false);
941pub(crate) fn mm_killed() -> bool {
942 MM_KILL.load(Ordering::Relaxed)
943}
944pub(crate) fn mm_kill() {
945 MM_KILL.store(true, Ordering::Relaxed);
946}
947
948#[allow(unused_variables, clippy::too_many_arguments)]
951pub fn q4t_ffn(
952 model: &Arc<CmfModel>,
953 w1: usize,
954 w3: usize,
955 w2: usize,
956 xs: &[f32],
957 b: usize,
958 hidden: usize,
959 inter: usize,
960 out: &mut [f32],
961) -> bool {
962 match backend() {
963 #[cfg(target_os = "macos")]
964 Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
965 #[cfg(feature = "gpu")]
966 Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
967 #[allow(unreachable_patterns)]
968 _ => false,
969 }
970}
971
972pub struct DitBlockArgs<'a> {
977 pub n: usize,
978 pub hidden: usize,
979 pub inter: usize,
980 pub nh: usize,
981 pub nkv: usize,
982 pub hd: usize,
983 pub eps: f32,
984 pub rope_cos: &'a [f32],
985 pub rope_sin: &'a [f32],
986 pub norm1: &'a [f32],
987 pub norm2: &'a [f32],
988 pub ffn_norm1: &'a [f32],
989 pub ffn_norm2: &'a [f32],
990 pub norm_q: &'a [f32],
991 pub norm_k: &'a [f32],
992 pub s_msa: &'a [f32],
993 pub gate_msa: &'a [f32],
994 pub s_mlp: &'a [f32],
995 pub gate_mlp: &'a [f32],
996 pub wq: usize,
997 pub wk: usize,
998 pub wv: usize,
999 pub wo: usize,
1000 pub w1: usize,
1001 pub w3: usize,
1002 pub w2: usize,
1003}
1004
1005#[allow(unused_variables)]
1009pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
1010 match backend() {
1011 #[cfg(target_os = "macos")]
1012 Backend::Metal => crate::gpu_metal::dit_block(model, a, x),
1013 _ => false,
1014 }
1015}
1016
1017pub struct VaeResnetArgs<'a> {
1021 pub groups: usize,
1022 pub ic: usize,
1023 pub oc: usize,
1024 pub h: usize,
1025 pub w: usize,
1026 pub n1w: &'a [f32],
1027 pub n1b: &'a [f32],
1028 pub c1w: &'a [f32],
1029 pub c1b: &'a [f32],
1030 pub c1k: usize,
1031 pub n2w: &'a [f32],
1032 pub n2b: &'a [f32],
1033 pub c2w: &'a [f32],
1034 pub c2b: &'a [f32],
1035 pub c2k: usize,
1036 pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
1037}
1038
1039#[allow(unused_variables)]
1042pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
1043 match backend() {
1044 #[cfg(target_os = "macos")]
1045 Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
1046 _ => false,
1047 }
1048}
1049
1050#[allow(unused_variables, clippy::too_many_arguments)]
1053pub fn vae_upsample_conv(
1054 w: &[f32],
1055 bias: &[f32],
1056 x: &[f32],
1057 ic: usize,
1058 oc: usize,
1059 h: usize,
1060 w_img: usize,
1061 k: usize,
1062 out: &mut [f32],
1063) -> bool {
1064 match backend() {
1065 #[cfg(target_os = "macos")]
1066 Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
1067 _ => false,
1068 }
1069}
1070
1071#[allow(unused_variables, clippy::too_many_arguments)]
1074pub fn vae_conv2d(
1075 w: &[f32],
1076 bias: &[f32],
1077 x: &[f32],
1078 ic: usize,
1079 oc: usize,
1080 h: usize,
1081 w_img: usize,
1082 k: usize,
1083 out: &mut [f32],
1084) -> bool {
1085 match backend() {
1086 #[cfg(target_os = "macos")]
1087 Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
1088 _ => false,
1089 }
1090}
1091
1092#[allow(unused_variables, clippy::too_many_arguments)]
1096pub fn dit_attention(
1097 qh: &[f32],
1098 kh: &[f32],
1099 vh: &[f32],
1100 nh: usize,
1101 nkv: usize,
1102 n: usize,
1103 hd: usize,
1104 scale: f32,
1105 out: &mut [f32],
1106) -> bool {
1107 match backend() {
1108 #[cfg(target_os = "macos")]
1109 Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
1110 _ => false,
1111 }
1112}
1113
1114#[allow(unused_variables)]
1119pub fn q4t_matmat(
1120 model: &Arc<CmfModel>,
1121 idx: usize,
1122 xs: &[f32],
1123 b: usize,
1124 rows: usize,
1125 cols: usize,
1126 out: &mut [f32],
1127) -> bool {
1128 match backend() {
1129 #[cfg(target_os = "macos")]
1130 Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
1131 #[cfg(feature = "gpu")]
1132 Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
1133 #[allow(unreachable_patterns)]
1134 _ => false,
1135 }
1136}
1137
1138#[cfg(target_os = "macos")]
1140pub use crate::gpu_metal::{
1141 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
1142 kv_mirror_read_last, kv_mirror_take_imp,
1143};
1144
1145#[cfg(target_os = "macos")]
1147pub fn gdn_block(
1148 model: &Arc<CmfModel>,
1149 layers: &[GdnGpuLayer],
1150 states: &mut [&mut [f32]],
1151 cfg: &GdnGpuCfg,
1152 h: &mut [f32],
1153) -> bool {
1154 match backend() {
1155 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
1156 _ => false,
1157 }
1158}
1159
1160#[allow(unused_variables)]
1162pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
1163 match backend() {
1164 #[cfg(target_os = "macos")]
1165 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
1166 #[cfg(feature = "gpu")]
1167 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
1168 Backend::None => false,
1169 }
1170}
1171
1172#[allow(unused_variables)]
1174pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
1175 match backend() {
1176 #[cfg(target_os = "macos")]
1177 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
1178 #[cfg(feature = "gpu")]
1179 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
1180 Backend::None => false,
1181 }
1182}
1183
1184static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
1200static 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)];
1204
1205const GRAPH_RACE_SAMPLES: u32 = 4;
1207
1208pub fn graph_race_begin_generation() {
1211 GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
1212 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1213 return;
1214 }
1215 let (gn, cn) = (
1216 GRAPH_N[1].load(Ordering::Relaxed),
1217 GRAPH_N[0].load(Ordering::Relaxed),
1218 );
1219 if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
1220 let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
1221 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1222 let verdict = if g_avg < c_avg { 1 } else { 2 };
1223 GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
1224 tracing::info!(
1225 "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
1226 g_avg as f64 / 1e6,
1227 c_avg as f64 / 1e6,
1228 if verdict == 1 { "graph" } else { "normal path" }
1229 );
1230 return;
1231 }
1232 let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
1233 GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
1234}
1235
1236pub fn graph_race_use_graph(trusted: bool) -> bool {
1240 if trusted {
1241 return true;
1242 }
1243 match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
1244 1 => true,
1245 2 => false,
1246 _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
1247 }
1248}
1249
1250pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
1255 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1256 return false;
1257 }
1258 let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
1259 let cn = GRAPH_N[0].load(Ordering::Relaxed);
1260 if !first || cn == 0 {
1261 return false;
1262 }
1263 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1264 let ns = dur.as_nanos() as u64;
1265 if ns > 1_000_000_000 && ns > 4 * c_avg {
1266 GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
1267 tracing::info!(
1268 "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
1269 ns as f64 / 1e6,
1270 c_avg as f64 / 1e6
1271 );
1272 return true;
1273 }
1274 false
1275}
1276
1277pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
1281 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1282 return;
1283 }
1284 let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
1285 if tok == 0 {
1286 return;
1287 }
1288 let i = used_graph as usize;
1289 GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
1290 GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
1291}