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
53fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
56 static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
57 R.get_or_init(|| {
58 let s = std::env::var("CMF_GPU_LAYERS").ok()?;
59 let mut v = Vec::new();
60 for part in s.split(',') {
61 let part = part.trim();
62 match part.split_once('-') {
63 Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
64 None => {
65 let x: i64 = part.parse().ok()?;
66 v.push((x, x));
67 }
68 }
69 }
70 Some(v)
71 })
72}
73
74fn layer_allowed() -> bool {
75 match layer_ranges() {
76 None => true,
77 Some(ranges) => {
78 let cur = CUR_LAYER.with(|c| c.get());
79 cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
80 }
81 }
82}
83
84pub fn enabled_here() -> bool {
88 !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
89}
90
91#[derive(Clone, Copy)]
103pub enum OpClass {
104 Ffn = 0,
106 Matvec = 1,
108 Matmat = 2,
110 Batch = 3,
112}
113
114pub enum ProbeArm {
116 Gpu,
118 CpuTimed,
120 Cpu,
122}
123
124const PROBE_SAMPLES: u32 = 6;
126
127struct Probe {
128 state: AtomicU8,
130 flip: AtomicU32,
131 gpu_ns: AtomicU64,
132 gpu_n: AtomicU32,
133 cpu_ns: AtomicU64,
134 cpu_n: AtomicU32,
135}
136
137impl Probe {
138 const fn new() -> Self {
139 Self {
140 state: AtomicU8::new(0),
141 flip: AtomicU32::new(0),
142 gpu_ns: AtomicU64::new(0),
143 gpu_n: AtomicU32::new(0),
144 cpu_ns: AtomicU64::new(0),
145 cpu_n: AtomicU32::new(0),
146 }
147 }
148}
149
150static PROBES: [Probe; 4] = [Probe::new(), Probe::new(), Probe::new(), Probe::new()];
151
152fn probe_on() -> bool {
153 static ON: OnceLock<bool> = OnceLock::new();
154 *ON.get_or_init(|| {
155 std::env::var("CMF_GPU_PROBE")
156 .map(|v| v != "0" && v != "off")
157 .unwrap_or(true)
158 })
159}
160
161pub fn q1_force() -> bool {
166 #[cfg(target_os = "macos")]
167 {
168 backend() == Backend::Metal
169 }
170 #[cfg(not(target_os = "macos"))]
171 {
172 false
173 }
174}
175
176pub fn probe_arm(c: OpClass) -> ProbeArm {
180 if !probe_on() {
181 return ProbeArm::Gpu;
182 }
183 let p = &PROBES[c as usize];
184 match p.state.load(Ordering::Relaxed) {
185 1 => ProbeArm::Gpu,
186 2 => ProbeArm::Cpu,
187 _ => {
188 PROBE_COLD.with(|f| f.set(false));
189 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
190 ProbeArm::Gpu
191 } else {
192 ProbeArm::CpuTimed
193 }
194 }
195 }
196}
197
198pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
201 let p = &PROBES[c as usize];
202 if p.state.load(Ordering::Relaxed) != 0 {
203 return;
204 }
205 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
206 return; }
208 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
209 if gpu {
210 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
211 p.gpu_n.fetch_add(1, Ordering::Relaxed);
212 } else {
213 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
214 p.cpu_n.fetch_add(1, Ordering::Relaxed);
215 }
216 let (gn, cn) = (p.gpu_n.load(Ordering::Relaxed), p.cpu_n.load(Ordering::Relaxed));
217 if gn >= 2 && cn >= 2 {
218 let g = p.gpu_ns.load(Ordering::Relaxed) as f64 / gn as f64;
219 let cp = p.cpu_ns.load(Ordering::Relaxed) as f64 / cn as f64;
220 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
223 return;
224 }
225 let winner = if g <= cp { 1 } else { 2 };
226 if p
227 .state
228 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
229 .is_ok()
230 {
231 tracing::info!(
232 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
233 ["ffn", "matvec", "matmat", "qkv-batch"][c as usize],
234 g / 1e6,
235 cp / 1e6,
236 if winner == 1 { "gpu" } else { "cpu" },
237 );
238 }
239 }
240}
241
242pub fn probe_deciding(c: OpClass) -> bool {
245 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
246}
247
248#[allow(unused_variables)]
258pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
259 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
260 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
261 let resident = match backend() {
262 #[cfg(target_os = "macos")]
263 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
264 #[cfg(feature = "gpu")]
265 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
266 Backend::None => false,
267 };
268 if !resident && may_upload {
269 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
270 }
271 resident
272}
273
274#[cfg(test)]
276pub(crate) fn probe_reset() {
277 for p in &PROBES {
278 p.state.store(0, Ordering::Relaxed);
279 p.flip.store(0, Ordering::Relaxed);
280 p.gpu_ns.store(0, Ordering::Relaxed);
281 p.gpu_n.store(0, Ordering::Relaxed);
282 p.cpu_ns.store(0, Ordering::Relaxed);
283 p.cpu_n.store(0, Ordering::Relaxed);
284 }
285}
286
287#[cfg(test)]
288mod probe_tests {
289 use super::*;
290 use std::time::Duration;
291
292 #[test]
295 fn probe_alternates_discards_cold_and_decides() {
296 probe_reset();
297 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
299 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
300
301 probe_note_cold();
305 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
306 for _ in 0..PROBE_SAMPLES {
307 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
308 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
309 }
310 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
311
312 for _ in 0..PROBE_SAMPLES {
314 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
315 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
316 }
317 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
318
319 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
321 CPU_ONLY.with(|c| assert!(!c.get()));
322 probe_reset();
323 }
324}
325
326pub const GPU_MIN_ROWS: usize = 65_536;
329
330pub fn min_rows() -> usize {
337 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS").ok().and_then(|v| v.parse().ok()) {
338 return v;
339 }
340 if discrete() {
341 4096
342 } else {
343 GPU_MIN_ROWS
344 }
345}
346
347pub fn discrete() -> bool {
349 match backend() {
350 #[cfg(feature = "gpu")]
351 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
352 #[cfg(target_os = "macos")]
353 Backend::Metal => false, Backend::None => false,
355 }
356}
357
358pub struct MoeJob<'a> {
362 pub gate: (usize, usize, usize, &'a [f32]),
363 pub up: (usize, usize, usize, &'a [f32]),
364 pub down: (usize, usize, usize, &'a [f32]),
365 pub xs_gate: Vec<f32>,
366 pub xs_up: Vec<f32>,
367 pub down_col: &'a [f32],
368 pub w: f32,
369 pub q1: bool,
372}
373
374pub struct BatchJob<'a> {
376 pub idx: usize,
377 pub rows: usize,
378 pub cols: usize,
379 pub row_scale: &'a [f32],
380 pub xs: Vec<f32>,
381 pub q1: bool,
383}
384
385#[derive(Clone, Copy, PartialEq, Eq)]
386enum Backend {
387 None,
388 #[cfg(target_os = "macos")]
389 Metal,
390 #[cfg(feature = "gpu")]
391 Wgpu,
392}
393
394fn backend() -> Backend {
395 #[cfg(feature = "gpu")]
396 if crate::gpu_wgpu::selected() {
397 return if crate::gpu_wgpu::enabled() { Backend::Wgpu } else { Backend::None };
398 }
399 #[cfg(target_os = "macos")]
400 if crate::gpu_metal::enabled() {
401 return Backend::Metal;
402 }
403 Backend::None
404}
405
406pub fn enabled() -> bool {
408 backend() != Backend::None
409}
410
411#[allow(clippy::too_many_arguments, unused_variables)]
413pub fn q8_matvec_range(
414 model: &Arc<CmfModel>,
415 idx: usize,
416 row0: usize,
417 row_scale: &[f32],
418 xs: &[f32],
419 rows: usize,
420 cols: usize,
421 out: &mut [f32],
422) -> bool {
423 match backend() {
424 #[cfg(target_os = "macos")]
425 Backend::Metal => {
426 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
427 }
428 #[cfg(feature = "gpu")]
429 Backend::Wgpu => {
430 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
431 }
432 Backend::None => false,
433 }
434}
435
436#[allow(clippy::too_many_arguments, unused_variables)]
439pub fn q8_matmat(
440 model: &Arc<CmfModel>,
441 idx: usize,
442 row_scale: &[f32],
443 pre: &[f32],
444 b: usize,
445 rows: usize,
446 cols: usize,
447 out: &mut [f32],
448) -> bool {
449 match backend() {
450 #[cfg(target_os = "macos")]
451 Backend::Metal => {
452 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
453 }
454 #[cfg(feature = "gpu")]
455 Backend::Wgpu => {
456 crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
457 }
458 Backend::None => false,
459 }
460}
461
462#[allow(unused_variables)]
465pub fn q1_matvec(
466 model: &Arc<CmfModel>,
467 idx: usize,
468 xs: &[f32],
469 rows: usize,
470 cols: usize,
471 out: &mut [f32],
472) -> bool {
473 match backend() {
474 #[cfg(target_os = "macos")]
475 Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
476 #[cfg(feature = "gpu")]
477 Backend::Wgpu => false,
478 Backend::None => false,
479 }
480}
481
482#[cfg(target_os = "macos")]
484pub use crate::gpu_metal::{
485 kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp, AttnDeviceParams, AttnGpuLayer,
486 GdnGpuCfg, GdnGpuLayer, GraphDims, TokenGraph,
487};
488
489#[cfg(target_os = "macos")]
491pub fn gdn_block(
492 model: &Arc<CmfModel>,
493 layers: &[GdnGpuLayer],
494 states: &mut [&mut [f32]],
495 cfg: &GdnGpuCfg,
496 h: &mut [f32],
497) -> bool {
498 match backend() {
499 Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
500 _ => false,
501 }
502}
503
504#[allow(unused_variables)]
506pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
507 match backend() {
508 #[cfg(target_os = "macos")]
509 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
510 #[cfg(feature = "gpu")]
511 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
512 Backend::None => false,
513 }
514}
515
516#[allow(unused_variables)]
518pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
519 match backend() {
520 #[cfg(target_os = "macos")]
521 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
522 #[cfg(feature = "gpu")]
523 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
524 Backend::None => false,
525 }
526}