Skip to main content

cortiq_engine/
gpu.rs

1//! Facade for GPU backends: a single call entry point for qtensor/pipeline/
2//! linear_core. Job types and the threshold are canonical HERE; behind the
3//! facade dispatch goes to a platform backend:
4//!   - `gpu_metal` (Apple Silicon, unified memory + no-copy buffers);
5//!   - `gpu_wgpu` (C1: Vulkan/DX12/Metal — NVIDIA/Radeon/Intel/Apple,
6//!     weights resident in VRAM), available under `--features gpu`.
7//!
8//! Runtime selection via `CMF_GPU`: `1` — native Metal (macOS) or wgpu
9//! (other OSes); `wgpu` — force wgpu (including for the local
10//! Metal-via-wgpu parity test). Any backend refusal — `false` and the honest
11//! CPU path, no partial results.
12
13use cortiq_core::CmfModel;
14use std::cell::Cell;
15use std::sync::atomic::{AtomicBool, AtomicU8, AtomicU32, AtomicU64, Ordering};
16use std::sync::{Arc, OnceLock};
17
18thread_local! {
19    /// Index of the current forward layer (−1 = outside a numbered layer:
20    /// lm_head/embed — always allowed). The pipeline sets it before
21    /// each layer so that the GPU/CPU layer-split works.
22    static CUR_LAYER: Cell<i64> = const { Cell::new(-1) };
23    /// Inside `cpu_scope` every GPU gate reports disabled: the timed CPU
24    /// arm of a probe (and a class that lost its probe) must run PURE
25    /// CPU, or inner per-op hooks would re-enter the GPU and poison the
26    /// comparison.
27    static CPU_ONLY: Cell<bool> = const { Cell::new(false) };
28    /// "This op paid a one-off cost" (weight upload / first pipeline
29    /// build): backends set it, `probe_record` discards the sample so
30    /// only steady-state timings compete.
31    static PROBE_COLD: Cell<bool> = const { Cell::new(false) };
32}
33
34/// RAII form of `cpu_scope`, used when a device-prefix graph hands a whole
35/// remainder of the forward pass to the host. Without a guard around that
36/// tail, its ordinary per-op hooks re-entered the GPU and streamed the rest of
37/// an over-size model through the residency arena, defeating the prefix's VRAM
38/// bound at the driver-allocation level.
39pub struct CpuScopeGuard(bool);
40
41impl Drop for CpuScopeGuard {
42    fn drop(&mut self) {
43        CPU_ONLY.with(|c| c.set(self.0));
44    }
45}
46
47pub fn enter_cpu_scope() -> CpuScopeGuard {
48    let previous = CPU_ONLY.with(|c| c.replace(true));
49    CpuScopeGuard(previous)
50}
51
52/// Run `f` with the GPU gates off on this thread (pure-CPU arm).
53pub fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
54    let _restore = enter_cpu_scope();
55    f()
56}
57
58/// Backends: name the device once at init. The probe cache is keyed by
59/// it, because a verdict is a property of THIS silicon and nothing else.
60/// First writer wins: a process runs one backend, and on the rare host
61/// where two initialize, the one that came up first is the one in use.
62pub fn probe_set_device(label: &str) {
63    let _ = DEVICE_LABEL.set(label.to_string());
64}
65
66fn device_label() -> &'static str {
67    DEVICE_LABEL.get().map(String::as_str).unwrap_or("unknown")
68}
69
70static DEVICE_LABEL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
71
72/// Somewhere this process may write small caches.
73///
74/// `std::env::temp_dir()` is NOT that place on Android: with no `TMPDIR`
75/// it answers `/tmp`, which does not exist in an app sandbox, and every
76/// write fails silently — measured, after the pipeline cache appeared to
77/// work in a shell (where `TMPDIR=/data/local/tmp`) and did nothing at
78/// all in the app. The loader points this at the model's own directory,
79/// which is somewhere the caller already writes.
80static CACHE_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
81
82/// Loader: name a directory this process can write to. First call wins.
83pub fn set_cache_dir(dir: std::path::PathBuf) {
84    let _ = CACHE_DIR.set(dir);
85}
86
87/// Same directory, for the backends.
88pub fn cache_dir_pub() -> std::path::PathBuf {
89    cache_dir()
90}
91
92fn cache_dir() -> std::path::PathBuf {
93    if let Some(d) = CACHE_DIR.get() {
94        return d.clone();
95    }
96    match std::env::var_os("TMPDIR") {
97        Some(t) => std::path::PathBuf::from(t),
98        None => std::env::temp_dir(),
99    }
100}
101
102/// Where decided verdicts are remembered between runs. `CMF_PROBE_CACHE`
103/// overrides the path; `0` disables the cache entirely.
104fn probe_cache_path() -> Option<std::path::PathBuf> {
105    match std::env::var("CMF_PROBE_CACHE") {
106        Ok(v) if v == "0" => None,
107        Ok(v) => Some(std::path::PathBuf::from(v)),
108        Err(_) => Some(cache_dir().join("cortiq-gpu-probe.tsv")),
109    }
110}
111
112/// One line per decided class: `version \t device \t class \t winner`.
113/// A different engine build or a different device simply does not match,
114/// so a stale file is inert rather than wrong.
115fn probe_cache_key_named(class: &str) -> String {
116    format!(
117        "{}\t{}\t{}",
118        env!("CARGO_PKG_VERSION"),
119        device_label(),
120        class
121    )
122}
123
124const CLASS_NAMES: [&str; 7] = [
125    "ffn",
126    "matvec",
127    "matmat",
128    "qkv-batch",
129    "matmat-wide",
130    "lm-head",
131    "gemm-nt",
132];
133
134/// Adopt every verdict this device already reached in an earlier run.
135///
136/// Probing is not cheap and it is not free of consequences: on a
137/// Snapdragon 778G the three deciding classes took **three minutes of
138/// wall clock** before the first token, every process, and in the phone
139/// app that was the whole first answer — 209.6 s for 25 tokens against
140/// 10.5 s on the CPU path. The verdict itself was the same every time.
141/// Paying to rediscover it is the defect; the answer is to write it down.
142fn probe_cache_load() {
143    static ONCE: std::sync::Once = std::sync::Once::new();
144    ONCE.call_once(|| {
145        let Some(path) = probe_cache_path() else {
146            return;
147        };
148        // Unit tests share this process and its default cache path; a
149        // verdict left by an earlier run would decide a class before the
150        // arbitration tests get to watch it alternate. Tests that mean to
151        // exercise the cache point `CMF_PROBE_CACHE` at their own file.
152        if cfg!(test) && std::env::var("CMF_PROBE_CACHE").is_err() {
153            return;
154        }
155        let Ok(text) = std::fs::read_to_string(&path) else {
156            return;
157        };
158        probe_cache_adopt(&text);
159    });
160}
161
162/// Apply verdicts from a cache file's text. Split out from the file
163/// reading so the adoption rule — including which lines must be IGNORED
164/// — is testable without a filesystem.
165fn probe_cache_adopt(text: &str) {
166    for line in text.lines() {
167        let Some((key, verdict)) = line.rsplit_once('\t') else {
168            continue;
169        };
170        let winner = match verdict.trim() {
171            "gpu" => 1u8,
172            "cpu" => 2u8,
173            _ => continue,
174        };
175        for (i, name) in CLASS_NAMES.iter().enumerate() {
176            if probe_cache_key_named(name) == key {
177                let _ = PROBES[i].state.compare_exchange(
178                    0,
179                    winner,
180                    Ordering::Relaxed,
181                    Ordering::Relaxed,
182                );
183                tracing::debug!("gpu probe [{name}]: remembered → {verdict}");
184            }
185        }
186    }
187}
188
189/// Remember a verdict for the next run. Best-effort: a read-only cache
190/// directory costs a re-probe, never a failure.
191fn probe_cache_store(c: OpClass, winner: u8) {
192    let Some(path) = probe_cache_path() else {
193        return;
194    };
195    let line = format!(
196        "{}\t{}\n",
197        probe_cache_key_named(CLASS_NAMES[c as usize]),
198        if winner == 1 { "gpu" } else { "cpu" }
199    );
200    use std::io::Write;
201    if let Ok(mut f) = std::fs::OpenOptions::new()
202        .create(true)
203        .append(true)
204        .open(&path)
205    {
206        let _ = f.write_all(line.as_bytes());
207    }
208}
209
210/// Backends: note a one-off cost (weight upload, buffer-cache fill) so
211/// the probe discards this sample.
212/// Every buffer creation anywhere bumps this; the graph's bind-group
213/// cache treats any cold event as total invalidation — a stale bind
214/// group is silent corruption, a cleared cache is one re-encoded token.
215pub fn cold_epoch() -> u64 {
216    COLD_EPOCH.load(std::sync::atomic::Ordering::Relaxed)
217}
218static COLD_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
219
220pub(crate) fn probe_note_cold() {
221    COLD_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
222    PROBE_COLD.with(|c| c.set(true));
223}
224
225/// Peek the cold flag without consuming it (`probe_record` consumes).
226/// Contention heuristics use this: a slow COLD op is a one-off build
227/// cost, not evidence the device is busy.
228pub(crate) fn probe_was_cold() -> bool {
229    PROBE_COLD.with(|c| c.get())
230}
231
232/// Pipeline: mark the current layer (or −1 outside layers) for layer-split.
233pub fn set_layer(l: i64) {
234    CUR_LAYER.with(|c| c.set(l));
235}
236
237/// The layer `set_layer` last marked on this thread (−1 outside layers).
238pub fn cur_layer() -> i64 {
239    CUR_LAYER.with(|c| c.get())
240}
241
242/// Capacity-derived layer prefix for per-op walks. The explicit
243/// `CMF_GPU_LAYERS` override is handled by the backend and takes precedence.
244pub fn automatic_layer_prefix(
245    model: &Arc<CmfModel>,
246    num_layers: usize,
247    physical_layers: usize,
248) -> Option<usize> {
249    match backend() {
250        #[cfg(feature = "gpu")]
251        Backend::Wgpu => {
252            crate::gpu_wgpu::automatic_layer_prefix(model, num_layers, physical_layers)
253        }
254        _ => None,
255    }
256}
257
258/// Parse `CMF_GPU_LAYERS` («0-19», «0,2,4», «0-9,30-39») once.
259/// None = no restriction (all layers on GPU). Garbage → also no restriction.
260fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
261    static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
262    R.get_or_init(|| {
263        let s = std::env::var("CMF_GPU_LAYERS").ok()?;
264        let mut v = Vec::new();
265        for part in s.split(',') {
266            let part = part.trim();
267            match part.split_once('-') {
268                Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
269                None => {
270                    let x: i64 = part.parse().ok()?;
271                    v.push((x, x));
272                }
273            }
274        }
275        Some(v)
276    })
277}
278
279fn layer_allowed() -> bool {
280    match layer_ranges() {
281        None => true,
282        Some(ranges) => {
283            let cur = CUR_LAYER.with(|c| c.get());
284            cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
285        }
286    }
287}
288
289/// GPU allowed FOR THE CURRENT LAYER: backend is initialized AND the layer
290/// falls within `CMF_GPU_LAYERS` (GPU/CPU layer-split) AND we are not
291/// inside a `cpu_scope`. Op gates call this.
292pub fn enabled_here() -> bool {
293    !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
294}
295
296// ── Runtime GPU-vs-CPU probe ────────────────────────────────────────────
297// CMF_GPU=1 does not TRUST that the device wins — it MEASURES. For each
298// op class the first calls alternate arms: GPU timed vs pure-CPU timed
299// (under cpu_scope). Cold GPU calls (weight upload / cache fill) are
300// discarded; after PROBE_SAMPLES clean samples per arm the faster arm is
301// chosen for the rest of the process. Rationale: submit+poll latency
302// differs by an order of magnitude across driver stacks (Metal/PCIe
303// ~3-4 ms, Vulkan/4090 ~0.3 ms) — a static threshold cannot know whether
304// per-op offload pays off HERE. CMF_GPU_PROBE=0 → always trust the GPU.
305
306/// GPU-eligible op classes, each with an independent probe.
307#[derive(Clone, Copy)]
308pub enum OpClass {
309    /// Whole FFN chain in one submission (dense / MoE block).
310    Ffn = 0,
311    /// Large hybrid CPU∥GPU matvec (lm_head class).
312    Matvec = 1,
313    /// Prefill GEMM (matmat).
314    Matmat = 2,
315    /// Batched matvecs of one input (QKV).
316    Batch = 3,
317    /// Prefill GEMM at image-diffusion widths (b ≥ 128). Probed apart
318    /// from `Matmat`: one imagegen process runs BOTH populations
319    /// (prompt encode b≈40 where the GPU wins big, DiT b≥256 where
320    /// the CPU AMX arm is competitive) — a single shared verdict locks
321    /// the wrong arm for whichever population samples second.
322    MatmatWide = 4,
323    /// The lm_head itself, apart from the merely-large matvecs. Same
324    /// reasoning as `MatmatWide`, and DeepSeek-V4 is where it bit: its
325    /// attention projections are 37M weights and its head is 529M, so
326    /// the projections' verdict — CPU, honestly measured at 0.19 ms —
327    /// decided for a matvec fourteen times their size that took 11 ms
328    /// a token on the host.
329    MatvecHead = 5,
330    /// The blocked f32 GEMM (`fcd_ops::gemm_nt`): attention's QKᵀ and
331    /// AV, and the VAE decoders' projections. It used to take every job
332    /// over 4 M MACs on sight, with no CPU arm to lose to — which on
333    /// the MiniMax-H3 video decoder was three times SLOWER than the
334    /// host it displaced. Its population is per-head slices, nothing
335    /// like the weight GEMMs above, so it probes on its own.
336    GemmNt = 6,
337}
338
339/// Which probe a large matvec belongs to. The head is an order of
340/// magnitude bigger than anything else that reaches this gate, and the
341/// two populations do not have the same answer.
342pub fn matvec_class(rows: usize, cols: usize) -> OpClass {
343    if rows * cols >= 67_108_864 {
344        OpClass::MatvecHead
345    } else {
346        OpClass::Matvec
347    }
348}
349
350/// Probe verdict for one call.
351pub enum ProbeArm {
352    /// Run the GPU path (during probing: timed, recorded).
353    Gpu,
354    /// Probing: run the CPU path under `cpu_scope`, timed, recorded.
355    CpuTimed,
356    /// Decided: CPU won — run the CPU path (under `cpu_scope`).
357    Cpu,
358}
359
360/// Clean samples per arm before a class decides.
361const PROBE_SAMPLES: u32 = 6;
362
363/// Declines before a class gives the work to the host for good. High
364/// enough that a transient refusal — an unsealed state during prefill, a
365/// shape the kernel skips this once — cannot settle the question.
366const PROBE_DECLINE_LIMIT: u32 = 16;
367
368/// Device samples discarded before any count — see `Probe::gpu_burn`.
369const PROBE_WARMUP: u32 = 1;
370
371struct Probe {
372    /// 0 = probing, 1 = GPU won, 2 = CPU won.
373    state: AtomicU8,
374    flip: AtomicU32,
375    gpu_ns: AtomicU64,
376    gpu_n: AtomicU32,
377    /// Times the device arm was chosen and the device DECLINED.
378    ///
379    /// A decline carries no timing, so nothing is recorded — and a class
380    /// whose device path always refuses therefore never reaches a
381    /// verdict, alternates arms forever, and pays a failed device
382    /// attempt on half of every token's calls. Measured on an M4 with
383    /// LFM2.5-2.6B: `ffn` was still undecided after 9000 calls, and a
384    /// token cost 83.55 ms against 41.85 with the device off — twice the
385    /// price for work the host did anyway.
386    declines: AtomicU32,
387    /// GPU samples still to discard as warm-up.
388    ///
389    /// The cold flag catches buffer and weight uploads, but a compute
390    /// pipeline is compiled on first use and not every creation site
391    /// raises it — the wgpu path has 21 pipeline creations against 12
392    /// cold notes. One uncaught shader compile is enough to lose a
393    /// class for the whole process: `gemm-nt` on an A100 was recorded at
394    /// 117.01 ms against the host's 3.19 and sent to the CPU, which
395    /// parked a 27B bake on 2.6 cores with the card idle. The decision
396    /// already uses each arm's BEST sample, so discarding the first
397    /// GPU sample costs one extra round trip and removes the whole
398    /// class of first-call artefacts.
399    gpu_burn: AtomicU32,
400    cpu_ns: AtomicU64,
401    cpu_n: AtomicU32,
402    /// Best (minimum) sample per arm. The DECISION compares these:
403    /// means are poisoned by one-off cold costs the cold-flag cannot
404    /// see — e.g. the CPU arm's first mmap-cold expert matvec page
405    /// faults its weights in and reads 3× its steady state, which
406    /// locked the GPU arm on a 35B MoE at a 4× real-world loss. The
407    /// minimum is each arm's honest steady-state pace.
408    gpu_min: AtomicU64,
409    cpu_min: AtomicU64,
410}
411
412impl Probe {
413    const fn new() -> Self {
414        Self {
415            state: AtomicU8::new(0),
416            flip: AtomicU32::new(0),
417            gpu_ns: AtomicU64::new(0),
418            gpu_n: AtomicU32::new(0),
419            declines: AtomicU32::new(0),
420            gpu_burn: AtomicU32::new(PROBE_WARMUP),
421            cpu_ns: AtomicU64::new(0),
422            cpu_n: AtomicU32::new(0),
423            gpu_min: AtomicU64::new(u64::MAX),
424            cpu_min: AtomicU64::new(u64::MAX),
425        }
426    }
427}
428
429static PROBES: [Probe; 7] = [
430    Probe::new(),
431    Probe::new(),
432    Probe::new(),
433    Probe::new(),
434    Probe::new(),
435    Probe::new(),
436    Probe::new(),
437];
438
439/// A caller that knows its loop is long, uniform and warm can say so: the
440/// probe times ops in isolation and alternates arms to do it, which reads a
441/// sustained diffusion step as slower on the device than it is. Measured on
442/// an M4 at 672 video tokens: the probe picked the CPU at 1.25 ms against
443/// 0.88 ms per op, and the loop it picked for ran 23.9 s a step against the
444/// device's 19.7 s.
445static TRUST_GPU: AtomicBool = AtomicBool::new(false);
446
447/// Take the probe out of the loop until the guard drops.
448pub fn trust_gpu() -> GpuTrust {
449    let was = TRUST_GPU.swap(true, Ordering::Relaxed);
450    GpuTrust(was)
451}
452
453pub struct GpuTrust(bool);
454
455impl Drop for GpuTrust {
456    fn drop(&mut self) {
457        TRUST_GPU.store(self.0, Ordering::Relaxed);
458    }
459}
460
461fn probe_on_for(c: OpClass) -> bool {
462    // The trust is only for the *wide* class. A sustained diffusion step is
463    // where the probe reads a warm device as cold; the narrow batches inside
464    // the same loop — an audio stream of fifty-one tokens against the same
465    // weights — are small enough that submit latency can genuinely beat the
466    // arithmetic, and there the probe is right and should keep deciding.
467    if TRUST_GPU.load(Ordering::Relaxed) && matches!(c, OpClass::MatmatWide | OpClass::Ffn) {
468        return false;
469    }
470    probe_on()
471}
472
473fn probe_on() -> bool {
474    static ON: OnceLock<bool> = OnceLock::new();
475    *ON.get_or_init(|| {
476        std::env::var("CMF_GPU_PROBE")
477            .map(|v| v != "0" && v != "off")
478            .unwrap_or(true)
479    })
480}
481
482/// q1 ops on the native Metal backend skip the probe entirely: the CPU
483/// q1 kernel is load-port-bound, the GPU one wins warm — and probe
484/// alternation itself cools the device between samples (measured: block
485/// times 5.8 ms warm vs 8.8 ms mixed). Other backends keep probing.
486pub fn q1_force() -> bool {
487    #[cfg(target_os = "macos")]
488    {
489        backend() == Backend::Metal
490    }
491    #[cfg(not(target_os = "macos"))]
492    {
493        false
494    }
495}
496
497/// Should a FUSED whole-block path trust the device instead of asking
498/// the per-op probe? True on native Metal and on discrete wgpu adapters.
499///
500/// The probe answers "is one wide matmat faster on the GPU", and for the
501/// DiT on Metal that is a coin flip — measured 2.62 ms GPU vs 2.56 ms
502/// CPU, a 2% spread that lands on either arm run to run. But the fused
503/// block's advantage is not per-op speed, it is that the hidden state,
504/// the packs and the attention panels never leave the device: end to end
505/// the whole-block path renders a 512² Lumina step in ~5.4 s against
506/// ~8.4 s when the probe happens to pick the CPU. Gating a fusion win on
507/// a per-op tie made every second render half-speed at random.
508///
509/// On a discrete card the verdict is never in doubt — an RTX 3090 against
510/// a 256-core EPYC measured 11.5 ms vs 31 ms per wide op, four runs out
511/// of four — so the probe's sampling phase is pure cost: it alone was 10%
512/// of a 512² render (74.3 s against 66.9 s with the probe off). Integrated
513/// and mobile adapters keep probing; there the submit latency is real and
514/// can genuinely lose.
515pub fn fused_block_trusted() -> bool {
516    #[cfg(target_os = "macos")]
517    if backend() == Backend::Metal {
518        return true;
519    }
520    wgpu_graph_default()
521}
522
523/// Which arm should this GPU-eligible call take? Consult AFTER the
524/// eligibility gates (`enabled_here` / `min_rows`) so only real
525/// candidates alternate.
526/// While a class is still probing, a call whose weights are NOT yet on
527/// the card should take the GPU arm anyway: the upload is work the next
528/// step needs regardless, and the sample it produces is discarded as
529/// cold — so handing that call to the CPU arm buys nothing and costs a
530/// host GEMM. Measured on a diffusion stack, where every layer is
531/// touched once per step and therefore EVERY first-step GPU sample is
532/// cold: one projection drew the CPU arm for the whole first step, 9.8 s
533/// against the 2.8 s it costs once the weights are warm.
534pub fn weight_is_resident(model: &Arc<CmfModel>, idx: usize) -> bool {
535    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
536    {
537        return crate::gpu_wgpu::weight_is_resident(model, idx);
538    }
539    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
540    {
541        let _ = (model, idx);
542        true
543    }
544}
545
546pub fn probe_arm_cold_prefers_gpu(c: OpClass, weights_resident: bool) -> ProbeArm {
547    if !weights_resident && probe_deciding(c) {
548        return ProbeArm::Gpu;
549    }
550    probe_arm(c)
551}
552
553pub fn probe_arm(c: OpClass) -> ProbeArm {
554    // Every arbitrated call starts with a clean cold flag: both the
555    // sample discard in `probe_record` and the contention kill-switch
556    // read it AFTER the op, so a stale note from a previous call on
557    // this thread must not leak in.
558    PROBE_COLD.with(|f| f.set(false));
559    if !probe_on_for(c) {
560        return ProbeArm::Gpu;
561    }
562    probe_cache_load();
563    let p = &PROBES[c as usize];
564    match p.state.load(Ordering::Relaxed) {
565        1 => ProbeArm::Gpu,
566        2 => ProbeArm::Cpu,
567        _ => {
568            if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
569                ProbeArm::Gpu
570            } else {
571                ProbeArm::CpuTimed
572            }
573        }
574    }
575}
576
577/// The device arm was chosen and the device refused the work, so there
578/// is no time to record. Callers that fall through to the host MUST say
579/// so here, or the class can never decide.
580pub fn probe_note_decline(c: OpClass) {
581    let p = &PROBES[c as usize];
582    if p.state.load(Ordering::Relaxed) != 0 {
583        return;
584    }
585    let n = p.declines.fetch_add(1, Ordering::Relaxed) + 1;
586    if n >= PROBE_DECLINE_LIMIT
587        && p.state
588            .compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed)
589            .is_ok()
590    {
591        tracing::info!(
592            "gpu probe [{}]: device declined {n} times → cpu",
593            CLASS_NAMES[c as usize]
594        );
595    }
596}
597
598/// Record a timed arm sample; on the `PROBE_SAMPLES`-th clean sample of
599/// BOTH arms the class decides for the rest of the process.
600pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
601    probe_record_into(
602        &PROBES[c as usize],
603        CLASS_NAMES[c as usize],
604        Some(c),
605        gpu,
606        dur,
607    )
608}
609
610/// The body of `probe_record` over ONE probe, so the decision can be
611/// driven in a test without touching the process-wide array.
612fn probe_record_into(
613    p: &Probe,
614    class_name: &str,
615    cache: Option<OpClass>,
616    gpu: bool,
617    dur: std::time::Duration,
618) {
619    if p.state.load(Ordering::Relaxed) != 0 {
620        return;
621    }
622    if gpu && PROBE_COLD.with(|f| f.replace(false)) {
623        return; // one-off cost in this call — not a steady-state sample
624    }
625    if gpu {
626        // Load-then-store rather than fetch_sub: a blind decrement at
627        // zero wraps a u32 to its maximum and mutes the arm forever.
628        // A benign race here burns one extra sample, which is free.
629        let left = p.gpu_burn.load(Ordering::Relaxed);
630        if left > 0 {
631            p.gpu_burn.store(left - 1, Ordering::Relaxed);
632            return; // warm-up: the first device sample builds its pipeline
633        }
634    }
635    let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
636    if gpu {
637        p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
638        p.gpu_n.fetch_add(1, Ordering::Relaxed);
639        p.gpu_min.fetch_min(ns, Ordering::Relaxed);
640    } else {
641        p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
642        p.cpu_n.fetch_add(1, Ordering::Relaxed);
643        p.cpu_min.fetch_min(ns, Ordering::Relaxed);
644    }
645    let (gn, cn) = (
646        p.gpu_n.load(Ordering::Relaxed),
647        p.cpu_n.load(Ordering::Relaxed),
648    );
649    if gn >= 2 && cn >= 2 {
650        // Decide on each arm's BEST sample — the steady-state pace.
651        // Means carry one-off cold costs (mmap page-in on the CPU arm)
652        // that the cold-flag machinery cannot see.
653        let g = p.gpu_min.load(Ordering::Relaxed) as f64;
654        let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
655        // Early verdict on a ≥2× gap — no reason to keep feeding the
656        // losing arm; close races take the full sample count. It was 3×,
657        // and the cost of that half-octave was measured: a DiT whose
658        // wide GEMMs run 11.4 ms on the device against 32.2 on the host
659        // (2.8×) kept ALTERNATING through the whole diffusion stack, and
660        // because the alternation counter is shared per class in call
661        // order, one projection drew the CPU arm every single time — 9.9
662        // seconds a step on a kernel that needs 0.4. Both arms are
663        // compared on their BEST sample, so a 2× gap is not noise.
664        if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 2.0 && cp < g * 2.0 {
665            return;
666        }
667        let winner = if g <= cp { 1 } else { 2 };
668        if p.state
669            .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
670            .is_ok()
671        {
672            tracing::info!(
673                "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
674                class_name,
675                g / 1e6,
676                cp / 1e6,
677                if winner == 1 { "gpu" } else { "cpu" },
678            );
679            if let Some(c) = cache {
680                probe_cache_store(c, winner);
681            }
682        }
683    }
684}
685
686/// Is the class still collecting samples? (Call sites use this to route
687/// cold-weight calls away from the GPU arm during probing.)
688pub fn probe_deciding(c: OpClass) -> bool {
689    probe_on_for(c) && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
690}
691
692/// Probing helper: true — tensor `idx`'s quant weights are ALREADY
693/// device-resident (a clean GPU sample is possible now); false — they
694/// were not (the upload starts within the VRAM budget, so a later call
695/// finds them warm) or the tensor cannot go to the GPU at all. Keeps the
696/// probe from billing a full cold dispatch+readback to a sample it will
697/// discard anyway. The verdict needs only a couple of warm tensors, so
698/// probe-driven uploads are capped — the losing-GPU machine should not
699/// pay for uploading the whole layer stack it will never use; if the GPU
700/// wins, the rest uploads lazily on demand, in the same first-touch order.
701#[allow(unused_variables)]
702pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
703    static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
704    let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
705    let resident = match backend() {
706        #[cfg(target_os = "macos")]
707        Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
708        #[cfg(feature = "gpu")]
709        Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
710        Backend::None => false,
711    };
712    if !resident && may_upload {
713        PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
714    }
715    resident
716}
717
718/// Test hook: reset all probes to the undecided state.
719#[cfg(test)]
720pub(crate) fn probe_reset() {
721    for p in &PROBES {
722        p.state.store(0, Ordering::Relaxed);
723        p.flip.store(0, Ordering::Relaxed);
724        p.gpu_ns.store(0, Ordering::Relaxed);
725        p.gpu_n.store(0, Ordering::Relaxed);
726        p.cpu_ns.store(0, Ordering::Relaxed);
727        p.cpu_n.store(0, Ordering::Relaxed);
728    }
729}
730
731#[cfg(test)]
732mod probe_tests {
733    use super::*;
734    use std::time::Duration;
735
736    // One test fn: PROBES is process-global and probe_reset touches all
737    // classes — parallel test threads would race.
738    #[test]
739    fn probe_alternates_discards_cold_and_decides() {
740        probe_reset();
741        // Probing: arms alternate.
742        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
743        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
744
745        // A cold GPU sample (upload noted) must be discarded: feed a
746        // catastrophic cold sample, then clean fast-GPU samples — GPU
747        // wins only if the cold one did not count.
748        probe_note_cold();
749        probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
750        for _ in 0..PROBE_SAMPLES {
751            probe_record(OpClass::Ffn, true, Duration::from_millis(1));
752            probe_record(OpClass::Ffn, false, Duration::from_millis(4));
753        }
754        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
755
756        // The reverse: a class where the CPU arm is faster decides CPU.
757        for _ in 0..PROBE_SAMPLES {
758            probe_record(OpClass::Matmat, true, Duration::from_millis(4));
759            probe_record(OpClass::Matmat, false, Duration::from_millis(1));
760        }
761        assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
762
763        // cpu_scope: gates off inside, restored after.
764        cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
765        CPU_ONLY.with(|c| assert!(!c.get()));
766        cpu_scope(|| {
767            cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
768            CPU_ONLY.with(|c| assert!(c.get()));
769        });
770        let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
771        CPU_ONLY.with(|c| assert!(!c.get()));
772        probe_reset();
773    }
774
775    #[test]
776    fn a_remembered_verdict_is_adopted_and_a_stranger_is_not() {
777        // Probing is not free: on a Snapdragon 778G the deciding classes
778        // cost minutes of wall clock before the first token, every
779        // process, and reached the same verdict every time. The cache
780        // exists so that price is paid once.
781        //
782        // The key is built from THIS process's device, never a name this
783        // test sets: `probe_set_device` is first-writer-wins and on a Mac
784        // the Metal backend may already have named the silicon before the
785        // tests run — which is exactly how this test failed on CI while
786        // passing locally. GemmNt on purpose: the arbitration test never
787        // touches it, and both run in one process.
788        let mine = probe_cache_key_named("gemm-nt");
789        let state = || {
790            PROBES[OpClass::GemmNt as usize]
791                .state
792                .load(Ordering::Relaxed)
793        };
794
795        // Another device's verdict is not mine, whatever it claims.
796        probe_cache_adopt("SomeOtherGPU/Vulkan\tgemm-nt\tgpu\n");
797        assert_eq!(state(), 0);
798        // Neither is one from another build of this engine.
799        let older = mine.replacen(env!("CARGO_PKG_VERSION"), "0.0.0-old", 1);
800        assert_ne!(older, mine);
801        probe_cache_adopt(&format!("{older}\tgpu\n"));
802        assert_eq!(state(), 0);
803        // Mine is.
804        probe_cache_adopt(&format!("{mine}\tcpu\n"));
805        assert_eq!(state(), 2);
806
807        PROBES[OpClass::GemmNt as usize]
808            .state
809            .store(0, Ordering::Relaxed);
810    }
811}
812
813/// Default row threshold: the GPU takes only larger matrices (lm_head
814/// class). Below it, the dispatch/readback cost does not pay off on unified memory.
815pub const GPU_MIN_ROWS: usize = 65_536;
816
817/// Effective threshold: `CMF_GPU_MIN_ROWS` overrides. Defaults differ
818/// by device class: on a DISCRETE card VRAM bandwidth pays off even for
819/// FFN/QKV-class matrices (4096), on unified memory only lm_head-class
820/// is worth the dispatch/readback (65536). Field case behind this: a
821/// 35B model on an RTX 4090 saw ~0 offload because every layer matrix
822/// sat below the old universal 65536.
823pub fn min_rows() -> usize {
824    if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
825        .ok()
826        .and_then(|v| v.parse().ok())
827    {
828        return v;
829    }
830    if discrete() { 4096 } else { GPU_MIN_ROWS }
831}
832
833/// Is the active backend a discrete card (PCIe VRAM)?
834pub fn discrete() -> bool {
835    match backend() {
836        #[cfg(feature = "gpu")]
837        Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
838        #[cfg(target_os = "macos")]
839        Backend::Metal => false, // UMA by the init() guard
840        Backend::None => false,
841    }
842}
843
844/// A single MoE-FFN job (an expert with its own weight), executed in one
845/// submission: (rows, cols, idx, row_scale) for gate/up/down + prescaled
846/// inputs + the down column scale + the blending weight.
847pub struct MoeJob<'a> {
848    pub gate: (usize, usize, usize, &'a [f32]),
849    pub up: (usize, usize, usize, &'a [f32]),
850    pub down: (usize, usize, usize, &'a [f32]),
851    pub xs_gate: Vec<f32>,
852    pub xs_up: Vec<f32>,
853    pub down_col: &'a [f32],
854    pub w: f32,
855    /// q1 trio: scales live inside the 6-byte tiles (row_scale slices
856    /// empty, xs raw f32). Backends without a q1 kernel refuse the job.
857    pub q1: bool,
858    /// q4_tiled trio: scales inside the 18-byte tiles (row_scale
859    /// slices empty, xs raw f32) — the MoE-hybrid coder class.
860    pub q4t: bool,
861    /// q4tp trio: same raw-xs contract, 16-byte nibble stride and the scale
862    /// on a per-row ladder. Without this the experts of a q4tp MoE model fall
863    /// to the CPU while every other dtype rides the device.
864    pub q4tp: bool,
865    /// Mixed 2-bit profile: gate/up are q2tp (8-byte chunks, zero rung),
866    /// down stays q4tp. Set together with `q4tp`; a backend without the
867    /// 2-bit kernel must refuse the whole job.
868    pub gu_q2: bool,
869    /// The reference's `swiglu_limit`; 0 disables the clamp. A backend that
870    /// cannot apply it must REFUSE the job rather than drop it silently —
871    /// the difference only shows on saturating activations, which is the
872    /// hardest kind of divergence to notice.
873    pub swiglu_limit: f32,
874}
875
876/// A single independent batch matvec (GDN projections of one input).
877pub struct BatchJob<'a> {
878    pub idx: usize,
879    pub rows: usize,
880    pub cols: usize,
881    pub row_scale: &'a [f32],
882    pub xs: Vec<f32>,
883    /// Weight layout. Was a bare `q1: bool`, which could only ever spell two
884    /// of the four and silently sent everything else back to the CPU — the
885    /// GDN projections of a q4t/q4tp model never reached the device at all.
886    pub layout: BatchLayout,
887}
888
889/// Which kernel a batched matvec needs. q8 carries row scales in a side
890/// buffer; the rest embed them in the payload and differ in stride.
891#[derive(Clone, Copy, PartialEq, Eq, Debug)]
892pub enum BatchLayout {
893    Q8,
894    Q1,
895    Q4t,
896    Q4tp,
897}
898
899#[derive(Clone, Copy, PartialEq, Eq)]
900enum Backend {
901    None,
902    #[cfg(target_os = "macos")]
903    Metal,
904    #[cfg(feature = "gpu")]
905    Wgpu,
906}
907
908fn backend() -> Backend {
909    #[cfg(feature = "gpu")]
910    if crate::gpu_wgpu::selected() {
911        return if crate::gpu_wgpu::enabled() {
912            Backend::Wgpu
913        } else {
914            Backend::None
915        };
916    }
917    #[cfg(target_os = "macos")]
918    if crate::gpu_metal::enabled() {
919        return Backend::Metal;
920    }
921    Backend::None
922}
923
924/// GPU enabled and initialized on the selected backend?
925/// Whether THIS build can bring a GPU up on THIS device: a compiled-in
926/// backend plus a live adapter. The mobile FFI exposes it so an app can
927/// tell "GPU off" from "GPU impossible" (a CPU-only .so ships no
928/// backend at all). Cached after the first call.
929pub fn backend_available() -> bool {
930    #[cfg(target_os = "macos")]
931    {
932        // The Metal path is always compiled on macOS.
933        true
934    }
935    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
936    {
937        static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
938        *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
939    }
940    #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
941    {
942        false
943    }
944}
945
946/// A process-wide, phase-scoped GPU gate. `cpu_scope` is thread-local and
947/// the pool's workers do not inherit it, so a caller that wants a whole
948/// *phase* off the device — a prompt encoder whose weights live in a part of
949/// the file the hot loop never touches, on a machine that cannot keep both
950/// wired — has to say so globally.
951static GPU_PAUSED: AtomicBool = AtomicBool::new(false);
952
953/// Park the device for every thread until the returned guard drops.
954pub fn pause_gpu() -> GpuPause {
955    GPU_PAUSED.store(true, Ordering::Relaxed);
956    GpuPause(())
957}
958
959pub struct GpuPause(());
960
961impl Drop for GpuPause {
962    fn drop(&mut self) {
963        GPU_PAUSED.store(false, Ordering::Relaxed);
964    }
965}
966
967pub fn enabled() -> bool {
968    !GPU_PAUSED.load(Ordering::Relaxed) && backend() != Backend::None
969}
970
971/// Default-on condition for the wgpu whole-token graph: the wgpu
972/// backend on a DISCRETE adapter. NOT plain `enabled()` (macOS/Metal
973/// must not pay a per-token layer scan for a graph its backend
974/// refuses), and NOT integrated adapters: the graph's ~300 barriered
975/// dispatches per token are cheap on desktop immediate-mode GPUs but
976/// tiled mobile GPUs (Adreno/Mali) drain the pipeline at every barrier
977/// — field report: 0.2 tok/s on-graph vs 15 tok/s on the CPU. On
978/// integrated adapters the per-op probe path arbitrates each op class
979/// against the CPU instead; CMF_GPU_WGPU_GRAPH=1 still forces the
980/// graph anywhere.
981/// Is the wgpu backend active at all (any adapter)? Eligibility gate
982/// for the whole-token graph — whether it actually RUNS is decided by
983/// `wgpu_graph_default` (trusted on discrete) or the generation race.
984pub fn wgpu_active() -> bool {
985    #[cfg(feature = "gpu")]
986    {
987        matches!(backend(), Backend::Wgpu)
988    }
989    #[cfg(not(feature = "gpu"))]
990    {
991        false
992    }
993}
994
995/// Which GPU this thread's engine calls address. Multi-card hosts hold
996/// one wgpu context PER card (weights, KV mirrors and scratch live
997/// inside a context, so per-device contexts give per-device caches for
998/// free); this thread-local says which one is current. Default: the
999/// process pin (CMF_GPU_ADAPTER) or 0 — so single-card runs behave
1000/// exactly as they always have.
1001pub fn default_device() -> usize {
1002    static D: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1003    *D.get_or_init(|| {
1004        std::env::var("CMF_GPU_ADAPTER")
1005            .ok()
1006            .and_then(|v| v.trim().parse::<usize>().ok())
1007            .unwrap_or(0)
1008    })
1009}
1010
1011thread_local! {
1012    static CUR_DEV: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
1013}
1014
1015/// The device this thread is pinned to.
1016pub fn current_device() -> usize {
1017    CUR_DEV.with(|c| c.get()).unwrap_or_else(default_device)
1018}
1019
1020/// Pin this thread to a device. Server slots call it once per request;
1021/// the worker pool propagates it into its threads, so a dispatch begun
1022/// on card 1 does not finish on card 0.
1023pub fn set_current_device(i: usize) {
1024    CUR_DEV.with(|c| c.set(Some(i)));
1025}
1026
1027/// Run `f` with this thread pinned to `dev`, restoring the previous pin.
1028pub fn with_device<R>(dev: usize, f: impl FnOnce() -> R) -> R {
1029    let prev = CUR_DEV.with(|c| c.replace(Some(dev)));
1030    let r = f();
1031    CUR_DEV.with(|c| c.set(prev));
1032    r
1033}
1034
1035/// How many GPUs this process can address (wgpu adapter count; 1 on
1036/// Metal, 0 without a backend).
1037pub fn device_count() -> usize {
1038    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1039    {
1040        return crate::gpu_wgpu::adapter_count();
1041    }
1042    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1043    {
1044        usize::from(backend_available())
1045    }
1046}
1047
1048/// Weight budget of the current GPU in bytes; 0 when there is none and
1049/// u64::MAX on unified memory (where the OS pages shared RAM and the
1050/// question "does the model fit the card" has no separate answer).
1051pub fn vram_budget() -> u64 {
1052    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1053    {
1054        return crate::gpu_wgpu::device_vram_budget();
1055    }
1056    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1057    {
1058        if backend_available() { u64::MAX } else { 0 }
1059    }
1060}
1061
1062/// Bytes currently accounted as resident weight buffers on the active wgpu
1063/// adapter.  This is the logical device-local weight set; physical driver
1064/// allocations are reported separately by the platform tools.
1065pub fn resident_bytes() -> u64 {
1066    #[cfg(feature = "gpu")]
1067    {
1068        if backend() == Backend::Wgpu {
1069            return crate::gpu_wgpu::resident_bytes();
1070        }
1071    }
1072    0
1073}
1074
1075/// Sealed O(1) device mirror count and logical bytes for one pipeline id.
1076/// Zero is returned when wgpu is unavailable or the sequence has not reached
1077/// an O(1) seal yet.
1078pub fn o1_device_stats(kv_id: u64) -> (usize, u64) {
1079    #[cfg(feature = "gpu")]
1080    {
1081        if backend() == Backend::Wgpu {
1082            return crate::gpu_wgpu::o1_device_stats(kv_id);
1083        }
1084    }
1085    let _ = kv_id;
1086    (0, 0)
1087}
1088
1089/// Device weight bytes uploaded so far (wgpu; 0 on other backends).
1090/// Steady-state windows must show a ZERO delta — growth mid-benchmark
1091/// means eviction/re-upload and disqualifies the number.
1092pub fn upload_bytes() -> u64 {
1093    #[cfg(feature = "gpu")]
1094    {
1095        return crate::gpu_wgpu::UPLOAD_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1096    }
1097    #[cfg(not(feature = "gpu"))]
1098    0
1099}
1100
1101/// Measure a transient host-to-device upload when the wgpu backend is
1102/// compiled in. CPU-only builds keep the benchmark command available and
1103/// report no device measurement instead of referring to the gated module.
1104pub fn upload_bandwidth_probe(block: usize, rounds: usize) -> Option<f64> {
1105    #[cfg(feature = "gpu")]
1106    {
1107        return crate::gpu_wgpu::upload_bandwidth_probe(block, rounds);
1108    }
1109    let _ = (block, rounds);
1110    None
1111}
1112
1113/// Which half of the run is asking.
1114///
1115/// The phase exists because the graph is plausibly two decisions, not
1116/// one — but on the hardware measured so far it is only ever a decode
1117/// decision. On an Adreno 642L with bonsai-1.7b, from identical clean
1118/// starts and two repeats each: decode 11.6 tok/s without it and 0.72
1119/// with, while prefill is 4.2 either way. A first reading of 3.4 -> 18.0
1120/// for prefill did not survive a controlled re-run — it was a dirty
1121/// probe cache between configurations, not the graph, and the prefill
1122/// route through the graph is GDN-only in the first place, which this
1123/// dense model never takes.
1124#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1125pub enum GraphPhase {
1126    Prefill,
1127    Decode,
1128}
1129
1130/// The one place that decides whether the whole-token graph runs.
1131///
1132/// `CMF_GPU_WGPU_GRAPH`: `0` off everywhere, `prefill` only for the
1133/// prompt, anything else on everywhere. Unset: desktop-class GPUs take
1134/// it for both phases; phone-class UMA takes it for PREFILL only, which
1135/// is the measurement above rather than a guess — the per-op path keeps
1136/// decode, where it is seventeen times better.
1137pub fn wgpu_graph_on(phase: GraphPhase) -> bool {
1138    match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
1139        Some("0") => false,
1140        Some("prefill") => phase == GraphPhase::Prefill,
1141        Some(_) => true,
1142        None => {
1143            if wgpu_graph_default() {
1144                return true;
1145            }
1146            // Integrated/mobile keeps the per-op path for BOTH phases —
1147            // unchanged, because the measurement that would have bought
1148            // prefill a graph did not reproduce. `=prefill` is there for
1149            // the device where it does; the default does not guess.
1150            let _ = phase;
1151            false
1152        }
1153    }
1154}
1155
1156pub fn wgpu_graph_default() -> bool {
1157    #[cfg(feature = "gpu")]
1158    {
1159        // Discrete cards always; Apple-silicon UMA on macOS too — desktop
1160        // -class GPUs where the graph measured ~2x the CPU on the Qwen3.6
1161        // family (M4: 13.3 tok/s against 7.3). Phone-class UMA (Android/
1162        // iOS builds) keeps the per-op probe path: tiled mobile GPUs have
1163        // turned the ~300-dispatch graph into seconds per token.
1164        matches!(backend(), Backend::Wgpu)
1165            && (crate::gpu_wgpu::discrete_active()
1166                || (cfg!(target_os = "macos") && crate::gpu_wgpu::adapter_up()))
1167    }
1168    #[cfg(not(feature = "gpu"))]
1169    {
1170        false
1171    }
1172}
1173
1174/// q8_row/q8_2f matvec, rows [row0, row0+rows). `xs` — prescaled by the column scale.
1175#[allow(clippy::too_many_arguments, unused_variables)]
1176pub fn q8_matvec_range(
1177    model: &Arc<CmfModel>,
1178    idx: usize,
1179    row0: usize,
1180    row_scale: &[f32],
1181    xs: &[f32],
1182    rows: usize,
1183    cols: usize,
1184    out: &mut [f32],
1185) -> bool {
1186    match backend() {
1187        #[cfg(target_os = "macos")]
1188        Backend::Metal => {
1189            crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1190        }
1191        #[cfg(feature = "gpu")]
1192        Backend::Wgpu => {
1193            crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1194        }
1195        Backend::None => false,
1196    }
1197}
1198
1199/// GEMM of a prefill batch: `pre` — prescaled inputs row-major [b, cols],
1200/// out — row-major [b, rows].
1201#[allow(clippy::too_many_arguments, unused_variables)]
1202/// The two-field int8 GEMM with the column field left for the device.
1203/// wgpu only — Metal's int8 kernel takes a pre-scaled activation, so the
1204/// caller keeps that path when this returns `false`.
1205#[allow(clippy::too_many_arguments)]
1206pub fn q8_matmat_2f(
1207    model: &Arc<CmfModel>,
1208    idx: usize,
1209    row_scale: &[f32],
1210    col_field: &[f32],
1211    xs: &[f32],
1212    b: usize,
1213    rows: usize,
1214    cols: usize,
1215    out: &mut [f32],
1216) -> bool {
1217    #[allow(unreachable_patterns)]
1218    match backend() {
1219        #[cfg(feature = "gpu")]
1220        Backend::Wgpu => {
1221            crate::gpu_wgpu::q8_matmat_2f(model, idx, row_scale, col_field, xs, b, rows, cols, out)
1222        }
1223        _ => false,
1224    }
1225}
1226
1227pub fn q8_matmat(
1228    model: &Arc<CmfModel>,
1229    idx: usize,
1230    row_scale: &[f32],
1231    pre: &[f32],
1232    b: usize,
1233    rows: usize,
1234    cols: usize,
1235    out: &mut [f32],
1236) -> bool {
1237    match backend() {
1238        #[cfg(target_os = "macos")]
1239        Backend::Metal => {
1240            crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
1241        }
1242        #[cfg(feature = "gpu")]
1243        Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
1244        Backend::None => false,
1245    }
1246}
1247
1248/// q1 matvec: raw f32 activations, tile-embedded scales. Metal only
1249/// for now (wgpu q1 WGSL is queued); false = CPU fallback.
1250#[allow(unused_variables)]
1251pub fn q1_matvec(
1252    model: &Arc<CmfModel>,
1253    idx: usize,
1254    xs: &[f32],
1255    rows: usize,
1256    cols: usize,
1257    out: &mut [f32],
1258) -> bool {
1259    match backend() {
1260        #[cfg(target_os = "macos")]
1261        Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
1262        #[cfg(feature = "gpu")]
1263        Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
1264        Backend::None => false,
1265    }
1266}
1267
1268/// Whole attention sub-block on the wgpu token graph (drop-in for
1269/// `qwen_attention`): normed hidden in, O-projection out, resident device
1270/// K/V mirror. false = refusal / not the wgpu backend → CPU path.
1271#[allow(clippy::too_many_arguments)]
1272pub fn attn_dropin(
1273    model: &Arc<CmfModel>,
1274    kv_id: u64,
1275    layer: usize,
1276    normed: &[f32],
1277    wq_idx: usize,
1278    wk_idx: usize,
1279    wv_idx: usize,
1280    wo_idx: usize,
1281    q_norm: Option<&[f32]>,
1282    k_norm: Option<&[f32]>,
1283    invf: &[f32],
1284    nh: usize,
1285    nkv: usize,
1286    hd: usize,
1287    rd: usize,
1288    hidden: usize,
1289    pos: usize,
1290    cap: usize,
1291    gemma: bool,
1292    eps: f32,
1293    cpu_k: &[Vec<f32>],
1294    cpu_v: &[Vec<f32>],
1295    out: &mut [f32],
1296) -> bool {
1297    match backend() {
1298        #[cfg(feature = "gpu")]
1299        Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
1300            model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
1301            nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
1302        ),
1303        #[allow(unused_variables)]
1304        _ => false,
1305    }
1306}
1307
1308/// One weight in the whole-token graph: tensor idx + a codec tag (0=q8_row,
1309/// 1=q1, 2=q4_tiled, 3=q1t, 4=f32) + per-row scales (q8_row only) + the raw f32
1310/// data (kind 4 only — small unquantized projections like GDN in_proj_a/b).
1311pub struct GraphW<'a> {
1312    pub idx: usize,
1313    pub kind: u8,
1314    pub row_scale: &'a [f32],
1315    pub data: &'a [f32],
1316}
1317
1318/// A layer's token-mixing op: standard attention or a GDN (linear-attention)
1319/// block. The surrounding norms + SwiGLU FFN are common to both.
1320pub enum GraphAttn<'a> {
1321    Full {
1322        wq: GraphW<'a>,
1323        wk: GraphW<'a>,
1324        wv: GraphW<'a>,
1325        wo: GraphW<'a>,
1326        q_norm: Option<&'a [f32]>,
1327        k_norm: Option<&'a [f32]>,
1328        /// (bq, bk, bv) attention biases (Qwen2). None ⇒ no bias.
1329        bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1330        /// Qwen3.5 gated attention: wq emits 2·nh·hd (q||gate per head), the
1331        /// attention output is scaled by sigmoid(gate) before the O projection.
1332        output_gate: bool,
1333        cpu_k: &'a [Vec<f32>],
1334        cpu_v: &'a [Vec<f32>],
1335    },
1336    Gdn {
1337        qkv: GraphW<'a>,
1338        z: GraphW<'a>,
1339        a: GraphW<'a>,
1340        b: GraphW<'a>,
1341        out: GraphW<'a>,
1342        conv1d: &'a [f32],
1343        a_log: &'a [f32],
1344        dt_bias: &'a [f32],
1345        norm: &'a [f32],
1346        nv: usize,
1347        nk: usize,
1348        dk: usize,
1349        dv: usize,
1350        kk: usize,
1351        /// CPU recurrent state `[ring (kk-1)·cdim | S nv·dk·dv]` — seeds the
1352        /// device mirror when prefill ran on the host (o1 collection, CPU
1353        /// fallback): a zero-initialized device state at decode is exactly
1354        /// the "coherent but contextless" garble.
1355        cpu_state: &'a [f32],
1356    },
1357    /// LFM2 gated short convolution: a fused (B, C, x) projection, a
1358    /// depthwise causal conv over a (kernel−1)-deep per-channel ring,
1359    /// C-gating, and an output projection. This mixer is what most of an
1360    /// LFM2 stack is (22 of the 2.6B's 30 layers), and before it had a
1361    /// graph arm the whole model fell to the per-op path — ~100 submits
1362    /// a token, 22 tok/s on an A100 for a 1.4 GB file.
1363    ShortConv {
1364        /// [3·hidden, hidden] fused input projection.
1365        inp: GraphW<'a>,
1366        /// [hidden, hidden] output projection.
1367        out: GraphW<'a>,
1368        /// [hidden · kernel] depthwise taps, `[channel][tap]`, tap
1369        /// kernel−1 multiplying the current position.
1370        taps: &'a [f32],
1371        kernel: usize,
1372        /// CPU conv ring `[channel][kernel−1]`, slot 0 newest — seeds
1373        /// the device mirror when prefill ran on the host, which for
1374        /// this mixer is always (the batch graph declines it).
1375        cpu_state: &'a [f32],
1376    },
1377}
1378
1379/// Per-layer weights for the whole-token wgpu graph.
1380pub struct GraphLayer<'a> {
1381    pub input_norm: &'a [f32],
1382    pub attn: GraphAttn<'a>,
1383    pub post_norm: &'a [f32],
1384    pub ffn: GraphFfn<'a>,
1385}
1386
1387/// The FFN of one graph layer: a dense SwiGLU trio, or a routed MoE —
1388/// router + top-k selection + all selected experts run ON DEVICE (the
1389/// routing decision depends on the resident hidden state, so a CPU
1390/// round-trip per layer would forfeit the one-submit design).
1391pub enum GraphFfn<'a> {
1392    Dense {
1393        gate: GraphW<'a>,
1394        up: GraphW<'a>,
1395        down: GraphW<'a>,
1396    },
1397    Moe {
1398        /// Router logits weight (f32, kind 4) `[n_exp, hidden]`.
1399        router: GraphW<'a>,
1400        /// Shared-expert sigmoid gate (f32) `[1, hidden]`.
1401        shared_gate: GraphW<'a>,
1402        /// Per-expert q4_tiled directory indices `(gate, up, down)`;
1403        /// the SHARED expert rides as the LAST entry — the select
1404        /// kernel pins it with the sigmoid weight.
1405        experts: Vec<(usize, usize, usize)>,
1406        /// Routed experts (shared excluded).
1407        n_exp: usize,
1408        top_k: usize,
1409        inter: usize,
1410        norm_topk: bool,
1411        /// Expert weight layout, uniform across the layer: `false` =
1412        /// q4_tiled (18 B tiles, inline f16 scale), `true` = q4tp
1413        /// (16 B nibbles + a per-row ladder plane). The two differ only
1414        /// in where the scale comes from, so they share every kernel
1415        /// but the weight-staging block.
1416        q4tp: bool,
1417        /// `true` = the gate/up experts are `q2tp` (2-bit plane) while
1418        /// `down` stays q4tp — the mixed profile a 2-bit-class checkpoint
1419        /// converts into. Only meaningful with `q4tp: true`.
1420        gu_q2: bool,
1421        /// LFM2-MoE / DeepSeek-V3 `noaux_tc` routing: per-expert sigmoid
1422        /// scores instead of a softmax, and `norm_topk` renormalises with
1423        /// the 1e-6 floor. The softmax arm is bit-identical to before.
1424        sigmoid: bool,
1425        /// Per-expert SELECTION bias: added to the score for the top-k
1426        /// choice only — the mixing weights stay unbiased (noaux_tc).
1427        bias: Option<&'a [f32]>,
1428        /// Whether a shared expert rides as the last `experts` entry.
1429        /// LFM2-MoE has none; the select kernel then leaves slot `top_k`
1430        /// unwritten and the expert loop runs `top_k` slots, not +1.
1431        has_shared: bool,
1432    },
1433}
1434
1435/// Outcome of one whole-token graph attempt. A failed attempt after sealed
1436/// O(1) state was admitted must not fall through to the stale CPU state.
1437#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1438pub enum TokenGraphOutcome {
1439    /// No command was committed; the caller may use its ordinary path.
1440    Declined,
1441    /// The graph completed and its hidden/logits output is valid.
1442    Completed,
1443    /// Sealed O(1) state was admitted and a later graph operation failed.
1444    Failed,
1445}
1446
1447/// Whole-token decode graph on wgpu: the entire layer stack in ONE submit,
1448/// hidden resident, one readback. Updates `h` in place.
1449/// `loop_norm_at`: virtual layer indices after which `final_norm` is applied
1450/// (Looped Transformer mid-stack norm). Empty for standard models.
1451#[allow(clippy::too_many_arguments)]
1452pub fn forward_token_graph(
1453    model: &Arc<CmfModel>,
1454    kv_id: u64,
1455    layers: &[GraphLayer],
1456    // Per-layer sealed o1 (Nystrom) state; Some = replace this layer's
1457    // exact attention with the O(1) kernels. wgpu only.
1458    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
1459    o1_epoch: u64,
1460    invf: &[f32],
1461    h: &mut [f32],
1462    nh: usize,
1463    nkv: usize,
1464    hd: usize,
1465    attn_scale: f32,
1466    rd: usize,
1467    hidden: usize,
1468    inter: usize,
1469    position: usize,
1470    cap: usize,
1471    gemma: bool,
1472    eps: f32,
1473    lm_head: Option<(&GraphW, usize)>,
1474    final_norm: &[f32],
1475    logits: &mut Vec<f32>,
1476    loop_norm_at: &[usize],
1477    steps: usize,
1478    embed: Option<(&GraphW, usize, f32)>,
1479    ids_out: Option<&mut Vec<u32>>,
1480    // How many leading layers the graph ran (see the wgpu twin) — smaller
1481    // than layers.len() when the expert budget ended the device prefix.
1482    layers_run: Option<&mut usize>,
1483    // Absolute index of layers[0] in the model — the KV/state mirrors key
1484    // on it, so a layer SPAN (network split segment) shares mirrors with
1485    // a full-stack run instead of colliding at slot 0.
1486    layer_base: usize,
1487    // Read the final hidden back alongside the fused head's logits.
1488    hidden_too: bool,
1489) -> TokenGraphOutcome {
1490    match backend() {
1491        #[cfg(feature = "gpu")]
1492        Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
1493            model,
1494            kv_id,
1495            layers,
1496            o1,
1497            o1_epoch,
1498            invf,
1499            h,
1500            nh,
1501            nkv,
1502            hd,
1503            attn_scale,
1504            rd,
1505            hidden,
1506            inter,
1507            position,
1508            cap,
1509            gemma,
1510            eps,
1511            lm_head,
1512            final_norm,
1513            logits,
1514            loop_norm_at,
1515            steps,
1516            embed,
1517            ids_out,
1518            layers_run,
1519            layer_base,
1520            hidden_too,
1521        ),
1522        #[allow(unused_variables)]
1523        _ => {
1524            let _ = (
1525                attn_scale,
1526                lm_head,
1527                final_norm,
1528                logits,
1529                loop_norm_at,
1530                layers_run,
1531                layer_base,
1532                hidden_too,
1533            );
1534            TokenGraphOutcome::Declined
1535        }
1536    }
1537}
1538
1539/// Speculative-verify tail for the batched graph: fold final-norm + lm_head
1540/// over every batch position and read all k logit rows back; the batch also
1541/// snapshots the GDN state per position for `gdn_spec_restore`.
1542#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1543pub enum BatchGraphOutcome {
1544    /// The graph declined before mutating persistent device state. Callers may
1545    /// safely use the existing per-position path.
1546    Declined,
1547    /// The complete batch committed and its readback succeeded.
1548    Completed,
1549    /// A batch that had admitted sealed O(1) state failed after admission.
1550    /// Falling back to CPU would mix two state machines, so the caller must
1551    /// abort and clear the sequence instead.
1552    Failed,
1553}
1554
1555pub struct SpecTail<'a> {
1556    pub lm: GraphW<'a>,
1557    pub lm_rows: usize,
1558    pub final_norm: &'a [f32],
1559    pub logits_out: &'a mut Vec<f32>,
1560}
1561
1562/// Batched prefill: k contiguous positions through the whole graph in one submit
1563/// (projections/FFN as GEMMs, attention/GDN looped over scratch). `h` is
1564/// [k·hidden] in/out; `positions` len k. wgpu only.
1565#[allow(clippy::too_many_arguments)]
1566pub fn forward_batch_graph(
1567    model: &Arc<CmfModel>,
1568    kv_id: u64,
1569    layers: &[GraphLayer],
1570    invf: &[f32],
1571    h: &mut [f32],
1572    nh: usize,
1573    nkv: usize,
1574    hd: usize,
1575    rd: usize,
1576    hidden: usize,
1577    inter: usize,
1578    positions: &[usize],
1579    cap: usize,
1580    gemma: bool,
1581    eps: f32,
1582    attn_scale: f32,
1583    k: usize,
1584    // Per-layer sealed O(1) device views. An empty slice means the ordinary
1585    // exact-KV path; otherwise it must have one entry per graph layer.
1586    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
1587    o1_epoch: u64,
1588    spec: Option<SpecTail<'_>>,
1589) -> BatchGraphOutcome {
1590    match backend() {
1591        #[cfg(feature = "gpu")]
1592        Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
1593            model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
1594            eps, attn_scale, k, o1, o1_epoch, spec,
1595        ),
1596        #[allow(unreachable_patterns)]
1597        _ => {
1598            let _ = (o1, o1_epoch, spec);
1599            BatchGraphOutcome::Declined
1600        }
1601    }
1602}
1603
1604/// After a partial speculative acceptance: restore every GDN layer's device
1605/// state to the snapshot after batch position `slot`. `base_pos` is the
1606/// absolute position of the first verify row and `expected_layers` makes the
1607/// restore all-or-nothing across the model's recurrent layers. wgpu only.
1608pub fn gdn_spec_restore(kv_id: u64, slot: usize, base_pos: usize, expected_layers: usize) -> bool {
1609    #[cfg(feature = "gpu")]
1610    if backend() == Backend::Wgpu {
1611        return crate::gpu_wgpu::gdn_spec_restore(kv_id, slot, base_pos, expected_layers);
1612    }
1613    #[allow(unreachable_code)]
1614    {
1615        let _ = (kv_id, slot, base_pos, expected_layers);
1616        false
1617    }
1618}
1619
1620/// Re-point one exact-attention device mirror after a speculative round has
1621/// discarded unaccepted rows. The rows beyond `stored` remain allocated and
1622/// are overwritten by the next append; only the logical cursor moves. This
1623/// is the wgpu twin of Metal's existing mirror cursor helper and keeps the
1624/// MTP graph's speculative/device cache coherent with its real anchor.
1625pub fn graph_kv_set_stored(kv_id: u64, layer: usize, stored: usize) -> bool {
1626    #[cfg(feature = "gpu")]
1627    if backend() == Backend::Wgpu {
1628        return crate::gpu_wgpu::kv_mirror_set_stored(kv_id, layer, stored);
1629    }
1630    #[cfg(target_os = "macos")]
1631    if backend() == Backend::Metal {
1632        crate::gpu_metal::kv_mirror_set_stored(kv_id, layer, stored);
1633        return true;
1634    }
1635    false
1636}
1637
1638/// Drop the wgpu token graph's device K/V mirror for a pipeline.
1639pub fn graph_kv_reset(_kv_id: u64) {
1640    #[cfg(feature = "gpu")]
1641    if backend() == Backend::Wgpu {
1642        crate::gpu_wgpu::kv_mirror_reset(_kv_id);
1643    }
1644}
1645
1646/// Ternary (q1t) BASE matvec on the GPU — fills `out` with the base dot; the
1647/// caller adds the sparse overlay on the CPU. Metal only for now (wgpu q1t not
1648/// yet written → CPU fallback).
1649pub fn q1t_matvec(
1650    model: &Arc<CmfModel>,
1651    idx: usize,
1652    xs: &[f32],
1653    rows: usize,
1654    cols: usize,
1655    out: &mut [f32],
1656) -> bool {
1657    match backend() {
1658        #[cfg(target_os = "macos")]
1659        Backend::Metal => {
1660            if metal_q1t_enabled() {
1661                crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
1662            } else {
1663                false
1664            }
1665        }
1666        #[cfg(feature = "gpu")]
1667        Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
1668        Backend::None => false,
1669    }
1670}
1671
1672/// q4_block matvec on the GPU — wgpu only (Metal drives q4_block through the
1673/// whole-token graph, not a standalone matvec).
1674#[allow(unused_variables)]
1675pub fn q4b_matvec(
1676    model: &Arc<CmfModel>,
1677    idx: usize,
1678    xs: &[f32],
1679    rows: usize,
1680    cols: usize,
1681    out: &mut [f32],
1682) -> bool {
1683    match backend() {
1684        #[cfg(target_os = "macos")]
1685        Backend::Metal => false,
1686        #[cfg(feature = "gpu")]
1687        Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
1688        Backend::None => false,
1689    }
1690}
1691
1692/// q1t batched GEMM (prefill) — base + overlay on-device (Metal simdgroup or
1693/// wgpu register-blocked).
1694pub fn q1t_matmat(
1695    model: &Arc<CmfModel>,
1696    idx: usize,
1697    xs: &[f32],
1698    b: usize,
1699    rows: usize,
1700    cols: usize,
1701    out: &mut [f32],
1702) -> bool {
1703    match backend() {
1704        #[cfg(target_os = "macos")]
1705        // Batched prefill and single-token decode are both enabled. On the
1706        // real 14.8B Q1T model prefill PPL was within 0.3% of CPU (7.942 vs
1707        // 7.966), and the alignment-safe decode kernel reached 3.52e-6 max_rel.
1708        Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
1709        #[cfg(feature = "gpu")]
1710        Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
1711        Backend::None => false,
1712    }
1713}
1714
1715/// Native Metal Q1T switch. Enabled by default after the byte-packed Q1T
1716/// fields were changed to alignment-safe loads; keep an explicit emergency
1717/// fallback for device/driver diagnostics.
1718#[cfg(target_os = "macos")]
1719pub(crate) fn metal_q1t_enabled() -> bool {
1720    std::env::var("CMF_METAL_Q1T")
1721        .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
1722        .unwrap_or(true)
1723}
1724
1725/// Batched q1 GEMM (prefill). wgpu only — Metal has its own block path.
1726pub fn q1_matmat(
1727    model: &Arc<CmfModel>,
1728    idx: usize,
1729    xs: &[f32],
1730    b: usize,
1731    rows: usize,
1732    cols: usize,
1733    out: &mut [f32],
1734) -> bool {
1735    match backend() {
1736        #[cfg(feature = "gpu")]
1737        Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
1738        #[allow(unused_variables)]
1739        _ => false,
1740    }
1741}
1742
1743/// Contention kill for the wide imagegen GEMM/FFN paths: one grossly
1744/// slow op under a work-proportional budget (fair-device ops are
1745/// ≤~100 ms even at 1024px) means another process owns the device —
1746/// verdicts are per-process, so CPU for the rest of this one.
1747static MM_KILL: AtomicBool = AtomicBool::new(false);
1748pub(crate) fn mm_killed() -> bool {
1749    MM_KILL.load(Ordering::Relaxed)
1750}
1751pub(crate) fn mm_kill() {
1752    MM_KILL.store(true, Ordering::Relaxed);
1753}
1754
1755/// Consecutive over-budget ops. ONE slow op is not contention: on a
1756/// 24 GB Mac running the 25.7 GB fl2va file the first ops after the
1757/// prompt encode page their weights in from the SSD and take seconds —
1758/// a field report (hololabs, HF discussion #2) had to neuter the kill
1759/// to keep the denoise on the GPU, and then measured 48 s/step where the
1760/// CPU fallback took >60. Contention is persistent; a page-in is not.
1761static MM_STRIKES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
1762const MM_STRIKES_TO_KILL: u32 = 3;
1763/// Whether the kill is armed at all. A one-shot phase whose slowness is
1764/// expected and not contention — the video prompt encoder streaming
1765/// 12 GB off the SSD on a 24 GB Mac (HF discussion #4: users had to
1766/// gut `mm_kill` to keep the denoise loop on the GPU) — disarms it and
1767/// re-arms it when the phase is over; strikes taken meanwhile are
1768/// forgotten.
1769static MM_ARMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
1770
1771/// Disarm / re-arm the contention kill around a phase whose GEMMs are
1772/// slow for reasons that are not another process (see `MM_ARMED`).
1773pub fn mm_kill_arm(on: bool) {
1774    MM_ARMED.store(on, Ordering::Relaxed);
1775    if on {
1776        MM_STRIKES.store(0, Ordering::Relaxed);
1777    }
1778}
1779
1780/// The contention verdict for one wide op: `el` against its
1781/// work-proportional `budget`. `exempt` marks ops whose time is not
1782/// evidence — the cold probe, or a weight that was not resident before
1783/// the call and rode in with it. Kills after `MM_STRIKES_TO_KILL`
1784/// consecutive strikes; a within-budget op clears the count.
1785/// `CMF_MM_KILL=0` disables the kill entirely (the device is trusted).
1786pub(crate) fn mm_budget_check(
1787    what: &str,
1788    el: std::time::Duration,
1789    budget: std::time::Duration,
1790    exempt: bool,
1791) {
1792    if el <= budget {
1793        MM_STRIKES.store(0, Ordering::Relaxed);
1794        return;
1795    }
1796    if exempt || !MM_ARMED.load(Ordering::Relaxed) {
1797        return;
1798    }
1799    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1800    let on = *ON.get_or_init(|| std::env::var("CMF_MM_KILL").as_deref() != Ok("0"));
1801    let n = MM_STRIKES.fetch_add(1, Ordering::Relaxed) + 1;
1802    if !on {
1803        tracing::info!(
1804            "gpu {what} took {el:?} (budget {budget:?}) — over budget, CMF_MM_KILL=0 keeps the device"
1805        );
1806        return;
1807    }
1808    if n >= MM_STRIKES_TO_KILL {
1809        tracing::warn!(
1810            "gpu {what} took {el:?} (budget {budget:?}), {n} in a row — \
1811             device contended, CPU for the rest of the process (CMF_MM_KILL=0 to override)"
1812        );
1813        mm_kill();
1814    } else {
1815        tracing::info!(
1816            "gpu {what} took {el:?} (budget {budget:?}) — strike {n} of {MM_STRIKES_TO_KILL}"
1817        );
1818    }
1819}
1820
1821/// Fused DiT SwiGLU FFN on the device: g=X·W1ᵀ, u=X·W3ᵀ, silu(g)·u,
1822/// Causal chunk attention on the device: `b` queries against `s0 + b`
1823/// cached keys. wgpu only — Metal's chunk graph keeps attention inside
1824/// the resident block and never calls out.
1825#[allow(unused_variables, clippy::too_many_arguments)]
1826pub fn chunk_attend(
1827    q: &[f32],
1828    k: &[&[f32]],
1829    v: &[&[f32]],
1830    b: usize,
1831    s0: usize,
1832    nh: usize,
1833    nkv: usize,
1834    hd: usize,
1835    scale: f32,
1836    out: &mut [f32],
1837) -> bool {
1838    match backend() {
1839        #[cfg(feature = "gpu")]
1840        Backend::Wgpu => crate::gpu_wgpu::chunk_attend(q, k, v, b, s0, nh, nkv, hd, scale, out),
1841        #[allow(unreachable_patterns)]
1842        _ => false,
1843    }
1844}
1845
1846/// Fused QKV projection: one upload of the normed chunk, three GEMMs,
1847/// one readback of Q|K|V back to back. Metal has no twin yet — its
1848/// chunk graph keeps the whole layer resident and never surfaces QKV.
1849#[allow(unused_variables, clippy::too_many_arguments)]
1850pub fn q4t_qkv(
1851    model: &Arc<CmfModel>,
1852    wq: usize,
1853    wk: usize,
1854    wv: usize,
1855    xs: &[f32],
1856    b: usize,
1857    cols: usize,
1858    rq: usize,
1859    rk: usize,
1860    rv: usize,
1861    out: &mut [f32],
1862) -> bool {
1863    match backend() {
1864        #[cfg(feature = "gpu")]
1865        Backend::Wgpu => crate::gpu_wgpu::q4t_qkv(model, wq, wk, wv, xs, b, cols, rq, rk, rv, out),
1866        #[allow(unreachable_patterns)]
1867        _ => false,
1868    }
1869}
1870
1871/// y=·W2ᵀ — one command buffer, only X and Y cross the CPU boundary.
1872#[allow(unused_variables, clippy::too_many_arguments)]
1873/// SwiGLU FFN with a row-packed [gate|up] fc1 (MiniMax-H3's DiT), run
1874/// end to end on the device. wgpu only: Metal keeps the host loop until
1875/// its own packed kernel exists.
1876#[allow(clippy::too_many_arguments, unused_variables)]
1877pub fn q4tp_ffn_packed(
1878    model: &Arc<CmfModel>,
1879    w1: usize,
1880    w2: usize,
1881    xs: &[f32],
1882    b: usize,
1883    hidden: usize,
1884    inter: usize,
1885    bias: Option<&[f32]>,
1886    out: &mut [f32],
1887) -> bool {
1888    match backend() {
1889        #[cfg(feature = "gpu")]
1890        Backend::Wgpu => {
1891            crate::gpu_wgpu::ffn_packed(model, w1, w2, xs, b, hidden, inter, bias, out)
1892        }
1893        #[allow(unreachable_patterns)]
1894        _ => false,
1895    }
1896}
1897
1898pub fn q4tp_ffn(
1899    model: &Arc<CmfModel>,
1900    w1: usize,
1901    w3: usize,
1902    w2: usize,
1903    xs: &[f32],
1904    b: usize,
1905    hidden: usize,
1906    inter: usize,
1907    out: &mut [f32],
1908) -> bool {
1909    match backend() {
1910        #[cfg(target_os = "macos")]
1911        Backend::Metal => crate::gpu_metal::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1912        #[cfg(feature = "gpu")]
1913        Backend::Wgpu => crate::gpu_wgpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1914        #[allow(unreachable_patterns)]
1915        _ => false,
1916    }
1917}
1918
1919/// Qwen Image's exact two-projection tanh-GELU FFN.  The WGPU arm keeps the
1920/// intermediate on the device; other backends decline so the caller retains
1921/// its bounded CPU path.  `bias_in` is applied before GELU and `bias_out`
1922/// after the second projection, matching the official transformer.
1923#[allow(clippy::too_many_arguments, unused_variables)]
1924pub fn q4tp_gelu_ffn(
1925    model: &Arc<CmfModel>,
1926    w_in: usize,
1927    w_out: usize,
1928    xs: &[f32],
1929    b: usize,
1930    hidden: usize,
1931    inter: usize,
1932    bias_in: &[f32],
1933    bias_out: &[f32],
1934    out: &mut [f32],
1935) -> bool {
1936    match backend() {
1937        #[cfg(feature = "gpu")]
1938        Backend::Wgpu => crate::gpu_wgpu::q4tp_gelu_ffn(
1939            model, w_in, w_out, xs, b, hidden, inter, bias_in, bias_out, out,
1940        ),
1941        #[allow(unreachable_patterns)]
1942        _ => false,
1943    }
1944}
1945
1946pub fn q4t_ffn(
1947    model: &Arc<CmfModel>,
1948    w1: usize,
1949    w3: usize,
1950    w2: usize,
1951    xs: &[f32],
1952    b: usize,
1953    hidden: usize,
1954    inter: usize,
1955    out: &mut [f32],
1956) -> bool {
1957    match backend() {
1958        #[cfg(target_os = "macos")]
1959        Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1960        #[cfg(feature = "gpu")]
1961        Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1962        #[allow(unreachable_patterns)]
1963        _ => false,
1964    }
1965}
1966
1967/// One whole modulated DiT block for `dit_block`: geometry, norm
1968/// weights, AdaLN scale/gate vectors (gates pre-tanh'd), a per-token
1969/// f32 RoPE cos/sin table, and the directory indices of the seven
1970/// q4t projections. `x` is in-out `[n, hidden]`.
1971pub struct DitBlockArgs<'a> {
1972    pub n: usize,
1973    pub hidden: usize,
1974    pub inter: usize,
1975    pub nh: usize,
1976    pub nkv: usize,
1977    pub hd: usize,
1978    pub eps: f32,
1979    pub rope_cos: &'a [f32],
1980    pub rope_sin: &'a [f32],
1981    pub norm1: &'a [f32],
1982    pub norm2: &'a [f32],
1983    pub ffn_norm1: &'a [f32],
1984    pub ffn_norm2: &'a [f32],
1985    pub norm_q: &'a [f32],
1986    pub norm_k: &'a [f32],
1987    pub s_msa: &'a [f32],
1988    pub gate_msa: &'a [f32],
1989    pub s_mlp: &'a [f32],
1990    pub gate_mlp: &'a [f32],
1991    pub wq: usize,
1992    pub wk: usize,
1993    pub wv: usize,
1994    pub wo: usize,
1995    pub w1: usize,
1996    pub w3: usize,
1997    pub w2: usize,
1998    /// The projections' layout: q4tp (ladder scales) vs plain q4_tiled.
1999    /// The recommended Lumina file is q4tp, and a backend that only
2000    /// knows q4t must decline rather than decode with the wrong reader.
2001    pub q4tp: bool,
2002    /// The hidden state is already on the device from the previous block,
2003    /// so `x` need not be uploaded.
2004    pub resident_in: bool,
2005    /// Leave the result on the device instead of reading it back. The DiT
2006    /// loop does not touch `x` between blocks, so 27 of every 28 readbacks
2007    /// were moving 19 MB across PCIe and stalling on it for nothing.
2008    pub resident_out: bool,
2009}
2010
2011/// Can the selected backend keep the DiT's hidden state on the device
2012/// between blocks? Only the wgpu whole-block path; the Metal entry takes
2013/// and returns host memory every call.
2014pub fn dit_chain_supported() -> bool {
2015    #[cfg(feature = "gpu")]
2016    {
2017        return matches!(backend(), Backend::Wgpu) && fused_dit_block_available();
2018    }
2019    #[allow(unreachable_code)]
2020    false
2021}
2022
2023/// Pull the resident hidden state back to the host. For the caller that
2024/// chained blocks and then hit one the device declined.
2025pub fn dit_state_fetch(_x: &mut [f32]) -> bool {
2026    #[cfg(feature = "gpu")]
2027    {
2028        if matches!(backend(), Backend::Wgpu) {
2029            return crate::gpu_wgpu::dit_state_fetch(_x);
2030        }
2031    }
2032    false
2033}
2034
2035/// One whole modulated DiT block on the device — norms, qkv, RoPE,
2036/// attention, residuals and the SwiGLU FFN in a single command
2037/// buffer; only `x` crosses the CPU boundary (in and out).
2038#[allow(unused_variables)]
2039/// The DiT's three projections in one submission (wgpu only; the
2040/// Metal path fuses the whole block instead). False = the caller keeps
2041/// its three separate calls.
2042#[allow(unused_variables, clippy::too_many_arguments)]
2043pub fn dit_qkv(
2044    model: &Arc<CmfModel>,
2045    wq: usize,
2046    wk: usize,
2047    wv: usize,
2048    xs: &[f32],
2049    b: usize,
2050    hidden: usize,
2051    qrows: usize,
2052    kvrows: usize,
2053    q_out: &mut [f32],
2054    k_out: &mut [f32],
2055    v_out: &mut [f32],
2056) -> bool {
2057    match backend() {
2058        #[cfg(feature = "gpu")]
2059        Backend::Wgpu => crate::gpu_wgpu::q4tp_qkv(
2060            model, wq, wk, wv, xs, b, hidden, qrows, kvrows, q_out, k_out, v_out,
2061        ),
2062        #[allow(unreachable_patterns)]
2063        _ => false,
2064    }
2065}
2066
2067/// The Qwen Image double-stream attention half.  The WGPU implementation
2068/// keeps the six Q/K/V projections, the stream join, qk-norm/RoPE, joint
2069/// attention, and both output projections on the device; the caller only
2070/// supplies the two normalized streams and receives the two projected
2071/// streams.  A backend or codec that cannot satisfy the full contract
2072/// returns `false` before changing either output, so the native host path
2073/// remains the portable fallback.
2074pub struct QwenImageAttentionArgs<'a> {
2075    pub image: &'a [f32],
2076    pub text: &'a [f32],
2077    pub image_tokens: usize,
2078    pub text_tokens: usize,
2079    pub heads: usize,
2080    pub head_dim: usize,
2081    pub image_q: usize,
2082    pub image_k: usize,
2083    pub image_v: usize,
2084    pub text_q: usize,
2085    pub text_k: usize,
2086    pub text_v: usize,
2087    pub image_out: usize,
2088    pub text_out: usize,
2089    pub image_q_norm: &'a [f32],
2090    pub image_k_norm: &'a [f32],
2091    pub text_q_norm: &'a [f32],
2092    pub text_k_norm: &'a [f32],
2093    pub image_cos: &'a [f32],
2094    pub image_sin: &'a [f32],
2095    pub text_cos: &'a [f32],
2096    pub text_sin: &'a [f32],
2097    pub image_q_bias: &'a [f32],
2098    pub image_k_bias: &'a [f32],
2099    pub image_v_bias: &'a [f32],
2100    pub text_q_bias: &'a [f32],
2101    pub text_k_bias: &'a [f32],
2102    pub text_v_bias: &'a [f32],
2103    pub image_out_bias: &'a [f32],
2104    pub text_out_bias: &'a [f32],
2105    pub image_proj: &'a mut [f32],
2106    pub text_proj: &'a mut [f32],
2107}
2108
2109/// The per-layer controls and Q4TP directory indices used by the native
2110/// Qwen block.  Keeping this descriptor separate from the stream buffers
2111/// lets a whole transformer forward reuse one explicit device state without
2112/// a global scratch slot or a hidden context label.
2113#[allow(clippy::too_many_fields)]
2114pub struct QwenImageChainBlock<'a> {
2115    pub image_mod: &'a [f32],
2116    pub text_mod: &'a [f32],
2117    pub image_q: usize,
2118    pub image_k: usize,
2119    pub image_v: usize,
2120    pub text_q: usize,
2121    pub text_k: usize,
2122    pub text_v: usize,
2123    pub image_out: usize,
2124    pub text_out: usize,
2125    pub image_q_norm: &'a [f32],
2126    pub image_k_norm: &'a [f32],
2127    pub text_q_norm: &'a [f32],
2128    pub text_k_norm: &'a [f32],
2129    pub image_q_bias: &'a [f32],
2130    pub image_k_bias: &'a [f32],
2131    pub image_v_bias: &'a [f32],
2132    pub text_q_bias: &'a [f32],
2133    pub text_k_bias: &'a [f32],
2134    pub text_v_bias: &'a [f32],
2135    pub image_out_bias: &'a [f32],
2136    pub text_out_bias: &'a [f32],
2137    pub image_attn_gate: &'a [f32],
2138    pub text_attn_gate: &'a [f32],
2139    pub image_mlp_in: usize,
2140    pub image_mlp_out: usize,
2141    pub text_mlp_in: usize,
2142    pub text_mlp_out: usize,
2143    pub image_mlp_in_bias: &'a [f32],
2144    pub image_mlp_out_bias: &'a [f32],
2145    pub text_mlp_in_bias: &'a [f32],
2146    pub text_mlp_out_bias: &'a [f32],
2147}
2148
2149/// Complete Qwen Image transformer block contract. The first norm/mod
2150/// panels are supplied by the native caller; the WGPU arm keeps both streams
2151/// resident through QKV, QK/RoPE, joint attention, output projections, both
2152/// gated residuals, and the exact tanh-GELU MLPs. A backend that cannot
2153/// satisfy the whole graph returns `false` without changing either output.
2154#[allow(clippy::too_many_fields)]
2155pub struct QwenImageBlockArgs<'a> {
2156    /// Raw stream state is read for the first gated residual and overwritten
2157    /// with the block's final state after the one readback.
2158    pub image: &'a mut [f32],
2159    pub text: &'a mut [f32],
2160    pub image_norm: &'a [f32],
2161    pub text_norm: &'a [f32],
2162    pub image_tokens: usize,
2163    pub text_tokens: usize,
2164    pub heads: usize,
2165    pub head_dim: usize,
2166    pub image_cos: &'a [f32],
2167    pub image_sin: &'a [f32],
2168    pub text_cos: &'a [f32],
2169    pub text_sin: &'a [f32],
2170    pub image_q: usize,
2171    pub image_k: usize,
2172    pub image_v: usize,
2173    pub text_q: usize,
2174    pub text_k: usize,
2175    pub text_v: usize,
2176    pub image_out: usize,
2177    pub text_out: usize,
2178    pub image_q_norm: &'a [f32],
2179    pub image_k_norm: &'a [f32],
2180    pub text_q_norm: &'a [f32],
2181    pub text_k_norm: &'a [f32],
2182    pub image_q_bias: &'a [f32],
2183    pub image_k_bias: &'a [f32],
2184    pub image_v_bias: &'a [f32],
2185    pub text_q_bias: &'a [f32],
2186    pub text_k_bias: &'a [f32],
2187    pub text_v_bias: &'a [f32],
2188    pub image_out_bias: &'a [f32],
2189    pub text_out_bias: &'a [f32],
2190    pub image_attn_gate: &'a [f32],
2191    pub text_attn_gate: &'a [f32],
2192    pub image_mlp_in: usize,
2193    pub image_mlp_out: usize,
2194    pub text_mlp_in: usize,
2195    pub text_mlp_out: usize,
2196    pub image_mlp_in_bias: &'a [f32],
2197    pub image_mlp_out_bias: &'a [f32],
2198    pub text_mlp_in_bias: &'a [f32],
2199    pub text_mlp_out_bias: &'a [f32],
2200    pub image_mlp_mod: &'a [f32],
2201    pub text_mlp_mod: &'a [f32],
2202    pub image_mlp_gate: &'a [f32],
2203    pub text_mlp_gate: &'a [f32],
2204}
2205
2206/// Explicit whole-forward Qwen state contract.  The WGPU backend uploads the
2207/// two initial streams once, encodes a bounded number of complete blocks per
2208/// submission, and reads the final state once.  `blocks` is immutable for the
2209/// call, while the two stream slices receive only the final readback.
2210pub struct QwenImageChainArgs<'a> {
2211    pub image: &'a mut [f32],
2212    pub text: &'a mut [f32],
2213    pub image_tokens: usize,
2214    pub text_tokens: usize,
2215    pub heads: usize,
2216    pub head_dim: usize,
2217    pub image_cos: &'a [f32],
2218    pub image_sin: &'a [f32],
2219    pub text_cos: &'a [f32],
2220    pub text_sin: &'a [f32],
2221    pub blocks: &'a [QwenImageChainBlock<'a>],
2222}
2223
2224#[allow(unused_variables)]
2225pub fn qwen_image_attention(
2226    model: &Arc<CmfModel>,
2227    args: &mut QwenImageAttentionArgs<'_>,
2228) -> bool {
2229    match backend() {
2230        #[cfg(feature = "gpu")]
2231        Backend::Wgpu => crate::gpu_wgpu::qwen_image_attention(model, args),
2232        #[allow(unreachable_patterns)]
2233        _ => false,
2234    }
2235}
2236
2237#[allow(unused_variables)]
2238pub fn qwen_image_block(model: &Arc<CmfModel>, args: &mut QwenImageBlockArgs<'_>) -> bool {
2239    match backend() {
2240        #[cfg(feature = "gpu")]
2241        Backend::Wgpu => crate::gpu_wgpu::qwen_image_block(model, args),
2242        #[allow(unreachable_patterns)]
2243        _ => false,
2244    }
2245}
2246
2247/// Keep all Qwen transformer blocks on the selected WGPU device, with only
2248/// bounded chunk submissions and one final readback.  Other backends decline
2249/// so the native caller can use its exact portable block loop.
2250#[allow(unused_variables)]
2251pub fn qwen_image_chain(model: &Arc<CmfModel>, args: &mut QwenImageChainArgs<'_>) -> bool {
2252    match backend() {
2253        #[cfg(feature = "gpu")]
2254        Backend::Wgpu => crate::gpu_wgpu::qwen_image_chain(model, args),
2255        #[allow(unreachable_patterns)]
2256        _ => false,
2257    }
2258}
2259
2260/// The Qwen Image second sub-block on WGPU: affine-free LayerNorm,
2261/// shift/scale modulation, Q4TP input projection, exact tanh-GELU, output
2262/// projection, bias and gated residual.  `data` is updated in place after a
2263/// single final readback.  Backends/codecs that cannot keep this chain on the
2264/// device return `false` before changing `data`, leaving the caller's
2265/// portable per-op path intact.
2266#[allow(unused_variables, clippy::too_many_arguments)]
2267pub fn qwen_image_mlp_inplace(
2268    model: &Arc<CmfModel>,
2269    w_in: usize,
2270    w_out: usize,
2271    data: &mut [f32],
2272    batch: usize,
2273    hidden: usize,
2274    inter: usize,
2275    bias_in: &[f32],
2276    bias_out: &[f32],
2277    modulation: &[f32],
2278    gate: &[f32],
2279) -> bool {
2280    match backend() {
2281        #[cfg(feature = "gpu")]
2282        Backend::Wgpu => crate::gpu_wgpu::qwen_image_mlp_inplace(
2283            model,
2284            w_in,
2285            w_out,
2286            data,
2287            batch,
2288            hidden,
2289            inter,
2290            bias_in,
2291            bias_out,
2292            modulation,
2293            gate,
2294        ),
2295        #[allow(unreachable_patterns)]
2296        _ => false,
2297    }
2298}
2299
2300/// Is a FUSED whole-block device path on offer? The batched-CFG shape
2301/// (two sequences in one tall batch) and the fused block (one sequence,
2302/// one command buffer) are alternatives, and the caller picks.
2303pub fn fused_dit_block_available() -> bool {
2304    #[cfg(target_os = "macos")]
2305    {
2306        matches!(backend(), Backend::Metal) && fused_block_trusted()
2307    }
2308    #[cfg(not(target_os = "macos"))]
2309    {
2310        false
2311    }
2312}
2313
2314pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
2315    dit_block_seg(model, a, &[a.n], x)
2316}
2317
2318/// The same block over a CONCATENATION of independent sequences:
2319/// attention per segment, everything position-wise batched. wgpu only —
2320/// the Metal path takes the single-sequence entry above.
2321pub fn dit_block_seg(
2322    model: &Arc<CmfModel>,
2323    a: &DitBlockArgs,
2324    segs: &[usize],
2325    x: &mut [f32],
2326) -> bool {
2327    match backend() {
2328        #[cfg(target_os = "macos")]
2329        Backend::Metal if segs.len() <= 1 => crate::gpu_metal::dit_block(model, a, x),
2330        // The wgpu whole-block path. What it buys is host round trips —
2331        // six a block become one — so it defaults ON where those cost
2332        // real time (a discrete card across PCIe) and OFF on unified
2333        // memory, where the per-op path shares the same pages and the
2334        // fusion measured slightly slower on an M4. `CMF_DIT_FUSED=1`
2335        // forces it anywhere, `=0` forbids it.
2336        #[cfg(feature = "gpu")]
2337        Backend::Wgpu
2338            if match std::env::var("CMF_DIT_FUSED").ok().as_deref() {
2339                Some("0") => false,
2340                Some(_) => true,
2341                None => crate::gpu_wgpu::discrete_active(),
2342            } =>
2343        {
2344            crate::gpu_wgpu::dit_block_seg(model, a, segs, x)
2345        }
2346        #[allow(unreachable_patterns)]
2347        _ => false,
2348    }
2349}
2350
2351/// One VAE resnet block for `vae_resnet`: norm/conv weights and the
2352/// channel/shape geometry. `shortcut` is the 1×1 projection (w, b, k)
2353/// when in/out channels differ.
2354pub struct VaeResnetArgs<'a> {
2355    pub groups: usize,
2356    pub ic: usize,
2357    pub oc: usize,
2358    pub h: usize,
2359    pub w: usize,
2360    pub n1w: &'a [f32],
2361    pub n1b: &'a [f32],
2362    pub c1w: &'a [f32],
2363    pub c1b: &'a [f32],
2364    pub c1k: usize,
2365    pub n2w: &'a [f32],
2366    pub n2b: &'a [f32],
2367    pub c2w: &'a [f32],
2368    pub c2b: &'a [f32],
2369    pub c2k: usize,
2370    pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
2371}
2372
2373/// One whole VAE resnet block on the device (norm+silu → conv ×2 →
2374/// shortcut → add, one command buffer).
2375#[allow(unused_variables)]
2376pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
2377    match backend() {
2378        #[cfg(target_os = "macos")]
2379        Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
2380        _ => false,
2381    }
2382}
2383
2384/// Nearest-2× upsample fused with the following conv — the small
2385/// pre-upsample image is what crosses the CPU boundary.
2386#[allow(unused_variables, clippy::too_many_arguments)]
2387pub fn vae_upsample_conv(
2388    w: &[f32],
2389    bias: &[f32],
2390    x: &[f32],
2391    ic: usize,
2392    oc: usize,
2393    h: usize,
2394    w_img: usize,
2395    k: usize,
2396    out: &mut [f32],
2397) -> bool {
2398    match backend() {
2399        #[cfg(target_os = "macos")]
2400        Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
2401        #[cfg(feature = "gpu")]
2402        Backend::Wgpu => crate::gpu_wgpu::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
2403        #[allow(unreachable_patterns)]
2404        _ => false,
2405    }
2406}
2407
2408/// VAE conv2d on the device (implicit GEMM — the CPU path pays for a
2409/// multi-GB im2col matrix at high resolutions).
2410#[allow(unused_variables, clippy::too_many_arguments)]
2411pub fn vae_conv2d(
2412    w: &[f32],
2413    bias: &[f32],
2414    x: &[f32],
2415    ic: usize,
2416    oc: usize,
2417    h: usize,
2418    w_img: usize,
2419    k: usize,
2420    out: &mut [f32],
2421) -> bool {
2422    match backend() {
2423        #[cfg(target_os = "macos")]
2424        Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
2425        #[cfg(feature = "gpu")]
2426        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
2427        #[allow(unreachable_patterns)]
2428        _ => false,
2429    }
2430}
2431
2432/// DiT full bidirectional attention on the device (all heads:
2433/// scores GEMM → row softmax → P·V → panel unstack, one command
2434/// buffer). Head-major inputs; out is [n, nh·hd].
2435#[allow(unused_variables, clippy::too_many_arguments)]
2436/// Attention from an interleaved qkv panel, splitting into head-major
2437/// planes ON the device. wgpu only; `false` elsewhere so the caller
2438/// keeps its host repack.
2439#[allow(unused_variables)]
2440#[allow(clippy::too_many_arguments)]
2441/// qkv projection + attention with the panel never leaving the card.
2442/// wgpu only; `false` elsewhere and the caller keeps its host chain.
2443#[allow(clippy::too_many_arguments, unused_variables)]
2444pub fn dit_qkv_attention(
2445    model: &Arc<CmfModel>,
2446    qkv_idx: usize,
2447    xn: &[f32],
2448    n: usize,
2449    hidden: usize,
2450    nh: usize,
2451    hd: usize,
2452    scale: f32,
2453    nr: (&[f32], &[f32], &[f32], f32),
2454    out: &mut [f32],
2455) -> bool {
2456    match backend() {
2457        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2458        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attention(
2459            model, qkv_idx, xn, n, hidden, nh, hd, scale, nr, out,
2460        ),
2461        #[allow(unreachable_patterns)]
2462        _ => false,
2463    }
2464}
2465
2466/// The whole attention half of a DiT block on the card: qkv GEMM,
2467/// attention, output projection. Only `proj` comes home.
2468#[allow(clippy::too_many_arguments)]
2469pub fn dit_qkv_attn_out(
2470    model: &Arc<CmfModel>,
2471    qkv_idx: usize,
2472    out_idx: usize,
2473    xn: &[f32],
2474    n: usize,
2475    hidden: usize,
2476    nh: usize,
2477    hd: usize,
2478    scale: f32,
2479    nr: (&[f32], &[f32], &[f32], f32),
2480    proj: &mut [f32],
2481) -> bool {
2482    match backend() {
2483        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2484        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attn_out(
2485            model, qkv_idx, out_idx, xn, n, hidden, nh, hd, scale, nr, proj,
2486        ),
2487        #[allow(unreachable_patterns)]
2488        _ => false,
2489    }
2490}
2491
2492/// The VAE decoder's attention half on the card. Only `proj` returns.
2493#[allow(clippy::too_many_arguments)]
2494pub fn vae_qkv_attn_out(
2495    model: &Arc<CmfModel>,
2496    qkv_idx: usize,
2497    out_idx: usize,
2498    xn: &[f32],
2499    n: usize,
2500    dim: usize,
2501    nh: usize,
2502    hd: usize,
2503    scale: f32,
2504    angles: &[f32],
2505    eps: f32,
2506    qkv_bias: &[f32],
2507    proj: &mut [f32],
2508) -> bool {
2509    match backend() {
2510        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2511        Backend::Wgpu => crate::gpu_wgpu::vae_qkv_attn_out(
2512            model, qkv_idx, out_idx, xn, n, dim, nh, hd, scale, angles, eps, qkv_bias, proj,
2513        ),
2514        #[allow(unreachable_patterns)]
2515        _ => false,
2516    }
2517}
2518
2519#[allow(clippy::too_many_arguments)]
2520pub fn vae_attention_packed(
2521    qkv: &[f32],
2522    nh: usize,
2523    n: usize,
2524    hd: usize,
2525    scale: f32,
2526    angles: &[f32],
2527    eps: f32,
2528    out: &mut [f32],
2529) -> bool {
2530    vae_attention_packed_layout(qkv, nh, n, hd, scale, angles, eps, out, 1)
2531}
2532
2533#[allow(clippy::too_many_arguments)]
2534pub fn vae_attention_packed_layout(
2535    qkv: &[f32],
2536    nh: usize,
2537    n: usize,
2538    hd: usize,
2539    scale: f32,
2540    angles: &[f32],
2541    eps: f32,
2542    out: &mut [f32],
2543    layout: u32,
2544) -> bool {
2545    match backend() {
2546        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2547        Backend::Wgpu => crate::gpu_wgpu::vae_attention_packed_layout(
2548            qkv, nh, n, hd, scale, angles, eps, out, layout,
2549        ),
2550        #[allow(unreachable_patterns)]
2551        _ => false,
2552    }
2553}
2554
2555#[allow(clippy::too_many_arguments)]
2556pub fn dit_split_only(
2557    qkv: &[f32],
2558    nh: usize,
2559    n: usize,
2560    hd: usize,
2561    layout: u32,
2562    norm: Option<(&[f32], f32)>,
2563    out_q: &mut [f32],
2564) -> bool {
2565    match backend() {
2566        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2567        Backend::Wgpu => crate::gpu_wgpu::dit_split_only(qkv, nh, n, hd, layout, norm, out_q),
2568        #[allow(unreachable_patterns)]
2569        _ => false,
2570    }
2571}
2572
2573/// The backend's f32 NT GEMM: `y[n×m] = x[n×k] · wᵀ[m×k]`. Tensor
2574/// cores where the card has them. Refuses under `CMF_BAKE_GPU=0` or
2575/// strict f32, and for jobs below n·k·m = 4M, where the round trip
2576/// costs more than the arithmetic saves.
2577/// `gemm_nt_f32` whose `w` is known to change every call (an
2578/// accumulation over fresh activations, not a weight): it skips the
2579/// resident ledger and its per-call fingerprint of the whole operand.
2580pub fn gemm_nt_f32_transient(
2581    x: &[f32],
2582    w: &[f32],
2583    y: &mut [f32],
2584    n: usize,
2585    k: usize,
2586    m: usize,
2587) -> bool {
2588    match backend() {
2589        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2590        Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32_transient(x, w, y, n, k, m),
2591        #[allow(unreachable_patterns)]
2592        _ => false,
2593    }
2594}
2595
2596pub fn gemm_nt_f32(x: &[f32], w: &[f32], y: &mut [f32], n: usize, k: usize, m: usize) -> bool {
2597    match backend() {
2598        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2599        Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32(x, w, y, n, k, m),
2600        #[allow(unreachable_patterns)]
2601        _ => false,
2602    }
2603}
2604
2605/// Music-3's FFN chain resident on the device — two GEMMs and the GLU
2606/// between them with no host round trip. `false` = refused, host runs.
2607#[allow(clippy::too_many_arguments)]
2608pub fn music3_ffn(
2609    model: &std::sync::Arc<CmfModel>,
2610    idx_in: usize,
2611    idx_out: usize,
2612    h: &[f32],
2613    bias_in: &[f32],
2614    n: usize,
2615    hs: usize,
2616    inter: usize,
2617    out: &mut [f32],
2618) -> bool {
2619    match backend() {
2620        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2621        Backend::Wgpu => {
2622            crate::gpu_wgpu::music3_ffn(model, idx_in, idx_out, h, bias_in, n, hs, inter, out)
2623        }
2624        #[allow(unreachable_patterns)]
2625        _ => false,
2626    }
2627}
2628
2629/// A 1D convolution as a GEMM whose column matrix is expanded on the
2630/// device instead of being built, transposed and uploaded by the host.
2631/// `yt` comes back `[out_n x oc]`. `false` = refused, caller runs host.
2632#[allow(clippy::too_many_arguments)]
2633pub fn conv1d_gemm(
2634    x: &[f32],
2635    w: &[f32],
2636    ic: usize,
2637    oc: usize,
2638    n: usize,
2639    k: usize,
2640    pad: usize,
2641    dil: usize,
2642    out_n: usize,
2643    yt: &mut [f32],
2644) -> bool {
2645    match backend() {
2646        #[cfg(target_os = "macos")]
2647        Backend::Metal => crate::gpu_metal::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
2648        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2649        Backend::Wgpu => crate::gpu_wgpu::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
2650        #[allow(unreachable_patterns)]
2651        _ => false,
2652    }
2653}
2654
2655/// The convolution as a GEMM on the matrix units. `false` = refused.
2656#[allow(clippy::too_many_arguments)]
2657pub fn vae_conv2d_coop(
2658    w: &[f32],
2659    bias: Option<&[f32]>,
2660    x: &[f32],
2661    ic: usize,
2662    oc: usize,
2663    h: usize,
2664    wi: usize,
2665    k: usize,
2666    out: &mut [f32],
2667) -> bool {
2668    match backend() {
2669        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
2670        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d_coop(w, bias, x, ic, oc, h, wi, k, out),
2671        #[allow(unreachable_patterns)]
2672        _ => false,
2673    }
2674}
2675
2676pub fn dit_attention_packed(
2677    qkv: &[f32],
2678    nh: usize,
2679    n: usize,
2680    hd: usize,
2681    scale: f32,
2682    // (rope angles, q norm weights, k norm weights, eps) when the device
2683    // should apply qk-norm and RoPE itself; None when the host already did.
2684    nr: Option<(&[f32], &[f32], &[f32], f32)>,
2685    out: &mut [f32],
2686) -> bool {
2687    match backend() {
2688        // wgpu carries the only implementation, and it is not
2689        // platform-specific: `CMF_GPU=wgpu` on macOS runs it over Metal
2690        // like anywhere else. It used to be compiled out here on macOS,
2691        // which made the call a silent `false` — and the caller's
2692        // `assert!` turned that refusal into a panic on every
2693        // `cortiq animate` this platform ever ran.
2694        #[cfg(feature = "gpu")]
2695        Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed(qkv, nh, n, hd, scale, nr, out),
2696        #[allow(unreachable_patterns)]
2697        _ => false,
2698    }
2699}
2700
2701/// Whether `dit_attention_packed` has an implementation on the backend
2702/// that is actually selected.
2703///
2704/// The caller has to know BEFORE it skips the host qk-norm: deferring
2705/// the norm to a device that then refuses leaves q/k unnormalized with
2706/// no way back. Native Metal has no packed kernel, so on macOS this is
2707/// false unless `CMF_GPU=wgpu` picked the other backend.
2708pub fn dit_attention_packed_available() -> bool {
2709    #[allow(unreachable_patterns)]
2710    match backend() {
2711        #[cfg(feature = "gpu")]
2712        Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed_ready(),
2713        _ => false,
2714    }
2715}
2716
2717pub fn dit_attention(
2718    qh: &[f32],
2719    kh: &[f32],
2720    vh: &[f32],
2721    nh: usize,
2722    nkv: usize,
2723    n: usize,
2724    hd: usize,
2725    scale: f32,
2726    out: &mut [f32],
2727) -> bool {
2728    match backend() {
2729        #[cfg(target_os = "macos")]
2730        Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
2731        #[cfg(feature = "gpu")]
2732        Backend::Wgpu => crate::gpu_wgpu::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
2733        #[allow(unreachable_patterns)]
2734        _ => false,
2735    }
2736}
2737
2738/// Batched q4t GEMM on the device (imagegen DiT prefill shapes).
2739/// Metal: q4t_mul_mm decodes the mmap-resident tiles inside the
2740/// GEMM's K loop. wgpu (Vulkan/DX12 → NVIDIA/AMD/Intel/Adreno/Mali):
2741/// the register-blocked WGSL twin, weights cached in VRAM.
2742#[allow(unused_variables)]
2743pub fn q4tp_matmat(
2744    model: &Arc<CmfModel>,
2745    idx: usize,
2746    xs: &[f32],
2747    b: usize,
2748    rows: usize,
2749    cols: usize,
2750    out: &mut [f32],
2751) -> bool {
2752    match backend() {
2753        #[cfg(target_os = "macos")]
2754        Backend::Metal => crate::gpu_metal::q4tp_matmat(model, idx, xs, b, rows, cols, out),
2755        #[cfg(feature = "gpu")]
2756        Backend::Wgpu => crate::gpu_wgpu::q4tp_matmat(model, idx, xs, b, rows, cols, out),
2757        #[allow(unreachable_patterns)]
2758        _ => false,
2759    }
2760}
2761
2762/// The same over a two-bit weight plane. Metal has no q2tp kernel, so
2763/// there it declines and the host takes it.
2764pub fn q2tp_matmat(
2765    model: &Arc<CmfModel>,
2766    idx: usize,
2767    xs: &[f32],
2768    b: usize,
2769    rows: usize,
2770    cols: usize,
2771    out: &mut [f32],
2772) -> bool {
2773    match backend() {
2774        #[cfg(feature = "gpu")]
2775        Backend::Wgpu => crate::gpu_wgpu::q2tp_matmat(model, idx, xs, b, rows, cols, out),
2776        #[allow(unreachable_patterns)]
2777        _ => false,
2778    }
2779}
2780
2781/// Single-token q4tp matvec on the device — the lm_head class. Through the
2782/// DEDICATED matvec kernel: the batched GEMM at b=1 measured 11.73 ms
2783/// against the host's 9.51 on the release head, so the route that was
2784/// supposed to save eleven milliseconds a token lost its own probe instead.
2785pub fn q4tp_matvec(
2786    model: &Arc<CmfModel>,
2787    idx: usize,
2788    xs: &[f32],
2789    rows: usize,
2790    cols: usize,
2791    out: &mut [f32],
2792) -> bool {
2793    match backend() {
2794        #[cfg(target_os = "macos")]
2795        Backend::Metal => crate::gpu_metal::q4tp_matvec_for_test(model, idx, xs, rows, cols, out),
2796        #[cfg(feature = "gpu")]
2797        Backend::Wgpu => crate::gpu_wgpu::q4tp_matvec(model, idx, xs, rows, cols, out),
2798        #[allow(unreachable_patterns)]
2799        _ => false,
2800    }
2801}
2802
2803/// Single-token q4_tiled matvec on the device — the lm_head class (a
2804/// q4t checkpoint's head is its biggest host matvec, exactly like the
2805/// q4tp twin above). wgpu holds q4t_mv pipelines only inside the graph
2806/// encoder — the standalone arm stays an honest refusal until a
2807/// discrete-GPU q4t model reaches the bench.
2808pub fn q4t_matvec(
2809    model: &Arc<CmfModel>,
2810    idx: usize,
2811    xs: &[f32],
2812    rows: usize,
2813    cols: usize,
2814    out: &mut [f32],
2815) -> bool {
2816    match backend() {
2817        #[cfg(target_os = "macos")]
2818        Backend::Metal => crate::gpu_metal::q4t_matvec_for_test(model, idx, xs, rows, cols, out),
2819        #[allow(unreachable_patterns)]
2820        _ => false,
2821    }
2822}
2823
2824pub fn q4t_matmat(
2825    model: &Arc<CmfModel>,
2826    idx: usize,
2827    xs: &[f32],
2828    b: usize,
2829    rows: usize,
2830    cols: usize,
2831    out: &mut [f32],
2832) -> bool {
2833    match backend() {
2834        #[cfg(target_os = "macos")]
2835        Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
2836        #[cfg(feature = "gpu")]
2837        Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
2838        #[allow(unreachable_patterns)]
2839        _ => false,
2840    }
2841}
2842
2843/// Whole-block token-graph types re-exported from the Metal backend.
2844#[cfg(target_os = "macos")]
2845pub use crate::gpu_metal::{
2846    AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GpuMoe, GraphDims, MetalFfn,
2847    O1AttnParams, TokenGraph, kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp,
2848};
2849
2850/// A BLOCK of consecutive q1 GDN layers in one submission (Metal only).
2851#[cfg(target_os = "macos")]
2852pub fn gdn_block(
2853    model: &Arc<CmfModel>,
2854    layers: &[GdnGpuLayer],
2855    states: &mut [&mut [f32]],
2856    cfg: &GdnGpuCfg,
2857    h: &mut [f32],
2858) -> bool {
2859    match backend() {
2860        Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
2861        _ => false,
2862    }
2863}
2864
2865/// A layer's MoE-FFN in one submission (amortizing the dispatch cost).
2866#[allow(unused_variables)]
2867pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
2868    match backend() {
2869        #[cfg(target_os = "macos")]
2870        Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
2871        #[cfg(feature = "gpu")]
2872        Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
2873        Backend::None => false,
2874    }
2875}
2876
2877/// Independent matvecs of one input in a single submission (GDN projections).
2878#[allow(unused_variables)]
2879pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
2880    match backend() {
2881        #[cfg(target_os = "macos")]
2882        Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
2883        #[cfg(feature = "gpu")]
2884        Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
2885        Backend::None => false,
2886    }
2887}
2888
2889// ── Whole-token wgpu graph race (generation granularity) ─────────────
2890// On integrated/mobile adapters the graph is neither trusted nor banned
2891// a priori — it RACES the normal path: generations alternate arms (the
2892// normal path first — known-good UX — then the graph), per-token wall
2893// times accumulate per arm, and once both arms have enough steady
2894// samples the faster one wins for the process. Arm switches happen ONLY
2895// at generation boundaries (`kv_cache.clear()` resets state), so the
2896// device KV mirror and the CPU cache never diverge mid-sequence. The
2897// single exception is the first-token bail: the very first decode token
2898// of a graph generation may be discarded and recomputed on the CPU
2899// path (the prompt KV is CPU-owned at that point, so this is safe) —
2900// a tiled mobile GPU that drains its pipeline at every barrier turns
2901// the ~300-dispatch graph into seconds per token (field report: 0.2
2902// tok/s vs 15 on the CPU), and one token is all it takes to see that.
2903static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); // 0 racing, 1 graph won, 2 normal won
2904static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
2905static GRAPH_RACE_ARM_GRAPH: AtomicU8 = AtomicU8::new(0); // this generation's arm
2906static GRAPH_RACE_TOK: AtomicU32 = AtomicU32::new(0); // token index within the generation
2907static GRAPH_NS: [AtomicU64; 2] = [AtomicU64::new(0), AtomicU64::new(0)]; // [normal, graph]
2908static GRAPH_N: [AtomicU32; 2] = [AtomicU32::new(0), AtomicU32::new(0)];
2909
2910/// Steady per-token samples per arm before the race decides.
2911const GRAPH_RACE_SAMPLES: u32 = 4;
2912
2913/// Called at every generation start (fresh KV). Applies a pending
2914/// verdict and picks this generation's arm while racing.
2915/// A graph that cannot be built for THIS model will never build: the
2916/// refusal is a property of the weights, not of the moment. Retrying it
2917/// per token is not free — the builder walks every layer and asks each
2918/// tensor for a graph view before giving up at layer 0 — and on an
2919/// Adreno 642L that retry cost 3x: forcing the graph on a model it
2920/// refuses measured 0.3 tok/s against 0.905 for the per-op path it falls
2921/// back to. Remembered once, the fallback runs at its own speed.
2922static GRAPH_UNSUPPORTED: AtomicBool = AtomicBool::new(false);
2923
2924/// The builder refused for a STRUCTURAL reason — an unsupported weight
2925/// or layer kind. Callers must NOT report the transient refusals (an
2926/// unsealed o1 state during prefill, a softcap): those clear on their
2927/// own and marking them would disable the graph for good.
2928pub fn graph_mark_unsupported() {
2929    if !GRAPH_UNSUPPORTED.swap(true, Ordering::Relaxed) {
2930        tracing::info!("wgpu token graph: unsupported for this model — not retrying");
2931    }
2932}
2933
2934pub fn graph_unsupported() -> bool {
2935    GRAPH_UNSUPPORTED.load(Ordering::Relaxed)
2936}
2937
2938/// A different model in the same process starts with a clean slate.
2939pub fn graph_unsupported_reset() {
2940    GRAPH_UNSUPPORTED.store(false, Ordering::Relaxed);
2941}
2942
2943pub fn graph_race_begin_generation() {
2944    // One generation has now compiled whatever this model needs; keep it
2945    // for the next process. Once per run: the blob does not grow after
2946    // the pipelines exist, and the write is megabytes against the ~200 s
2947    // of compiling it saves on the device that needed this.
2948    #[cfg(feature = "gpu")]
2949    {
2950        // Save once, at the start of the SECOND generation: the first
2951        // has dispatched, so there is something to keep, and nothing is
2952        // saved before any work (the driver compiles at first use, not
2953        // at pipeline creation — the context comes up in 1.5 s while the
2954        // compiling costs minutes).
2955        //
2956        // Flushing again on 4, 8, 16 … was tried on the theory that a
2957        // chat turn compiles shapes the first one did not. It buys
2958        // nothing: a fresh app process still spent 49.0 s, then 58.7,
2959        // then 61.3 on its first answer with the backoff in place. One
2960        // flush it is.
2961        static FLUSHED: std::sync::Once = std::sync::Once::new();
2962        static FIRST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
2963        if FIRST.swap(false, Ordering::Relaxed) {
2964            // Nothing dispatched yet.
2965        } else {
2966            FLUSHED.call_once(crate::gpu_wgpu::pipeline_cache_flush);
2967        }
2968    }
2969    GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
2970    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
2971        return;
2972    }
2973    let (gn, cn) = (
2974        GRAPH_N[1].load(Ordering::Relaxed),
2975        GRAPH_N[0].load(Ordering::Relaxed),
2976    );
2977    if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
2978        let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
2979        let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
2980        let verdict = if g_avg < c_avg { 1 } else { 2 };
2981        GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
2982        tracing::info!(
2983            "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
2984            g_avg as f64 / 1e6,
2985            c_avg as f64 / 1e6,
2986            if verdict == 1 { "graph" } else { "normal path" }
2987        );
2988        return;
2989    }
2990    let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
2991    GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
2992}
2993
2994/// Should this decode token try the graph? `trusted` (discrete adapter,
2995/// explicit env, or a GDN hybrid whose state lives on the device) skips
2996/// the race entirely.
2997pub fn graph_race_use_graph(trusted: bool) -> bool {
2998    if trusted {
2999        return true;
3000    }
3001    match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
3002        1 => true,
3003        2 => false,
3004        _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
3005    }
3006}
3007
3008/// First decode token of a racing graph generation: hopeless already?
3009/// (>4x the normal path's per-token average AND over a second.) Settles
3010/// the race immediately; the caller discards the graph result and
3011/// recomputes this token on the normal path.
3012pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
3013    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
3014        return false;
3015    }
3016    let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
3017    let cn = GRAPH_N[0].load(Ordering::Relaxed);
3018    if !first || cn == 0 {
3019        return false;
3020    }
3021    let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
3022    let ns = dur.as_nanos() as u64;
3023    if ns > 1_000_000_000 && ns > 4 * c_avg {
3024        GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
3025        tracing::info!(
3026            "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
3027            ns as f64 / 1e6,
3028            c_avg as f64 / 1e6
3029        );
3030        return true;
3031    }
3032    false
3033}
3034
3035/// Record one decode-token wall time for the racing arm. The first
3036/// token of each generation is discarded (KV-mirror upload / cold
3037/// caches on the graph arm; cold mmap on the normal arm).
3038pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
3039    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
3040        return;
3041    }
3042    let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
3043    if tok == 0 {
3044        return;
3045    }
3046    let i = used_graph as usize;
3047    GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
3048    GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
3049}
3050
3051/// Bounded-cost content fingerprint for the backends' pointer-keyed device
3052/// caches: FNV over the whole slice up to 4 KiB, over 64 spread 64-byte
3053/// windows (plus the length) above. An address-keyed hit must also prove
3054/// the bytes are still the ones it uploaded — the allocator reuses heap
3055/// and mmap addresses freely, so a reloaded model or a re-dequantized
3056/// layer lands where the old bytes were — and sampling keeps that proof at
3057/// ~a microsecond even for a 126 MB matrix. Real replacements (another
3058/// model's tensor, an Adam-updated master) differ densely, so a 4 KiB
3059/// spread cannot miss them.
3060pub(crate) fn fp_bytes(data: &[u8]) -> u64 {
3061    #[inline]
3062    fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
3063        let (chunks, tail) = bytes.split_at(bytes.len() & !7);
3064        for c in chunks.chunks_exact(8) {
3065            h ^= u64::from_le_bytes(c.try_into().unwrap());
3066            h = h.wrapping_mul(0x100_0000_01b3);
3067        }
3068        for &b in tail {
3069            h ^= b as u64;
3070            h = h.wrapping_mul(0x100_0000_01b3);
3071        }
3072        h
3073    }
3074    let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (data.len() as u64);
3075    if data.len() <= 4096 {
3076        return fnv(h, data);
3077    }
3078    let step = (data.len() - 64) / 63;
3079    for i in 0..64 {
3080        h = fnv(h, &data[i * step..i * step + 64]);
3081    }
3082    h
3083}
3084
3085/// `fp_bytes` over an f32 slice without a bytemuck dependency (the Metal
3086/// backend builds with no GPU feature flags).
3087pub(crate) fn fp_f32(data: &[f32]) -> u64 {
3088    let bytes = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
3089    fp_bytes(bytes)
3090}
3091
3092#[cfg(test)]
3093mod fp_tests {
3094    use super::fp_bytes;
3095
3096    /// The pointer-keyed caches survive on `fp_bytes` telling two different
3097    /// tensors apart at a reused address. Its sampling must therefore see a
3098    /// change ANYWHERE — head, tail, and the stretches between windows are
3099    /// the places a cheaper hash would go blind.
3100    #[test]
3101    fn fp_bytes_sees_a_change_anywhere_in_a_sampled_slice() {
3102        let n = 1 << 20; // 1 MiB — far above the 4 KiB full-hash threshold
3103        let base: Vec<u8> = (0..n).map(|i| (i * 31 + 7) as u8).collect();
3104        let h0 = fp_bytes(&base);
3105        assert_eq!(h0, fp_bytes(&base), "fingerprint must be deterministic");
3106        // A DENSE change (every requantized/redequantized tensor is one)
3107        // must flip the fingerprint no matter how the windows fall.
3108        let mut dense = base.clone();
3109        for b in dense.iter_mut() {
3110            *b = b.wrapping_add(1);
3111        }
3112        assert_ne!(
3113            h0,
3114            fp_bytes(&dense),
3115            "a fully different tensor slipped through"
3116        );
3117        // Length participates: the same prefix at a shorter length is a
3118        // different key AND a different fingerprint.
3119        assert_ne!(h0, fp_bytes(&base[..n - 64]));
3120        // Below the threshold the hash is exact: a single flipped byte in
3121        // a norm-sized vector must be seen.
3122        let mut small = vec![3u8; 4096];
3123        let hs = fp_bytes(&small);
3124        small[2048] ^= 1;
3125        assert_ne!(hs, fp_bytes(&small), "full hash missed a one-byte change");
3126        // And the sampled windows land within bounds on awkward sizes.
3127        for n in [4097usize, 5000, 64 * 64, 1 << 16] {
3128            let v = vec![9u8; n];
3129            let _ = fp_bytes(&v); // must not panic on window math
3130        }
3131    }
3132}
3133
3134/// Hand the card back after a bake: drop its resident weights, planes and
3135/// pools so the ordinary engine (the runtime gate, a serve that follows)
3136/// starts from a clean budget. No-op off the wgpu backend.
3137pub fn bake_release() {
3138    #[cfg(feature = "gpu")]
3139    crate::gpu_wgpu::bake_release();
3140}
3141
3142/// Strict-f32 for the bake's GEMMs (phase A mask training): the mask
3143/// selects neurons by a gradient signal, and f16 operand rounding on
3144/// that signal closes the wrong ones. No-op off the wgpu backend.
3145pub fn bake_precision_strict(on: bool) {
3146    #[cfg(feature = "gpu")]
3147    crate::gpu_wgpu::bake_precision_strict(on);
3148    #[cfg(not(feature = "gpu"))]
3149    let _ = on;
3150}
3151
3152/// CMF_GRAPH_HOSTPROF=1: how a graph token's wall splits between the
3153/// host encoding the command stream and the tail the GPU still owes
3154/// after encode. Fifteen GPU-side suspects measured null while the
3155/// bench counted 17.7k allocations a token — this is the instrument
3156/// that says whether the thief was on the host all along.
3157pub fn hostprof_encode_done(t0: std::time::Instant) {
3158    use std::sync::atomic::{AtomicU64, Ordering};
3159    static ENC: AtomicU64 = AtomicU64::new(0);
3160    static N: AtomicU64 = AtomicU64::new(0);
3161    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
3162        return;
3163    }
3164    ENC.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3165    let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3166    if n % 100 == 0 {
3167        eprintln!(
3168            "hostprof: encode {:.2} ms/token over {n} tokens",
3169            ENC.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3170        );
3171    }
3172}
3173
3174pub fn hostprof_total(t0: std::time::Instant) {
3175    use std::sync::atomic::{AtomicU64, Ordering};
3176    static TOT: AtomicU64 = AtomicU64::new(0);
3177    static N: AtomicU64 = AtomicU64::new(0);
3178    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
3179        return;
3180    }
3181    TOT.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
3182    let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3183    if n % 100 == 0 {
3184        eprintln!(
3185            "hostprof: total {:.2} ms/token over {n} tokens",
3186            TOT.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3187        );
3188    }
3189}
3190
3191/// Per-stage host-encode accumulator for the Metal token loop
3192/// (CMF_GRAPH_HOSTPROF=1). Stage 0 = GDN-run encode; everything else
3193/// falls out by subtraction from hostprof's encode total.
3194pub fn stageprof(stage: u32, dt: std::time::Duration) {
3195    use std::sync::atomic::{AtomicU64, Ordering};
3196    static NS: [AtomicU64; 4] = [
3197        AtomicU64::new(0),
3198        AtomicU64::new(0),
3199        AtomicU64::new(0),
3200        AtomicU64::new(0),
3201    ];
3202    static N: AtomicU64 = AtomicU64::new(0);
3203    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
3204        return;
3205    }
3206    NS[stage as usize % 4].fetch_add(dt.as_nanos() as u64, Ordering::Relaxed);
3207    if stage == 1 {
3208        let n = N.fetch_add(1, Ordering::Relaxed) + 1;
3209        if n % 200 == 0 {
3210            eprintln!(
3211                "stageprof: planning {:.2} ms/tok | gdn-item {:.2} ms/tok | attn-item {:.2} ms/tok ({n} tok)",
3212                NS[1].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3213                NS[2].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
3214                NS[3].load(Ordering::Relaxed) as f64 / n as f64 / 1e6
3215            );
3216        }
3217    }
3218}
3219
3220/// Active weight bytes dispatched so far (Metal decode path); 0 where
3221/// the backend does not count. The honest floor's numerator.
3222pub fn weight_bytes_dispatched() -> u64 {
3223    let mut total = 0u64;
3224    #[cfg(target_os = "macos")]
3225    {
3226        total += crate::gpu_metal::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
3227    }
3228    #[cfg(feature = "gpu")]
3229    {
3230        total += crate::gpu_wgpu::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
3231    }
3232    total
3233}
3234
3235/// The per-stage split of `weight_bytes_dispatched`:
3236/// [misc, dense-ffn, moe, attn, gdn, head].
3237pub fn weight_bytes_by() -> [u64; 6] {
3238    #[cfg(target_os = "macos")]
3239    {
3240        let mut o = [0u64; 6];
3241        for (i, a) in crate::gpu_metal::WEIGHT_BYTES_BY.iter().enumerate() {
3242            o[i] = a.load(std::sync::atomic::Ordering::Relaxed);
3243        }
3244        return o;
3245    }
3246    #[allow(unreachable_code)]
3247    [0; 6]
3248}
3249
3250#[cfg(test)]
3251mod probe_warmup_tests {
3252    use super::*;
3253    use std::time::Duration;
3254
3255    fn ms(v: f64) -> Duration {
3256        Duration::from_nanos((v * 1e6) as u64)
3257    }
3258
3259    /// The bug this pins, measured on an A100: the first device call for
3260    /// a class compiles its pipeline, was timed at 117.01 ms against the
3261    /// host's 3.19, and sent `gemm-nt` to the CPU for the whole process —
3262    /// which ran a 27B bake on 2.6 cores with the card idle.
3263    #[test]
3264    fn one_cold_first_sample_does_not_lose_the_class() {
3265        let p = Probe::new();
3266        // First device sample is the pipeline build. Then the truth.
3267        probe_record_into(&p, "gemm-nt", None, true, ms(117.01));
3268        probe_record_into(&p, "gemm-nt", None, true, ms(1.1));
3269        probe_record_into(&p, "gemm-nt", None, true, ms(1.0));
3270        probe_record_into(&p, "gemm-nt", None, false, ms(3.19));
3271        probe_record_into(&p, "gemm-nt", None, false, ms(3.20));
3272        assert_eq!(
3273            p.state.load(Ordering::Relaxed),
3274            1,
3275            "the device is 3x faster once warm and must win"
3276        );
3277    }
3278
3279    /// The warm-up must not become a way to never decide, and must not
3280    /// underflow: a blind decrement at zero wraps a u32 to its maximum
3281    /// and mutes the arm for the life of the process.
3282    #[test]
3283    fn the_warmup_is_spent_once_and_never_underflows() {
3284        let p = Probe::new();
3285        for _ in 0..8 {
3286            probe_record_into(&p, "matmat", None, true, ms(10.0));
3287        }
3288        assert_eq!(p.gpu_burn.load(Ordering::Relaxed), 0, "spent, not wrapped");
3289        assert_eq!(
3290            p.gpu_n.load(Ordering::Relaxed),
3291            7,
3292            "one sample burned, the rest counted"
3293        );
3294    }
3295
3296    /// A device path that always refuses records no timing, so without
3297    /// counting the refusals the class can never reach a verdict. On an
3298    /// M4 with LFM2.5-2.6B `ffn` was still undecided after 9000 calls,
3299    /// alternating arms and paying a failed device attempt on half of
3300    /// them.
3301    #[test]
3302    fn a_class_whose_device_always_declines_settles_on_the_host() {
3303        // A class no other test in this file touches: `probe_note_decline`
3304        // works on the process-wide probes by design, and the tests in
3305        // this binary share them.
3306        let c = OpClass::MatmatWide;
3307        let p = &PROBES[c as usize];
3308        p.state.store(0, Ordering::Relaxed);
3309        p.declines.store(0, Ordering::Relaxed);
3310        for _ in 0..(PROBE_DECLINE_LIMIT - 1) {
3311            probe_note_decline(c);
3312        }
3313        assert_eq!(
3314            p.state.load(Ordering::Relaxed),
3315            0,
3316            "one short of the limit is still a question, not an answer"
3317        );
3318        probe_note_decline(c);
3319        assert_eq!(p.state.load(Ordering::Relaxed), 2, "settled on the host");
3320        assert!(matches!(probe_arm(c), ProbeArm::Cpu));
3321        p.state.store(0, Ordering::Relaxed);
3322        p.declines.store(0, Ordering::Relaxed);
3323    }
3324
3325    /// A genuinely slower device still loses — the warm-up removes an
3326    /// artefact, it does not put a thumb on the scale.
3327    #[test]
3328    fn a_slow_device_still_loses_after_the_warmup() {
3329        let p = Probe::new();
3330        for _ in 0..4 {
3331            probe_record_into(&p, "matvec", None, true, ms(40.0));
3332        }
3333        for _ in 0..4 {
3334            probe_record_into(&p, "matvec", None, false, ms(2.0));
3335        }
3336        assert_eq!(p.state.load(Ordering::Relaxed), 2, "host wins on merit");
3337    }
3338}
3339
3340/// Scratch/weight lifetime for a synchronous image-pipeline stage. Declare
3341/// this before the stage model so the model drops before cache collection.
3342pub(crate) struct ImageStageGuard {
3343    #[cfg(target_os = "macos")]
3344    metal: Option<crate::gpu_metal::ImageStageGuard>,
3345    #[cfg(feature = "gpu")]
3346    wgpu: crate::gpu_wgpu::ImageStageGuard,
3347}
3348
3349pub(crate) fn image_stage_scope() -> ImageStageGuard {
3350    ImageStageGuard {
3351        #[cfg(target_os = "macos")]
3352        metal: if matches!(backend(), Backend::Metal) {
3353            Some(crate::gpu_metal::image_stage_scope())
3354        } else {
3355            None
3356        },
3357        #[cfg(feature = "gpu")]
3358        wgpu: crate::gpu_wgpu::image_stage_scope(),
3359    }
3360}
3361
3362impl ImageStageGuard {
3363    pub(crate) fn track_model(&mut self, uid: u64) {
3364        #[cfg(target_os = "macos")]
3365        if let Some(metal) = &mut self.metal {
3366            metal.track_model(uid);
3367        }
3368        #[cfg(feature = "gpu")]
3369        self.wgpu.track_model(uid);
3370        #[cfg(not(target_os = "macos"))]
3371        let _ = uid;
3372    }
3373}