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
432pub fn wgpu_active() -> bool {
437 #[cfg(feature = "gpu")]
438 {
439 matches!(backend(), Backend::Wgpu)
440 }
441 #[cfg(not(feature = "gpu"))]
442 {
443 false
444 }
445}
446
447#[allow(clippy::too_many_arguments, unused_variables)]
449pub fn q8_matvec_range(
450 model: &Arc<CmfModel>,
451 idx: usize,
452 row0: usize,
453 row_scale: &[f32],
454 xs: &[f32],
455 rows: usize,
456 cols: usize,
457 out: &mut [f32],
458) -> bool {
459 match backend() {
460 #[cfg(target_os = "macos")]
461 Backend::Metal => {
462 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
463 }
464 #[cfg(feature = "gpu")]
465 Backend::Wgpu => {
466 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
467 }
468 Backend::None => false,
469 }
470}
471
472#[allow(clippy::too_many_arguments, unused_variables)]
475pub fn q8_matmat(
476 model: &Arc<CmfModel>,
477 idx: usize,
478 row_scale: &[f32],
479 pre: &[f32],
480 b: usize,
481 rows: usize,
482 cols: usize,
483 out: &mut [f32],
484) -> bool {
485 match backend() {
486 #[cfg(target_os = "macos")]
487 Backend::Metal => {
488 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
489 }
490 #[cfg(feature = "gpu")]
491 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
492 Backend::None => false,
493 }
494}
495
496#[allow(unused_variables)]
499pub fn q1_matvec(
500 model: &Arc<CmfModel>,
501 idx: usize,
502 xs: &[f32],
503 rows: usize,
504 cols: usize,
505 out: &mut [f32],
506) -> bool {
507 match backend() {
508 #[cfg(target_os = "macos")]
509 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
510 #[cfg(feature = "gpu")]
511 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
512 Backend::None => false,
513 }
514}
515
516#[allow(clippy::too_many_arguments)]
520pub fn attn_dropin(
521 model: &Arc<CmfModel>,
522 kv_id: u64,
523 layer: usize,
524 normed: &[f32],
525 wq_idx: usize,
526 wk_idx: usize,
527 wv_idx: usize,
528 wo_idx: usize,
529 q_norm: Option<&[f32]>,
530 k_norm: Option<&[f32]>,
531 invf: &[f32],
532 nh: usize,
533 nkv: usize,
534 hd: usize,
535 rd: usize,
536 hidden: usize,
537 pos: usize,
538 cap: usize,
539 gemma: bool,
540 eps: f32,
541 cpu_k: &[Vec<f32>],
542 cpu_v: &[Vec<f32>],
543 out: &mut [f32],
544) -> bool {
545 match backend() {
546 #[cfg(feature = "gpu")]
547 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
548 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
549 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
550 ),
551 #[allow(unused_variables)]
552 _ => false,
553 }
554}
555
556pub struct GraphW<'a> {
560 pub idx: usize,
561 pub kind: u8,
562 pub row_scale: &'a [f32],
563 pub data: &'a [f32],
564}
565
566pub enum GraphAttn<'a> {
569 Full {
570 wq: GraphW<'a>,
571 wk: GraphW<'a>,
572 wv: GraphW<'a>,
573 wo: GraphW<'a>,
574 q_norm: Option<&'a [f32]>,
575 k_norm: Option<&'a [f32]>,
576 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
578 output_gate: bool,
581 cpu_k: &'a [Vec<f32>],
582 cpu_v: &'a [Vec<f32>],
583 },
584 Gdn {
585 qkv: GraphW<'a>,
586 z: GraphW<'a>,
587 a: GraphW<'a>,
588 b: GraphW<'a>,
589 out: GraphW<'a>,
590 conv1d: &'a [f32],
591 a_log: &'a [f32],
592 dt_bias: &'a [f32],
593 norm: &'a [f32],
594 nv: usize,
595 nk: usize,
596 dk: usize,
597 dv: usize,
598 kk: usize,
599 },
600}
601
602pub struct GraphLayer<'a> {
604 pub input_norm: &'a [f32],
605 pub attn: GraphAttn<'a>,
606 pub post_norm: &'a [f32],
607 pub gate: GraphW<'a>,
608 pub up: GraphW<'a>,
609 pub down: GraphW<'a>,
610}
611
612#[allow(clippy::too_many_arguments)]
617pub fn forward_token_graph(
618 model: &Arc<CmfModel>,
619 kv_id: u64,
620 layers: &[GraphLayer],
621 invf: &[f32],
622 h: &mut [f32],
623 nh: usize,
624 nkv: usize,
625 hd: usize,
626 rd: usize,
627 hidden: usize,
628 inter: usize,
629 position: usize,
630 cap: usize,
631 gemma: bool,
632 eps: f32,
633 lm_head: Option<(&GraphW, usize)>,
634 final_norm: &[f32],
635 logits: &mut Vec<f32>,
636 loop_norm_at: &[usize],
637) -> bool {
638 match backend() {
639 #[cfg(feature = "gpu")]
640 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
641 model,
642 kv_id,
643 layers,
644 invf,
645 h,
646 nh,
647 nkv,
648 hd,
649 rd,
650 hidden,
651 inter,
652 position,
653 cap,
654 gemma,
655 eps,
656 lm_head,
657 final_norm,
658 logits,
659 loop_norm_at,
660 ),
661 #[allow(unused_variables)]
662 _ => {
663 let _ = (lm_head, final_norm, logits, loop_norm_at);
664 false
665 }
666 }
667}
668
669#[allow(clippy::too_many_arguments)]
673pub fn forward_batch_graph(
674 model: &Arc<CmfModel>,
675 kv_id: u64,
676 layers: &[GraphLayer],
677 invf: &[f32],
678 h: &mut [f32],
679 nh: usize,
680 nkv: usize,
681 hd: usize,
682 rd: usize,
683 hidden: usize,
684 inter: usize,
685 positions: &[usize],
686 cap: usize,
687 gemma: bool,
688 eps: f32,
689 k: usize,
690) -> bool {
691 match backend() {
692 #[cfg(feature = "gpu")]
693 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
694 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
695 eps, k,
696 ),
697 _ => false,
698 }
699}
700
701pub fn graph_kv_reset(_kv_id: u64) {
703 #[cfg(feature = "gpu")]
704 if backend() == Backend::Wgpu {
705 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
706 }
707}
708
709pub fn q1t_matvec(
713 model: &Arc<CmfModel>,
714 idx: usize,
715 xs: &[f32],
716 rows: usize,
717 cols: usize,
718 out: &mut [f32],
719) -> bool {
720 match backend() {
721 #[cfg(target_os = "macos")]
722 Backend::Metal => {
723 if metal_q1t_enabled() {
724 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
725 } else {
726 false
727 }
728 }
729 #[cfg(feature = "gpu")]
730 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
731 Backend::None => false,
732 }
733}
734
735#[allow(unused_variables)]
738pub fn q4b_matvec(
739 model: &Arc<CmfModel>,
740 idx: usize,
741 xs: &[f32],
742 rows: usize,
743 cols: usize,
744 out: &mut [f32],
745) -> bool {
746 match backend() {
747 #[cfg(target_os = "macos")]
748 Backend::Metal => false,
749 #[cfg(feature = "gpu")]
750 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
751 Backend::None => false,
752 }
753}
754
755pub fn q1t_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(target_os = "macos")]
768 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
772 #[cfg(feature = "gpu")]
773 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
774 Backend::None => false,
775 }
776}
777
778#[cfg(target_os = "macos")]
782pub(crate) fn metal_q1t_enabled() -> bool {
783 std::env::var("CMF_METAL_Q1T")
784 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
785 .unwrap_or(true)
786}
787
788pub fn q1_matmat(
790 model: &Arc<CmfModel>,
791 idx: usize,
792 xs: &[f32],
793 b: usize,
794 rows: usize,
795 cols: usize,
796 out: &mut [f32],
797) -> bool {
798 match backend() {
799 #[cfg(feature = "gpu")]
800 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
801 #[allow(unused_variables)]
802 _ => false,
803 }
804}
805
806#[cfg(target_os = "macos")]
808pub use crate::gpu_metal::{
809 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
810 kv_mirror_read_last, kv_mirror_take_imp,
811};
812
813#[cfg(target_os = "macos")]
815pub fn gdn_block(
816 model: &Arc<CmfModel>,
817 layers: &[GdnGpuLayer],
818 states: &mut [&mut [f32]],
819 cfg: &GdnGpuCfg,
820 h: &mut [f32],
821) -> bool {
822 match backend() {
823 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
824 _ => false,
825 }
826}
827
828#[allow(unused_variables)]
830pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
831 match backend() {
832 #[cfg(target_os = "macos")]
833 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
834 #[cfg(feature = "gpu")]
835 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
836 Backend::None => false,
837 }
838}
839
840#[allow(unused_variables)]
842pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
843 match backend() {
844 #[cfg(target_os = "macos")]
845 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
846 #[cfg(feature = "gpu")]
847 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
848 Backend::None => false,
849 }
850}