1use cortiq_core::CmfModel;
14use std::cell::Cell;
15use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, 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 CPU_ONLY.with(|c| c.set(true));
37 let r = f();
38 CPU_ONLY.with(|c| c.set(false));
39 r
40}
41
42pub(crate) fn probe_note_cold() {
45 PROBE_COLD.with(|c| c.set(true));
46}
47
48pub fn set_layer(l: i64) {
50 CUR_LAYER.with(|c| c.set(l));
51}
52
53pub fn cur_layer() -> i64 {
55 CUR_LAYER.with(|c| c.get())
56}
57
58fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
61 static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
62 R.get_or_init(|| {
63 let s = std::env::var("CMF_GPU_LAYERS").ok()?;
64 let mut v = Vec::new();
65 for part in s.split(',') {
66 let part = part.trim();
67 match part.split_once('-') {
68 Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
69 None => {
70 let x: i64 = part.parse().ok()?;
71 v.push((x, x));
72 }
73 }
74 }
75 Some(v)
76 })
77}
78
79fn layer_allowed() -> bool {
80 match layer_ranges() {
81 None => true,
82 Some(ranges) => {
83 let cur = CUR_LAYER.with(|c| c.get());
84 cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
85 }
86 }
87}
88
89pub fn enabled_here() -> bool {
93 !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
94}
95
96#[derive(Clone, Copy)]
108pub enum OpClass {
109 Ffn = 0,
111 Matvec = 1,
113 Matmat = 2,
115 Batch = 3,
117}
118
119pub enum ProbeArm {
121 Gpu,
123 CpuTimed,
125 Cpu,
127}
128
129const PROBE_SAMPLES: u32 = 6;
131
132struct Probe {
133 state: AtomicU8,
135 flip: AtomicU32,
136 gpu_ns: AtomicU64,
137 gpu_n: AtomicU32,
138 cpu_ns: AtomicU64,
139 cpu_n: AtomicU32,
140}
141
142impl Probe {
143 const fn new() -> Self {
144 Self {
145 state: AtomicU8::new(0),
146 flip: AtomicU32::new(0),
147 gpu_ns: AtomicU64::new(0),
148 gpu_n: AtomicU32::new(0),
149 cpu_ns: AtomicU64::new(0),
150 cpu_n: AtomicU32::new(0),
151 }
152 }
153}
154
155static PROBES: [Probe; 4] = [Probe::new(), Probe::new(), Probe::new(), Probe::new()];
156
157fn probe_on() -> bool {
158 static ON: OnceLock<bool> = OnceLock::new();
159 *ON.get_or_init(|| {
160 std::env::var("CMF_GPU_PROBE")
161 .map(|v| v != "0" && v != "off")
162 .unwrap_or(true)
163 })
164}
165
166pub fn q1_force() -> bool {
171 #[cfg(target_os = "macos")]
172 {
173 backend() == Backend::Metal
174 }
175 #[cfg(not(target_os = "macos"))]
176 {
177 false
178 }
179}
180
181pub fn probe_arm(c: OpClass) -> ProbeArm {
185 if !probe_on() {
186 return ProbeArm::Gpu;
187 }
188 let p = &PROBES[c as usize];
189 match p.state.load(Ordering::Relaxed) {
190 1 => ProbeArm::Gpu,
191 2 => ProbeArm::Cpu,
192 _ => {
193 PROBE_COLD.with(|f| f.set(false));
194 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
195 ProbeArm::Gpu
196 } else {
197 ProbeArm::CpuTimed
198 }
199 }
200 }
201}
202
203pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
206 let p = &PROBES[c as usize];
207 if p.state.load(Ordering::Relaxed) != 0 {
208 return;
209 }
210 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
211 return; }
213 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
214 if gpu {
215 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
216 p.gpu_n.fetch_add(1, Ordering::Relaxed);
217 } else {
218 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
219 p.cpu_n.fetch_add(1, Ordering::Relaxed);
220 }
221 let (gn, cn) = (p.gpu_n.load(Ordering::Relaxed), p.cpu_n.load(Ordering::Relaxed));
222 if gn >= 2 && cn >= 2 {
223 let g = p.gpu_ns.load(Ordering::Relaxed) as f64 / gn as f64;
224 let cp = p.cpu_ns.load(Ordering::Relaxed) as f64 / cn as f64;
225 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
228 return;
229 }
230 let winner = if g <= cp { 1 } else { 2 };
231 if p
232 .state
233 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
234 .is_ok()
235 {
236 tracing::info!(
237 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
238 ["ffn", "matvec", "matmat", "qkv-batch"][c as usize],
239 g / 1e6,
240 cp / 1e6,
241 if winner == 1 { "gpu" } else { "cpu" },
242 );
243 }
244 }
245}
246
247pub fn probe_deciding(c: OpClass) -> bool {
250 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
251}
252
253#[allow(unused_variables)]
263pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
264 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
265 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
266 let resident = match backend() {
267 #[cfg(target_os = "macos")]
268 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
269 #[cfg(feature = "gpu")]
270 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
271 Backend::None => false,
272 };
273 if !resident && may_upload {
274 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
275 }
276 resident
277}
278
279#[cfg(test)]
281pub(crate) fn probe_reset() {
282 for p in &PROBES {
283 p.state.store(0, Ordering::Relaxed);
284 p.flip.store(0, Ordering::Relaxed);
285 p.gpu_ns.store(0, Ordering::Relaxed);
286 p.gpu_n.store(0, Ordering::Relaxed);
287 p.cpu_ns.store(0, Ordering::Relaxed);
288 p.cpu_n.store(0, Ordering::Relaxed);
289 }
290}
291
292#[cfg(test)]
293mod probe_tests {
294 use super::*;
295 use std::time::Duration;
296
297 #[test]
300 fn probe_alternates_discards_cold_and_decides() {
301 probe_reset();
302 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
304 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
305
306 probe_note_cold();
310 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
311 for _ in 0..PROBE_SAMPLES {
312 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
313 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
314 }
315 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
316
317 for _ in 0..PROBE_SAMPLES {
319 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
320 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
321 }
322 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
323
324 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
326 CPU_ONLY.with(|c| assert!(!c.get()));
327 probe_reset();
328 }
329}
330
331pub const GPU_MIN_ROWS: usize = 65_536;
334
335pub fn min_rows() -> usize {
342 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS").ok().and_then(|v| v.parse().ok()) {
343 return v;
344 }
345 if discrete() {
346 4096
347 } else {
348 GPU_MIN_ROWS
349 }
350}
351
352pub fn discrete() -> bool {
354 match backend() {
355 #[cfg(feature = "gpu")]
356 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
357 #[cfg(target_os = "macos")]
358 Backend::Metal => false, Backend::None => false,
360 }
361}
362
363pub struct MoeJob<'a> {
367 pub gate: (usize, usize, usize, &'a [f32]),
368 pub up: (usize, usize, usize, &'a [f32]),
369 pub down: (usize, usize, usize, &'a [f32]),
370 pub xs_gate: Vec<f32>,
371 pub xs_up: Vec<f32>,
372 pub down_col: &'a [f32],
373 pub w: f32,
374 pub q1: bool,
377}
378
379pub struct BatchJob<'a> {
381 pub idx: usize,
382 pub rows: usize,
383 pub cols: usize,
384 pub row_scale: &'a [f32],
385 pub xs: Vec<f32>,
386 pub q1: bool,
388}
389
390#[derive(Clone, Copy, PartialEq, Eq)]
391enum Backend {
392 None,
393 #[cfg(target_os = "macos")]
394 Metal,
395 #[cfg(feature = "gpu")]
396 Wgpu,
397}
398
399fn backend() -> Backend {
400 #[cfg(feature = "gpu")]
401 if crate::gpu_wgpu::selected() {
402 return if crate::gpu_wgpu::enabled() { Backend::Wgpu } else { Backend::None };
403 }
404 #[cfg(target_os = "macos")]
405 if crate::gpu_metal::enabled() {
406 return Backend::Metal;
407 }
408 Backend::None
409}
410
411pub fn enabled() -> bool {
413 backend() != Backend::None
414}
415
416#[allow(clippy::too_many_arguments, unused_variables)]
418pub fn q8_matvec_range(
419 model: &Arc<CmfModel>,
420 idx: usize,
421 row0: usize,
422 row_scale: &[f32],
423 xs: &[f32],
424 rows: usize,
425 cols: usize,
426 out: &mut [f32],
427) -> bool {
428 match backend() {
429 #[cfg(target_os = "macos")]
430 Backend::Metal => {
431 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
432 }
433 #[cfg(feature = "gpu")]
434 Backend::Wgpu => {
435 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
436 }
437 Backend::None => false,
438 }
439}
440
441#[allow(clippy::too_many_arguments, unused_variables)]
444pub fn q8_matmat(
445 model: &Arc<CmfModel>,
446 idx: usize,
447 row_scale: &[f32],
448 pre: &[f32],
449 b: usize,
450 rows: usize,
451 cols: usize,
452 out: &mut [f32],
453) -> bool {
454 match backend() {
455 #[cfg(target_os = "macos")]
456 Backend::Metal => {
457 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
458 }
459 #[cfg(feature = "gpu")]
460 Backend::Wgpu => {
461 crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
462 }
463 Backend::None => false,
464 }
465}
466
467#[allow(unused_variables)]
470pub fn q1_matvec(
471 model: &Arc<CmfModel>,
472 idx: usize,
473 xs: &[f32],
474 rows: usize,
475 cols: usize,
476 out: &mut [f32],
477) -> bool {
478 match backend() {
479 #[cfg(target_os = "macos")]
480 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
481 #[cfg(feature = "gpu")]
482 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
483 Backend::None => false,
484 }
485}
486
487#[allow(clippy::too_many_arguments)]
491pub fn attn_dropin(
492 model: &Arc<CmfModel>,
493 kv_id: u64,
494 layer: usize,
495 normed: &[f32],
496 wq_idx: usize,
497 wk_idx: usize,
498 wv_idx: usize,
499 wo_idx: usize,
500 q_norm: Option<&[f32]>,
501 k_norm: Option<&[f32]>,
502 invf: &[f32],
503 nh: usize,
504 nkv: usize,
505 hd: usize,
506 rd: usize,
507 hidden: usize,
508 pos: usize,
509 cap: usize,
510 gemma: bool,
511 eps: f32,
512 cpu_k: &[Vec<f32>],
513 cpu_v: &[Vec<f32>],
514 out: &mut [f32],
515) -> bool {
516 match backend() {
517 #[cfg(feature = "gpu")]
518 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
519 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf,
520 nh, nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
521 ),
522 _ => false,
523 }
524}
525
526pub struct GraphW<'a> {
530 pub idx: usize,
531 pub kind: u8,
532 pub row_scale: &'a [f32],
533 pub data: &'a [f32],
534}
535
536pub enum GraphAttn<'a> {
539 Full {
540 wq: GraphW<'a>,
541 wk: GraphW<'a>,
542 wv: GraphW<'a>,
543 wo: GraphW<'a>,
544 q_norm: Option<&'a [f32]>,
545 k_norm: Option<&'a [f32]>,
546 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
548 output_gate: bool,
551 cpu_k: &'a [Vec<f32>],
552 cpu_v: &'a [Vec<f32>],
553 },
554 Gdn {
555 qkv: GraphW<'a>,
556 z: GraphW<'a>,
557 a: GraphW<'a>,
558 b: GraphW<'a>,
559 out: GraphW<'a>,
560 conv1d: &'a [f32],
561 a_log: &'a [f32],
562 dt_bias: &'a [f32],
563 norm: &'a [f32],
564 nv: usize,
565 nk: usize,
566 dk: usize,
567 dv: usize,
568 kk: usize,
569 },
570}
571
572pub struct GraphLayer<'a> {
574 pub input_norm: &'a [f32],
575 pub attn: GraphAttn<'a>,
576 pub post_norm: &'a [f32],
577 pub gate: GraphW<'a>,
578 pub up: GraphW<'a>,
579 pub down: GraphW<'a>,
580}
581
582#[allow(clippy::too_many_arguments)]
585pub fn forward_token_graph(
586 model: &Arc<CmfModel>,
587 kv_id: u64,
588 layers: &[GraphLayer],
589 invf: &[f32],
590 h: &mut [f32],
591 nh: usize,
592 nkv: usize,
593 hd: usize,
594 rd: usize,
595 hidden: usize,
596 inter: usize,
597 position: usize,
598 cap: usize,
599 gemma: bool,
600 eps: f32,
601 lm_head: Option<(&GraphW, usize)>,
602 final_norm: &[f32],
603 logits: &mut Vec<f32>,
604) -> bool {
605 match backend() {
606 #[cfg(feature = "gpu")]
607 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
608 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, position, cap, gemma, eps,
609 lm_head, final_norm, logits,
610 ),
611 _ => {
612 let _ = (lm_head, final_norm, logits);
613 false
614 }
615 }
616}
617
618#[allow(clippy::too_many_arguments)]
622pub fn forward_batch_graph(
623 model: &Arc<CmfModel>,
624 kv_id: u64,
625 layers: &[GraphLayer],
626 invf: &[f32],
627 h: &mut [f32],
628 nh: usize,
629 nkv: usize,
630 hd: usize,
631 rd: usize,
632 hidden: usize,
633 inter: usize,
634 positions: &[usize],
635 cap: usize,
636 gemma: bool,
637 eps: f32,
638 k: usize,
639) -> bool {
640 match backend() {
641 #[cfg(feature = "gpu")]
642 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
643 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma, eps, k,
644 ),
645 _ => false,
646 }
647}
648
649pub fn graph_kv_reset(_kv_id: u64) {
651 #[cfg(feature = "gpu")]
652 if backend() == Backend::Wgpu {
653 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
654 }
655}
656
657pub fn q1t_matvec(
661 model: &Arc<CmfModel>,
662 idx: usize,
663 xs: &[f32],
664 rows: usize,
665 cols: usize,
666 out: &mut [f32],
667) -> bool {
668 match backend() {
669 #[cfg(target_os = "macos")]
670 Backend::Metal => crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out),
671 #[cfg(feature = "gpu")]
672 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
673 Backend::None => false,
674 }
675}
676
677#[allow(unused_variables)]
680pub fn q4b_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 => false,
691 #[cfg(feature = "gpu")]
692 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
693 Backend::None => false,
694 }
695}
696
697pub fn q1t_matmat(
700 model: &Arc<CmfModel>,
701 idx: usize,
702 xs: &[f32],
703 b: usize,
704 rows: usize,
705 cols: usize,
706 out: &mut [f32],
707) -> bool {
708 match backend() {
709 #[cfg(target_os = "macos")]
710 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
711 #[cfg(feature = "gpu")]
712 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
713 Backend::None => false,
714 }
715}
716
717pub fn q1_matmat(
719 model: &Arc<CmfModel>,
720 idx: usize,
721 xs: &[f32],
722 b: usize,
723 rows: usize,
724 cols: usize,
725 out: &mut [f32],
726) -> bool {
727 match backend() {
728 #[cfg(feature = "gpu")]
729 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
730 _ => false,
731 }
732}
733
734#[cfg(target_os = "macos")]
736pub use crate::gpu_metal::{
737 kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp, AttnDeviceParams, AttnGpuLayer,
738 GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph,
739};
740
741#[cfg(target_os = "macos")]
743pub fn gdn_block(
744 model: &Arc<CmfModel>,
745 layers: &[GdnGpuLayer],
746 states: &mut [&mut [f32]],
747 cfg: &GdnGpuCfg,
748 h: &mut [f32],
749) -> bool {
750 match backend() {
751 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
752 _ => false,
753 }
754}
755
756#[allow(unused_variables)]
758pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
759 match backend() {
760 #[cfg(target_os = "macos")]
761 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
762 #[cfg(feature = "gpu")]
763 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
764 Backend::None => false,
765 }
766}
767
768#[allow(unused_variables)]
770pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
771 match backend() {
772 #[cfg(target_os = "macos")]
773 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
774 #[cfg(feature = "gpu")]
775 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
776 Backend::None => false,
777 }
778}