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 {
446 #[cfg(feature = "gpu")]
447 {
448 matches!(backend(), Backend::Wgpu)
449 }
450 #[cfg(not(feature = "gpu"))]
451 {
452 false
453 }
454}
455
456pub fn wgpu_graph_default() -> bool {
457 #[cfg(feature = "gpu")]
458 {
459 matches!(backend(), Backend::Wgpu) && crate::gpu_wgpu::discrete_active()
460 }
461 #[cfg(not(feature = "gpu"))]
462 {
463 false
464 }
465}
466
467#[allow(clippy::too_many_arguments, unused_variables)]
469pub fn q8_matvec_range(
470 model: &Arc<CmfModel>,
471 idx: usize,
472 row0: usize,
473 row_scale: &[f32],
474 xs: &[f32],
475 rows: usize,
476 cols: usize,
477 out: &mut [f32],
478) -> bool {
479 match backend() {
480 #[cfg(target_os = "macos")]
481 Backend::Metal => {
482 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
483 }
484 #[cfg(feature = "gpu")]
485 Backend::Wgpu => {
486 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
487 }
488 Backend::None => false,
489 }
490}
491
492#[allow(clippy::too_many_arguments, unused_variables)]
495pub fn q8_matmat(
496 model: &Arc<CmfModel>,
497 idx: usize,
498 row_scale: &[f32],
499 pre: &[f32],
500 b: usize,
501 rows: usize,
502 cols: usize,
503 out: &mut [f32],
504) -> bool {
505 match backend() {
506 #[cfg(target_os = "macos")]
507 Backend::Metal => {
508 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
509 }
510 #[cfg(feature = "gpu")]
511 Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
512 Backend::None => false,
513 }
514}
515
516#[allow(unused_variables)]
519pub fn q1_matvec(
520 model: &Arc<CmfModel>,
521 idx: usize,
522 xs: &[f32],
523 rows: usize,
524 cols: usize,
525 out: &mut [f32],
526) -> bool {
527 match backend() {
528 #[cfg(target_os = "macos")]
529 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
530 #[cfg(feature = "gpu")]
531 Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
532 Backend::None => false,
533 }
534}
535
536#[allow(clippy::too_many_arguments)]
540pub fn attn_dropin(
541 model: &Arc<CmfModel>,
542 kv_id: u64,
543 layer: usize,
544 normed: &[f32],
545 wq_idx: usize,
546 wk_idx: usize,
547 wv_idx: usize,
548 wo_idx: usize,
549 q_norm: Option<&[f32]>,
550 k_norm: Option<&[f32]>,
551 invf: &[f32],
552 nh: usize,
553 nkv: usize,
554 hd: usize,
555 rd: usize,
556 hidden: usize,
557 pos: usize,
558 cap: usize,
559 gemma: bool,
560 eps: f32,
561 cpu_k: &[Vec<f32>],
562 cpu_v: &[Vec<f32>],
563 out: &mut [f32],
564) -> bool {
565 match backend() {
566 #[cfg(feature = "gpu")]
567 Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
568 model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
569 nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
570 ),
571 #[allow(unused_variables)]
572 _ => false,
573 }
574}
575
576pub struct GraphW<'a> {
580 pub idx: usize,
581 pub kind: u8,
582 pub row_scale: &'a [f32],
583 pub data: &'a [f32],
584}
585
586pub enum GraphAttn<'a> {
589 Full {
590 wq: GraphW<'a>,
591 wk: GraphW<'a>,
592 wv: GraphW<'a>,
593 wo: GraphW<'a>,
594 q_norm: Option<&'a [f32]>,
595 k_norm: Option<&'a [f32]>,
596 bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
598 output_gate: bool,
601 cpu_k: &'a [Vec<f32>],
602 cpu_v: &'a [Vec<f32>],
603 },
604 Gdn {
605 qkv: GraphW<'a>,
606 z: GraphW<'a>,
607 a: GraphW<'a>,
608 b: GraphW<'a>,
609 out: GraphW<'a>,
610 conv1d: &'a [f32],
611 a_log: &'a [f32],
612 dt_bias: &'a [f32],
613 norm: &'a [f32],
614 nv: usize,
615 nk: usize,
616 dk: usize,
617 dv: usize,
618 kk: usize,
619 },
620}
621
622pub struct GraphLayer<'a> {
624 pub input_norm: &'a [f32],
625 pub attn: GraphAttn<'a>,
626 pub post_norm: &'a [f32],
627 pub gate: GraphW<'a>,
628 pub up: GraphW<'a>,
629 pub down: GraphW<'a>,
630}
631
632#[allow(clippy::too_many_arguments)]
637pub fn forward_token_graph(
638 model: &Arc<CmfModel>,
639 kv_id: u64,
640 layers: &[GraphLayer],
641 invf: &[f32],
642 h: &mut [f32],
643 nh: usize,
644 nkv: usize,
645 hd: usize,
646 rd: usize,
647 hidden: usize,
648 inter: usize,
649 position: usize,
650 cap: usize,
651 gemma: bool,
652 eps: f32,
653 lm_head: Option<(&GraphW, usize)>,
654 final_norm: &[f32],
655 logits: &mut Vec<f32>,
656 loop_norm_at: &[usize],
657) -> bool {
658 match backend() {
659 #[cfg(feature = "gpu")]
660 Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
661 model,
662 kv_id,
663 layers,
664 invf,
665 h,
666 nh,
667 nkv,
668 hd,
669 rd,
670 hidden,
671 inter,
672 position,
673 cap,
674 gemma,
675 eps,
676 lm_head,
677 final_norm,
678 logits,
679 loop_norm_at,
680 ),
681 #[allow(unused_variables)]
682 _ => {
683 let _ = (lm_head, final_norm, logits, loop_norm_at);
684 false
685 }
686 }
687}
688
689#[allow(clippy::too_many_arguments)]
693pub fn forward_batch_graph(
694 model: &Arc<CmfModel>,
695 kv_id: u64,
696 layers: &[GraphLayer],
697 invf: &[f32],
698 h: &mut [f32],
699 nh: usize,
700 nkv: usize,
701 hd: usize,
702 rd: usize,
703 hidden: usize,
704 inter: usize,
705 positions: &[usize],
706 cap: usize,
707 gemma: bool,
708 eps: f32,
709 k: usize,
710) -> bool {
711 match backend() {
712 #[cfg(feature = "gpu")]
713 Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
714 model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
715 eps, k,
716 ),
717 _ => false,
718 }
719}
720
721pub fn graph_kv_reset(_kv_id: u64) {
723 #[cfg(feature = "gpu")]
724 if backend() == Backend::Wgpu {
725 crate::gpu_wgpu::kv_mirror_reset(_kv_id);
726 }
727}
728
729pub fn q1t_matvec(
733 model: &Arc<CmfModel>,
734 idx: usize,
735 xs: &[f32],
736 rows: usize,
737 cols: usize,
738 out: &mut [f32],
739) -> bool {
740 match backend() {
741 #[cfg(target_os = "macos")]
742 Backend::Metal => {
743 if metal_q1t_enabled() {
744 crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
745 } else {
746 false
747 }
748 }
749 #[cfg(feature = "gpu")]
750 Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
751 Backend::None => false,
752 }
753}
754
755#[allow(unused_variables)]
758pub fn q4b_matvec(
759 model: &Arc<CmfModel>,
760 idx: usize,
761 xs: &[f32],
762 rows: usize,
763 cols: usize,
764 out: &mut [f32],
765) -> bool {
766 match backend() {
767 #[cfg(target_os = "macos")]
768 Backend::Metal => false,
769 #[cfg(feature = "gpu")]
770 Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
771 Backend::None => false,
772 }
773}
774
775pub fn q1t_matmat(
778 model: &Arc<CmfModel>,
779 idx: usize,
780 xs: &[f32],
781 b: usize,
782 rows: usize,
783 cols: usize,
784 out: &mut [f32],
785) -> bool {
786 match backend() {
787 #[cfg(target_os = "macos")]
788 Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
792 #[cfg(feature = "gpu")]
793 Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
794 Backend::None => false,
795 }
796}
797
798#[cfg(target_os = "macos")]
802pub(crate) fn metal_q1t_enabled() -> bool {
803 std::env::var("CMF_METAL_Q1T")
804 .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
805 .unwrap_or(true)
806}
807
808pub fn q1_matmat(
810 model: &Arc<CmfModel>,
811 idx: usize,
812 xs: &[f32],
813 b: usize,
814 rows: usize,
815 cols: usize,
816 out: &mut [f32],
817) -> bool {
818 match backend() {
819 #[cfg(feature = "gpu")]
820 Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
821 #[allow(unused_variables)]
822 _ => false,
823 }
824}
825
826#[cfg(target_os = "macos")]
828pub use crate::gpu_metal::{
829 AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph, kv_mirror_drop,
830 kv_mirror_read_last, kv_mirror_take_imp,
831};
832
833#[cfg(target_os = "macos")]
835pub fn gdn_block(
836 model: &Arc<CmfModel>,
837 layers: &[GdnGpuLayer],
838 states: &mut [&mut [f32]],
839 cfg: &GdnGpuCfg,
840 h: &mut [f32],
841) -> bool {
842 match backend() {
843 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
844 _ => false,
845 }
846}
847
848#[allow(unused_variables)]
850pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
851 match backend() {
852 #[cfg(target_os = "macos")]
853 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
854 #[cfg(feature = "gpu")]
855 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
856 Backend::None => false,
857 }
858}
859
860#[allow(unused_variables)]
862pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
863 match backend() {
864 #[cfg(target_os = "macos")]
865 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
866 #[cfg(feature = "gpu")]
867 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
868 Backend::None => false,
869 }
870}
871
872static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
888static 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)];
892
893const GRAPH_RACE_SAMPLES: u32 = 4;
895
896pub fn graph_race_begin_generation() {
899 GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
900 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
901 return;
902 }
903 let (gn, cn) = (
904 GRAPH_N[1].load(Ordering::Relaxed),
905 GRAPH_N[0].load(Ordering::Relaxed),
906 );
907 if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
908 let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
909 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
910 let verdict = if g_avg < c_avg { 1 } else { 2 };
911 GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
912 tracing::info!(
913 "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
914 g_avg as f64 / 1e6,
915 c_avg as f64 / 1e6,
916 if verdict == 1 { "graph" } else { "normal path" }
917 );
918 return;
919 }
920 let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
921 GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
922}
923
924pub fn graph_race_use_graph(trusted: bool) -> bool {
928 if trusted {
929 return true;
930 }
931 match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
932 1 => true,
933 2 => false,
934 _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
935 }
936}
937
938pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
943 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
944 return false;
945 }
946 let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
947 let cn = GRAPH_N[0].load(Ordering::Relaxed);
948 if !first || cn == 0 {
949 return false;
950 }
951 let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
952 let ns = dur.as_nanos() as u64;
953 if ns > 1_000_000_000 && ns > 4 * c_avg {
954 GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
955 tracing::info!(
956 "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
957 ns as f64 / 1e6,
958 c_avg as f64 / 1e6
959 );
960 return true;
961 }
962 false
963}
964
965pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
969 if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
970 return;
971 }
972 let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
973 if tok == 0 {
974 return;
975 }
976 let i = used_graph as usize;
977 GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
978 GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
979}