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, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, position, cap, gemma,
627 eps, lm_head, final_norm, logits, loop_norm_at,
628 ),
629 #[allow(unused_variables)]
630 _ => {
631 let _ = (lm_head, final_norm, logits, loop_norm_at);
632 false
633 }
634 }
635}
636
637#[allow(clippy::too_many_arguments)]
641pub fn forward_batch_graph(
642 model: &Arc<CmfModel>,
643 kv_id: u64,
644 layers: &[GraphLayer],
645 invf: &[f32],
646 h: &mut [f32],
647 nh: usize,
648 nkv: usize,
649 hd: usize,
650 rd: usize,
651 hidden: usize,
652 inter: usize,
653 positions: &[usize],
654 cap: usize,
655 gemma: bool,
656 eps: f32,
657 k: usize,
658) -> bool {
659 match backend() {
660 #[cfg(feature = "gpu")]
661 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
662 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
663 eps, k,
664 ),
665 _ => false,
666 }
667}
668
669pub fn graph_kv_reset(_kv_id: u64) {
671 #[cfg(feature = "gpu")]
672 if backend() == Backend::Wgpu {
673 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
674 }
675}
676
677pub fn q1t_matvec(
681 model: &Arc<CmfModel>,
682 idx: usize,
683 xs: &[f32],
684 rows: usize,
685 cols: usize,
686 out: &mut [f32],
687) -> bool {
688 match backend() {
689 #[cfg(target_os = "macos")]
690 Backend::Metal => {
691 if metal_q1t_enabled() {
692 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
693 } else {
694 false
695 }
696 }
697 #[cfg(feature = "gpu")]
698 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
699 Backend::None => false,
700 }
701}
702
703#[allow(unused_variables)]
706pub fn q4b_matvec(
707 model: &Arc<CmfModel>,
708 idx: usize,
709 xs: &[f32],
710 rows: usize,
711 cols: usize,
712 out: &mut [f32],
713) -> bool {
714 match backend() {
715 #[cfg(target_os = "macos")]
716 Backend::Metal => false,
717 #[cfg(feature = "gpu")]
718 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
719 Backend::None => false,
720 }
721}
722
723pub fn q1t_matmat(
726 model: &Arc<CmfModel>,
727 idx: usize,
728 xs: &[f32],
729 b: usize,
730 rows: usize,
731 cols: usize,
732 out: &mut [f32],
733) -> bool {
734 match backend() {
735 #[cfg(target_os = "macos")]
736 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
740 #[cfg(feature = "gpu")]
741 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
742 Backend::None => false,
743 }
744}
745
746#[cfg(target_os = "macos")]
750pub(crate) fn metal_q1t_enabled() -> bool {
751 std::env::var("CMF_METAL_Q1T")
752 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
753 .unwrap_or(true)
754}
755
756pub fn q1_matmat(
758 model: &Arc<CmfModel>,
759 idx: usize,
760 xs: &[f32],
761 b: usize,
762 rows: usize,
763 cols: usize,
764 out: &mut [f32],
765) -> bool {
766 match backend() {
767 #[cfg(feature = "gpu")]
768 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
769 #[allow(unused_variables)]
770 _ => false,
771 }
772}
773
774#[cfg(target_os = "macos")]
776pub use crate::gpu_metal::{
777 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
778 kv_mirror_read_last, kv_mirror_take_imp,
779};
780
781#[cfg(target_os = "macos")]
783pub fn gdn_block(
784 model: &Arc<CmfModel>,
785 layers: &[GdnGpuLayer],
786 states: &mut [&mut [f32]],
787 cfg: &GdnGpuCfg,
788 h: &mut [f32],
789) -> bool {
790 match backend() {
791 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
792 _ => false,
793 }
794}
795
796#[allow(unused_variables)]
798pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
799 match backend() {
800 #[cfg(target_os = "macos")]
801 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
802 #[cfg(feature = "gpu")]
803 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
804 Backend::None => false,
805 }
806}
807
808#[allow(unused_variables)]
810pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
811 match backend() {
812 #[cfg(target_os = "macos")]
813 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
814 #[cfg(feature = "gpu")]
815 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
816 Backend::None => false,
817 }
818}