use cortiq_core::CmfModel;
use std::cell::Cell;
use std::sync::atomic::{AtomicU32, AtomicU64, AtomicU8, Ordering};
use std::sync::{Arc, OnceLock};
thread_local! {
static CUR_LAYER: Cell<i64> = const { Cell::new(-1) };
static CPU_ONLY: Cell<bool> = const { Cell::new(false) };
static PROBE_COLD: Cell<bool> = const { Cell::new(false) };
}
pub fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
CPU_ONLY.with(|c| c.set(true));
let r = f();
CPU_ONLY.with(|c| c.set(false));
r
}
pub(crate) fn probe_note_cold() {
PROBE_COLD.with(|c| c.set(true));
}
pub fn set_layer(l: i64) {
CUR_LAYER.with(|c| c.set(l));
}
fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
R.get_or_init(|| {
let s = std::env::var("CMF_GPU_LAYERS").ok()?;
let mut v = Vec::new();
for part in s.split(',') {
let part = part.trim();
match part.split_once('-') {
Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
None => {
let x: i64 = part.parse().ok()?;
v.push((x, x));
}
}
}
Some(v)
})
}
fn layer_allowed() -> bool {
match layer_ranges() {
None => true,
Some(ranges) => {
let cur = CUR_LAYER.with(|c| c.get());
cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
}
}
}
pub fn enabled_here() -> bool {
!CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
}
#[derive(Clone, Copy)]
pub enum OpClass {
Ffn = 0,
Matvec = 1,
Matmat = 2,
Batch = 3,
}
pub enum ProbeArm {
Gpu,
CpuTimed,
Cpu,
}
const PROBE_SAMPLES: u32 = 6;
struct Probe {
state: AtomicU8,
flip: AtomicU32,
gpu_ns: AtomicU64,
gpu_n: AtomicU32,
cpu_ns: AtomicU64,
cpu_n: AtomicU32,
}
impl Probe {
const fn new() -> Self {
Self {
state: AtomicU8::new(0),
flip: AtomicU32::new(0),
gpu_ns: AtomicU64::new(0),
gpu_n: AtomicU32::new(0),
cpu_ns: AtomicU64::new(0),
cpu_n: AtomicU32::new(0),
}
}
}
static PROBES: [Probe; 4] = [Probe::new(), Probe::new(), Probe::new(), Probe::new()];
fn probe_on() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| {
std::env::var("CMF_GPU_PROBE")
.map(|v| v != "0" && v != "off")
.unwrap_or(true)
})
}
pub fn probe_arm(c: OpClass) -> ProbeArm {
if !probe_on() {
return ProbeArm::Gpu;
}
let p = &PROBES[c as usize];
match p.state.load(Ordering::Relaxed) {
1 => ProbeArm::Gpu,
2 => ProbeArm::Cpu,
_ => {
PROBE_COLD.with(|f| f.set(false));
if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
ProbeArm::Gpu
} else {
ProbeArm::CpuTimed
}
}
}
}
pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
let p = &PROBES[c as usize];
if p.state.load(Ordering::Relaxed) != 0 {
return;
}
if gpu && PROBE_COLD.with(|f| f.replace(false)) {
return; }
let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
if gpu {
p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
p.gpu_n.fetch_add(1, Ordering::Relaxed);
} else {
p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
p.cpu_n.fetch_add(1, Ordering::Relaxed);
}
let (gn, cn) = (p.gpu_n.load(Ordering::Relaxed), p.cpu_n.load(Ordering::Relaxed));
if gn >= 2 && cn >= 2 {
let g = p.gpu_ns.load(Ordering::Relaxed) as f64 / gn as f64;
let cp = p.cpu_ns.load(Ordering::Relaxed) as f64 / cn as f64;
if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 3.0 && cp < g * 3.0 {
return;
}
let winner = if g <= cp { 1 } else { 2 };
if p
.state
.compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
.is_ok()
{
tracing::info!(
"gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
["ffn", "matvec", "matmat", "qkv-batch"][c as usize],
g / 1e6,
cp / 1e6,
if winner == 1 { "gpu" } else { "cpu" },
);
}
}
}
pub fn probe_deciding(c: OpClass) -> bool {
probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
}
#[allow(unused_variables)]
pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
let resident = match backend() {
#[cfg(target_os = "macos")]
Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
#[cfg(feature = "gpu")]
Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
Backend::None => false,
};
if !resident && may_upload {
PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
}
resident
}
#[cfg(test)]
pub(crate) fn probe_reset() {
for p in &PROBES {
p.state.store(0, Ordering::Relaxed);
p.flip.store(0, Ordering::Relaxed);
p.gpu_ns.store(0, Ordering::Relaxed);
p.gpu_n.store(0, Ordering::Relaxed);
p.cpu_ns.store(0, Ordering::Relaxed);
p.cpu_n.store(0, Ordering::Relaxed);
}
}
#[cfg(test)]
mod probe_tests {
use super::*;
use std::time::Duration;
#[test]
fn probe_alternates_discards_cold_and_decides() {
probe_reset();
assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
probe_note_cold();
probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
for _ in 0..PROBE_SAMPLES {
probe_record(OpClass::Ffn, true, Duration::from_millis(1));
probe_record(OpClass::Ffn, false, Duration::from_millis(4));
}
assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
for _ in 0..PROBE_SAMPLES {
probe_record(OpClass::Matmat, true, Duration::from_millis(4));
probe_record(OpClass::Matmat, false, Duration::from_millis(1));
}
assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
CPU_ONLY.with(|c| assert!(!c.get()));
probe_reset();
}
}
pub const GPU_MIN_ROWS: usize = 65_536;
pub fn min_rows() -> usize {
if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS").ok().and_then(|v| v.parse().ok()) {
return v;
}
if discrete() {
4096
} else {
GPU_MIN_ROWS
}
}
pub fn discrete() -> bool {
match backend() {
#[cfg(feature = "gpu")]
Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
#[cfg(target_os = "macos")]
Backend::Metal => false, Backend::None => false,
}
}
pub struct MoeJob<'a> {
pub gate: (usize, usize, usize, &'a [f32]),
pub up: (usize, usize, usize, &'a [f32]),
pub down: (usize, usize, usize, &'a [f32]),
pub xs_gate: Vec<f32>,
pub xs_up: Vec<f32>,
pub down_col: &'a [f32],
pub w: f32,
}
pub struct BatchJob<'a> {
pub idx: usize,
pub rows: usize,
pub cols: usize,
pub row_scale: &'a [f32],
pub xs: Vec<f32>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Backend {
None,
#[cfg(target_os = "macos")]
Metal,
#[cfg(feature = "gpu")]
Wgpu,
}
fn backend() -> Backend {
#[cfg(feature = "gpu")]
if crate::gpu_wgpu::selected() {
return if crate::gpu_wgpu::enabled() { Backend::Wgpu } else { Backend::None };
}
#[cfg(target_os = "macos")]
if crate::gpu_metal::enabled() {
return Backend::Metal;
}
Backend::None
}
pub fn enabled() -> bool {
backend() != Backend::None
}
#[allow(clippy::too_many_arguments, unused_variables)]
pub fn q8_matvec_range(
model: &Arc<CmfModel>,
idx: usize,
row0: usize,
row_scale: &[f32],
xs: &[f32],
rows: usize,
cols: usize,
out: &mut [f32],
) -> bool {
match backend() {
#[cfg(target_os = "macos")]
Backend::Metal => {
crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
}
#[cfg(feature = "gpu")]
Backend::Wgpu => {
crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
}
Backend::None => false,
}
}
#[allow(clippy::too_many_arguments, unused_variables)]
pub fn q8_matmat(
model: &Arc<CmfModel>,
idx: usize,
row_scale: &[f32],
pre: &[f32],
b: usize,
rows: usize,
cols: usize,
out: &mut [f32],
) -> bool {
match backend() {
#[cfg(target_os = "macos")]
Backend::Metal => {
crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
}
#[cfg(feature = "gpu")]
Backend::Wgpu => {
crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
}
Backend::None => false,
}
}
#[allow(unused_variables)]
pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
match backend() {
#[cfg(target_os = "macos")]
Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
#[cfg(feature = "gpu")]
Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
Backend::None => false,
}
}
#[allow(unused_variables)]
pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
match backend() {
#[cfg(target_os = "macos")]
Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
#[cfg(feature = "gpu")]
Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
Backend::None => false,
}
}