1use cortiq_core::CmfModel;
14use std::cell::Cell;
15use std::sync::atomic::{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 fn set_layer(l: i64) {
55 CUR_LAYER.with(|c| c.set(l));
56}
57
58pub fn cur_layer() -> i64 {
60 CUR_LAYER.with(|c| c.get())
61}
62
63fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
66 static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
67 R.get_or_init(|| {
68 let s = std::env::var("CMF_GPU_LAYERS").ok()?;
69 let mut v = Vec::new();
70 for part in s.split(',') {
71 let part = part.trim();
72 match part.split_once('-') {
73 Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
74 None => {
75 let x: i64 = part.parse().ok()?;
76 v.push((x, x));
77 }
78 }
79 }
80 Some(v)
81 })
82}
83
84fn layer_allowed() -> bool {
85 match layer_ranges() {
86 None => true,
87 Some(ranges) => {
88 let cur = CUR_LAYER.with(|c| c.get());
89 cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
90 }
91 }
92}
93
94pub fn enabled_here() -> bool {
98 !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
99}
100
101#[derive(Clone, Copy)]
113pub enum OpClass {
114 Ffn = 0,
116 Matvec = 1,
118 Matmat = 2,
120 Batch = 3,
122}
123
124pub enum ProbeArm {
126 Gpu,
128 CpuTimed,
130 Cpu,
132}
133
134const PROBE_SAMPLES: u32 = 6;
136
137struct Probe {
138 state: AtomicU8,
140 flip: AtomicU32,
141 gpu_ns: AtomicU64,
142 gpu_n: AtomicU32,
143 cpu_ns: AtomicU64,
144 cpu_n: AtomicU32,
145}
146
147impl Probe {
148 const fn new() -> Self {
149 Self {
150 state: AtomicU8::new(0),
151 flip: AtomicU32::new(0),
152 gpu_ns: AtomicU64::new(0),
153 gpu_n: AtomicU32::new(0),
154 cpu_ns: AtomicU64::new(0),
155 cpu_n: AtomicU32::new(0),
156 }
157 }
158}
159
160static PROBES: [Probe; 4] = [Probe::new(), Probe::new(), Probe::new(), Probe::new()];
161
162fn probe_on() -> bool {
163 static ON: OnceLock<bool> = OnceLock::new();
164 *ON.get_or_init(|| {
165 std::env::var("CMF_GPU_PROBE")
166 .map(|v| v != "0" && v != "off")
167 .unwrap_or(true)
168 })
169}
170
171pub fn q1_force() -> bool {
176 #[cfg(target_os = "macos")]
177 {
178 backend() == Backend::Metal
179 }
180 #[cfg(not(target_os = "macos"))]
181 {
182 false
183 }
184}
185
186pub fn probe_arm(c: OpClass) -> ProbeArm {
190 if !probe_on() {
191 return ProbeArm::Gpu;
192 }
193 let p = &PROBES[c as usize];
194 match p.state.load(Ordering::Relaxed) {
195 1 => ProbeArm::Gpu,
196 2 => ProbeArm::Cpu,
197 _ => {
198 PROBE_COLD.with(|f| f.set(false));
199 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
200 ProbeArm::Gpu
201 } else {
202 ProbeArm::CpuTimed
203 }
204 }
205 }
206}
207
208pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
211 let p = &PROBES[c as usize];
212 if p.state.load(Ordering::Relaxed) != 0 {
213 return;
214 }
215 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
216 return; }
218 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
219 if gpu {
220 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
221 p.gpu_n.fetch_add(1, Ordering::Relaxed);
222 } else {
223 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
224 p.cpu_n.fetch_add(1, Ordering::Relaxed);
225 }
226 let (gn, cn) = (
227 p.gpu_n.load(Ordering::Relaxed),
228 p.cpu_n.load(Ordering::Relaxed),
229 );
230 if gn >= 2 && cn >= 2 {
231 let g = p.gpu_ns.load(Ordering::Relaxed) as f64 / gn as f64;
232 let cp = p.cpu_ns.load(Ordering::Relaxed) as f64 / cn as f64;
233 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
236 return;
237 }
238 let winner = if g <= cp { 1 } else { 2 };
239 if p.state
240 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
241 .is_ok()
242 {
243 tracing::info!(
244 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
245 ["ffn", "matvec", "matmat", "qkv-batch"][c as usize],
246 g / 1e6,
247 cp / 1e6,
248 if winner == 1 { "gpu" } else { "cpu" },
249 );
250 }
251 }
252}
253
254pub fn probe_deciding(c: OpClass) -> bool {
257 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
258}
259
260#[allow(unused_variables)]
270pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
271 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
272 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
273 let resident = match backend() {
274 #[cfg(target_os = "macos")]
275 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
276 #[cfg(feature = "gpu")]
277 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
278 Backend::None => false,
279 };
280 if !resident && may_upload {
281 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
282 }
283 resident
284}
285
286#[cfg(test)]
288pub(crate) fn probe_reset() {
289 for p in &PROBES {
290 p.state.store(0, Ordering::Relaxed);
291 p.flip.store(0, Ordering::Relaxed);
292 p.gpu_ns.store(0, Ordering::Relaxed);
293 p.gpu_n.store(0, Ordering::Relaxed);
294 p.cpu_ns.store(0, Ordering::Relaxed);
295 p.cpu_n.store(0, Ordering::Relaxed);
296 }
297}
298
299#[cfg(test)]
300mod probe_tests {
301 use super::*;
302 use std::time::Duration;
303
304 #[test]
307 fn probe_alternates_discards_cold_and_decides() {
308 probe_reset();
309 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
311 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
312
313 probe_note_cold();
317 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
318 for _ in 0..PROBE_SAMPLES {
319 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
320 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
321 }
322 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
323
324 for _ in 0..PROBE_SAMPLES {
326 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
327 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
328 }
329 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
330
331 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
333 CPU_ONLY.with(|c| assert!(!c.get()));
334 cpu_scope(|| {
335 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
336 CPU_ONLY.with(|c| assert!(c.get()));
337 });
338 let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
339 CPU_ONLY.with(|c| assert!(!c.get()));
340 probe_reset();
341 }
342}
343
344pub const GPU_MIN_ROWS: usize = 65_536;
347
348pub fn min_rows() -> usize {
355 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
356 .ok()
357 .and_then(|v| v.parse().ok())
358 {
359 return v;
360 }
361 if discrete() { 4096 } else { GPU_MIN_ROWS }
362}
363
364pub fn discrete() -> bool {
366 match backend() {
367 #[cfg(feature = "gpu")]
368 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
369 #[cfg(target_os = "macos")]
370 Backend::Metal => false, Backend::None => false,
372 }
373}
374
375pub struct MoeJob<'a> {
379 pub gate: (usize, usize, usize, &'a [f32]),
380 pub up: (usize, usize, usize, &'a [f32]),
381 pub down: (usize, usize, usize, &'a [f32]),
382 pub xs_gate: Vec<f32>,
383 pub xs_up: Vec<f32>,
384 pub down_col: &'a [f32],
385 pub w: f32,
386 pub q1: bool,
389}
390
391pub struct BatchJob<'a> {
393 pub idx: usize,
394 pub rows: usize,
395 pub cols: usize,
396 pub row_scale: &'a [f32],
397 pub xs: Vec<f32>,
398 pub q1: bool,
400}
401
402#[derive(Clone, Copy, PartialEq, Eq)]
403enum Backend {
404 None,
405 #[cfg(target_os = "macos")]
406 Metal,
407 #[cfg(feature = "gpu")]
408 Wgpu,
409}
410
411fn backend() -> Backend {
412 #[cfg(feature = "gpu")]
413 if crate::gpu_wgpu::selected() {
414 return if crate::gpu_wgpu::enabled() {
415 Backend::Wgpu
416 } else {
417 Backend::None
418 };
419 }
420 #[cfg(target_os = "macos")]
421 if crate::gpu_metal::enabled() {
422 return Backend::Metal;
423 }
424 Backend::None
425}
426
427pub fn enabled() -> bool {
429 backend() != Backend::None
430}
431
432#[allow(clippy::too_many_arguments, unused_variables)]
434pub fn q8_matvec_range(
435 model: &Arc<CmfModel>,
436 idx: usize,
437 row0: usize,
438 row_scale: &[f32],
439 xs: &[f32],
440 rows: usize,
441 cols: usize,
442 out: &mut [f32],
443) -> bool {
444 match backend() {
445 #[cfg(target_os = "macos")]
446 Backend::Metal => {
447 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
448 }
449 #[cfg(feature = "gpu")]
450 Backend::Wgpu => {
451 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
452 }
453 Backend::None => false,
454 }
455}
456
457#[allow(clippy::too_many_arguments, unused_variables)]
460pub fn q8_matmat(
461 model: &Arc<CmfModel>,
462 idx: usize,
463 row_scale: &[f32],
464 pre: &[f32],
465 b: usize,
466 rows: usize,
467 cols: usize,
468 out: &mut [f32],
469) -> bool {
470 match backend() {
471 #[cfg(target_os = "macos")]
472 Backend::Metal => {
473 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
474 }
475 #[cfg(feature = "gpu")]
476 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
477 Backend::None => false,
478 }
479}
480
481#[allow(unused_variables)]
484pub fn q1_matvec(
485 model: &Arc<CmfModel>,
486 idx: usize,
487 xs: &[f32],
488 rows: usize,
489 cols: usize,
490 out: &mut [f32],
491) -> bool {
492 match backend() {
493 #[cfg(target_os = "macos")]
494 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
495 #[cfg(feature = "gpu")]
496 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
497 Backend::None => false,
498 }
499}
500
501#[allow(clippy::too_many_arguments)]
505pub fn attn_dropin(
506 model: &Arc<CmfModel>,
507 kv_id: u64,
508 layer: usize,
509 normed: &[f32],
510 wq_idx: usize,
511 wk_idx: usize,
512 wv_idx: usize,
513 wo_idx: usize,
514 q_norm: Option<&[f32]>,
515 k_norm: Option<&[f32]>,
516 invf: &[f32],
517 nh: usize,
518 nkv: usize,
519 hd: usize,
520 rd: usize,
521 hidden: usize,
522 pos: usize,
523 cap: usize,
524 gemma: bool,
525 eps: f32,
526 cpu_k: &[Vec<f32>],
527 cpu_v: &[Vec<f32>],
528 out: &mut [f32],
529) -> bool {
530 match backend() {
531 #[cfg(feature = "gpu")]
532 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
533 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
534 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
535 ),
536 #[allow(unused_variables)]
537 _ => false,
538 }
539}
540
541pub struct GraphW<'a> {
545 pub idx: usize,
546 pub kind: u8,
547 pub row_scale: &'a [f32],
548 pub data: &'a [f32],
549}
550
551pub enum GraphAttn<'a> {
554 Full {
555 wq: GraphW<'a>,
556 wk: GraphW<'a>,
557 wv: GraphW<'a>,
558 wo: GraphW<'a>,
559 q_norm: Option<&'a [f32]>,
560 k_norm: Option<&'a [f32]>,
561 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
563 output_gate: bool,
566 cpu_k: &'a [Vec<f32>],
567 cpu_v: &'a [Vec<f32>],
568 },
569 Gdn {
570 qkv: GraphW<'a>,
571 z: GraphW<'a>,
572 a: GraphW<'a>,
573 b: GraphW<'a>,
574 out: GraphW<'a>,
575 conv1d: &'a [f32],
576 a_log: &'a [f32],
577 dt_bias: &'a [f32],
578 norm: &'a [f32],
579 nv: usize,
580 nk: usize,
581 dk: usize,
582 dv: usize,
583 kk: usize,
584 },
585}
586
587pub struct GraphLayer<'a> {
589 pub input_norm: &'a [f32],
590 pub attn: GraphAttn<'a>,
591 pub post_norm: &'a [f32],
592 pub gate: GraphW<'a>,
593 pub up: GraphW<'a>,
594 pub down: GraphW<'a>,
595}
596
597#[allow(clippy::too_many_arguments)]
602pub fn forward_token_graph(
603 model: &Arc<CmfModel>,
604 kv_id: u64,
605 layers: &[GraphLayer],
606 invf: &[f32],
607 h: &mut [f32],
608 nh: usize,
609 nkv: usize,
610 hd: usize,
611 rd: usize,
612 hidden: usize,
613 inter: usize,
614 position: usize,
615 cap: usize,
616 gemma: bool,
617 eps: f32,
618 lm_head: Option<(&GraphW, usize)>,
619 final_norm: &[f32],
620 logits: &mut Vec<f32>,
621 loop_norm_at: &[usize],
622) -> bool {
623 match backend() {
624 #[cfg(feature = "gpu")]
625 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
626 model,
627 kv_id,
628 layers,
629 invf,
630 h,
631 nh,
632 nkv,
633 hd,
634 rd,
635 hidden,
636 inter,
637 position,
638 cap,
639 gemma,
640 eps,
641 lm_head,
642 final_norm,
643 logits,
644 loop_norm_at,
645 ),
646 #[allow(unused_variables)]
647 _ => {
648 let _ = (lm_head, final_norm, logits, loop_norm_at);
649 false
650 }
651 }
652}
653
654#[allow(clippy::too_many_arguments)]
658pub fn forward_batch_graph(
659 model: &Arc<CmfModel>,
660 kv_id: u64,
661 layers: &[GraphLayer],
662 invf: &[f32],
663 h: &mut [f32],
664 nh: usize,
665 nkv: usize,
666 hd: usize,
667 rd: usize,
668 hidden: usize,
669 inter: usize,
670 positions: &[usize],
671 cap: usize,
672 gemma: bool,
673 eps: f32,
674 k: usize,
675) -> bool {
676 match backend() {
677 #[cfg(feature = "gpu")]
678 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
679 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
680 eps, k,
681 ),
682 _ => false,
683 }
684}
685
686pub fn graph_kv_reset(_kv_id: u64) {
688 #[cfg(feature = "gpu")]
689 if backend() == Backend::Wgpu {
690 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
691 }
692}
693
694pub fn q1t_matvec(
698 model: &Arc<CmfModel>,
699 idx: usize,
700 xs: &[f32],
701 rows: usize,
702 cols: usize,
703 out: &mut [f32],
704) -> bool {
705 match backend() {
706 #[cfg(target_os = "macos")]
707 Backend::Metal => {
708 if metal_q1t_enabled() {
709 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
710 } else {
711 false
712 }
713 }
714 #[cfg(feature = "gpu")]
715 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
716 Backend::None => false,
717 }
718}
719
720#[allow(unused_variables)]
723pub fn q4b_matvec(
724 model: &Arc<CmfModel>,
725 idx: usize,
726 xs: &[f32],
727 rows: usize,
728 cols: usize,
729 out: &mut [f32],
730) -> bool {
731 match backend() {
732 #[cfg(target_os = "macos")]
733 Backend::Metal => false,
734 #[cfg(feature = "gpu")]
735 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
736 Backend::None => false,
737 }
738}
739
740pub fn q1t_matmat(
743 model: &Arc<CmfModel>,
744 idx: usize,
745 xs: &[f32],
746 b: usize,
747 rows: usize,
748 cols: usize,
749 out: &mut [f32],
750) -> bool {
751 match backend() {
752 #[cfg(target_os = "macos")]
753 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
757 #[cfg(feature = "gpu")]
758 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
759 Backend::None => false,
760 }
761}
762
763#[cfg(target_os = "macos")]
767pub(crate) fn metal_q1t_enabled() -> bool {
768 std::env::var("CMF_METAL_Q1T")
769 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
770 .unwrap_or(true)
771}
772
773pub fn q1_matmat(
775 model: &Arc<CmfModel>,
776 idx: usize,
777 xs: &[f32],
778 b: usize,
779 rows: usize,
780 cols: usize,
781 out: &mut [f32],
782) -> bool {
783 match backend() {
784 #[cfg(feature = "gpu")]
785 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
786 #[allow(unused_variables)]
787 _ => false,
788 }
789}
790
791#[cfg(target_os = "macos")]
793pub use crate::gpu_metal::{
794 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
795 kv_mirror_read_last, kv_mirror_take_imp,
796};
797
798#[cfg(target_os = "macos")]
800pub fn gdn_block(
801 model: &Arc<CmfModel>,
802 layers: &[GdnGpuLayer],
803 states: &mut [&mut [f32]],
804 cfg: &GdnGpuCfg,
805 h: &mut [f32],
806) -> bool {
807 match backend() {
808 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
809 _ => false,
810 }
811}
812
813#[allow(unused_variables)]
815pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
816 match backend() {
817 #[cfg(target_os = "macos")]
818 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
819 #[cfg(feature = "gpu")]
820 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
821 Backend::None => false,
822 }
823}
824
825#[allow(unused_variables)]
827pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
828 match backend() {
829 #[cfg(target_os = "macos")]
830 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
831 #[cfg(feature = "gpu")]
832 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
833 Backend::None => false,
834 }
835}