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 probe_arm(c: OpClass) -> ProbeArm {
165 if !probe_on() {
166 return ProbeArm::Gpu;
167 }
168 let p = &PROBES[c as usize];
169 match p.state.load(Ordering::Relaxed) {
170 1 => ProbeArm::Gpu,
171 2 => ProbeArm::Cpu,
172 _ => {
173 PROBE_COLD.with(|f| f.set(false));
174 if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
175 ProbeArm::Gpu
176 } else {
177 ProbeArm::CpuTimed
178 }
179 }
180 }
181}
182
183pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
186 let p = &PROBES[c as usize];
187 if p.state.load(Ordering::Relaxed) != 0 {
188 return;
189 }
190 if gpu && PROBE_COLD.with(|f| f.replace(false)) {
191 return; }
193 let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
194 if gpu {
195 p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
196 p.gpu_n.fetch_add(1, Ordering::Relaxed);
197 } else {
198 p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
199 p.cpu_n.fetch_add(1, Ordering::Relaxed);
200 }
201 let (gn, cn) = (p.gpu_n.load(Ordering::Relaxed), p.cpu_n.load(Ordering::Relaxed));
202 if gn >= 2 && cn >= 2 {
203 let g = p.gpu_ns.load(Ordering::Relaxed) as f64 / gn as f64;
204 let cp = p.cpu_ns.load(Ordering::Relaxed) as f64 / cn as f64;
205 if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
208 return;
209 }
210 let winner = if g <= cp { 1 } else { 2 };
211 if p
212 .state
213 .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
214 .is_ok()
215 {
216 tracing::info!(
217 "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
218 ["ffn", "matvec", "matmat", "qkv-batch"][c as usize],
219 g / 1e6,
220 cp / 1e6,
221 if winner == 1 { "gpu" } else { "cpu" },
222 );
223 }
224 }
225}
226
227pub fn probe_deciding(c: OpClass) -> bool {
230 probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
231}
232
233#[allow(unused_variables)]
243pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
244 static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
245 let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
246 let resident = match backend() {
247 #[cfg(target_os = "macos")]
248 Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
249 #[cfg(feature = "gpu")]
250 Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
251 Backend::None => false,
252 };
253 if !resident && may_upload {
254 PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
255 }
256 resident
257}
258
259#[cfg(test)]
261pub(crate) fn probe_reset() {
262 for p in &PROBES {
263 p.state.store(0, Ordering::Relaxed);
264 p.flip.store(0, Ordering::Relaxed);
265 p.gpu_ns.store(0, Ordering::Relaxed);
266 p.gpu_n.store(0, Ordering::Relaxed);
267 p.cpu_ns.store(0, Ordering::Relaxed);
268 p.cpu_n.store(0, Ordering::Relaxed);
269 }
270}
271
272#[cfg(test)]
273mod probe_tests {
274 use super::*;
275 use std::time::Duration;
276
277 #[test]
280 fn probe_alternates_discards_cold_and_decides() {
281 probe_reset();
282 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
284 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
285
286 probe_note_cold();
290 probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
291 for _ in 0..PROBE_SAMPLES {
292 probe_record(OpClass::Ffn, true, Duration::from_millis(1));
293 probe_record(OpClass::Ffn, false, Duration::from_millis(4));
294 }
295 assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
296
297 for _ in 0..PROBE_SAMPLES {
299 probe_record(OpClass::Matmat, true, Duration::from_millis(4));
300 probe_record(OpClass::Matmat, false, Duration::from_millis(1));
301 }
302 assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
303
304 cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
306 CPU_ONLY.with(|c| assert!(!c.get()));
307 probe_reset();
308 }
309}
310
311pub const GPU_MIN_ROWS: usize = 65_536;
314
315pub fn min_rows() -> usize {
322 if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS").ok().and_then(|v| v.parse().ok()) {
323 return v;
324 }
325 if discrete() {
326 4096
327 } else {
328 GPU_MIN_ROWS
329 }
330}
331
332pub fn discrete() -> bool {
334 match backend() {
335 #[cfg(feature = "gpu")]
336 Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
337 #[cfg(target_os = "macos")]
338 Backend::Metal => false, Backend::None => false,
340 }
341}
342
343pub struct MoeJob<'a> {
347 pub gate: (usize, usize, usize, &'a [f32]),
348 pub up: (usize, usize, usize, &'a [f32]),
349 pub down: (usize, usize, usize, &'a [f32]),
350 pub xs_gate: Vec<f32>,
351 pub xs_up: Vec<f32>,
352 pub down_col: &'a [f32],
353 pub w: f32,
354}
355
356pub struct BatchJob<'a> {
358 pub idx: usize,
359 pub rows: usize,
360 pub cols: usize,
361 pub row_scale: &'a [f32],
362 pub xs: Vec<f32>,
363}
364
365#[derive(Clone, Copy, PartialEq, Eq)]
366enum Backend {
367 None,
368 #[cfg(target_os = "macos")]
369 Metal,
370 #[cfg(feature = "gpu")]
371 Wgpu,
372}
373
374fn backend() -> Backend {
375 #[cfg(feature = "gpu")]
376 if crate::gpu_wgpu::selected() {
377 return if crate::gpu_wgpu::enabled() { Backend::Wgpu } else { Backend::None };
378 }
379 #[cfg(target_os = "macos")]
380 if crate::gpu_metal::enabled() {
381 return Backend::Metal;
382 }
383 Backend::None
384}
385
386pub fn enabled() -> bool {
388 backend() != Backend::None
389}
390
391#[allow(clippy::too_many_arguments, unused_variables)]
393pub fn q8_matvec_range(
394 model: &Arc<CmfModel>,
395 idx: usize,
396 row0: usize,
397 row_scale: &[f32],
398 xs: &[f32],
399 rows: usize,
400 cols: usize,
401 out: &mut [f32],
402) -> bool {
403 match backend() {
404 #[cfg(target_os = "macos")]
405 Backend::Metal => {
406 crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
407 }
408 #[cfg(feature = "gpu")]
409 Backend::Wgpu => {
410 crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
411 }
412 Backend::None => false,
413 }
414}
415
416#[allow(clippy::too_many_arguments, unused_variables)]
419pub fn q8_matmat(
420 model: &Arc<CmfModel>,
421 idx: usize,
422 row_scale: &[f32],
423 pre: &[f32],
424 b: usize,
425 rows: usize,
426 cols: usize,
427 out: &mut [f32],
428) -> bool {
429 match backend() {
430 #[cfg(target_os = "macos")]
431 Backend::Metal => {
432 crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
433 }
434 #[cfg(feature = "gpu")]
435 Backend::Wgpu => {
436 crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
437 }
438 Backend::None => false,
439 }
440}
441
442#[allow(unused_variables)]
444pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
445 match backend() {
446 #[cfg(target_os = "macos")]
447 Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
448 #[cfg(feature = "gpu")]
449 Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
450 Backend::None => false,
451 }
452}
453
454#[allow(unused_variables)]
456pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
457 match backend() {
458 #[cfg(target_os = "macos")]
459 Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
460 #[cfg(feature = "gpu")]
461 Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
462 Backend::None => false,
463 }
464}