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/// Run `f` with the GPU gates off on this thread (pure-CPU arm).
35pub fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
36    struct Restore(bool);
37    impl Drop for Restore {
38        fn drop(&mut self) {
39            CPU_ONLY.with(|c| c.set(self.0));
40        }
41    }
42    let previous = CPU_ONLY.with(|c| c.replace(true));
43    let _restore = Restore(previous);
44    f()
45}
46
47/// Backends: note a one-off cost (weight upload, buffer-cache fill) so
48/// the probe discards this sample.
49pub(crate) fn probe_note_cold() {
50    PROBE_COLD.with(|c| c.set(true));
51}
52
53/// Peek the cold flag without consuming it (`probe_record` consumes).
54/// Contention heuristics use this: a slow COLD op is a one-off build
55/// cost, not evidence the device is busy.
56pub(crate) fn probe_was_cold() -> bool {
57    PROBE_COLD.with(|c| c.get())
58}
59
60/// Pipeline: mark the current layer (or −1 outside layers) for layer-split.
61pub fn set_layer(l: i64) {
62    CUR_LAYER.with(|c| c.set(l));
63}
64
65/// The layer `set_layer` last marked on this thread (−1 outside layers).
66pub fn cur_layer() -> i64 {
67    CUR_LAYER.with(|c| c.get())
68}
69
70/// Parse `CMF_GPU_LAYERS` («0-19», «0,2,4», «0-9,30-39») once.
71/// None = no restriction (all layers on GPU). Garbage → also no restriction.
72fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
73    static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
74    R.get_or_init(|| {
75        let s = std::env::var("CMF_GPU_LAYERS").ok()?;
76        let mut v = Vec::new();
77        for part in s.split(',') {
78            let part = part.trim();
79            match part.split_once('-') {
80                Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
81                None => {
82                    let x: i64 = part.parse().ok()?;
83                    v.push((x, x));
84                }
85            }
86        }
87        Some(v)
88    })
89}
90
91fn layer_allowed() -> bool {
92    match layer_ranges() {
93        None => true,
94        Some(ranges) => {
95            let cur = CUR_LAYER.with(|c| c.get());
96            cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
97        }
98    }
99}
100
101/// GPU allowed FOR THE CURRENT LAYER: backend is initialized AND the layer
102/// falls within `CMF_GPU_LAYERS` (GPU/CPU layer-split) AND we are not
103/// inside a `cpu_scope`. Op gates call this.
104pub fn enabled_here() -> bool {
105    !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
106}
107
108// ── Runtime GPU-vs-CPU probe ────────────────────────────────────────────
109// CMF_GPU=1 does not TRUST that the device wins — it MEASURES. For each
110// op class the first calls alternate arms: GPU timed vs pure-CPU timed
111// (under cpu_scope). Cold GPU calls (weight upload / cache fill) are
112// discarded; after PROBE_SAMPLES clean samples per arm the faster arm is
113// chosen for the rest of the process. Rationale: submit+poll latency
114// differs by an order of magnitude across driver stacks (Metal/PCIe
115// ~3-4 ms, Vulkan/4090 ~0.3 ms) — a static threshold cannot know whether
116// per-op offload pays off HERE. CMF_GPU_PROBE=0 → always trust the GPU.
117
118/// GPU-eligible op classes, each with an independent probe.
119#[derive(Clone, Copy)]
120pub enum OpClass {
121    /// Whole FFN chain in one submission (dense / MoE block).
122    Ffn = 0,
123    /// Large hybrid CPU∥GPU matvec (lm_head class).
124    Matvec = 1,
125    /// Prefill GEMM (matmat).
126    Matmat = 2,
127    /// Batched matvecs of one input (QKV).
128    Batch = 3,
129    /// Prefill GEMM at image-diffusion widths (b ≥ 128). Probed apart
130    /// from `Matmat`: one imagegen process runs BOTH populations
131    /// (prompt encode b≈40 where the GPU wins big, DiT b≥256 where
132    /// the CPU AMX arm is competitive) — a single shared verdict locks
133    /// the wrong arm for whichever population samples second.
134    MatmatWide = 4,
135    /// The lm_head itself, apart from the merely-large matvecs. Same
136    /// reasoning as `MatmatWide`, and DeepSeek-V4 is where it bit: its
137    /// attention projections are 37M weights and its head is 529M, so
138    /// the projections' verdict — CPU, honestly measured at 0.19 ms —
139    /// decided for a matvec fourteen times their size that took 11 ms
140    /// a token on the host.
141    MatvecHead = 5,
142    /// The blocked f32 GEMM (`fcd_ops::gemm_nt`): attention's QKᵀ and
143    /// AV, and the VAE decoders' projections. It used to take every job
144    /// over 4 M MACs on sight, with no CPU arm to lose to — which on
145    /// the MiniMax-H3 video decoder was three times SLOWER than the
146    /// host it displaced. Its population is per-head slices, nothing
147    /// like the weight GEMMs above, so it probes on its own.
148    GemmNt = 6,
149}
150
151/// Which probe a large matvec belongs to. The head is an order of
152/// magnitude bigger than anything else that reaches this gate, and the
153/// two populations do not have the same answer.
154pub fn matvec_class(rows: usize, cols: usize) -> OpClass {
155    if rows * cols >= 67_108_864 {
156        OpClass::MatvecHead
157    } else {
158        OpClass::Matvec
159    }
160}
161
162/// Probe verdict for one call.
163pub enum ProbeArm {
164    /// Run the GPU path (during probing: timed, recorded).
165    Gpu,
166    /// Probing: run the CPU path under `cpu_scope`, timed, recorded.
167    CpuTimed,
168    /// Decided: CPU won — run the CPU path (under `cpu_scope`).
169    Cpu,
170}
171
172/// Clean samples per arm before a class decides.
173const PROBE_SAMPLES: u32 = 6;
174
175struct Probe {
176    /// 0 = probing, 1 = GPU won, 2 = CPU won.
177    state: AtomicU8,
178    flip: AtomicU32,
179    gpu_ns: AtomicU64,
180    gpu_n: AtomicU32,
181    cpu_ns: AtomicU64,
182    cpu_n: AtomicU32,
183    /// Best (minimum) sample per arm. The DECISION compares these:
184    /// means are poisoned by one-off cold costs the cold-flag cannot
185    /// see — e.g. the CPU arm's first mmap-cold expert matvec page
186    /// faults its weights in and reads 3× its steady state, which
187    /// locked the GPU arm on a 35B MoE at a 4× real-world loss. The
188    /// minimum is each arm's honest steady-state pace.
189    gpu_min: AtomicU64,
190    cpu_min: AtomicU64,
191}
192
193impl Probe {
194    const fn new() -> Self {
195        Self {
196            state: AtomicU8::new(0),
197            flip: AtomicU32::new(0),
198            gpu_ns: AtomicU64::new(0),
199            gpu_n: AtomicU32::new(0),
200            cpu_ns: AtomicU64::new(0),
201            cpu_n: AtomicU32::new(0),
202            gpu_min: AtomicU64::new(u64::MAX),
203            cpu_min: AtomicU64::new(u64::MAX),
204        }
205    }
206}
207
208static PROBES: [Probe; 7] = [
209    Probe::new(),
210    Probe::new(),
211    Probe::new(),
212    Probe::new(),
213    Probe::new(),
214    Probe::new(),
215    Probe::new(),
216];
217
218fn probe_on() -> bool {
219    static ON: OnceLock<bool> = OnceLock::new();
220    *ON.get_or_init(|| {
221        std::env::var("CMF_GPU_PROBE")
222            .map(|v| v != "0" && v != "off")
223            .unwrap_or(true)
224    })
225}
226
227/// q1 ops on the native Metal backend skip the probe entirely: the CPU
228/// q1 kernel is load-port-bound, the GPU one wins warm — and probe
229/// alternation itself cools the device between samples (measured: block
230/// times 5.8 ms warm vs 8.8 ms mixed). Other backends keep probing.
231pub fn q1_force() -> bool {
232    #[cfg(target_os = "macos")]
233    {
234        backend() == Backend::Metal
235    }
236    #[cfg(not(target_os = "macos"))]
237    {
238        false
239    }
240}
241
242/// Should a FUSED whole-block path trust the device instead of asking
243/// the per-op probe? True on native Metal and on discrete wgpu adapters.
244///
245/// The probe answers "is one wide matmat faster on the GPU", and for the
246/// DiT on Metal that is a coin flip — measured 2.62 ms GPU vs 2.56 ms
247/// CPU, a 2% spread that lands on either arm run to run. But the fused
248/// block's advantage is not per-op speed, it is that the hidden state,
249/// the packs and the attention panels never leave the device: end to end
250/// the whole-block path renders a 512² Lumina step in ~5.4 s against
251/// ~8.4 s when the probe happens to pick the CPU. Gating a fusion win on
252/// a per-op tie made every second render half-speed at random.
253///
254/// On a discrete card the verdict is never in doubt — an RTX 3090 against
255/// a 256-core EPYC measured 11.5 ms vs 31 ms per wide op, four runs out
256/// of four — so the probe's sampling phase is pure cost: it alone was 10%
257/// of a 512² render (74.3 s against 66.9 s with the probe off). Integrated
258/// and mobile adapters keep probing; there the submit latency is real and
259/// can genuinely lose.
260pub fn fused_block_trusted() -> bool {
261    #[cfg(target_os = "macos")]
262    if backend() == Backend::Metal {
263        return true;
264    }
265    wgpu_graph_default()
266}
267
268/// Which arm should this GPU-eligible call take? Consult AFTER the
269/// eligibility gates (`enabled_here` / `min_rows`) so only real
270/// candidates alternate.
271/// While a class is still probing, a call whose weights are NOT yet on
272/// the card should take the GPU arm anyway: the upload is work the next
273/// step needs regardless, and the sample it produces is discarded as
274/// cold — so handing that call to the CPU arm buys nothing and costs a
275/// host GEMM. Measured on a diffusion stack, where every layer is
276/// touched once per step and therefore EVERY first-step GPU sample is
277/// cold: one projection drew the CPU arm for the whole first step, 9.8 s
278/// against the 2.8 s it costs once the weights are warm.
279pub fn weight_is_resident(model: &Arc<CmfModel>, idx: usize) -> bool {
280    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
281    {
282        return crate::gpu_wgpu::weight_is_resident(model, idx);
283    }
284    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
285    {
286        let _ = (model, idx);
287        true
288    }
289}
290
291pub fn probe_arm_cold_prefers_gpu(c: OpClass, weights_resident: bool) -> ProbeArm {
292    if !weights_resident && probe_deciding(c) {
293        return ProbeArm::Gpu;
294    }
295    probe_arm(c)
296}
297
298pub fn probe_arm(c: OpClass) -> ProbeArm {
299    // Every arbitrated call starts with a clean cold flag: both the
300    // sample discard in `probe_record` and the contention kill-switch
301    // read it AFTER the op, so a stale note from a previous call on
302    // this thread must not leak in.
303    PROBE_COLD.with(|f| f.set(false));
304    if !probe_on() {
305        return ProbeArm::Gpu;
306    }
307    let p = &PROBES[c as usize];
308    match p.state.load(Ordering::Relaxed) {
309        1 => ProbeArm::Gpu,
310        2 => ProbeArm::Cpu,
311        _ => {
312            if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
313                ProbeArm::Gpu
314            } else {
315                ProbeArm::CpuTimed
316            }
317        }
318    }
319}
320
321/// Record a timed arm sample; on the `PROBE_SAMPLES`-th clean sample of
322/// BOTH arms the class decides for the rest of the process.
323pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
324    let p = &PROBES[c as usize];
325    if p.state.load(Ordering::Relaxed) != 0 {
326        return;
327    }
328    if gpu && PROBE_COLD.with(|f| f.replace(false)) {
329        return; // one-off cost in this call — not a steady-state sample
330    }
331    let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
332    if gpu {
333        p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
334        p.gpu_n.fetch_add(1, Ordering::Relaxed);
335        p.gpu_min.fetch_min(ns, Ordering::Relaxed);
336    } else {
337        p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
338        p.cpu_n.fetch_add(1, Ordering::Relaxed);
339        p.cpu_min.fetch_min(ns, Ordering::Relaxed);
340    }
341    let (gn, cn) = (
342        p.gpu_n.load(Ordering::Relaxed),
343        p.cpu_n.load(Ordering::Relaxed),
344    );
345    if gn >= 2 && cn >= 2 {
346        // Decide on each arm's BEST sample — the steady-state pace.
347        // Means carry one-off cold costs (mmap page-in on the CPU arm)
348        // that the cold-flag machinery cannot see.
349        let g = p.gpu_min.load(Ordering::Relaxed) as f64;
350        let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
351        // Early verdict on a ≥2× gap — no reason to keep feeding the
352        // losing arm; close races take the full sample count. It was 3×,
353        // and the cost of that half-octave was measured: a DiT whose
354        // wide GEMMs run 11.4 ms on the device against 32.2 on the host
355        // (2.8×) kept ALTERNATING through the whole diffusion stack, and
356        // because the alternation counter is shared per class in call
357        // order, one projection drew the CPU arm every single time — 9.9
358        // seconds a step on a kernel that needs 0.4. Both arms are
359        // compared on their BEST sample, so a 2× gap is not noise.
360        if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 2.0 && cp < g * 2.0 {
361            return;
362        }
363        let winner = if g <= cp { 1 } else { 2 };
364        if p.state
365            .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
366            .is_ok()
367        {
368            tracing::info!(
369                "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
370                ["ffn", "matvec", "matmat", "qkv-batch", "matmat-wide", "lm-head", "gemm-nt"]
371                    [c as usize],
372                g / 1e6,
373                cp / 1e6,
374                if winner == 1 { "gpu" } else { "cpu" },
375            );
376        }
377    }
378}
379
380/// Is the class still collecting samples? (Call sites use this to route
381/// cold-weight calls away from the GPU arm during probing.)
382pub fn probe_deciding(c: OpClass) -> bool {
383    probe_on() && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
384}
385
386/// Probing helper: true — tensor `idx`'s quant weights are ALREADY
387/// device-resident (a clean GPU sample is possible now); false — they
388/// were not (the upload starts within the VRAM budget, so a later call
389/// finds them warm) or the tensor cannot go to the GPU at all. Keeps the
390/// probe from billing a full cold dispatch+readback to a sample it will
391/// discard anyway. The verdict needs only a couple of warm tensors, so
392/// probe-driven uploads are capped — the losing-GPU machine should not
393/// pay for uploading the whole layer stack it will never use; if the GPU
394/// wins, the rest uploads lazily on demand, in the same first-touch order.
395#[allow(unused_variables)]
396pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
397    static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
398    let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
399    let resident = match backend() {
400        #[cfg(target_os = "macos")]
401        Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
402        #[cfg(feature = "gpu")]
403        Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
404        Backend::None => false,
405    };
406    if !resident && may_upload {
407        PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
408    }
409    resident
410}
411
412/// Test hook: reset all probes to the undecided state.
413#[cfg(test)]
414pub(crate) fn probe_reset() {
415    for p in &PROBES {
416        p.state.store(0, Ordering::Relaxed);
417        p.flip.store(0, Ordering::Relaxed);
418        p.gpu_ns.store(0, Ordering::Relaxed);
419        p.gpu_n.store(0, Ordering::Relaxed);
420        p.cpu_ns.store(0, Ordering::Relaxed);
421        p.cpu_n.store(0, Ordering::Relaxed);
422    }
423}
424
425#[cfg(test)]
426mod probe_tests {
427    use super::*;
428    use std::time::Duration;
429
430    // One test fn: PROBES is process-global and probe_reset touches all
431    // classes — parallel test threads would race.
432    #[test]
433    fn probe_alternates_discards_cold_and_decides() {
434        probe_reset();
435        // Probing: arms alternate.
436        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
437        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
438
439        // A cold GPU sample (upload noted) must be discarded: feed a
440        // catastrophic cold sample, then clean fast-GPU samples — GPU
441        // wins only if the cold one did not count.
442        probe_note_cold();
443        probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
444        for _ in 0..PROBE_SAMPLES {
445            probe_record(OpClass::Ffn, true, Duration::from_millis(1));
446            probe_record(OpClass::Ffn, false, Duration::from_millis(4));
447        }
448        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
449
450        // The reverse: a class where the CPU arm is faster decides CPU.
451        for _ in 0..PROBE_SAMPLES {
452            probe_record(OpClass::Matmat, true, Duration::from_millis(4));
453            probe_record(OpClass::Matmat, false, Duration::from_millis(1));
454        }
455        assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
456
457        // cpu_scope: gates off inside, restored after.
458        cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
459        CPU_ONLY.with(|c| assert!(!c.get()));
460        cpu_scope(|| {
461            cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
462            CPU_ONLY.with(|c| assert!(c.get()));
463        });
464        let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
465        CPU_ONLY.with(|c| assert!(!c.get()));
466        probe_reset();
467    }
468}
469
470/// Default row threshold: the GPU takes only larger matrices (lm_head
471/// class). Below it, the dispatch/readback cost does not pay off on unified memory.
472pub const GPU_MIN_ROWS: usize = 65_536;
473
474/// Effective threshold: `CMF_GPU_MIN_ROWS` overrides. Defaults differ
475/// by device class: on a DISCRETE card VRAM bandwidth pays off even for
476/// FFN/QKV-class matrices (4096), on unified memory only lm_head-class
477/// is worth the dispatch/readback (65536). Field case behind this: a
478/// 35B model on an RTX 4090 saw ~0 offload because every layer matrix
479/// sat below the old universal 65536.
480pub fn min_rows() -> usize {
481    if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
482        .ok()
483        .and_then(|v| v.parse().ok())
484    {
485        return v;
486    }
487    if discrete() { 4096 } else { GPU_MIN_ROWS }
488}
489
490/// Is the active backend a discrete card (PCIe VRAM)?
491pub fn discrete() -> bool {
492    match backend() {
493        #[cfg(feature = "gpu")]
494        Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
495        #[cfg(target_os = "macos")]
496        Backend::Metal => false, // UMA by the init() guard
497        Backend::None => false,
498    }
499}
500
501/// A single MoE-FFN job (an expert with its own weight), executed in one
502/// submission: (rows, cols, idx, row_scale) for gate/up/down + prescaled
503/// inputs + the down θ-field + the blending weight.
504pub struct MoeJob<'a> {
505    pub gate: (usize, usize, usize, &'a [f32]),
506    pub up: (usize, usize, usize, &'a [f32]),
507    pub down: (usize, usize, usize, &'a [f32]),
508    pub xs_gate: Vec<f32>,
509    pub xs_up: Vec<f32>,
510    pub down_col: &'a [f32],
511    pub w: f32,
512    /// q1 trio: scales live inside the 6-byte tiles (row_scale slices
513    /// empty, xs raw f32). Backends without a q1 kernel refuse the job.
514    pub q1: bool,
515    /// q4_tiled trio: scales inside the 18-byte tiles (row_scale
516    /// slices empty, xs raw f32) — the MoE-hybrid coder class.
517    pub q4t: bool,
518    /// q4tp trio: same raw-xs contract, 16-byte nibble stride and the scale
519    /// on a per-row ladder. Without this the experts of a q4tp MoE model fall
520    /// to the CPU while every other dtype rides the device.
521    pub q4tp: bool,
522    /// Mixed 2-bit profile: gate/up are q2tp (8-byte chunks, zero rung),
523    /// down stays q4tp. Set together with `q4tp`; a backend without the
524    /// 2-bit kernel must refuse the whole job.
525    pub gu_q2: bool,
526    /// The reference's `swiglu_limit`; 0 disables the clamp. A backend that
527    /// cannot apply it must REFUSE the job rather than drop it silently —
528    /// the difference only shows on saturating activations, which is the
529    /// hardest kind of divergence to notice.
530    pub swiglu_limit: f32,
531}
532
533/// A single independent batch matvec (GDN projections of one input).
534pub struct BatchJob<'a> {
535    pub idx: usize,
536    pub rows: usize,
537    pub cols: usize,
538    pub row_scale: &'a [f32],
539    pub xs: Vec<f32>,
540    /// Weight layout. Was a bare `q1: bool`, which could only ever spell two
541    /// of the four and silently sent everything else back to the CPU — the
542    /// GDN projections of a q4t/q4tp model never reached the device at all.
543    pub layout: BatchLayout,
544}
545
546/// Which kernel a batched matvec needs. q8 carries row scales in a side
547/// buffer; the rest embed them in the payload and differ in stride.
548#[derive(Clone, Copy, PartialEq, Eq, Debug)]
549pub enum BatchLayout {
550    Q8,
551    Q1,
552    Q4t,
553    Q4tp,
554}
555
556#[derive(Clone, Copy, PartialEq, Eq)]
557enum Backend {
558    None,
559    #[cfg(target_os = "macos")]
560    Metal,
561    #[cfg(feature = "gpu")]
562    Wgpu,
563}
564
565fn backend() -> Backend {
566    #[cfg(feature = "gpu")]
567    if crate::gpu_wgpu::selected() {
568        return if crate::gpu_wgpu::enabled() {
569            Backend::Wgpu
570        } else {
571            Backend::None
572        };
573    }
574    #[cfg(target_os = "macos")]
575    if crate::gpu_metal::enabled() {
576        return Backend::Metal;
577    }
578    Backend::None
579}
580
581/// GPU enabled and initialized on the selected backend?
582/// Whether THIS build can bring a GPU up on THIS device: a compiled-in
583/// backend plus a live adapter. The mobile FFI exposes it so an app can
584/// tell "GPU off" from "GPU impossible" (a CPU-only .so ships no
585/// backend at all). Cached after the first call.
586pub fn backend_available() -> bool {
587    #[cfg(target_os = "macos")]
588    {
589        // The Metal path is always compiled on macOS.
590        true
591    }
592    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
593    {
594        static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
595        *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
596    }
597    #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
598    {
599        false
600    }
601}
602
603pub fn enabled() -> bool {
604    backend() != Backend::None
605}
606
607/// Default-on condition for the wgpu whole-token graph: the wgpu
608/// backend on a DISCRETE adapter. NOT plain `enabled()` (macOS/Metal
609/// must not pay a per-token layer scan for a graph its backend
610/// refuses), and NOT integrated adapters: the graph's ~300 barriered
611/// dispatches per token are cheap on desktop immediate-mode GPUs but
612/// tiled mobile GPUs (Adreno/Mali) drain the pipeline at every barrier
613/// — field report: 0.2 tok/s on-graph vs 15 tok/s on the CPU. On
614/// integrated adapters the per-op probe path arbitrates each op class
615/// against the CPU instead; CMF_GPU_WGPU_GRAPH=1 still forces the
616/// graph anywhere.
617/// Is the wgpu backend active at all (any adapter)? Eligibility gate
618/// for the whole-token graph — whether it actually RUNS is decided by
619/// `wgpu_graph_default` (trusted on discrete) or the generation race.
620pub fn wgpu_active() -> bool {
621    #[cfg(feature = "gpu")]
622    {
623        matches!(backend(), Backend::Wgpu)
624    }
625    #[cfg(not(feature = "gpu"))]
626    {
627        false
628    }
629}
630
631/// Which GPU this thread's engine calls address. Multi-card hosts hold
632/// one wgpu context PER card (weights, KV mirrors and scratch live
633/// inside a context, so per-device contexts give per-device caches for
634/// free); this thread-local says which one is current. Default: the
635/// process pin (CMF_GPU_ADAPTER) or 0 — so single-card runs behave
636/// exactly as they always have.
637pub fn default_device() -> usize {
638    static D: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
639    *D.get_or_init(|| {
640        std::env::var("CMF_GPU_ADAPTER")
641            .ok()
642            .and_then(|v| v.trim().parse::<usize>().ok())
643            .unwrap_or(0)
644    })
645}
646
647thread_local! {
648    static CUR_DEV: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
649}
650
651/// The device this thread is pinned to.
652pub fn current_device() -> usize {
653    CUR_DEV.with(|c| c.get()).unwrap_or_else(default_device)
654}
655
656/// Pin this thread to a device. Server slots call it once per request;
657/// the worker pool propagates it into its threads, so a dispatch begun
658/// on card 1 does not finish on card 0.
659pub fn set_current_device(i: usize) {
660    CUR_DEV.with(|c| c.set(Some(i)));
661}
662
663/// Run `f` with this thread pinned to `dev`, restoring the previous pin.
664pub fn with_device<R>(dev: usize, f: impl FnOnce() -> R) -> R {
665    let prev = CUR_DEV.with(|c| c.replace(Some(dev)));
666    let r = f();
667    CUR_DEV.with(|c| c.set(prev));
668    r
669}
670
671/// How many GPUs this process can address (wgpu adapter count; 1 on
672/// Metal, 0 without a backend).
673pub fn device_count() -> usize {
674    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
675    {
676        return crate::gpu_wgpu::adapter_count();
677    }
678    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
679    {
680        usize::from(backend_available())
681    }
682}
683
684/// Weight budget of the current GPU in bytes; 0 when there is none and
685/// u64::MAX on unified memory (where the OS pages shared RAM and the
686/// question "does the model fit the card" has no separate answer).
687pub fn vram_budget() -> u64 {
688    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
689    {
690        return crate::gpu_wgpu::device_vram_budget();
691    }
692    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
693    {
694        if backend_available() { u64::MAX } else { 0 }
695    }
696}
697
698/// Device weight bytes uploaded so far (wgpu; 0 on other backends).
699/// Steady-state windows must show a ZERO delta — growth mid-benchmark
700/// means eviction/re-upload and disqualifies the number.
701pub fn upload_bytes() -> u64 {
702    #[cfg(feature = "gpu")]
703    {
704        return crate::gpu_wgpu::UPLOAD_BYTES.load(std::sync::atomic::Ordering::Relaxed);
705    }
706    #[cfg(not(feature = "gpu"))]
707    0
708}
709
710pub fn wgpu_graph_default() -> bool {
711    #[cfg(feature = "gpu")]
712    {
713        // Discrete cards always; Apple-silicon UMA on macOS too — desktop
714        // -class GPUs where the graph measured ~2x the CPU on the Qwen3.6
715        // family (M4: 13.3 tok/s against 7.3). Phone-class UMA (Android/
716        // iOS builds) keeps the per-op probe path: tiled mobile GPUs have
717        // turned the ~300-dispatch graph into seconds per token.
718        matches!(backend(), Backend::Wgpu)
719            && (crate::gpu_wgpu::discrete_active()
720                || (cfg!(target_os = "macos") && crate::gpu_wgpu::adapter_up()))
721    }
722    #[cfg(not(feature = "gpu"))]
723    {
724        false
725    }
726}
727
728/// q8_row/q8_2f matvec, rows [row0, row0+rows). `xs` — prescaled by the θ-field.
729#[allow(clippy::too_many_arguments, unused_variables)]
730pub fn q8_matvec_range(
731    model: &Arc<CmfModel>,
732    idx: usize,
733    row0: usize,
734    row_scale: &[f32],
735    xs: &[f32],
736    rows: usize,
737    cols: usize,
738    out: &mut [f32],
739) -> bool {
740    match backend() {
741        #[cfg(target_os = "macos")]
742        Backend::Metal => {
743            crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
744        }
745        #[cfg(feature = "gpu")]
746        Backend::Wgpu => {
747            crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
748        }
749        Backend::None => false,
750    }
751}
752
753/// GEMM of a prefill batch: `pre` — prescaled inputs row-major [b, cols],
754/// out — row-major [b, rows].
755#[allow(clippy::too_many_arguments, unused_variables)]
756pub fn q8_matmat(
757    model: &Arc<CmfModel>,
758    idx: usize,
759    row_scale: &[f32],
760    pre: &[f32],
761    b: usize,
762    rows: usize,
763    cols: usize,
764    out: &mut [f32],
765) -> bool {
766    match backend() {
767        #[cfg(target_os = "macos")]
768        Backend::Metal => {
769            crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
770        }
771        #[cfg(feature = "gpu")]
772        Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
773        Backend::None => false,
774    }
775}
776
777/// q1 matvec: raw f32 activations, tile-embedded scales. Metal only
778/// for now (wgpu q1 WGSL is queued); false = CPU fallback.
779#[allow(unused_variables)]
780pub fn q1_matvec(
781    model: &Arc<CmfModel>,
782    idx: usize,
783    xs: &[f32],
784    rows: usize,
785    cols: usize,
786    out: &mut [f32],
787) -> bool {
788    match backend() {
789        #[cfg(target_os = "macos")]
790        Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
791        #[cfg(feature = "gpu")]
792        Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
793        Backend::None => false,
794    }
795}
796
797/// Whole attention sub-block on the wgpu token graph (drop-in for
798/// `qwen_attention`): normed hidden in, O-projection out, resident device
799/// K/V mirror. false = refusal / not the wgpu backend → CPU path.
800#[allow(clippy::too_many_arguments)]
801pub fn attn_dropin(
802    model: &Arc<CmfModel>,
803    kv_id: u64,
804    layer: usize,
805    normed: &[f32],
806    wq_idx: usize,
807    wk_idx: usize,
808    wv_idx: usize,
809    wo_idx: usize,
810    q_norm: Option<&[f32]>,
811    k_norm: Option<&[f32]>,
812    invf: &[f32],
813    nh: usize,
814    nkv: usize,
815    hd: usize,
816    rd: usize,
817    hidden: usize,
818    pos: usize,
819    cap: usize,
820    gemma: bool,
821    eps: f32,
822    cpu_k: &[Vec<f32>],
823    cpu_v: &[Vec<f32>],
824    out: &mut [f32],
825) -> bool {
826    match backend() {
827        #[cfg(feature = "gpu")]
828        Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
829            model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm, invf, nh,
830            nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
831        ),
832        #[allow(unused_variables)]
833        _ => false,
834    }
835}
836
837/// One weight in the whole-token graph: tensor idx + a codec tag (0=q8_row,
838/// 1=q1, 2=q4_tiled, 3=q1t, 4=f32) + per-row scales (q8_row only) + the raw f32
839/// data (kind 4 only — small unquantized projections like GDN in_proj_a/b).
840pub struct GraphW<'a> {
841    pub idx: usize,
842    pub kind: u8,
843    pub row_scale: &'a [f32],
844    pub data: &'a [f32],
845}
846
847/// A layer's token-mixing op: standard attention or a GDN (linear-attention)
848/// block. The surrounding norms + SwiGLU FFN are common to both.
849pub enum GraphAttn<'a> {
850    Full {
851        wq: GraphW<'a>,
852        wk: GraphW<'a>,
853        wv: GraphW<'a>,
854        wo: GraphW<'a>,
855        q_norm: Option<&'a [f32]>,
856        k_norm: Option<&'a [f32]>,
857        /// (bq, bk, bv) attention biases (Qwen2). None ⇒ no bias.
858        bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
859        /// Qwen3.5 gated attention: wq emits 2·nh·hd (q||gate per head), the
860        /// attention output is scaled by sigmoid(gate) before the O projection.
861        output_gate: bool,
862        cpu_k: &'a [Vec<f32>],
863        cpu_v: &'a [Vec<f32>],
864    },
865    Gdn {
866        qkv: GraphW<'a>,
867        z: GraphW<'a>,
868        a: GraphW<'a>,
869        b: GraphW<'a>,
870        out: GraphW<'a>,
871        conv1d: &'a [f32],
872        a_log: &'a [f32],
873        dt_bias: &'a [f32],
874        norm: &'a [f32],
875        nv: usize,
876        nk: usize,
877        dk: usize,
878        dv: usize,
879        kk: usize,
880        /// CPU recurrent state `[ring (kk-1)·cdim | S nv·dk·dv]` — seeds the
881        /// device mirror when prefill ran on the host (o1 collection, CPU
882        /// fallback): a zero-initialized device state at decode is exactly
883        /// the "coherent but contextless" garble.
884        cpu_state: &'a [f32],
885    },
886}
887
888/// Per-layer weights for the whole-token wgpu graph.
889pub struct GraphLayer<'a> {
890    pub input_norm: &'a [f32],
891    pub attn: GraphAttn<'a>,
892    pub post_norm: &'a [f32],
893    pub ffn: GraphFfn<'a>,
894}
895
896/// The FFN of one graph layer: a dense SwiGLU trio, or a routed MoE —
897/// router + top-k selection + all selected experts run ON DEVICE (the
898/// routing decision depends on the resident hidden state, so a CPU
899/// round-trip per layer would forfeit the one-submit design).
900pub enum GraphFfn<'a> {
901    Dense {
902        gate: GraphW<'a>,
903        up: GraphW<'a>,
904        down: GraphW<'a>,
905    },
906    Moe {
907        /// Router logits weight (f32, kind 4) `[n_exp, hidden]`.
908        router: GraphW<'a>,
909        /// Shared-expert sigmoid gate (f32) `[1, hidden]`.
910        shared_gate: GraphW<'a>,
911        /// Per-expert q4_tiled directory indices `(gate, up, down)`;
912        /// the SHARED expert rides as the LAST entry — the select
913        /// kernel pins it with the sigmoid weight.
914        experts: Vec<(usize, usize, usize)>,
915        /// Routed experts (shared excluded).
916        n_exp: usize,
917        top_k: usize,
918        inter: usize,
919        norm_topk: bool,
920        /// Expert weight layout, uniform across the layer: `false` =
921        /// q4_tiled (18 B tiles, inline f16 scale), `true` = q4tp
922        /// (16 B nibbles + a per-row ladder plane). The two differ only
923        /// in where the scale comes from, so they share every kernel
924        /// but the weight-staging block.
925        q4tp: bool,
926        /// `true` = the gate/up experts are `q2tp` (2-bit plane) while
927        /// `down` stays q4tp — the mixed profile a 2-bit-class checkpoint
928        /// converts into. Only meaningful with `q4tp: true`.
929        gu_q2: bool,
930    },
931}
932
933/// Whole-token decode graph on wgpu: the entire layer stack in ONE submit,
934/// hidden resident, one readback. Updates `h` in place. false = refusal.
935/// `loop_norm_at`: virtual layer indices after which `final_norm` is applied
936/// (Looped Transformer mid-stack norm). Empty for standard models.
937#[allow(clippy::too_many_arguments)]
938pub fn forward_token_graph(
939    model: &Arc<CmfModel>,
940    kv_id: u64,
941    layers: &[GraphLayer],
942    // Per-layer sealed o1 (Nystrom) state; Some = replace this layer's
943    // exact attention with the O(1) kernels. wgpu only.
944    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
945    o1_epoch: u64,
946    invf: &[f32],
947    h: &mut [f32],
948    nh: usize,
949    nkv: usize,
950    hd: usize,
951    rd: usize,
952    hidden: usize,
953    inter: usize,
954    position: usize,
955    cap: usize,
956    gemma: bool,
957    eps: f32,
958    lm_head: Option<(&GraphW, usize)>,
959    final_norm: &[f32],
960    logits: &mut Vec<f32>,
961    loop_norm_at: &[usize],
962    steps: usize,
963    embed: Option<(&GraphW, usize, f32)>,
964    ids_out: Option<&mut Vec<u32>>,
965    // How many leading layers the graph ran (see the wgpu twin) — smaller
966    // than layers.len() when the expert budget ended the device prefix.
967    layers_run: Option<&mut usize>,
968    // Absolute index of layers[0] in the model — the KV/state mirrors key
969    // on it, so a layer SPAN (network split segment) shares mirrors with
970    // a full-stack run instead of colliding at slot 0.
971    layer_base: usize,
972) -> bool {
973    match backend() {
974        #[cfg(feature = "gpu")]
975        Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
976            model,
977            kv_id,
978            layers,
979            o1,
980            o1_epoch,
981            invf,
982            h,
983            nh,
984            nkv,
985            hd,
986            rd,
987            hidden,
988            inter,
989            position,
990            cap,
991            gemma,
992            eps,
993            lm_head,
994            final_norm,
995            logits,
996            loop_norm_at,
997            steps,
998            embed,
999            ids_out,
1000            layers_run,
1001            layer_base,
1002        ),
1003        #[allow(unused_variables)]
1004        _ => {
1005            let _ = (lm_head, final_norm, logits, loop_norm_at, layers_run, layer_base);
1006            false
1007        }
1008    }
1009}
1010
1011/// Speculative-verify tail for the batched graph: fold final-norm + lm_head
1012/// over every batch position and read all k logit rows back; the batch also
1013/// snapshots the GDN state per position for `gdn_spec_restore`.
1014pub struct SpecTail<'a> {
1015    pub lm: GraphW<'a>,
1016    pub lm_rows: usize,
1017    pub final_norm: &'a [f32],
1018    pub logits_out: &'a mut Vec<f32>,
1019}
1020
1021/// Batched prefill: k contiguous positions through the whole graph in one submit
1022/// (projections/FFN as GEMMs, attention/GDN looped over scratch). `h` is
1023/// [k·hidden] in/out; `positions` len k. wgpu only.
1024#[allow(clippy::too_many_arguments)]
1025pub fn forward_batch_graph(
1026    model: &Arc<CmfModel>,
1027    kv_id: u64,
1028    layers: &[GraphLayer],
1029    invf: &[f32],
1030    h: &mut [f32],
1031    nh: usize,
1032    nkv: usize,
1033    hd: usize,
1034    rd: usize,
1035    hidden: usize,
1036    inter: usize,
1037    positions: &[usize],
1038    cap: usize,
1039    gemma: bool,
1040    eps: f32,
1041    k: usize,
1042    spec: Option<SpecTail<'_>>,
1043) -> bool {
1044    match backend() {
1045        #[cfg(feature = "gpu")]
1046        Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph(
1047            model, kv_id, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma,
1048            eps, k, spec,
1049        ),
1050        #[allow(unreachable_patterns)]
1051        _ => {
1052            let _ = spec;
1053            false
1054        }
1055    }
1056}
1057
1058/// After a partial speculative acceptance: restore every GDN layer's device
1059/// state to the snapshot after batch position `slot`. wgpu only.
1060pub fn gdn_spec_restore(kv_id: u64, slot: usize) -> bool {
1061    #[cfg(feature = "gpu")]
1062    if backend() == Backend::Wgpu {
1063        return crate::gpu_wgpu::gdn_spec_restore(kv_id, slot);
1064    }
1065    #[allow(unreachable_code)]
1066    {
1067        let _ = (kv_id, slot);
1068        false
1069    }
1070}
1071
1072/// Drop the wgpu token graph's device K/V mirror for a pipeline.
1073pub fn graph_kv_reset(_kv_id: u64) {
1074    #[cfg(feature = "gpu")]
1075    if backend() == Backend::Wgpu {
1076        crate::gpu_wgpu::kv_mirror_reset(_kv_id);
1077    }
1078}
1079
1080/// Ternary (q1t) BASE matvec on the GPU — fills `out` with the base dot; the
1081/// caller adds the sparse overlay on the CPU. Metal only for now (wgpu q1t not
1082/// yet written → CPU fallback).
1083pub fn q1t_matvec(
1084    model: &Arc<CmfModel>,
1085    idx: usize,
1086    xs: &[f32],
1087    rows: usize,
1088    cols: usize,
1089    out: &mut [f32],
1090) -> bool {
1091    match backend() {
1092        #[cfg(target_os = "macos")]
1093        Backend::Metal => {
1094            if metal_q1t_enabled() {
1095                crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
1096            } else {
1097                false
1098            }
1099        }
1100        #[cfg(feature = "gpu")]
1101        Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
1102        Backend::None => false,
1103    }
1104}
1105
1106/// q4_block matvec on the GPU — wgpu only (Metal drives q4_block through the
1107/// whole-token graph, not a standalone matvec).
1108#[allow(unused_variables)]
1109pub fn q4b_matvec(
1110    model: &Arc<CmfModel>,
1111    idx: usize,
1112    xs: &[f32],
1113    rows: usize,
1114    cols: usize,
1115    out: &mut [f32],
1116) -> bool {
1117    match backend() {
1118        #[cfg(target_os = "macos")]
1119        Backend::Metal => false,
1120        #[cfg(feature = "gpu")]
1121        Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
1122        Backend::None => false,
1123    }
1124}
1125
1126/// q1t batched GEMM (prefill) — base + overlay on-device (Metal simdgroup or
1127/// wgpu register-blocked).
1128pub fn q1t_matmat(
1129    model: &Arc<CmfModel>,
1130    idx: usize,
1131    xs: &[f32],
1132    b: usize,
1133    rows: usize,
1134    cols: usize,
1135    out: &mut [f32],
1136) -> bool {
1137    match backend() {
1138        #[cfg(target_os = "macos")]
1139        // Batched prefill and single-token decode are both enabled. On the
1140        // real 14.8B Q1T model prefill PPL was within 0.3% of CPU (7.942 vs
1141        // 7.966), and the alignment-safe decode kernel reached 3.52e-6 max_rel.
1142        Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
1143        #[cfg(feature = "gpu")]
1144        Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
1145        Backend::None => false,
1146    }
1147}
1148
1149/// Native Metal Q1T switch. Enabled by default after the byte-packed Q1T
1150/// fields were changed to alignment-safe loads; keep an explicit emergency
1151/// fallback for device/driver diagnostics.
1152#[cfg(target_os = "macos")]
1153pub(crate) fn metal_q1t_enabled() -> bool {
1154    std::env::var("CMF_METAL_Q1T")
1155        .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
1156        .unwrap_or(true)
1157}
1158
1159/// Batched q1 GEMM (prefill). wgpu only — Metal has its own block path.
1160pub fn q1_matmat(
1161    model: &Arc<CmfModel>,
1162    idx: usize,
1163    xs: &[f32],
1164    b: usize,
1165    rows: usize,
1166    cols: usize,
1167    out: &mut [f32],
1168) -> bool {
1169    match backend() {
1170        #[cfg(feature = "gpu")]
1171        Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
1172        #[allow(unused_variables)]
1173        _ => false,
1174    }
1175}
1176
1177/// Contention kill for the wide imagegen GEMM/FFN paths: one grossly
1178/// slow op under a work-proportional budget (fair-device ops are
1179/// ≤~100 ms even at 1024px) means another process owns the device —
1180/// verdicts are per-process, so CPU for the rest of this one.
1181static MM_KILL: AtomicBool = AtomicBool::new(false);
1182pub(crate) fn mm_killed() -> bool {
1183    MM_KILL.load(Ordering::Relaxed)
1184}
1185pub(crate) fn mm_kill() {
1186    MM_KILL.store(true, Ordering::Relaxed);
1187}
1188
1189/// Fused DiT SwiGLU FFN on the device: g=X·W1ᵀ, u=X·W3ᵀ, silu(g)·u,
1190/// Causal chunk attention on the device: `b` queries against `s0 + b`
1191/// cached keys. wgpu only — Metal's chunk graph keeps attention inside
1192/// the resident block and never calls out.
1193#[allow(unused_variables, clippy::too_many_arguments)]
1194pub fn chunk_attend(
1195    q: &[f32],
1196    k: &[&[f32]],
1197    v: &[&[f32]],
1198    b: usize,
1199    s0: usize,
1200    nh: usize,
1201    nkv: usize,
1202    hd: usize,
1203    scale: f32,
1204    out: &mut [f32],
1205) -> bool {
1206    match backend() {
1207        #[cfg(feature = "gpu")]
1208        Backend::Wgpu => crate::gpu_wgpu::chunk_attend(q, k, v, b, s0, nh, nkv, hd, scale, out),
1209        #[allow(unreachable_patterns)]
1210        _ => false,
1211    }
1212}
1213
1214/// Fused QKV projection: one upload of the normed chunk, three GEMMs,
1215/// one readback of Q|K|V back to back. Metal has no twin yet — its
1216/// chunk graph keeps the whole layer resident and never surfaces QKV.
1217#[allow(unused_variables, clippy::too_many_arguments)]
1218pub fn q4t_qkv(
1219    model: &Arc<CmfModel>,
1220    wq: usize,
1221    wk: usize,
1222    wv: usize,
1223    xs: &[f32],
1224    b: usize,
1225    cols: usize,
1226    rq: usize,
1227    rk: usize,
1228    rv: usize,
1229    out: &mut [f32],
1230) -> bool {
1231    match backend() {
1232        #[cfg(feature = "gpu")]
1233        Backend::Wgpu => crate::gpu_wgpu::q4t_qkv(model, wq, wk, wv, xs, b, cols, rq, rk, rv, out),
1234        #[allow(unreachable_patterns)]
1235        _ => false,
1236    }
1237}
1238
1239/// y=·W2ᵀ — one command buffer, only X and Y cross the CPU boundary.
1240#[allow(unused_variables, clippy::too_many_arguments)]
1241/// SwiGLU FFN with a row-packed [gate|up] fc1 (MiniMax-H3's DiT), run
1242/// end to end on the device. wgpu only: Metal keeps the host loop until
1243/// its own packed kernel exists.
1244#[allow(clippy::too_many_arguments, unused_variables)]
1245pub fn q4tp_ffn_packed(
1246    model: &Arc<CmfModel>,
1247    w1: usize,
1248    w2: usize,
1249    xs: &[f32],
1250    b: usize,
1251    hidden: usize,
1252    inter: usize,
1253    bias: Option<&[f32]>,
1254    out: &mut [f32],
1255) -> bool {
1256    match backend() {
1257        #[cfg(feature = "gpu")]
1258        Backend::Wgpu => {
1259            crate::gpu_wgpu::q4tp_ffn_packed(model, w1, w2, xs, b, hidden, inter, bias, out)
1260        }
1261        #[allow(unreachable_patterns)]
1262        _ => false,
1263    }
1264}
1265
1266pub fn q4tp_ffn(
1267    model: &Arc<CmfModel>,
1268    w1: usize,
1269    w3: usize,
1270    w2: usize,
1271    xs: &[f32],
1272    b: usize,
1273    hidden: usize,
1274    inter: usize,
1275    out: &mut [f32],
1276) -> bool {
1277    match backend() {
1278        #[cfg(target_os = "macos")]
1279        Backend::Metal => crate::gpu_metal::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1280        #[cfg(feature = "gpu")]
1281        Backend::Wgpu => crate::gpu_wgpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1282        #[allow(unreachable_patterns)]
1283        _ => false,
1284    }
1285}
1286
1287pub fn q4t_ffn(
1288    model: &Arc<CmfModel>,
1289    w1: usize,
1290    w3: usize,
1291    w2: usize,
1292    xs: &[f32],
1293    b: usize,
1294    hidden: usize,
1295    inter: usize,
1296    out: &mut [f32],
1297) -> bool {
1298    match backend() {
1299        #[cfg(target_os = "macos")]
1300        Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1301        #[cfg(feature = "gpu")]
1302        Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
1303        #[allow(unreachable_patterns)]
1304        _ => false,
1305    }
1306}
1307
1308/// One whole modulated DiT block for `dit_block`: geometry, norm
1309/// weights, AdaLN scale/gate vectors (gates pre-tanh'd), a per-token
1310/// f32 RoPE cos/sin table, and the directory indices of the seven
1311/// q4t projections. `x` is in-out `[n, hidden]`.
1312pub struct DitBlockArgs<'a> {
1313    pub n: usize,
1314    pub hidden: usize,
1315    pub inter: usize,
1316    pub nh: usize,
1317    pub nkv: usize,
1318    pub hd: usize,
1319    pub eps: f32,
1320    pub rope_cos: &'a [f32],
1321    pub rope_sin: &'a [f32],
1322    pub norm1: &'a [f32],
1323    pub norm2: &'a [f32],
1324    pub ffn_norm1: &'a [f32],
1325    pub ffn_norm2: &'a [f32],
1326    pub norm_q: &'a [f32],
1327    pub norm_k: &'a [f32],
1328    pub s_msa: &'a [f32],
1329    pub gate_msa: &'a [f32],
1330    pub s_mlp: &'a [f32],
1331    pub gate_mlp: &'a [f32],
1332    pub wq: usize,
1333    pub wk: usize,
1334    pub wv: usize,
1335    pub wo: usize,
1336    pub w1: usize,
1337    pub w3: usize,
1338    pub w2: usize,
1339    /// The projections' layout: q4tp (ladder scales) vs plain q4_tiled.
1340    /// The recommended Lumina file is q4tp, and a backend that only
1341    /// knows q4t must decline rather than decode with the wrong reader.
1342    pub q4tp: bool,
1343    /// The hidden state is already on the device from the previous block,
1344    /// so `x` need not be uploaded.
1345    pub resident_in: bool,
1346    /// Leave the result on the device instead of reading it back. The DiT
1347    /// loop does not touch `x` between blocks, so 27 of every 28 readbacks
1348    /// were moving 19 MB across PCIe and stalling on it for nothing.
1349    pub resident_out: bool,
1350}
1351
1352/// Can the selected backend keep the DiT's hidden state on the device
1353/// between blocks? Only the wgpu whole-block path; the Metal entry takes
1354/// and returns host memory every call.
1355pub fn dit_chain_supported() -> bool {
1356    #[cfg(feature = "gpu")]
1357    {
1358        return matches!(backend(), Backend::Wgpu) && fused_dit_block_available();
1359    }
1360    #[allow(unreachable_code)]
1361    false
1362}
1363
1364/// Pull the resident hidden state back to the host. For the caller that
1365/// chained blocks and then hit one the device declined.
1366pub fn dit_state_fetch(_x: &mut [f32]) -> bool {
1367    #[cfg(feature = "gpu")]
1368    {
1369        if matches!(backend(), Backend::Wgpu) {
1370            return crate::gpu_wgpu::dit_state_fetch(_x);
1371        }
1372    }
1373    false
1374}
1375
1376/// One whole modulated DiT block on the device — norms, qkv, RoPE,
1377/// attention, residuals and the SwiGLU FFN in a single command
1378/// buffer; only `x` crosses the CPU boundary (in and out).
1379#[allow(unused_variables)]
1380/// The DiT's three projections in one submission (wgpu only; the
1381/// Metal path fuses the whole block instead). False = the caller keeps
1382/// its three separate calls.
1383#[allow(unused_variables, clippy::too_many_arguments)]
1384pub fn dit_qkv(
1385    model: &Arc<CmfModel>,
1386    wq: usize,
1387    wk: usize,
1388    wv: usize,
1389    xs: &[f32],
1390    b: usize,
1391    hidden: usize,
1392    qrows: usize,
1393    kvrows: usize,
1394    q_out: &mut [f32],
1395    k_out: &mut [f32],
1396    v_out: &mut [f32],
1397) -> bool {
1398    match backend() {
1399        #[cfg(feature = "gpu")]
1400        Backend::Wgpu => crate::gpu_wgpu::q4tp_qkv(
1401            model, wq, wk, wv, xs, b, hidden, qrows, kvrows, q_out, k_out, v_out,
1402        ),
1403        #[allow(unreachable_patterns)]
1404        _ => false,
1405    }
1406}
1407
1408/// Is a FUSED whole-block device path on offer? The batched-CFG shape
1409/// (two sequences in one tall batch) and the fused block (one sequence,
1410/// one command buffer) are alternatives, and the caller picks.
1411pub fn fused_dit_block_available() -> bool {
1412    #[cfg(target_os = "macos")]
1413    {
1414        matches!(backend(), Backend::Metal) && fused_block_trusted()
1415    }
1416    #[cfg(not(target_os = "macos"))]
1417    {
1418        false
1419    }
1420}
1421
1422pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
1423    dit_block_seg(model, a, &[a.n], x)
1424}
1425
1426/// The same block over a CONCATENATION of independent sequences:
1427/// attention per segment, everything position-wise batched. wgpu only —
1428/// the Metal path takes the single-sequence entry above.
1429pub fn dit_block_seg(
1430    model: &Arc<CmfModel>,
1431    a: &DitBlockArgs,
1432    segs: &[usize],
1433    x: &mut [f32],
1434) -> bool {
1435    match backend() {
1436        #[cfg(target_os = "macos")]
1437        Backend::Metal if segs.len() <= 1 => crate::gpu_metal::dit_block(model, a, x),
1438        // The wgpu whole-block path. What it buys is host round trips —
1439        // six a block become one — so it defaults ON where those cost
1440        // real time (a discrete card across PCIe) and OFF on unified
1441        // memory, where the per-op path shares the same pages and the
1442        // fusion measured slightly slower on an M4. `CMF_DIT_FUSED=1`
1443        // forces it anywhere, `=0` forbids it.
1444        #[cfg(feature = "gpu")]
1445        Backend::Wgpu
1446            if match std::env::var("CMF_DIT_FUSED").ok().as_deref() {
1447                Some("0") => false,
1448                Some(_) => true,
1449                None => crate::gpu_wgpu::discrete_active(),
1450            } =>
1451        {
1452            crate::gpu_wgpu::dit_block_seg(model, a, segs, x)
1453        }
1454        #[allow(unreachable_patterns)]
1455        _ => false,
1456    }
1457}
1458
1459/// One VAE resnet block for `vae_resnet`: norm/conv weights and the
1460/// channel/shape geometry. `shortcut` is the 1×1 projection (w, b, k)
1461/// when in/out channels differ.
1462pub struct VaeResnetArgs<'a> {
1463    pub groups: usize,
1464    pub ic: usize,
1465    pub oc: usize,
1466    pub h: usize,
1467    pub w: usize,
1468    pub n1w: &'a [f32],
1469    pub n1b: &'a [f32],
1470    pub c1w: &'a [f32],
1471    pub c1b: &'a [f32],
1472    pub c1k: usize,
1473    pub n2w: &'a [f32],
1474    pub n2b: &'a [f32],
1475    pub c2w: &'a [f32],
1476    pub c2b: &'a [f32],
1477    pub c2k: usize,
1478    pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
1479}
1480
1481/// One whole VAE resnet block on the device (norm+silu → conv ×2 →
1482/// shortcut → add, one command buffer).
1483#[allow(unused_variables)]
1484pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
1485    match backend() {
1486        #[cfg(target_os = "macos")]
1487        Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
1488        _ => false,
1489    }
1490}
1491
1492/// Nearest-2× upsample fused with the following conv — the small
1493/// pre-upsample image is what crosses the CPU boundary.
1494#[allow(unused_variables, clippy::too_many_arguments)]
1495pub fn vae_upsample_conv(
1496    w: &[f32],
1497    bias: &[f32],
1498    x: &[f32],
1499    ic: usize,
1500    oc: usize,
1501    h: usize,
1502    w_img: usize,
1503    k: usize,
1504    out: &mut [f32],
1505) -> bool {
1506    match backend() {
1507        #[cfg(target_os = "macos")]
1508        Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
1509        #[cfg(feature = "gpu")]
1510        Backend::Wgpu => {
1511            crate::gpu_wgpu::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out)
1512        }
1513        #[allow(unreachable_patterns)]
1514        _ => false,
1515    }
1516}
1517
1518/// VAE conv2d on the device (implicit GEMM — the CPU path pays for a
1519/// multi-GB im2col matrix at high resolutions).
1520#[allow(unused_variables, clippy::too_many_arguments)]
1521pub fn vae_conv2d(
1522    w: &[f32],
1523    bias: &[f32],
1524    x: &[f32],
1525    ic: usize,
1526    oc: usize,
1527    h: usize,
1528    w_img: usize,
1529    k: usize,
1530    out: &mut [f32],
1531) -> bool {
1532    match backend() {
1533        #[cfg(target_os = "macos")]
1534        Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
1535        #[cfg(feature = "gpu")]
1536        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
1537        #[allow(unreachable_patterns)]
1538        _ => false,
1539    }
1540}
1541
1542/// DiT full bidirectional attention on the device (all heads:
1543/// scores GEMM → row softmax → P·V → panel unstack, one command
1544/// buffer). Head-major inputs; out is [n, nh·hd].
1545#[allow(unused_variables, clippy::too_many_arguments)]
1546/// Attention from an interleaved qkv panel, splitting into head-major
1547/// planes ON the device. wgpu only; `false` elsewhere so the caller
1548/// keeps its host repack.
1549#[allow(unused_variables)]
1550#[allow(clippy::too_many_arguments)]
1551/// qkv projection + attention with the panel never leaving the card.
1552/// wgpu only; `false` elsewhere and the caller keeps its host chain.
1553#[allow(clippy::too_many_arguments, unused_variables)]
1554pub fn dit_qkv_attention(
1555    model: &Arc<CmfModel>,
1556    qkv_idx: usize,
1557    xn: &[f32],
1558    n: usize,
1559    hidden: usize,
1560    nh: usize,
1561    hd: usize,
1562    scale: f32,
1563    nr: (&[f32], &[f32], &[f32], f32),
1564    out: &mut [f32],
1565) -> bool {
1566    match backend() {
1567        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1568        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attention(
1569            model, qkv_idx, xn, n, hidden, nh, hd, scale, nr, out,
1570        ),
1571        #[allow(unreachable_patterns)]
1572        _ => false,
1573    }
1574}
1575
1576/// The whole attention half of a DiT block on the card: qkv GEMM,
1577/// attention, output projection. Only `proj` comes home.
1578#[allow(clippy::too_many_arguments)]
1579pub fn dit_qkv_attn_out(
1580    model: &Arc<CmfModel>,
1581    qkv_idx: usize,
1582    out_idx: usize,
1583    xn: &[f32],
1584    n: usize,
1585    hidden: usize,
1586    nh: usize,
1587    hd: usize,
1588    scale: f32,
1589    nr: (&[f32], &[f32], &[f32], f32),
1590    proj: &mut [f32],
1591) -> bool {
1592    match backend() {
1593        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1594        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attn_out(
1595            model, qkv_idx, out_idx, xn, n, hidden, nh, hd, scale, nr, proj,
1596        ),
1597        #[allow(unreachable_patterns)]
1598        _ => false,
1599    }
1600}
1601
1602/// The VAE decoder's attention half on the card. Only `proj` returns.
1603#[allow(clippy::too_many_arguments)]
1604pub fn vae_qkv_attn_out(
1605    model: &Arc<CmfModel>,
1606    qkv_idx: usize,
1607    out_idx: usize,
1608    xn: &[f32],
1609    n: usize,
1610    dim: usize,
1611    nh: usize,
1612    hd: usize,
1613    scale: f32,
1614    angles: &[f32],
1615    eps: f32,
1616    qkv_bias: &[f32],
1617    proj: &mut [f32],
1618) -> bool {
1619    match backend() {
1620        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1621        Backend::Wgpu => crate::gpu_wgpu::vae_qkv_attn_out(
1622            model, qkv_idx, out_idx, xn, n, dim, nh, hd, scale, angles, eps, qkv_bias, proj,
1623        ),
1624        #[allow(unreachable_patterns)]
1625        _ => false,
1626    }
1627}
1628
1629#[allow(clippy::too_many_arguments)]
1630pub fn vae_attention_packed(
1631    qkv: &[f32],
1632    nh: usize,
1633    n: usize,
1634    hd: usize,
1635    scale: f32,
1636    angles: &[f32],
1637    eps: f32,
1638    out: &mut [f32],
1639) -> bool {
1640    vae_attention_packed_layout(qkv, nh, n, hd, scale, angles, eps, out, 1)
1641}
1642
1643#[allow(clippy::too_many_arguments)]
1644pub fn vae_attention_packed_layout(
1645    qkv: &[f32],
1646    nh: usize,
1647    n: usize,
1648    hd: usize,
1649    scale: f32,
1650    angles: &[f32],
1651    eps: f32,
1652    out: &mut [f32],
1653    layout: u32,
1654) -> bool {
1655    match backend() {
1656        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1657        Backend::Wgpu => crate::gpu_wgpu::vae_attention_packed_layout(
1658            qkv, nh, n, hd, scale, angles, eps, out, layout,
1659        ),
1660        #[allow(unreachable_patterns)]
1661        _ => false,
1662    }
1663}
1664
1665#[allow(clippy::too_many_arguments)]
1666pub fn dit_split_only(
1667    qkv: &[f32],
1668    nh: usize,
1669    n: usize,
1670    hd: usize,
1671    layout: u32,
1672    norm: Option<(&[f32], f32)>,
1673    out_q: &mut [f32],
1674) -> bool {
1675    match backend() {
1676        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1677        Backend::Wgpu => crate::gpu_wgpu::dit_split_only(qkv, nh, n, hd, layout, norm, out_q),
1678        #[allow(unreachable_patterns)]
1679        _ => false,
1680    }
1681}
1682
1683/// The backend's f32 NT GEMM: `y[n×m] = x[n×k] · wᵀ[m×k]`. Tensor
1684/// cores where the card has them. Refuses under `CMF_BAKE_GPU=0` or
1685/// strict f32, and for jobs below n·k·m = 4M, where the round trip
1686/// costs more than the arithmetic saves.
1687pub fn gemm_nt_f32(x: &[f32], w: &[f32], y: &mut [f32], n: usize, k: usize, m: usize) -> bool {
1688    match backend() {
1689        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1690        Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32(x, w, y, n, k, m),
1691        #[allow(unreachable_patterns)]
1692        _ => false,
1693    }
1694}
1695
1696/// The convolution as a GEMM on the matrix units. `false` = refused.
1697#[allow(clippy::too_many_arguments)]
1698pub fn vae_conv2d_coop(
1699    w: &[f32],
1700    bias: Option<&[f32]>,
1701    x: &[f32],
1702    ic: usize,
1703    oc: usize,
1704    h: usize,
1705    wi: usize,
1706    k: usize,
1707    out: &mut [f32],
1708) -> bool {
1709    match backend() {
1710        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1711        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d_coop(w, bias, x, ic, oc, h, wi, k, out),
1712        #[allow(unreachable_patterns)]
1713        _ => false,
1714    }
1715}
1716
1717pub fn dit_attention_packed(
1718    qkv: &[f32],
1719    nh: usize,
1720    n: usize,
1721    hd: usize,
1722    scale: f32,
1723    // (rope angles, q norm weights, k norm weights, eps) when the device
1724    // should apply qk-norm and RoPE itself; None when the host already did.
1725    nr: Option<(&[f32], &[f32], &[f32], f32)>,
1726    out: &mut [f32],
1727) -> bool {
1728    match backend() {
1729        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1730        Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed(qkv, nh, n, hd, scale, nr, out),
1731        #[allow(unreachable_patterns)]
1732        _ => false,
1733    }
1734}
1735
1736pub fn dit_attention(
1737    qh: &[f32],
1738    kh: &[f32],
1739    vh: &[f32],
1740    nh: usize,
1741    nkv: usize,
1742    n: usize,
1743    hd: usize,
1744    scale: f32,
1745    out: &mut [f32],
1746) -> bool {
1747    match backend() {
1748        #[cfg(target_os = "macos")]
1749        Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
1750        #[cfg(feature = "gpu")]
1751        Backend::Wgpu => crate::gpu_wgpu::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
1752        #[allow(unreachable_patterns)]
1753        _ => false,
1754    }
1755}
1756
1757/// Batched q4t GEMM on the device (imagegen DiT prefill shapes).
1758/// Metal: q4t_mul_mm decodes the mmap-resident tiles inside the
1759/// GEMM's K loop. wgpu (Vulkan/DX12 → NVIDIA/AMD/Intel/Adreno/Mali):
1760/// the register-blocked WGSL twin, weights cached in VRAM.
1761#[allow(unused_variables)]
1762pub fn q4tp_matmat(
1763    model: &Arc<CmfModel>,
1764    idx: usize,
1765    xs: &[f32],
1766    b: usize,
1767    rows: usize,
1768    cols: usize,
1769    out: &mut [f32],
1770) -> bool {
1771    match backend() {
1772        #[cfg(target_os = "macos")]
1773        Backend::Metal => crate::gpu_metal::q4tp_matmat(model, idx, xs, b, rows, cols, out),
1774        #[cfg(feature = "gpu")]
1775        Backend::Wgpu => crate::gpu_wgpu::q4tp_matmat(model, idx, xs, b, rows, cols, out),
1776        #[allow(unreachable_patterns)]
1777        _ => false,
1778    }
1779}
1780
1781/// The same over a two-bit weight plane. Metal has no q2tp kernel, so
1782/// there it declines and the host takes it.
1783pub fn q2tp_matmat(
1784    model: &Arc<CmfModel>,
1785    idx: usize,
1786    xs: &[f32],
1787    b: usize,
1788    rows: usize,
1789    cols: usize,
1790    out: &mut [f32],
1791) -> bool {
1792    match backend() {
1793        #[cfg(feature = "gpu")]
1794        Backend::Wgpu => crate::gpu_wgpu::q2tp_matmat(model, idx, xs, b, rows, cols, out),
1795        #[allow(unreachable_patterns)]
1796        _ => false,
1797    }
1798}
1799
1800/// Single-token q4tp matvec on the device — the lm_head class. Through the
1801/// DEDICATED matvec kernel: the batched GEMM at b=1 measured 11.73 ms
1802/// against the host's 9.51 on the release head, so the route that was
1803/// supposed to save eleven milliseconds a token lost its own probe instead.
1804pub fn q4tp_matvec(
1805    model: &Arc<CmfModel>,
1806    idx: usize,
1807    xs: &[f32],
1808    rows: usize,
1809    cols: usize,
1810    out: &mut [f32],
1811) -> bool {
1812    match backend() {
1813        #[cfg(target_os = "macos")]
1814        Backend::Metal => crate::gpu_metal::q4tp_matvec_for_test(model, idx, xs, rows, cols, out),
1815        #[cfg(feature = "gpu")]
1816        Backend::Wgpu => crate::gpu_wgpu::q4tp_matvec(model, idx, xs, rows, cols, out),
1817        #[allow(unreachable_patterns)]
1818        _ => false,
1819    }
1820}
1821
1822/// Single-token q4_tiled matvec on the device — the lm_head class (a
1823/// q4t checkpoint's head is its biggest host matvec, exactly like the
1824/// q4tp twin above). wgpu holds q4t_mv pipelines only inside the graph
1825/// encoder — the standalone arm stays an honest refusal until a
1826/// discrete-GPU q4t model reaches the bench.
1827pub fn q4t_matvec(
1828    model: &Arc<CmfModel>,
1829    idx: usize,
1830    xs: &[f32],
1831    rows: usize,
1832    cols: usize,
1833    out: &mut [f32],
1834) -> bool {
1835    match backend() {
1836        #[cfg(target_os = "macos")]
1837        Backend::Metal => crate::gpu_metal::q4t_matvec_for_test(model, idx, xs, rows, cols, out),
1838        #[allow(unreachable_patterns)]
1839        _ => false,
1840    }
1841}
1842
1843pub fn q4t_matmat(
1844    model: &Arc<CmfModel>,
1845    idx: usize,
1846    xs: &[f32],
1847    b: usize,
1848    rows: usize,
1849    cols: usize,
1850    out: &mut [f32],
1851) -> bool {
1852    match backend() {
1853        #[cfg(target_os = "macos")]
1854        Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
1855        #[cfg(feature = "gpu")]
1856        Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
1857        #[allow(unreachable_patterns)]
1858        _ => false,
1859    }
1860}
1861
1862/// Whole-block token-graph types re-exported from the Metal backend.
1863#[cfg(target_os = "macos")]
1864pub use crate::gpu_metal::{
1865    AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GpuMoe, GraphDims, MetalFfn,
1866    TokenGraph, kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp,
1867};
1868
1869/// A BLOCK of consecutive q1 GDN layers in one submission (Metal only).
1870#[cfg(target_os = "macos")]
1871pub fn gdn_block(
1872    model: &Arc<CmfModel>,
1873    layers: &[GdnGpuLayer],
1874    states: &mut [&mut [f32]],
1875    cfg: &GdnGpuCfg,
1876    h: &mut [f32],
1877) -> bool {
1878    match backend() {
1879        Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
1880        _ => false,
1881    }
1882}
1883
1884/// A layer's MoE-FFN in one submission (amortizing the dispatch cost).
1885#[allow(unused_variables)]
1886pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
1887    match backend() {
1888        #[cfg(target_os = "macos")]
1889        Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
1890        #[cfg(feature = "gpu")]
1891        Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
1892        Backend::None => false,
1893    }
1894}
1895
1896/// Independent matvecs of one input in a single submission (GDN projections).
1897#[allow(unused_variables)]
1898pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
1899    match backend() {
1900        #[cfg(target_os = "macos")]
1901        Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
1902        #[cfg(feature = "gpu")]
1903        Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
1904        Backend::None => false,
1905    }
1906}
1907
1908// ── Whole-token wgpu graph race (generation granularity) ─────────────
1909// On integrated/mobile adapters the graph is neither trusted nor banned
1910// a priori — it RACES the normal path: generations alternate arms (the
1911// normal path first — known-good UX — then the graph), per-token wall
1912// times accumulate per arm, and once both arms have enough steady
1913// samples the faster one wins for the process. Arm switches happen ONLY
1914// at generation boundaries (`kv_cache.clear()` resets state), so the
1915// device KV mirror and the CPU cache never diverge mid-sequence. The
1916// single exception is the first-token bail: the very first decode token
1917// of a graph generation may be discarded and recomputed on the CPU
1918// path (the prompt KV is CPU-owned at that point, so this is safe) —
1919// a tiled mobile GPU that drains its pipeline at every barrier turns
1920// the ~300-dispatch graph into seconds per token (field report: 0.2
1921// tok/s vs 15 on the CPU), and one token is all it takes to see that.
1922static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); // 0 racing, 1 graph won, 2 normal won
1923static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
1924static GRAPH_RACE_ARM_GRAPH: AtomicU8 = AtomicU8::new(0); // this generation's arm
1925static GRAPH_RACE_TOK: AtomicU32 = AtomicU32::new(0); // token index within the generation
1926static GRAPH_NS: [AtomicU64; 2] = [AtomicU64::new(0), AtomicU64::new(0)]; // [normal, graph]
1927static GRAPH_N: [AtomicU32; 2] = [AtomicU32::new(0), AtomicU32::new(0)];
1928
1929/// Steady per-token samples per arm before the race decides.
1930const GRAPH_RACE_SAMPLES: u32 = 4;
1931
1932/// Called at every generation start (fresh KV). Applies a pending
1933/// verdict and picks this generation's arm while racing.
1934pub fn graph_race_begin_generation() {
1935    GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
1936    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1937        return;
1938    }
1939    let (gn, cn) = (
1940        GRAPH_N[1].load(Ordering::Relaxed),
1941        GRAPH_N[0].load(Ordering::Relaxed),
1942    );
1943    if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
1944        let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
1945        let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1946        let verdict = if g_avg < c_avg { 1 } else { 2 };
1947        GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
1948        tracing::info!(
1949            "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
1950            g_avg as f64 / 1e6,
1951            c_avg as f64 / 1e6,
1952            if verdict == 1 { "graph" } else { "normal path" }
1953        );
1954        return;
1955    }
1956    let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
1957    GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
1958}
1959
1960/// Should this decode token try the graph? `trusted` (discrete adapter,
1961/// explicit env, or a GDN hybrid whose state lives on the device) skips
1962/// the race entirely.
1963pub fn graph_race_use_graph(trusted: bool) -> bool {
1964    if trusted {
1965        return true;
1966    }
1967    match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
1968        1 => true,
1969        2 => false,
1970        _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
1971    }
1972}
1973
1974/// First decode token of a racing graph generation: hopeless already?
1975/// (>4x the normal path's per-token average AND over a second.) Settles
1976/// the race immediately; the caller discards the graph result and
1977/// recomputes this token on the normal path.
1978pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
1979    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
1980        return false;
1981    }
1982    let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
1983    let cn = GRAPH_N[0].load(Ordering::Relaxed);
1984    if !first || cn == 0 {
1985        return false;
1986    }
1987    let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
1988    let ns = dur.as_nanos() as u64;
1989    if ns > 1_000_000_000 && ns > 4 * c_avg {
1990        GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
1991        tracing::info!(
1992            "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
1993            ns as f64 / 1e6,
1994            c_avg as f64 / 1e6
1995        );
1996        return true;
1997    }
1998    false
1999}
2000
2001/// Record one decode-token wall time for the racing arm. The first
2002/// token of each generation is discarded (KV-mirror upload / cold
2003/// caches on the graph arm; cold mmap on the normal arm).
2004pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
2005    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
2006        return;
2007    }
2008    let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
2009    if tok == 0 {
2010        return;
2011    }
2012    let i = used_graph as usize;
2013    GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
2014    GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
2015}
2016
2017/// Bounded-cost content fingerprint for the backends' pointer-keyed device
2018/// caches: FNV over the whole slice up to 4 KiB, over 64 spread 64-byte
2019/// windows (plus the length) above. An address-keyed hit must also prove
2020/// the bytes are still the ones it uploaded — the allocator reuses heap
2021/// and mmap addresses freely, so a reloaded model or a re-dequantized
2022/// layer lands where the old bytes were — and sampling keeps that proof at
2023/// ~a microsecond even for a 126 MB matrix. Real replacements (another
2024/// model's tensor, an Adam-updated master) differ densely, so a 4 KiB
2025/// spread cannot miss them.
2026pub(crate) fn fp_bytes(data: &[u8]) -> u64 {
2027    #[inline]
2028    fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
2029        let (chunks, tail) = bytes.split_at(bytes.len() & !7);
2030        for c in chunks.chunks_exact(8) {
2031            h ^= u64::from_le_bytes(c.try_into().unwrap());
2032            h = h.wrapping_mul(0x100_0000_01b3);
2033        }
2034        for &b in tail {
2035            h ^= b as u64;
2036            h = h.wrapping_mul(0x100_0000_01b3);
2037        }
2038        h
2039    }
2040    let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (data.len() as u64);
2041    if data.len() <= 4096 {
2042        return fnv(h, data);
2043    }
2044    let step = (data.len() - 64) / 63;
2045    for i in 0..64 {
2046        h = fnv(h, &data[i * step..i * step + 64]);
2047    }
2048    h
2049}
2050
2051/// `fp_bytes` over an f32 slice without a bytemuck dependency (the Metal
2052/// backend builds with no GPU feature flags).
2053pub(crate) fn fp_f32(data: &[f32]) -> u64 {
2054    let bytes =
2055        unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
2056    fp_bytes(bytes)
2057}
2058
2059#[cfg(test)]
2060mod fp_tests {
2061    use super::fp_bytes;
2062
2063    /// The pointer-keyed caches survive on `fp_bytes` telling two different
2064    /// tensors apart at a reused address. Its sampling must therefore see a
2065    /// change ANYWHERE — head, tail, and the stretches between windows are
2066    /// the places a cheaper hash would go blind.
2067    #[test]
2068    fn fp_bytes_sees_a_change_anywhere_in_a_sampled_slice() {
2069        let n = 1 << 20; // 1 MiB — far above the 4 KiB full-hash threshold
2070        let base: Vec<u8> = (0..n).map(|i| (i * 31 + 7) as u8).collect();
2071        let h0 = fp_bytes(&base);
2072        assert_eq!(h0, fp_bytes(&base), "fingerprint must be deterministic");
2073        // A DENSE change (every requantized/redequantized tensor is one)
2074        // must flip the fingerprint no matter how the windows fall.
2075        let mut dense = base.clone();
2076        for b in dense.iter_mut() {
2077            *b = b.wrapping_add(1);
2078        }
2079        assert_ne!(h0, fp_bytes(&dense), "a fully different tensor slipped through");
2080        // Length participates: the same prefix at a shorter length is a
2081        // different key AND a different fingerprint.
2082        assert_ne!(h0, fp_bytes(&base[..n - 64]));
2083        // Below the threshold the hash is exact: a single flipped byte in
2084        // a norm-sized vector must be seen.
2085        let mut small = vec![3u8; 4096];
2086        let hs = fp_bytes(&small);
2087        small[2048] ^= 1;
2088        assert_ne!(hs, fp_bytes(&small), "full hash missed a one-byte change");
2089        // And the sampled windows land within bounds on awkward sizes.
2090        for n in [4097usize, 5000, 64 * 64, 1 << 16] {
2091            let v = vec![9u8; n];
2092            let _ = fp_bytes(&v); // must not panic on window math
2093        }
2094    }
2095}
2096
2097/// Hand the card back after a bake: drop its resident weights, planes and
2098/// pools so the ordinary engine (the runtime gate, a serve that follows)
2099/// starts from a clean budget. No-op off the wgpu backend.
2100pub fn bake_release() {
2101    #[cfg(feature = "gpu")]
2102    crate::gpu_wgpu::bake_release();
2103}
2104
2105/// Strict-f32 for the bake's GEMMs (phase A mask training): the mask
2106/// selects neurons by a gradient signal, and f16 operand rounding on
2107/// that signal closes the wrong ones. No-op off the wgpu backend.
2108pub fn bake_precision_strict(on: bool) {
2109    #[cfg(feature = "gpu")]
2110    crate::gpu_wgpu::bake_precision_strict(on);
2111    #[cfg(not(feature = "gpu"))]
2112    let _ = on;
2113}