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
18/// Packed, f32-only Embryo model consumed by the resident Vulkan graph.
19///
20/// This is deliberately a separate representation from `CmfModel`: the
21/// Embryo graph owns one contiguous device copy and never dereferences the
22/// mmap or asks the ordinary per-op residency path for a tensor.  `meta`
23/// contains byte-free element offsets into `weights`; `UINT_MAX` marks an
24/// absent optional matrix.  The builder lives in `pipeline.rs`, where the
25/// architecture-specific weight types are visible.
26pub struct EmbryoGraphModel {
27    pub id: u64,
28    pub hidden: usize,
29    pub intermediate: usize,
30    pub vocab: usize,
31    pub layers: usize,
32    pub phase_heads: usize,
33    pub nphase: usize,
34    pub phase_dv: usize,
35    pub anchor_q_heads: usize,
36    pub anchor_kv_heads: usize,
37    pub anchor_head_dim: usize,
38    pub rotary_dim: usize,
39    pub max_seq: usize,
40    pub cluster_count: usize,
41    pub cluster_size: usize,
42    pub phase_state_len: usize,
43    pub state_stride: usize,
44    pub kv_stride: usize,
45    pub norm_gemma: bool,
46    pub phase_mass: f32,
47    pub weights: Vec<f32>,
48    pub meta: Vec<u32>,
49    pub lm_head: Vec<f32>,
50    pub clusters: Vec<f32>,
51    pub final_norm: Vec<f32>,
52    pub inv_freq: Vec<f32>,
53    /// Natively bounded anchors (`swa_sink_v1`): every anchor layer owns a
54    /// ring of `anchor_window` raw keys/values instead of a `max_seq` KV
55    /// plane, so `kv_stride` is the ring size, `kv_layers` counts the
56    /// anchors, and no position cap applies (the operator has none).
57    pub bounded: bool,
58    /// Layers that own a KV/ring slot of `kv_stride` f32 (all layers for
59    /// the legacy full anchor — offsets are `layer·kv_stride` — or the
60    /// bounded anchors only).
61    pub kv_layers: usize,
62    /// Recurrent (phase or GDN) layers — the state buffer holds one
63    /// `state_stride` per such layer; anchors own none.
64    pub state_layers: usize,
65    pub anchor_window: usize,
66    pub anchor_sink: usize,
67    /// vmf_phase mixer layers (kind 0/1) in the stack.
68    pub phase_layers: usize,
69    /// GatedDeltaNet mixer layers (kind 4) in the stack; their geometry
70    /// is `gdn_heads` (nv), `gdn_k_heads` (nk), `gdn_dk`, `gdn_dv`,
71    /// `gdn_kk` (conv taps) — header words 24..29 of `meta`.
72    pub gdn_layers: usize,
73    pub gdn_heads: usize,
74    pub gdn_k_heads: usize,
75    pub gdn_dk: usize,
76    pub gdn_dv: usize,
77    pub gdn_kk: usize,
78}
79
80impl EmbryoGraphModel {
81    /// Fused q/k/v projection width of the GDN mixer (`2·nk·dk + nv·dv`).
82    pub fn gdn_c_dim(&self) -> usize {
83        2 * self.gdn_k_heads * self.gdn_dk + self.gdn_heads * self.gdn_dv
84    }
85}
86
87/// Words in the resident graph's `meta` header before the per-layer
88/// records (64 words each). Shared by the host packer and the encoder.
89pub const EMBRYO_META_HEADER: usize = 32;
90
91/// Positions one chunked-prefill submit of the resident Embryo graph
92/// covers (the shader's `CMAX`; the chunk window of the scratch buffer is
93/// sized by it).
94pub const EMBRYO_CHUNK_MAX: usize = 64;
95
96/// `n` contiguous prompt positions (`rows` = n × hidden embeddings from
97/// `position`) through the resident Embryo graph in one submit; logits of
98/// the last position. False = refused before any device work (the caller
99/// keeps the per-position path), or the sequence went cold.
100pub fn forward_embryo_graph_chunk(
101    model: &Arc<EmbryoGraphModel>,
102    kv_id: u64,
103    rows: &[f32],
104    position: usize,
105    n: usize,
106    logits: &mut Vec<f32>,
107) -> bool {
108    #[cfg(feature = "gpu")]
109    if backend() == Backend::Wgpu {
110        return crate::gpu_wgpu::forward_embryo_graph_chunk(model, kv_id, rows, position, n, logits);
111    }
112    let _ = (model, kv_id, rows, position, n, logits);
113    false
114}
115
116/// Bytes the resident Embryo graph holds on the device for a sequence:
117/// `(recurrent state, anchor KV/ring)`. None when the sequence has no
118/// device image (host path, or not started yet). This is the measured
119/// long-context claim of the resident path — a bounded genome reports
120/// the same two numbers at every context depth.
121pub fn embryo_device_state_bytes(kv_id: u64) -> Option<(u64, u64)> {
122    #[cfg(feature = "gpu")]
123    if backend() == Backend::Wgpu {
124        return crate::gpu_wgpu::embryo_device_state_bytes(kv_id);
125    }
126    let _ = kv_id;
127    None
128}
129
130/// Does this directory entry count as WEIGHT in the device placement
131/// heuristics (layer prefix, live-weight budget, `serve` replicas vs
132/// split)? On a genome file only the trunk does: skill tensors replace
133/// trunk tensors of the same shape inside their own lane, so counting
134/// them would give the backbone of F1 another GPU prefix than F0's
135/// (NF-7). Other files keep the historical whole-directory count.
136pub fn counts_as_placement_weight(model: &CmfModel, name: &str) -> bool {
137    model.header.genome.is_none() || cortiq_core::knowledge::is_trunk_tensor(name)
138}
139
140/// Weight bytes the placement heuristics budget for `model`: the trunk's
141/// payload on a genome file ([`counts_as_placement_weight`]), the whole
142/// mapped file otherwise (the historical measure).
143pub fn placement_weight_bytes(model: &CmfModel) -> u64 {
144    if model.header.genome.is_none() {
145        return model.primary_bytes().len() as u64;
146    }
147    model
148        .tensors
149        .iter()
150        .filter(|t| counts_as_placement_weight(model, &t.name))
151        .map(|t| t.nbytes)
152        .sum()
153}
154
155/// Next position of the resident Embryo sequence `kv_id` (`Some(n)` = the
156/// device holds positions `0..n` of it), `None` when the device holds no
157/// initialized image of that sequence — the host owns it (or none began).
158pub fn embryo_device_next_position(kv_id: u64) -> Option<usize> {
159    #[cfg(feature = "gpu")]
160    if backend() == Backend::Wgpu {
161        return crate::gpu_wgpu::embryo_device_next_position(kv_id);
162    }
163    let _ = kv_id;
164    None
165}
166
167thread_local! {
168    /// Index of the current forward layer (−1 = outside a numbered layer:
169    /// lm_head/embed — always allowed). The pipeline sets it before
170    /// each layer so that the GPU/CPU layer-split works.
171    static CUR_LAYER: Cell<i64> = const { Cell::new(-1) };
172    /// Inside `cpu_scope` every GPU gate reports disabled: the timed CPU
173    /// arm of a probe (and a class that lost its probe) must run PURE
174    /// CPU, or inner per-op hooks would re-enter the GPU and poison the
175    /// comparison.
176    static CPU_ONLY: Cell<bool> = const { Cell::new(false) };
177    /// "This op paid a one-off cost" (weight upload / first pipeline
178    /// build): backends set it, `probe_record` discards the sample so
179    /// only steady-state timings compete.
180    static PROBE_COLD: Cell<bool> = const { Cell::new(false) };
181}
182
183/// RAII form of `cpu_scope`, used when a device-prefix graph hands a whole
184/// remainder of the forward pass to the host. Without a guard around that
185/// tail, its ordinary per-op hooks re-entered the GPU and streamed the rest of
186/// an over-size model through the residency arena, defeating the prefix's VRAM
187/// bound at the driver-allocation level.
188pub struct CpuScopeGuard(bool);
189
190impl Drop for CpuScopeGuard {
191    fn drop(&mut self) {
192        CPU_ONLY.with(|c| c.set(self.0));
193    }
194}
195
196pub fn enter_cpu_scope() -> CpuScopeGuard {
197    let previous = CPU_ONLY.with(|c| c.replace(true));
198    CpuScopeGuard(previous)
199}
200
201/// Run `f` with the GPU gates off on this thread (pure-CPU arm).
202pub fn cpu_scope<R>(f: impl FnOnce() -> R) -> R {
203    let _restore = enter_cpu_scope();
204    f()
205}
206
207/// Capture CPU-only placement before dispatching whole operators to workers.
208/// `cpu_scope` is thread-local, while a MoE panel worker calls QTensor again;
209/// without inheritance, which expert happened to land on the caller changed
210/// its precision/backend from run to run.
211pub(crate) fn inherit_cpu_scope() -> impl Fn() -> Option<CpuScopeGuard> + Copy {
212    let on = CPU_ONLY.get();
213    move || on.then(enter_cpu_scope)
214}
215
216/// Backends: name the device once at init. The probe cache is keyed by
217/// it, because a verdict is a property of THIS silicon and nothing else.
218/// First writer wins: a process runs one backend, and on the rare host
219/// where two initialize, the one that came up first is the one in use.
220pub fn probe_set_device(label: &str) {
221    let _ = DEVICE_LABEL.set(label.to_string());
222}
223
224fn device_label() -> &'static str {
225    DEVICE_LABEL.get().map(String::as_str).unwrap_or("unknown")
226}
227
228static DEVICE_LABEL: std::sync::OnceLock<String> = std::sync::OnceLock::new();
229
230/// Somewhere this process may write small caches.
231///
232/// `std::env::temp_dir()` is NOT that place on Android: with no `TMPDIR`
233/// it answers `/tmp`, which does not exist in an app sandbox, and every
234/// write fails silently — measured, after the pipeline cache appeared to
235/// work in a shell (where `TMPDIR=/data/local/tmp`) and did nothing at
236/// all in the app. The loader points this at the model's own directory,
237/// which is somewhere the caller already writes.
238static CACHE_DIR: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
239
240/// Loader: name a directory this process can write to. First call wins.
241pub fn set_cache_dir(dir: std::path::PathBuf) {
242    let _ = CACHE_DIR.set(dir);
243}
244
245/// Same directory, for the backends.
246pub fn cache_dir_pub() -> std::path::PathBuf {
247    cache_dir()
248}
249
250fn cache_dir() -> std::path::PathBuf {
251    if let Some(d) = CACHE_DIR.get() {
252        return d.clone();
253    }
254    match std::env::var_os("TMPDIR") {
255        Some(t) => std::path::PathBuf::from(t),
256        None => std::env::temp_dir(),
257    }
258}
259
260/// Where decided verdicts are remembered between runs. `CMF_PROBE_CACHE`
261/// overrides the path; `0` disables the cache entirely.
262fn probe_cache_path() -> Option<std::path::PathBuf> {
263    match std::env::var("CMF_PROBE_CACHE") {
264        Ok(v) if v == "0" => None,
265        Ok(v) => Some(std::path::PathBuf::from(v)),
266        Err(_) => Some(cache_dir().join("cortiq-gpu-probe.tsv")),
267    }
268}
269
270/// One line per decided class: `version \t device \t class \t winner`.
271/// A different engine build or a different device simply does not match,
272/// so a stale file is inert rather than wrong.
273fn probe_cache_key_named(class: &str) -> String {
274    format!(
275        "{}\t{}\t{}",
276        env!("CARGO_PKG_VERSION"),
277        device_label(),
278        class
279    )
280}
281
282const CLASS_NAMES: [&str; 8] = [
283    "ffn",
284    "matvec",
285    "matmat",
286    "qkv-batch",
287    "matmat-wide",
288    "lm-head",
289    "gemm-nt",
290    // MoE has a different residency policy from a dense FFN. In
291    // particular, an out-of-core Metal MoE block can become viable after a
292    // dense `ffn` probe has (correctly) decided that ordinary per-op FFNs
293    // lose. Never let that remembered verdict suppress the separate path.
294    "moe",
295];
296
297/// Adopt every verdict this device already reached in an earlier run.
298///
299/// Probing is not cheap and it is not free of consequences: on a
300/// Snapdragon 778G the three deciding classes took **three minutes of
301/// wall clock** before the first token, every process, and in the phone
302/// app that was the whole first answer — 209.6 s for 25 tokens against
303/// 10.5 s on the CPU path. The verdict itself was the same every time.
304/// Paying to rediscover it is the defect; the answer is to write it down.
305fn probe_cache_load() {
306    static ONCE: std::sync::Once = std::sync::Once::new();
307    ONCE.call_once(|| {
308        let Some(path) = probe_cache_path() else {
309            return;
310        };
311        // Unit tests share this process and its default cache path; a
312        // verdict left by an earlier run would decide a class before the
313        // arbitration tests get to watch it alternate. Tests that mean to
314        // exercise the cache point `CMF_PROBE_CACHE` at their own file.
315        if cfg!(test) && std::env::var("CMF_PROBE_CACHE").is_err() {
316            return;
317        }
318        let Ok(text) = std::fs::read_to_string(&path) else {
319            return;
320        };
321        probe_cache_adopt(&text);
322    });
323}
324
325/// Apply verdicts from a cache file's text. Split out from the file
326/// reading so the adoption rule — including which lines must be IGNORED
327/// — is testable without a filesystem.
328fn probe_cache_adopt(text: &str) {
329    for line in text.lines() {
330        let Some((key, verdict)) = line.rsplit_once('\t') else {
331            continue;
332        };
333        let winner = match verdict.trim() {
334            "gpu" => 1u8,
335            "cpu" => 2u8,
336            _ => continue,
337        };
338        for (i, name) in CLASS_NAMES.iter().enumerate() {
339            if probe_cache_key_named(name) == key {
340                let _ = PROBES[i].state.compare_exchange(
341                    0,
342                    winner,
343                    Ordering::Relaxed,
344                    Ordering::Relaxed,
345                );
346                tracing::debug!("gpu probe [{name}]: remembered → {verdict}");
347            }
348        }
349    }
350}
351
352/// Remember a verdict for the next run. Best-effort: a read-only cache
353/// directory costs a re-probe, never a failure.
354fn probe_cache_store(c: OpClass, winner: u8) {
355    let Some(path) = probe_cache_path() else {
356        return;
357    };
358    let line = format!(
359        "{}\t{}\n",
360        probe_cache_key_named(CLASS_NAMES[c as usize]),
361        if winner == 1 { "gpu" } else { "cpu" }
362    );
363    use std::io::Write;
364    if let Ok(mut f) = std::fs::OpenOptions::new()
365        .create(true)
366        .append(true)
367        .open(&path)
368    {
369        let _ = f.write_all(line.as_bytes());
370    }
371}
372
373/// Backends: note a one-off cost (weight upload, buffer-cache fill) so
374/// the probe discards this sample.
375/// Every buffer creation anywhere bumps this; the graph's bind-group
376/// cache treats any cold event as total invalidation — a stale bind
377/// group is silent corruption, a cleared cache is one re-encoded token.
378pub fn cold_epoch() -> u64 {
379    COLD_EPOCH.load(std::sync::atomic::Ordering::Relaxed)
380}
381static COLD_EPOCH: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
382
383pub(crate) fn probe_note_cold() {
384    COLD_EPOCH.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
385    PROBE_COLD.with(|c| c.set(true));
386}
387
388/// Peek the cold flag without consuming it (`probe_record` consumes).
389/// Contention heuristics use this: a slow COLD op is a one-off build
390/// cost, not evidence the device is busy.
391pub(crate) fn probe_was_cold() -> bool {
392    PROBE_COLD.with(|c| c.get())
393}
394
395/// Pipeline: mark the current layer (or −1 outside layers) for layer-split.
396pub fn set_layer(l: i64) {
397    CUR_LAYER.with(|c| c.set(l));
398}
399
400/// The layer `set_layer` last marked on this thread (−1 outside layers).
401pub fn cur_layer() -> i64 {
402    CUR_LAYER.with(|c| c.get())
403}
404
405/// Capacity-derived layer prefix for per-op walks. The explicit
406/// `CMF_GPU_LAYERS` override is handled by the backend and takes precedence.
407pub fn automatic_layer_prefix(
408    model: &Arc<CmfModel>,
409    num_layers: usize,
410    physical_layers: usize,
411) -> Option<usize> {
412    match backend() {
413        #[cfg(feature = "gpu")]
414        Backend::Wgpu => {
415            crate::gpu_wgpu::automatic_layer_prefix(model, num_layers, physical_layers)
416        }
417        _ => None,
418    }
419}
420
421/// Parse `CMF_GPU_LAYERS` («0-19», «0,2,4», «0-9,30-39») once.
422/// None = no restriction (all layers on GPU). Garbage → also no restriction.
423fn layer_ranges() -> &'static Option<Vec<(i64, i64)>> {
424    static R: OnceLock<Option<Vec<(i64, i64)>>> = OnceLock::new();
425    R.get_or_init(|| {
426        let s = std::env::var("CMF_GPU_LAYERS").ok()?;
427        let mut v = Vec::new();
428        for part in s.split(',') {
429            let part = part.trim();
430            match part.split_once('-') {
431                Some((a, b)) => v.push((a.trim().parse().ok()?, b.trim().parse().ok()?)),
432                None => {
433                    let x: i64 = part.parse().ok()?;
434                    v.push((x, x));
435                }
436            }
437        }
438        Some(v)
439    })
440}
441
442fn layer_allowed() -> bool {
443    match layer_ranges() {
444        None => true,
445        Some(ranges) => {
446            let cur = CUR_LAYER.with(|c| c.get());
447            cur < 0 || ranges.iter().any(|(a, b)| cur >= *a && cur <= *b)
448        }
449    }
450}
451
452/// GPU allowed FOR THE CURRENT LAYER: backend is initialized AND the layer
453/// falls within `CMF_GPU_LAYERS` (GPU/CPU layer-split) AND we are not
454/// inside a `cpu_scope`. Op gates call this.
455pub fn enabled_here() -> bool {
456    !CPU_ONLY.with(|c| c.get()) && enabled() && layer_allowed()
457}
458
459/// Descriptor-aware q2tp Vulkan kernels are kept behind an explicit opt-in
460/// until the Prism full-graph/resident-weight path has a coherent generation
461/// gate.  `CMF_GPU=1` alone must not silently turn synchronous per-op
462/// readbacks into the default model path; callers and validation tests can
463/// request the measured kernels with `CMF_Q2TP_GPU=1`.
464pub fn q2tp_gpu_opt_in() -> bool {
465    std::env::var("CMF_Q2TP_GPU").as_deref() == Ok("1")
466}
467
468// ── Runtime GPU-vs-CPU probe ────────────────────────────────────────────
469// CMF_GPU=1 does not TRUST that the device wins — it MEASURES. For each
470// op class the first calls alternate arms: GPU timed vs pure-CPU timed
471// (under cpu_scope). Cold GPU calls (weight upload / cache fill) are
472// discarded; after PROBE_SAMPLES clean samples per arm the faster arm is
473// chosen for the rest of the process. Rationale: submit+poll latency
474// differs by an order of magnitude across driver stacks (Metal/PCIe
475// ~3-4 ms, Vulkan/4090 ~0.3 ms) — a static threshold cannot know whether
476// per-op offload pays off HERE. CMF_GPU_PROBE=0 → always trust the GPU.
477
478/// GPU-eligible op classes, each with an independent probe.
479#[derive(Clone, Copy)]
480pub enum OpClass {
481    /// Whole FFN chain in one submission (dense / MoE block).
482    Ffn = 0,
483    /// Large hybrid CPU∥GPU matvec (lm_head class).
484    Matvec = 1,
485    /// Prefill GEMM (matmat).
486    Matmat = 2,
487    /// Batched matvecs of one input (QKV).
488    Batch = 3,
489    /// Prefill GEMM at image-diffusion widths (b ≥ 128). Probed apart
490    /// from `Matmat`: one imagegen process runs BOTH populations
491    /// (prompt encode b≈40 where the GPU wins big, DiT b≥256 where
492    /// the CPU AMX arm is competitive) — a single shared verdict locks
493    /// the wrong arm for whichever population samples second.
494    MatmatWide = 4,
495    /// The lm_head itself, apart from the merely-large matvecs. Same
496    /// reasoning as `MatmatWide`, and DeepSeek-V4 is where it bit: its
497    /// attention projections are 37M weights and its head is 529M, so
498    /// the projections' verdict — CPU, honestly measured at 0.19 ms —
499    /// decided for a matvec fourteen times their size that took 11 ms
500    /// a token on the host.
501    MatvecHead = 5,
502    /// The blocked f32 GEMM (`fcd_ops::gemm_nt`): attention's QKᵀ and
503    /// AV, and the VAE decoders' projections. It used to take every job
504    /// over 4 M MACs on sight, with no CPU arm to lose to — which on
505    /// the MiniMax-H3 video decoder was three times SLOWER than the
506    /// host it displaced. Its population is per-head slices, nothing
507    /// like the weight GEMMs above, so it probes on its own.
508    GemmNt = 6,
509    /// A host-routed MoE block. Kept separate from [`Self::Ffn`] because
510    /// its selected-weight pager has different memory and latency costs.
511    Moe = 7,
512}
513
514/// Which probe a large matvec belongs to. The head is an order of
515/// magnitude bigger than anything else that reaches this gate, and the
516/// two populations do not have the same answer.
517pub fn matvec_class(rows: usize, cols: usize) -> OpClass {
518    if rows * cols >= 67_108_864 {
519        OpClass::MatvecHead
520    } else {
521        OpClass::Matvec
522    }
523}
524
525/// Probe verdict for one call.
526pub enum ProbeArm {
527    /// Run the GPU path (during probing: timed, recorded).
528    Gpu,
529    /// Probing: run the CPU path under `cpu_scope`, timed, recorded.
530    CpuTimed,
531    /// Decided: CPU won — run the CPU path (under `cpu_scope`).
532    Cpu,
533}
534
535/// Clean samples per arm before a class decides.
536const PROBE_SAMPLES: u32 = 6;
537
538/// Declines before a class gives the work to the host for good. High
539/// enough that a transient refusal — an unsealed state during prefill, a
540/// shape the kernel skips this once — cannot settle the question.
541const PROBE_DECLINE_LIMIT: u32 = 16;
542
543/// Device samples discarded before any count — see `Probe::gpu_burn`.
544const PROBE_WARMUP: u32 = 1;
545
546struct Probe {
547    /// 0 = probing, 1 = GPU won, 2 = CPU won.
548    state: AtomicU8,
549    flip: AtomicU32,
550    gpu_ns: AtomicU64,
551    gpu_n: AtomicU32,
552    /// Times the device arm was chosen and the device DECLINED.
553    ///
554    /// A decline carries no timing, so nothing is recorded — and a class
555    /// whose device path always refuses therefore never reaches a
556    /// verdict, alternates arms forever, and pays a failed device
557    /// attempt on half of every token's calls. Measured on an M4 with
558    /// LFM2.5-2.6B: `ffn` was still undecided after 9000 calls, and a
559    /// token cost 83.55 ms against 41.85 with the device off — twice the
560    /// price for work the host did anyway.
561    declines: AtomicU32,
562    /// GPU samples still to discard as warm-up.
563    ///
564    /// The cold flag catches buffer and weight uploads, but a compute
565    /// pipeline is compiled on first use and not every creation site
566    /// raises it — the wgpu path has 21 pipeline creations against 12
567    /// cold notes. One uncaught shader compile is enough to lose a
568    /// class for the whole process: `gemm-nt` on an A100 was recorded at
569    /// 117.01 ms against the host's 3.19 and sent to the CPU, which
570    /// parked a 27B bake on 2.6 cores with the card idle. The decision
571    /// already uses each arm's BEST sample, so discarding the first
572    /// GPU sample costs one extra round trip and removes the whole
573    /// class of first-call artefacts.
574    gpu_burn: AtomicU32,
575    cpu_ns: AtomicU64,
576    cpu_n: AtomicU32,
577    /// Best (minimum) sample per arm. The DECISION compares these:
578    /// means are poisoned by one-off cold costs the cold-flag cannot
579    /// see — e.g. the CPU arm's first mmap-cold expert matvec page
580    /// faults its weights in and reads 3× its steady state, which
581    /// locked the GPU arm on a 35B MoE at a 4× real-world loss. The
582    /// minimum is each arm's honest steady-state pace.
583    gpu_min: AtomicU64,
584    cpu_min: AtomicU64,
585}
586
587impl Probe {
588    const fn new() -> Self {
589        Self {
590            state: AtomicU8::new(0),
591            flip: AtomicU32::new(0),
592            gpu_ns: AtomicU64::new(0),
593            gpu_n: AtomicU32::new(0),
594            declines: AtomicU32::new(0),
595            gpu_burn: AtomicU32::new(PROBE_WARMUP),
596            cpu_ns: AtomicU64::new(0),
597            cpu_n: AtomicU32::new(0),
598            gpu_min: AtomicU64::new(u64::MAX),
599            cpu_min: AtomicU64::new(u64::MAX),
600        }
601    }
602}
603
604static PROBES: [Probe; 8] = [
605    Probe::new(),
606    Probe::new(),
607    Probe::new(),
608    Probe::new(),
609    Probe::new(),
610    Probe::new(),
611    Probe::new(),
612    Probe::new(),
613];
614
615/// A caller that knows its loop is long, uniform and warm can say so: the
616/// probe times ops in isolation and alternates arms to do it, which reads a
617/// sustained diffusion step as slower on the device than it is. Measured on
618/// an M4 at 672 video tokens: the probe picked the CPU at 1.25 ms against
619/// 0.88 ms per op, and the loop it picked for ran 23.9 s a step against the
620/// device's 19.7 s.
621static TRUST_GPU: AtomicBool = AtomicBool::new(false);
622
623/// Take the probe out of the loop until the guard drops.
624pub fn trust_gpu() -> GpuTrust {
625    let was = TRUST_GPU.swap(true, Ordering::Relaxed);
626    GpuTrust(was)
627}
628
629pub struct GpuTrust(bool);
630
631impl Drop for GpuTrust {
632    fn drop(&mut self) {
633        TRUST_GPU.store(self.0, Ordering::Relaxed);
634    }
635}
636
637fn probe_on_for(c: OpClass) -> bool {
638    // The trust is only for the *wide* class. A sustained diffusion step is
639    // where the probe reads a warm device as cold; the narrow batches inside
640    // the same loop — an audio stream of fifty-one tokens against the same
641    // weights — are small enough that submit latency can genuinely beat the
642    // arithmetic, and there the probe is right and should keep deciding.
643    if TRUST_GPU.load(Ordering::Relaxed) && matches!(c, OpClass::MatmatWide | OpClass::Ffn) {
644        return false;
645    }
646    probe_on()
647}
648
649/// Is the per-op GPU/CPU probe enabled (`CMF_GPU_PROBE`, default on)?
650/// The native Metal decode route never consults it — `q1_force` routes
651/// the token graph to the device outright — so it is reported, not used.
652pub fn probe_enabled() -> bool {
653    probe_on()
654}
655
656fn probe_on() -> bool {
657    static ON: OnceLock<bool> = OnceLock::new();
658    *ON.get_or_init(|| {
659        std::env::var("CMF_GPU_PROBE")
660            .map(|v| v != "0" && v != "off")
661            .unwrap_or(true)
662    })
663}
664
665/// q1 ops on the native Metal backend skip the probe entirely: the CPU
666/// q1 kernel is load-port-bound, the GPU one wins warm — and probe
667/// alternation itself cools the device between samples (measured: block
668/// times 5.8 ms warm vs 8.8 ms mixed). Other backends keep probing.
669pub fn q1_force() -> bool {
670    #[cfg(target_os = "macos")]
671    {
672        backend() == Backend::Metal
673    }
674    #[cfg(not(target_os = "macos"))]
675    {
676        false
677    }
678}
679
680/// Should a FUSED whole-block path trust the device instead of asking
681/// the per-op probe? True on native Metal and on discrete wgpu adapters.
682///
683/// The probe answers "is one wide matmat faster on the GPU", and for the
684/// DiT on Metal that is a coin flip — measured 2.62 ms GPU vs 2.56 ms
685/// CPU, a 2% spread that lands on either arm run to run. But the fused
686/// block's advantage is not per-op speed, it is that the hidden state,
687/// the packs and the attention panels never leave the device: end to end
688/// the whole-block path renders a 512² Lumina step in ~5.4 s against
689/// ~8.4 s when the probe happens to pick the CPU. Gating a fusion win on
690/// a per-op tie made every second render half-speed at random.
691///
692/// On a discrete card the verdict is never in doubt — an RTX 3090 against
693/// a 256-core EPYC measured 11.5 ms vs 31 ms per wide op, four runs out
694/// of four — so the probe's sampling phase is pure cost: it alone was 10%
695/// of a 512² render (74.3 s against 66.9 s with the probe off). Integrated
696/// and mobile adapters keep probing; there the submit latency is real and
697/// can genuinely lose.
698pub fn fused_block_trusted() -> bool {
699    #[cfg(target_os = "macos")]
700    if backend() == Backend::Metal {
701        return true;
702    }
703    wgpu_graph_default()
704}
705
706/// Which arm should this GPU-eligible call take? Consult AFTER the
707/// eligibility gates (`enabled_here` / `min_rows`) so only real
708/// candidates alternate.
709/// While a class is still probing, a call whose weights are NOT yet on
710/// the card should take the GPU arm anyway: the upload is work the next
711/// step needs regardless, and the sample it produces is discarded as
712/// cold — so handing that call to the CPU arm buys nothing and costs a
713/// host GEMM. Measured on a diffusion stack, where every layer is
714/// touched once per step and therefore EVERY first-step GPU sample is
715/// cold: one projection drew the CPU arm for the whole first step, 9.8 s
716/// against the 2.8 s it costs once the weights are warm.
717pub fn weight_is_resident(model: &Arc<CmfModel>, idx: usize) -> bool {
718    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
719    {
720        return crate::gpu_wgpu::weight_is_resident(model, idx);
721    }
722    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
723    {
724        let _ = (model, idx);
725        true
726    }
727}
728
729pub fn probe_arm_cold_prefers_gpu(c: OpClass, weights_resident: bool) -> ProbeArm {
730    if !weights_resident && probe_deciding(c) {
731        return ProbeArm::Gpu;
732    }
733    probe_arm(c)
734}
735
736pub fn probe_arm(c: OpClass) -> ProbeArm {
737    // Every arbitrated call starts with a clean cold flag: both the
738    // sample discard in `probe_record` and the contention kill-switch
739    // read it AFTER the op, so a stale note from a previous call on
740    // this thread must not leak in.
741    PROBE_COLD.with(|f| f.set(false));
742    if !probe_on_for(c) {
743        return ProbeArm::Gpu;
744    }
745    probe_cache_load();
746    let p = &PROBES[c as usize];
747    match p.state.load(Ordering::Relaxed) {
748        1 => ProbeArm::Gpu,
749        2 => ProbeArm::Cpu,
750        _ => {
751            if p.flip.fetch_add(1, Ordering::Relaxed) % 2 == 0 {
752                ProbeArm::Gpu
753            } else {
754                ProbeArm::CpuTimed
755            }
756        }
757    }
758}
759
760/// The device arm was chosen and the device refused the work, so there
761/// is no time to record. Callers that fall through to the host MUST say
762/// so here, or the class can never decide.
763pub fn probe_note_decline(c: OpClass) {
764    let p = &PROBES[c as usize];
765    if p.state.load(Ordering::Relaxed) != 0 {
766        return;
767    }
768    let n = p.declines.fetch_add(1, Ordering::Relaxed) + 1;
769    if n >= PROBE_DECLINE_LIMIT
770        && p.state
771            .compare_exchange(0, 2, Ordering::Relaxed, Ordering::Relaxed)
772            .is_ok()
773    {
774        tracing::info!(
775            "gpu probe [{}]: device declined {n} times → cpu",
776            CLASS_NAMES[c as usize]
777        );
778    }
779}
780
781/// Record a timed arm sample; on the `PROBE_SAMPLES`-th clean sample of
782/// BOTH arms the class decides for the rest of the process.
783pub fn probe_record(c: OpClass, gpu: bool, dur: std::time::Duration) {
784    probe_record_into(
785        &PROBES[c as usize],
786        CLASS_NAMES[c as usize],
787        Some(c),
788        gpu,
789        dur,
790    )
791}
792
793/// The body of `probe_record` over ONE probe, so the decision can be
794/// driven in a test without touching the process-wide array.
795fn probe_record_into(
796    p: &Probe,
797    class_name: &str,
798    cache: Option<OpClass>,
799    gpu: bool,
800    dur: std::time::Duration,
801) {
802    if p.state.load(Ordering::Relaxed) != 0 {
803        return;
804    }
805    if gpu && PROBE_COLD.with(|f| f.replace(false)) {
806        return; // one-off cost in this call — not a steady-state sample
807    }
808    if gpu {
809        // Load-then-store rather than fetch_sub: a blind decrement at
810        // zero wraps a u32 to its maximum and mutes the arm forever.
811        // A benign race here burns one extra sample, which is free.
812        let left = p.gpu_burn.load(Ordering::Relaxed);
813        if left > 0 {
814            p.gpu_burn.store(left - 1, Ordering::Relaxed);
815            return; // warm-up: the first device sample builds its pipeline
816        }
817    }
818    let ns = dur.as_nanos().min(u64::MAX as u128) as u64;
819    if gpu {
820        p.gpu_ns.fetch_add(ns, Ordering::Relaxed);
821        p.gpu_n.fetch_add(1, Ordering::Relaxed);
822        p.gpu_min.fetch_min(ns, Ordering::Relaxed);
823    } else {
824        p.cpu_ns.fetch_add(ns, Ordering::Relaxed);
825        p.cpu_n.fetch_add(1, Ordering::Relaxed);
826        p.cpu_min.fetch_min(ns, Ordering::Relaxed);
827    }
828    let (gn, cn) = (
829        p.gpu_n.load(Ordering::Relaxed),
830        p.cpu_n.load(Ordering::Relaxed),
831    );
832    if gn >= 2 && cn >= 2 {
833        // Decide on each arm's BEST sample — the steady-state pace.
834        // Means carry one-off cold costs (mmap page-in on the CPU arm)
835        // that the cold-flag machinery cannot see.
836        let g = p.gpu_min.load(Ordering::Relaxed) as f64;
837        let cp = p.cpu_min.load(Ordering::Relaxed) as f64;
838        // Early verdict on a ≥2× gap — no reason to keep feeding the
839        // losing arm; close races take the full sample count. It was 3×,
840        // and the cost of that half-octave was measured: a DiT whose
841        // wide GEMMs run 11.4 ms on the device against 32.2 on the host
842        // (2.8×) kept ALTERNATING through the whole diffusion stack, and
843        // because the alternation counter is shared per class in call
844        // order, one projection drew the CPU arm every single time — 9.9
845        // seconds a step on a kernel that needs 0.4. Both arms are
846        // compared on their BEST sample, so a 2× gap is not noise.
847        if (gn < PROBE_SAMPLES || cn < PROBE_SAMPLES) && g < cp * 2.0 && cp < g * 2.0 {
848            return;
849        }
850        let winner = if g <= cp { 1 } else { 2 };
851        if p.state
852            .compare_exchange(0, winner, Ordering::Relaxed, Ordering::Relaxed)
853            .is_ok()
854        {
855            tracing::info!(
856                "gpu probe [{}]: gpu {:.2} ms vs cpu {:.2} ms per op → {}",
857                class_name,
858                g / 1e6,
859                cp / 1e6,
860                if winner == 1 { "gpu" } else { "cpu" },
861            );
862            if let Some(c) = cache {
863                probe_cache_store(c, winner);
864            }
865        }
866    }
867}
868
869/// Is the class still collecting samples? (Call sites use this to route
870/// cold-weight calls away from the GPU arm during probing.)
871pub fn probe_deciding(c: OpClass) -> bool {
872    probe_on_for(c) && PROBES[c as usize].state.load(Ordering::Relaxed) == 0
873}
874
875/// Probing helper: true — tensor `idx`'s quant weights are ALREADY
876/// device-resident (a clean GPU sample is possible now); false — they
877/// were not (the upload starts within the VRAM budget, so a later call
878/// finds them warm) or the tensor cannot go to the GPU at all. Keeps the
879/// probe from billing a full cold dispatch+readback to a sample it will
880/// discard anyway. The verdict needs only a couple of warm tensors, so
881/// probe-driven uploads are capped — the losing-GPU machine should not
882/// pay for uploading the whole layer stack it will never use; if the GPU
883/// wins, the rest uploads lazily on demand, in the same first-touch order.
884#[allow(unused_variables)]
885pub fn q8_resident_or_upload(model: &Arc<CmfModel>, idx: usize) -> bool {
886    static PROBE_UPLOADS: AtomicU32 = AtomicU32::new(0);
887    let may_upload = PROBE_UPLOADS.load(Ordering::Relaxed) < 4;
888    let resident = match backend() {
889        #[cfg(target_os = "macos")]
890        Backend::Metal => crate::gpu_metal::q8_resident_or_upload(model, idx, may_upload),
891        #[cfg(feature = "gpu")]
892        Backend::Wgpu => crate::gpu_wgpu::q8_resident_or_upload(model, idx, may_upload),
893        Backend::None => false,
894    };
895    if !resident && may_upload {
896        PROBE_UPLOADS.fetch_add(1, Ordering::Relaxed);
897    }
898    resident
899}
900
901/// Test hook: reset all probes to the undecided state.
902#[cfg(test)]
903pub(crate) fn probe_reset() {
904    for p in &PROBES {
905        p.state.store(0, Ordering::Relaxed);
906        p.flip.store(0, Ordering::Relaxed);
907        p.gpu_ns.store(0, Ordering::Relaxed);
908        p.gpu_n.store(0, Ordering::Relaxed);
909        p.cpu_ns.store(0, Ordering::Relaxed);
910        p.cpu_n.store(0, Ordering::Relaxed);
911    }
912}
913
914/// The probe table is process-global by design, while these unit tests reset
915/// and seed selected entries to exercise arbitration. Keep only those tests
916/// out of each other's way; production callers still probe concurrently.
917#[cfg(test)]
918static PROBE_TEST_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
919
920#[cfg(test)]
921fn probe_test_guard() -> std::sync::MutexGuard<'static, ()> {
922    PROBE_TEST_LOCK
923        .lock()
924        .unwrap_or_else(std::sync::PoisonError::into_inner)
925}
926
927#[cfg(test)]
928mod probe_tests {
929    use super::*;
930    use std::time::Duration;
931
932    #[test]
933    fn cpu_only_whole_operator_dispatch_inherits_and_restores_scope() {
934        let pool = crate::pool::Pool::with_spin(3, 0);
935        cpu_scope(|| {
936            let inherit = inherit_cpu_scope();
937            pool.run_rows(64, &|_, _| {
938                let _guard = inherit();
939                assert!(CPU_ONLY.get());
940            });
941        });
942        pool.run_rows(64, &|_, _| assert!(!CPU_ONLY.get()));
943    }
944
945    // One test fn: PROBES is process-global and probe_reset touches all
946    // classes — parallel test threads would race.
947    #[test]
948    fn probe_alternates_discards_cold_and_decides() {
949        let _probe_guard = probe_test_guard();
950        probe_reset();
951        // Probing: arms alternate.
952        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
953        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::CpuTimed));
954
955        // A cold GPU sample (upload noted) must be discarded: feed a
956        // catastrophic cold sample, then clean fast-GPU samples — GPU
957        // wins only if the cold one did not count.
958        probe_note_cold();
959        probe_record(OpClass::Ffn, true, Duration::from_secs(1000));
960        for _ in 0..PROBE_SAMPLES {
961            probe_record(OpClass::Ffn, true, Duration::from_millis(1));
962            probe_record(OpClass::Ffn, false, Duration::from_millis(4));
963        }
964        assert!(matches!(probe_arm(OpClass::Ffn), ProbeArm::Gpu));
965
966        // The reverse: a class where the CPU arm is faster decides CPU.
967        for _ in 0..PROBE_SAMPLES {
968            probe_record(OpClass::Matmat, true, Duration::from_millis(4));
969            probe_record(OpClass::Matmat, false, Duration::from_millis(1));
970        }
971        assert!(matches!(probe_arm(OpClass::Matmat), ProbeArm::Cpu));
972
973        // cpu_scope: gates off inside, restored after.
974        cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
975        CPU_ONLY.with(|c| assert!(!c.get()));
976        cpu_scope(|| {
977            cpu_scope(|| CPU_ONLY.with(|c| assert!(c.get())));
978            CPU_ONLY.with(|c| assert!(c.get()));
979        });
980        let _ = std::panic::catch_unwind(|| cpu_scope(|| panic!("scope test")));
981        CPU_ONLY.with(|c| assert!(!c.get()));
982        probe_reset();
983    }
984
985    #[test]
986    fn a_remembered_verdict_is_adopted_and_a_stranger_is_not() {
987        let _probe_guard = probe_test_guard();
988        // Probing is not free: on a Snapdragon 778G the deciding classes
989        // cost minutes of wall clock before the first token, every
990        // process, and reached the same verdict every time. The cache
991        // exists so that price is paid once.
992        //
993        // The key is built from THIS process's device, never a name this
994        // test sets: `probe_set_device` is first-writer-wins and on a Mac
995        // the Metal backend may already have named the silicon before the
996        // tests run — which is exactly how this test failed on CI while
997        // passing locally. GemmNt on purpose: the arbitration test never
998        // touches it, and both run in one process.
999        let mine = probe_cache_key_named("gemm-nt");
1000        let state = || {
1001            PROBES[OpClass::GemmNt as usize]
1002                .state
1003                .load(Ordering::Relaxed)
1004        };
1005
1006        // Another device's verdict is not mine, whatever it claims.
1007        probe_cache_adopt("SomeOtherGPU/Vulkan\tgemm-nt\tgpu\n");
1008        assert_eq!(state(), 0);
1009        // Neither is one from another build of this engine.
1010        let older = mine.replacen(env!("CARGO_PKG_VERSION"), "0.0.0-old", 1);
1011        assert_ne!(older, mine);
1012        probe_cache_adopt(&format!("{older}\tgpu\n"));
1013        assert_eq!(state(), 0);
1014        // Mine is.
1015        probe_cache_adopt(&format!("{mine}\tcpu\n"));
1016        assert_eq!(state(), 2);
1017
1018        PROBES[OpClass::GemmNt as usize]
1019            .state
1020            .store(0, Ordering::Relaxed);
1021    }
1022}
1023
1024/// Default row threshold: the GPU takes only larger matrices (lm_head
1025/// class). Below it, the dispatch/readback cost does not pay off on unified memory.
1026pub const GPU_MIN_ROWS: usize = 65_536;
1027
1028/// Effective threshold: `CMF_GPU_MIN_ROWS` overrides. Defaults differ
1029/// by device class: on a DISCRETE card VRAM bandwidth pays off even for
1030/// FFN/QKV-class matrices (4096), on unified memory only lm_head-class
1031/// is worth the dispatch/readback (65536). Field case behind this: a
1032/// 35B model on an RTX 4090 saw ~0 offload because every layer matrix
1033/// sat below the old universal 65536.
1034pub fn min_rows() -> usize {
1035    if let Some(v) = std::env::var("CMF_GPU_MIN_ROWS")
1036        .ok()
1037        .and_then(|v| v.parse().ok())
1038    {
1039        return v;
1040    }
1041    if discrete() { 4096 } else { GPU_MIN_ROWS }
1042}
1043
1044/// Is the active backend a discrete card (PCIe VRAM)?
1045pub fn discrete() -> bool {
1046    match backend() {
1047        #[cfg(feature = "gpu")]
1048        Backend::Wgpu => crate::gpu_wgpu::is_discrete(),
1049        #[cfg(target_os = "macos")]
1050        Backend::Metal => false, // UMA by the init() guard
1051        Backend::None => false,
1052    }
1053}
1054
1055/// A single MoE-FFN job (an expert with its own weight), executed in one
1056/// submission: (rows, cols, idx, row_scale) for gate/up/down + prescaled
1057/// inputs + the down column scale + the blending weight.
1058pub struct MoeJob<'a> {
1059    pub gate: (usize, usize, usize, &'a [f32]),
1060    pub up: (usize, usize, usize, &'a [f32]),
1061    pub down: (usize, usize, usize, &'a [f32]),
1062    pub xs_gate: Vec<f32>,
1063    pub xs_up: Vec<f32>,
1064    pub down_col: &'a [f32],
1065    pub w: f32,
1066    /// q1 trio: scales live inside the 6-byte tiles (row_scale slices
1067    /// empty, xs raw f32). Backends without a q1 kernel refuse the job.
1068    pub q1: bool,
1069    /// q4_tiled trio: scales inside the 18-byte tiles (row_scale
1070    /// slices empty, xs raw f32) — the MoE-hybrid coder class.
1071    pub q4t: bool,
1072    /// q4tp trio: same raw-xs contract, 16-byte nibble stride and the scale
1073    /// on a per-row ladder. Without this the experts of a q4tp MoE model fall
1074    /// to the CPU while every other dtype rides the device.
1075    pub q4tp: bool,
1076    /// Mixed 2-bit profile: gate/up are q2tp (8-byte chunks, zero rung),
1077    /// down stays q4tp. Set together with `q4tp`; a backend without the
1078    /// 2-bit kernel must refuse the whole job.
1079    pub gu_q2: bool,
1080    /// The reference's `swiglu_limit`; 0 disables the clamp. A backend that
1081    /// cannot apply it must REFUSE the job rather than drop it silently —
1082    /// the difference only shows on saturating activations, which is the
1083    /// hardest kind of divergence to notice.
1084    pub swiglu_limit: f32,
1085    /// `GraphAct::code` of act(gate)·up: 0 SiLU (every MoE expert), 1 exact
1086    /// GELU, 2 tanh-GELU (a Gemma dense FFN riding the single-job block).
1087    /// A backend without the activation must refuse the job.
1088    pub act: u32,
1089}
1090
1091/// A single independent batch matvec (GDN projections of one input).
1092pub struct BatchJob<'a> {
1093    pub idx: usize,
1094    pub rows: usize,
1095    pub cols: usize,
1096    pub row_scale: &'a [f32],
1097    pub xs: Vec<f32>,
1098    /// Weight layout. Was a bare `q1: bool`, which could only ever spell two
1099    /// of the four and silently sent everything else back to the CPU — the
1100    /// GDN projections of a q4t/q4tp model never reached the device at all.
1101    pub layout: BatchLayout,
1102}
1103
1104/// Which kernel a batched matvec needs. q8 carries row scales in a side
1105/// buffer; the rest embed them in the payload and differ in stride.
1106#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1107pub enum BatchLayout {
1108    Q8,
1109    Q1,
1110    /// q1g: sign + scale planes, one scale per 128 (q1's raw-xs contract).
1111    Q1g,
1112    Q4t,
1113    Q4tp,
1114}
1115
1116#[derive(Clone, Copy, PartialEq, Eq)]
1117enum Backend {
1118    None,
1119    #[cfg(target_os = "macos")]
1120    Metal,
1121    #[cfg(feature = "gpu")]
1122    Wgpu,
1123}
1124
1125fn backend() -> Backend {
1126    #[cfg(feature = "gpu")]
1127    if crate::gpu_wgpu::selected() {
1128        return if crate::gpu_wgpu::enabled() {
1129            Backend::Wgpu
1130        } else {
1131            Backend::None
1132        };
1133    }
1134    #[cfg(target_os = "macos")]
1135    if crate::gpu_metal::enabled() {
1136        return Backend::Metal;
1137    }
1138    Backend::None
1139}
1140
1141/// GPU enabled and initialized on the selected backend?
1142/// Whether THIS build can bring a GPU up on THIS device: a compiled-in
1143/// backend plus a live adapter. The mobile FFI exposes it so an app can
1144/// tell "GPU off" from "GPU impossible" (a CPU-only .so ships no
1145/// backend at all). Cached after the first call.
1146pub fn backend_available() -> bool {
1147    #[cfg(target_os = "macos")]
1148    {
1149        // The Metal path is always compiled on macOS.
1150        true
1151    }
1152    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1153    {
1154        static AVAIL: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1155        *AVAIL.get_or_init(crate::gpu_wgpu::adapter_probe)
1156    }
1157    #[cfg(all(not(feature = "gpu"), not(target_os = "macos")))]
1158    {
1159        false
1160    }
1161}
1162
1163/// A process-wide, phase-scoped GPU gate. `cpu_scope` is thread-local and
1164/// the pool's workers do not inherit it, so a caller that wants a whole
1165/// *phase* off the device — a prompt encoder whose weights live in a part of
1166/// the file the hot loop never touches, on a machine that cannot keep both
1167/// wired — has to say so globally.
1168static GPU_PAUSED: PauseGate = PauseGate::new();
1169
1170/// The pause as a count of live guards, not a flag: with a flag, the first
1171/// guard to drop lifted every other holder's pause too — two overlapping
1172/// phases, or two tests of one binary (a bit-exact CPU reference whose
1173/// neighbour's guard drops mid-forward), put the device back early.
1174struct PauseGate(std::sync::atomic::AtomicUsize);
1175
1176impl PauseGate {
1177    const fn new() -> Self {
1178        Self(std::sync::atomic::AtomicUsize::new(0))
1179    }
1180
1181    fn enter(&self) {
1182        self.0.fetch_add(1, Ordering::Relaxed);
1183    }
1184
1185    fn leave(&self) {
1186        self.0.fetch_sub(1, Ordering::Relaxed);
1187    }
1188
1189    fn paused(&self) -> bool {
1190        self.0.load(Ordering::Relaxed) != 0
1191    }
1192}
1193
1194/// Park the device for every thread until the returned guard drops (and
1195/// every other live guard with it).
1196pub fn pause_gpu() -> GpuPause {
1197    GPU_PAUSED.enter();
1198    GpuPause(())
1199}
1200
1201pub struct GpuPause(());
1202
1203impl Drop for GpuPause {
1204    fn drop(&mut self) {
1205        GPU_PAUSED.leave();
1206    }
1207}
1208
1209pub fn enabled() -> bool {
1210    !GPU_PAUSED.paused() && backend() != Backend::None
1211}
1212
1213#[cfg(test)]
1214mod pause_gate_tests {
1215    use super::PauseGate;
1216
1217    /// Overlapping holders: the pause lasts until the last one leaves (a
1218    /// local gate, so the process-wide one other tests see is untouched).
1219    #[test]
1220    fn pause_holds_until_the_last_guard() {
1221        let g = PauseGate::new();
1222        assert!(!g.paused());
1223        g.enter();
1224        g.enter();
1225        g.leave();
1226        assert!(g.paused(), "one holder left, the other's pause was lifted");
1227        g.leave();
1228        assert!(!g.paused());
1229    }
1230}
1231
1232/// Default-on condition for the wgpu whole-token graph: the wgpu
1233/// backend on a DISCRETE adapter. NOT plain `enabled()` (macOS/Metal
1234/// must not pay a per-token layer scan for a graph its backend
1235/// refuses), and NOT integrated adapters: the graph's ~300 barriered
1236/// dispatches per token are cheap on desktop immediate-mode GPUs but
1237/// tiled mobile GPUs (Adreno/Mali) drain the pipeline at every barrier
1238/// — field report: 0.2 tok/s on-graph vs 15 tok/s on the CPU. On
1239/// integrated adapters the per-op probe path arbitrates each op class
1240/// against the CPU instead; CMF_GPU_WGPU_GRAPH=1 still forces the
1241/// graph anywhere.
1242/// Is the wgpu backend active at all (any adapter)? Eligibility gate
1243/// for the whole-token graph — whether it actually RUNS is decided by
1244/// `wgpu_graph_default` (trusted on discrete) or the generation race.
1245pub fn wgpu_active() -> bool {
1246    #[cfg(feature = "gpu")]
1247    {
1248        matches!(backend(), Backend::Wgpu)
1249    }
1250    #[cfg(not(feature = "gpu"))]
1251    {
1252        false
1253    }
1254}
1255
1256/// Which GPU this thread's engine calls address. Multi-card hosts hold
1257/// one wgpu context PER card (weights, KV mirrors and scratch live
1258/// inside a context, so per-device contexts give per-device caches for
1259/// free); this thread-local says which one is current. Default: the
1260/// process pin (CMF_GPU_ADAPTER) or 0 — so single-card runs behave
1261/// exactly as they always have.
1262pub fn default_device() -> usize {
1263    static D: std::sync::OnceLock<usize> = std::sync::OnceLock::new();
1264    *D.get_or_init(|| {
1265        std::env::var("CMF_GPU_ADAPTER")
1266            .ok()
1267            .and_then(|v| v.trim().parse::<usize>().ok())
1268            .unwrap_or(0)
1269    })
1270}
1271
1272thread_local! {
1273    static CUR_DEV: std::cell::Cell<Option<usize>> = const { std::cell::Cell::new(None) };
1274}
1275
1276/// The device this thread is pinned to.
1277pub fn current_device() -> usize {
1278    CUR_DEV.with(|c| c.get()).unwrap_or_else(default_device)
1279}
1280
1281/// Pin this thread to a device. Server slots call it once per request;
1282/// the worker pool propagates it into its threads, so a dispatch begun
1283/// on card 1 does not finish on card 0.
1284pub fn set_current_device(i: usize) {
1285    CUR_DEV.with(|c| c.set(Some(i)));
1286}
1287
1288/// Run `f` with this thread pinned to `dev`, restoring the previous pin.
1289pub fn with_device<R>(dev: usize, f: impl FnOnce() -> R) -> R {
1290    let prev = CUR_DEV.with(|c| c.replace(Some(dev)));
1291    let r = f();
1292    CUR_DEV.with(|c| c.set(prev));
1293    r
1294}
1295
1296/// How many GPUs this process can address (wgpu adapter count; 1 on
1297/// Metal, 0 without a backend).
1298pub fn device_count() -> usize {
1299    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1300    {
1301        return crate::gpu_wgpu::adapter_count();
1302    }
1303    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1304    {
1305        usize::from(backend_available())
1306    }
1307}
1308
1309/// Weight budget of the current GPU in bytes; 0 when there is none and
1310/// u64::MAX on unified memory (where the OS pages shared RAM and the
1311/// question "does the model fit the card" has no separate answer).
1312pub fn vram_budget() -> u64 {
1313    #[cfg(all(feature = "gpu", not(target_os = "macos")))]
1314    {
1315        return crate::gpu_wgpu::device_vram_budget();
1316    }
1317    #[cfg(not(all(feature = "gpu", not(target_os = "macos"))))]
1318    {
1319        if backend_available() { u64::MAX } else { 0 }
1320    }
1321}
1322
1323/// Bytes currently accounted as resident weight buffers on the active wgpu
1324/// adapter.  This is the logical device-local weight set; physical driver
1325/// allocations are reported separately by the platform tools.
1326pub fn resident_bytes() -> u64 {
1327    #[cfg(feature = "gpu")]
1328    {
1329        if backend() == Backend::Wgpu {
1330            return crate::gpu_wgpu::resident_bytes();
1331        }
1332    }
1333    0
1334}
1335
1336/// Sealed O(1) device mirror count and logical bytes for one pipeline id.
1337/// Zero is returned when wgpu is unavailable or the sequence has not reached
1338/// an O(1) seal yet.
1339pub fn o1_device_stats(kv_id: u64) -> (usize, u64) {
1340    #[cfg(feature = "gpu")]
1341    {
1342        if backend() == Backend::Wgpu {
1343            return crate::gpu_wgpu::o1_device_stats(kv_id);
1344        }
1345    }
1346    let _ = kv_id;
1347    (0, 0)
1348}
1349
1350/// Device weight bytes uploaded so far (wgpu; 0 on other backends).
1351/// Steady-state windows must show a ZERO delta — growth mid-benchmark
1352/// means eviction/re-upload and disqualifies the number.
1353pub fn upload_bytes() -> u64 {
1354    #[cfg(feature = "gpu")]
1355    {
1356        return crate::gpu_wgpu::UPLOAD_BYTES.load(std::sync::atomic::Ordering::Relaxed);
1357    }
1358    #[cfg(not(feature = "gpu"))]
1359    0
1360}
1361
1362/// Measure a transient host-to-device upload when the wgpu backend is
1363/// compiled in. CPU-only builds keep the benchmark command available and
1364/// report no device measurement instead of referring to the gated module.
1365pub fn upload_bandwidth_probe(block: usize, rounds: usize) -> Option<f64> {
1366    #[cfg(feature = "gpu")]
1367    {
1368        return crate::gpu_wgpu::upload_bandwidth_probe(block, rounds);
1369    }
1370    let _ = (block, rounds);
1371    None
1372}
1373
1374/// Which half of the run is asking.
1375///
1376/// The phase exists because the graph is plausibly two decisions, not
1377/// one — but on the hardware measured so far it is only ever a decode
1378/// decision. On an Adreno 642L with bonsai-1.7b, from identical clean
1379/// starts and two repeats each: decode 11.6 tok/s without it and 0.72
1380/// with, while prefill is 4.2 either way. A first reading of 3.4 -> 18.0
1381/// for prefill did not survive a controlled re-run — it was a dirty
1382/// probe cache between configurations, not the graph, and the prefill
1383/// route through the graph is GDN-only in the first place, which this
1384/// dense model never takes.
1385#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1386pub enum GraphPhase {
1387    Prefill,
1388    Decode,
1389}
1390
1391/// The one place that decides whether the whole-token graph runs.
1392///
1393/// `CMF_GPU_WGPU_GRAPH`: `0` off everywhere, `prefill` only for the
1394/// prompt, anything else on everywhere. Unset: desktop-class GPUs take
1395/// it for both phases; phone-class UMA takes it for PREFILL only, which
1396/// is the measurement above rather than a guess — the per-op path keeps
1397/// decode, where it is seventeen times better.
1398pub fn wgpu_graph_on(phase: GraphPhase) -> bool {
1399    match std::env::var("CMF_GPU_WGPU_GRAPH").ok().as_deref() {
1400        Some("0") => false,
1401        Some("prefill") => phase == GraphPhase::Prefill,
1402        Some(_) => true,
1403        None => {
1404            if wgpu_graph_default() {
1405                return true;
1406            }
1407            // Integrated/mobile keeps the per-op path for BOTH phases —
1408            // unchanged, because the measurement that would have bought
1409            // prefill a graph did not reproduce. `=prefill` is there for
1410            // the device where it does; the default does not guess.
1411            let _ = phase;
1412            false
1413        }
1414    }
1415}
1416
1417pub fn wgpu_graph_default() -> bool {
1418    #[cfg(feature = "gpu")]
1419    {
1420        // Discrete cards always; Apple-silicon UMA on macOS too — desktop
1421        // -class GPUs where the graph measured ~2x the CPU on the Qwen3.6
1422        // family (M4: 13.3 tok/s against 7.3). Phone-class UMA (Android/
1423        // iOS builds) keeps the per-op probe path: tiled mobile GPUs have
1424        // turned the ~300-dispatch graph into seconds per token.
1425        matches!(backend(), Backend::Wgpu)
1426            && (crate::gpu_wgpu::discrete_active()
1427                || (cfg!(target_os = "macos") && crate::gpu_wgpu::adapter_up()))
1428    }
1429    #[cfg(not(feature = "gpu"))]
1430    {
1431        false
1432    }
1433}
1434
1435/// q8_row/q8_2f matvec, rows [row0, row0+rows). `xs` — prescaled by the column scale.
1436#[allow(clippy::too_many_arguments, unused_variables)]
1437pub fn q8_matvec_range(
1438    model: &Arc<CmfModel>,
1439    idx: usize,
1440    row0: usize,
1441    row_scale: &[f32],
1442    xs: &[f32],
1443    rows: usize,
1444    cols: usize,
1445    out: &mut [f32],
1446) -> bool {
1447    match backend() {
1448        #[cfg(target_os = "macos")]
1449        Backend::Metal => {
1450            crate::gpu_metal::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1451        }
1452        #[cfg(feature = "gpu")]
1453        Backend::Wgpu => {
1454            crate::gpu_wgpu::q8_matvec_range(model, idx, row0, row_scale, xs, rows, cols, out)
1455        }
1456        Backend::None => false,
1457    }
1458}
1459
1460/// Decode-exact short q8_2f panel for the banked MiMo head.
1461pub(crate) fn q82_short_rows(
1462    model: &Arc<CmfModel>,
1463    idx: usize,
1464    xs: &[f32],
1465    b: usize,
1466    rows: usize,
1467    cols: usize,
1468    out: &mut [f32],
1469) -> bool {
1470    #[cfg(feature = "gpu")]
1471    if enabled_here() && backend() == Backend::Wgpu {
1472        return crate::gpu_wgpu::q82_short_rows(model, idx, xs, b, rows, cols, out);
1473    }
1474    let _ = (model, idx, xs, b, rows, cols, out);
1475    false
1476}
1477
1478/// GEMM of a prefill batch: `pre` — prescaled inputs row-major [b, cols],
1479/// out — row-major [b, rows].
1480#[allow(clippy::too_many_arguments, unused_variables)]
1481/// The two-field int8 GEMM with the column field left for the device.
1482/// wgpu only — Metal's int8 kernel takes a pre-scaled activation, so the
1483/// caller keeps that path when this returns `false`.
1484#[allow(clippy::too_many_arguments)]
1485pub fn q8_matmat_2f(
1486    model: &Arc<CmfModel>,
1487    idx: usize,
1488    row_scale: &[f32],
1489    col_field: &[f32],
1490    xs: &[f32],
1491    b: usize,
1492    rows: usize,
1493    cols: usize,
1494    out: &mut [f32],
1495) -> bool {
1496    #[allow(unreachable_patterns)]
1497    match backend() {
1498        #[cfg(feature = "gpu")]
1499        Backend::Wgpu => {
1500            crate::gpu_wgpu::q8_matmat_2f(model, idx, row_scale, col_field, xs, b, rows, cols, out)
1501        }
1502        _ => false,
1503    }
1504}
1505
1506pub fn q8_matmat(
1507    model: &Arc<CmfModel>,
1508    idx: usize,
1509    row_scale: &[f32],
1510    pre: &[f32],
1511    b: usize,
1512    rows: usize,
1513    cols: usize,
1514    out: &mut [f32],
1515) -> bool {
1516    match backend() {
1517        #[cfg(target_os = "macos")]
1518        Backend::Metal => {
1519            crate::gpu_metal::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out)
1520        }
1521        #[cfg(feature = "gpu")]
1522        Backend::Wgpu => crate::gpu_wgpu::q8_matmat(model, idx, row_scale, pre, b, rows, cols, out),
1523        Backend::None => false,
1524    }
1525}
1526
1527/// q1 matvec: raw f32 activations, tile-embedded scales. Metal only
1528/// for now (wgpu q1 WGSL is queued); false = CPU fallback.
1529#[allow(unused_variables)]
1530pub fn q1_matvec(
1531    model: &Arc<CmfModel>,
1532    idx: usize,
1533    xs: &[f32],
1534    rows: usize,
1535    cols: usize,
1536    out: &mut [f32],
1537) -> bool {
1538    match backend() {
1539        #[cfg(target_os = "macos")]
1540        Backend::Metal => crate::gpu_metal::q1_matvec(model, idx, xs, rows, cols, out),
1541        #[cfg(feature = "gpu")]
1542        Backend::Wgpu => crate::gpu_wgpu::q1_matvec(model, idx, xs, rows, cols, out),
1543        Backend::None => false,
1544    }
1545}
1546
1547/// q1g matvec (sign + scale planes, raw f32 activations); the q1 kernel's
1548/// walk and sums, so a uniform-scale repack matches `q1_matvec` bit for
1549/// bit. false = CPU fallback.
1550#[allow(unused_variables)]
1551pub fn q1g_matvec(
1552    model: &Arc<CmfModel>,
1553    idx: usize,
1554    xs: &[f32],
1555    rows: usize,
1556    cols: usize,
1557    out: &mut [f32],
1558) -> bool {
1559    match backend() {
1560        #[cfg(target_os = "macos")]
1561        Backend::Metal => crate::gpu_metal::q1g_matvec(model, idx, xs, rows, cols, out),
1562        #[cfg(feature = "gpu")]
1563        Backend::Wgpu => crate::gpu_wgpu::q1g_matvec(model, idx, xs, rows, cols, out),
1564        Backend::None => false,
1565    }
1566}
1567
1568/// Whole attention sub-block on the wgpu token graph (drop-in for
1569/// `qwen_attention`): normed hidden in, O-projection out, resident device
1570/// K/V mirror. false = refusal / not the wgpu backend → CPU path.
1571#[allow(clippy::too_many_arguments)]
1572pub fn attn_dropin(
1573    model: &Arc<CmfModel>,
1574    kv_id: u64,
1575    layer: usize,
1576    normed: &[f32],
1577    wq_idx: usize,
1578    wk_idx: usize,
1579    wv_idx: usize,
1580    wo_idx: usize,
1581    q_norm: Option<&[f32]>,
1582    k_norm: Option<&[f32]>,
1583    late_qk_norm: bool,
1584    invf: &[f32],
1585    nh: usize,
1586    nkv: usize,
1587    hd: usize,
1588    rd: usize,
1589    hidden: usize,
1590    pos: usize,
1591    cap: usize,
1592    gemma: bool,
1593    eps: f32,
1594    cpu_k: &[Vec<f32>],
1595    cpu_v: &[Vec<f32>],
1596    out: &mut [f32],
1597) -> bool {
1598    match backend() {
1599        #[cfg(feature = "gpu")]
1600        Backend::Wgpu => crate::gpu_wgpu::attn_dropin_gpu(
1601            model, kv_id, layer, normed, wq_idx, wk_idx, wv_idx, wo_idx, q_norm, k_norm,
1602            late_qk_norm, invf, nh, nkv, hd, rd, hidden, pos, cap, gemma, eps, cpu_k, cpu_v, out,
1603        ),
1604        #[allow(unused_variables)]
1605        _ => false,
1606    }
1607}
1608
1609/// Descriptor operation attached to a graph weight.  `None` is the default
1610/// for ordinary CMF files; Prism weights are admitted only when the token
1611/// graph carries this explicit transform contract.
1612#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1613pub enum GraphPrismOp {
1614    None,
1615    Forward,
1616    InverseEmbedding,
1617}
1618
1619/// Graph kind of a binary low-rank (`blr`, dtype 19) weight: two stage
1620/// dispatches (V, U) over the resident payload instead of one matvec. Only
1621/// the wgpu token graph builder asks for it (`QTensor::graph_weight_or_blr`).
1622pub const GRAPH_KIND_BLR: u8 = 11;
1623
1624/// One weight in the whole-token graph: tensor idx + a codec tag (0=q8_row,
1625/// 1=q1, 2=q4_tiled, 3=q1t, 4=f32, … 11=blr) + per-row scales (q8_row only) + the raw f32
1626/// data (kind 4 only — small unquantized projections like GDN in_proj_a/b).
1627pub struct GraphW<'a> {
1628    pub idx: usize,
1629    pub kind: u8,
1630    pub row_scale: &'a [f32],
1631    pub data: &'a [f32],
1632    pub prism: GraphPrismOp,
1633    pub affine: bool,
1634}
1635
1636/// A layer's token-mixing op: standard attention or a GDN (linear-attention)
1637/// block. The surrounding norms + SwiGLU FFN are common to both.
1638pub enum GraphAttn<'a> {
1639    Full {
1640        wq: GraphW<'a>,
1641        wk: GraphW<'a>,
1642        wv: GraphW<'a>,
1643        wo: GraphW<'a>,
1644        q_norm: Option<&'a [f32]>,
1645        k_norm: Option<&'a [f32]>,
1646        /// HunYuan dense: q/k norm after RoPE (rope-kernel flag bit 32).
1647        late_qk_norm: bool,
1648        /// (bq, bk, bv) attention biases (Qwen2). None ⇒ no bias.
1649        bias: Option<(&'a [f32], &'a [f32], &'a [f32])>,
1650        /// Qwen3.5 gated attention: wq emits 2·nh·hd (q||gate per head), the
1651        /// attention output is scaled by sigmoid(gate) before the O projection.
1652        output_gate: bool,
1653        cpu_k: &'a [Vec<f32>],
1654        cpu_v: &'a [Vec<f32>],
1655        /// Absolute position of `cpu_k`/`cpu_v` row 0
1656        /// (`LayerKvCache::base`): a sliding layer that keeps only its
1657        /// tail stores rows from there on. 0 for every untrimmed layer.
1658        cpu_base: usize,
1659        /// This layer's own attention geometry, when the model's layers do
1660        /// not share one (MiMo-V2: 4/8 KV heads, 128-wide V under 192-wide
1661        /// heads, sliding windows with learned sinks, two RoPE tables).
1662        /// None = the call-wide (nkv, hd, rd, invf), V as wide as K, full
1663        /// context and a plain softmax — the historical contract, whose
1664        /// kernels and dispatch are untouched.
1665        geom: Option<GraphAttnGeom<'a>>,
1666        /// Spark-X2.5 head-wise output gate: `self_attn.g_proj`
1667        /// `[num_heads, hidden]` against the attention's normed input; head
1668        /// h's output is scaled by sigmoid(g[h]) before the O projection.
1669        /// None = no such gate (every other model).
1670        head_gate: Option<GraphW<'a>>,
1671    },
1672    Gdn {
1673        qkv: GraphW<'a>,
1674        z: GraphW<'a>,
1675        a: GraphW<'a>,
1676        b: GraphW<'a>,
1677        out: GraphW<'a>,
1678        conv1d: &'a [f32],
1679        a_log: &'a [f32],
1680        dt_bias: &'a [f32],
1681        norm: &'a [f32],
1682        nv: usize,
1683        nk: usize,
1684        dk: usize,
1685        dv: usize,
1686        kk: usize,
1687        /// CPU recurrent state `[ring (kk-1)·cdim | S nv·dk·dv]` — seeds the
1688        /// device mirror when prefill ran on the host (o1 collection, CPU
1689        /// fallback): a zero-initialized device state at decode is exactly
1690        /// the "coherent but contextless" garble.
1691        cpu_state: &'a [f32],
1692    },
1693    /// LFM2 gated short convolution: a fused (B, C, x) projection, a
1694    /// depthwise causal conv over a (kernel−1)-deep per-channel ring,
1695    /// C-gating, and an output projection. This mixer is what most of an
1696    /// LFM2 stack is (22 of the 2.6B's 30 layers), and before it had a
1697    /// graph arm the whole model fell to the per-op path — ~100 submits
1698    /// a token, 22 tok/s on an A100 for a 1.4 GB file.
1699    ShortConv {
1700        /// [3·hidden, hidden] fused input projection.
1701        inp: GraphW<'a>,
1702        /// [hidden, hidden] output projection.
1703        out: GraphW<'a>,
1704        /// [hidden · kernel] depthwise taps, `[channel][tap]`, tap
1705        /// kernel−1 multiplying the current position.
1706        taps: &'a [f32],
1707        kernel: usize,
1708        /// CPU conv ring `[channel][kernel−1]`, slot 0 newest — seeds
1709        /// the device mirror when prefill ran on the host, which for
1710        /// this mixer is always (the batch graph declines it).
1711        cpu_state: &'a [f32],
1712    },
1713}
1714
1715/// Per-layer attention geometry for the wgpu graphs (see
1716/// `GraphAttn::Full::geom`). The layer's CPU cache keeps K rows `hd` wide
1717/// and V rows zero-padded to `hd`; the device mirror stores V `dv` wide,
1718/// and a windowed layer keeps a ring of the last positions only.
1719#[derive(Clone, Copy)]
1720pub struct GraphAttnGeom<'a> {
1721    /// KV heads of this layer (divides the Q heads).
1722    pub nkv: usize,
1723    /// Q/K head width of this layer. The call-wide `hd` for every model
1724    /// but Gemma-4, whose global layers are 512 wide over 256-wide sliding
1725    /// ones (`<= 512`, a multiple of 4).
1726    pub hd: usize,
1727    /// V head width, `4 <= dv <= hd`, a multiple of 4.
1728    pub dv: usize,
1729    /// Rotary width of this layer (NeoX half-split over `[0, rd)`).
1730    pub rd: usize,
1731    /// This layer's RoPE inverse frequencies (`rd / 2` of them).
1732    pub invf: &'a [f32],
1733    /// YaRN attention-amplitude factor applied after the rotary pair
1734    /// transform. `1.0` is ordinary RoPE; Mellum's global layers use
1735    /// `1.277258872…`. This is deliberately per-layer: local and global
1736    /// layers may have different RoPE tables and factors.
1737    pub rope_scale: f32,
1738    /// Positions a query sees, its own included (MiMo-V2 SWA: 128);
1739    /// None = the whole context.
1740    pub window: Option<usize>,
1741    /// Learned per-Q-head sink logits (gpt-oss / MiMo-V2): they join the
1742    /// softmax max and denominator and carry no value row.
1743    pub sink: Option<&'a [f32]>,
1744    /// Gemma-4's scale-less V norm: every V head is RMS-normalized (no
1745    /// weight) before it is cached, as `attention::vnorm_head` does.
1746    pub v_norm: bool,
1747}
1748
1749/// Activation of a graph layer's dense FFN, `act(gate)·up`. The code is
1750/// the word the kernels read (`silu_mul_pre`'s `_c`, the fused gate+up
1751/// kernels' `_p1`): 0 keeps the historical SiLU expression untouched.
1752#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1753pub enum GraphAct {
1754    Silu,
1755    /// Exact erf GELU (HF `gelu`), erf by Abramowitz–Stegun 7.1.26 — the
1756    /// formula `inference::erf_f32` uses on the host.
1757    GeluErf,
1758    /// tanh-approximated GELU (HF `gelu_pytorch_tanh`, the Gemma family):
1759    /// `inference::gelu_tanh` term for term.
1760    GeluTanh,
1761}
1762
1763impl GraphAct {
1764    pub fn code(self) -> u32 {
1765        match self {
1766            Self::Silu => 0,
1767            Self::GeluErf => 1,
1768            Self::GeluTanh => 2,
1769        }
1770    }
1771}
1772
1773/// Per-layer weights for the whole-token wgpu graph.
1774pub struct GraphLayer<'a> {
1775    pub input_norm: &'a [f32],
1776    pub attn: GraphAttn<'a>,
1777    pub post_norm: &'a [f32],
1778    pub ffn: GraphFfn<'a>,
1779    /// Gemma sandwich norms: the token mixer's output and the FFN's output
1780    /// are RMS-normed with these weights before their residual adds.
1781    pub attn_out_norm: Option<&'a [f32]>,
1782    pub ffn_out_norm: Option<&'a [f32]>,
1783    /// Gemma-4 `layer_scalar`: the residual stream is multiplied by it at
1784    /// the end of the layer.
1785    pub layer_scale: Option<f32>,
1786}
1787
1788/// The FFN of one graph layer: a dense SwiGLU trio, or a routed MoE —
1789/// router + top-k selection + all selected experts run ON DEVICE (the
1790/// routing decision depends on the resident hidden state, so a CPU
1791/// round-trip per layer would forfeit the one-submit design).
1792pub enum GraphFfn<'a> {
1793    /// A singleton attention-only batch graph. Returns the post-attention
1794    /// residual, allowing a dynamic expert bank to own the FFN separately.
1795    AttentionOnly,
1796    Dense {
1797        gate: GraphW<'a>,
1798        up: GraphW<'a>,
1799        down: GraphW<'a>,
1800        /// act(gate)·up. The builders refuse an activation with no arm here
1801        /// (before this field the graph computed SiLU for any dense FFN).
1802        act: GraphAct,
1803    },
1804    Moe {
1805        /// Router logits weight (f32, kind 4) `[n_exp, hidden]`.
1806        router: GraphW<'a>,
1807        /// Shared-expert sigmoid gate (f32) `[1, hidden]`.
1808        shared_gate: GraphW<'a>,
1809        /// Per-expert q4_tiled directory indices `(gate, up, down)`;
1810        /// the SHARED expert rides as the LAST entry — the select
1811        /// kernel pins it with the sigmoid weight.
1812        experts: Vec<(usize, usize, usize)>,
1813        /// Routed experts (shared excluded).
1814        n_exp: usize,
1815        top_k: usize,
1816        inter: usize,
1817        norm_topk: bool,
1818        /// Expert weight layout, uniform across the layer: `false` =
1819        /// q4_tiled (18 B tiles, inline f16 scale), `true` = q4tp
1820        /// (16 B nibbles + a per-row ladder plane). The two differ only
1821        /// in where the scale comes from, so they share every kernel
1822        /// but the weight-staging block.
1823        q4tp: bool,
1824        /// `true` = the gate/up experts are `q2tp` (2-bit plane) while
1825        /// `down` stays q4tp — the mixed profile a 2-bit-class checkpoint
1826        /// converts into. Only meaningful with `q4tp: true`.
1827        gu_q2: bool,
1828        /// `true` = every expert's gate, up AND down is q8_2f (int8 body,
1829        /// f16 row scales, f16 column field; Mellum2.1's quality file).
1830        /// `q4tp` and `gu_q2` are false then. Its own kernels: each
1831        /// expert's input is multiplied by THAT expert's column field.
1832        q82: bool,
1833        /// LFM2-MoE / DeepSeek-V3 `noaux_tc` routing: per-expert sigmoid
1834        /// scores instead of a softmax, and `norm_topk` renormalises with
1835        /// the 1e-6 floor. The softmax arm is bit-identical to before.
1836        sigmoid: bool,
1837        /// Per-expert SELECTION bias: added to the score for the top-k
1838        /// choice only — the mixing weights stay unbiased (noaux_tc).
1839        bias: Option<&'a [f32]>,
1840        /// Whether a shared expert rides as the last `experts` entry.
1841        /// LFM2-MoE has none; the select kernel then leaves slot `top_k`
1842        /// unwritten and the expert loop runs `top_k` slots, not +1.
1843        has_shared: bool,
1844        /// The shared expert carries a sigmoid gate (Qwen2/3-MoE). `false`
1845        /// with `has_shared`: the shared expert enters with weight 1
1846        /// (DeepSeek-V3 / HunYuan hy_v3) and `shared_gate` is a stand-in
1847        /// the select kernels ignore.
1848        shared_gated: bool,
1849        /// Multiplier on the routed mixing weights after the optional
1850        /// renormalization (`routed_scaling_factor`); 1.0 = none.
1851        route_scale: f32,
1852    },
1853}
1854
1855/// Outcome of one whole-token graph attempt. A failed attempt after sealed
1856/// O(1) state was admitted must not fall through to the stale CPU state.
1857#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1858pub enum TokenGraphOutcome {
1859    /// No command was committed; the caller may use its ordinary path.
1860    Declined,
1861    /// The graph completed and its hidden/logits output is valid.
1862    Completed,
1863    /// Sealed O(1) state was admitted and a later graph operation failed.
1864    Failed,
1865}
1866
1867/// Whole-token decode graph on wgpu: the entire layer stack in ONE submit,
1868/// hidden resident, one readback. Updates `h` in place.
1869/// `loop_norm_at`: virtual layer indices after which `final_norm` is applied
1870/// (Looped Transformer mid-stack norm). Empty for standard models.
1871#[allow(clippy::too_many_arguments)]
1872pub fn forward_token_graph(
1873    model: &Arc<CmfModel>,
1874    kv_id: u64,
1875    layers: &[GraphLayer],
1876    // Per-layer sealed o1 (Nystrom) state; Some = replace this layer's
1877    // exact attention with the O(1) kernels. wgpu only.
1878    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
1879    o1_epoch: u64,
1880    invf: &[f32],
1881    h: &mut [f32],
1882    nh: usize,
1883    nkv: usize,
1884    hd: usize,
1885    attn_scale: f32,
1886    rd: usize,
1887    hidden: usize,
1888    inter: usize,
1889    position: usize,
1890    cap: usize,
1891    gemma: bool,
1892    eps: f32,
1893    lm_head: Option<(&GraphW, usize)>,
1894    final_norm: &[f32],
1895    logits: &mut Vec<f32>,
1896    loop_norm_at: &[usize],
1897    steps: usize,
1898    embed: Option<(&GraphW, usize, f32)>,
1899    ids_out: Option<&mut Vec<u32>>,
1900    // How many leading layers the graph ran (see the wgpu twin) — smaller
1901    // than layers.len() when the expert budget ended the device prefix.
1902    layers_run: Option<&mut usize>,
1903    // Absolute index of layers[0] in the model — the KV/state mirrors key
1904    // on it, so a layer SPAN (network split segment) shares mirrors with
1905    // a full-stack run instead of colliding at slot 0.
1906    layer_base: usize,
1907    // Read the final hidden back alongside the fused head's logits.
1908    hidden_too: bool,
1909) -> TokenGraphOutcome {
1910    match backend() {
1911        #[cfg(feature = "gpu")]
1912        Backend::Wgpu => crate::gpu_wgpu::forward_token_graph(
1913            model,
1914            kv_id,
1915            layers,
1916            o1,
1917            o1_epoch,
1918            invf,
1919            h,
1920            nh,
1921            nkv,
1922            hd,
1923            attn_scale,
1924            rd,
1925            hidden,
1926            inter,
1927            position,
1928            cap,
1929            gemma,
1930            eps,
1931            lm_head,
1932            final_norm,
1933            logits,
1934            loop_norm_at,
1935            steps,
1936            embed,
1937            ids_out,
1938            layers_run,
1939            layer_base,
1940            hidden_too,
1941        ),
1942        #[allow(unused_variables)]
1943        _ => {
1944            let _ = (
1945                attn_scale,
1946                lm_head,
1947                final_norm,
1948                logits,
1949                loop_norm_at,
1950                layers_run,
1951                layer_base,
1952                hidden_too,
1953            );
1954            TokenGraphOutcome::Declined
1955        }
1956    }
1957}
1958
1959/// Whole-token resident graph for the Embryo working model.  Unlike the
1960/// generic graph this path accepts a packed f32 model and keeps both phase
1961/// recurrent state and the anchor KV cache on the device.  `false` is an
1962/// honest capability refusal; callers must retain the CPU executor.
1963pub fn forward_embryo_graph(
1964    model: &Arc<EmbryoGraphModel>,
1965    kv_id: u64,
1966    hidden: &[f32],
1967    position: usize,
1968    logits: &mut Vec<f32>,
1969) -> bool {
1970    #[cfg(feature = "gpu")]
1971    if backend() == Backend::Wgpu {
1972        return crate::gpu_wgpu::forward_embryo_graph(model, kv_id, hidden, position, logits);
1973    }
1974    let _ = (model, kv_id, hidden, position, logits);
1975    false
1976}
1977
1978/// Speculative-verify tail for the batched graph: fold final-norm + lm_head
1979/// over every batch position and read all k logit rows back; the batch also
1980/// snapshots the GDN state per position for `gdn_spec_restore`.
1981#[derive(Clone, Copy, Debug, Eq, PartialEq)]
1982pub enum BatchGraphOutcome {
1983    /// The graph declined before mutating persistent device state. Callers may
1984    /// safely use the existing per-position path.
1985    Declined,
1986    /// The complete batch committed and its readback succeeded.
1987    Completed,
1988    /// A batch that had admitted sealed O(1) state failed after admission.
1989    /// Falling back to CPU would mix two state machines, so the caller must
1990    /// abort and clear the sequence instead.
1991    Failed,
1992}
1993
1994pub struct SpecTail<'a> {
1995    pub lm: GraphW<'a>,
1996    pub lm_rows: usize,
1997    pub final_norm: &'a [f32],
1998    pub logits_out: &'a mut Vec<f32>,
1999}
2000
2001/// Batched prefill: k contiguous positions through the whole graph in one submit
2002/// (projections/FFN as GEMMs, attention/GDN looped over scratch). `h` is
2003/// [k·hidden] in/out; `positions` len k. wgpu only.
2004#[allow(clippy::too_many_arguments)]
2005pub fn forward_batch_graph(
2006    model: &Arc<CmfModel>,
2007    kv_id: u64,
2008    layers: &[GraphLayer],
2009    invf: &[f32],
2010    h: &mut [f32],
2011    nh: usize,
2012    nkv: usize,
2013    hd: usize,
2014    rd: usize,
2015    hidden: usize,
2016    inter: usize,
2017    positions: &[usize],
2018    cap: usize,
2019    gemma: bool,
2020    eps: f32,
2021    attn_scale: f32,
2022    k: usize,
2023    // Per-layer sealed O(1) device views. An empty slice means the ordinary
2024    // exact-KV path; otherwise it must have one entry per graph layer.
2025    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
2026    o1_epoch: u64,
2027    spec: Option<SpecTail<'_>>,
2028    // Device-prefix mode (plain prefill only): Some = when the whole stack
2029    // does not fit the weight budget, run the leading layers that do — the
2030    // same prefix rule as the token graph — leave the boundary hidden in
2031    // `h` and report the count here; the caller runs the rest on the host.
2032    // None = all layers or a decline, as before.
2033    layers_run: Option<&mut usize>,
2034) -> BatchGraphOutcome {
2035    forward_batch_graph_at(model, kv_id, 0, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions, cap, gemma, eps, attn_scale, k, o1, o1_epoch, spec, layers_run)
2036}
2037
2038thread_local! {
2039    static MIMO_ATTN_SCRATCH: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
2040}
2041
2042pub(crate) fn mimo_attention_scratch_enabled() -> bool {
2043    MIMO_ATTN_SCRATCH.with(std::cell::Cell::get)
2044}
2045
2046/// Scoped diagnostic A/B switch; unlike process environment mutations it
2047/// cannot race a model's background workers. Restores on panic as well.
2048#[doc(hidden)]
2049pub fn mimo_attention_scratch_scope<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
2050    struct Restore(bool);
2051    impl Drop for Restore {
2052        fn drop(&mut self) {
2053            MIMO_ATTN_SCRATCH.with(|v| v.set(self.0));
2054        }
2055    }
2056    let _restore = Restore(MIMO_ATTN_SCRATCH.with(|v| v.replace(enabled)));
2057    f()
2058}
2059
2060/// Gemma-4's prompt on the batched wgpu graph (`CMF_G4_BATCH=0` keeps the
2061/// chunked host prefill it used before).
2062pub fn g4_batch_on() -> bool {
2063    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2064    *ON.get_or_init(|| std::env::var("CMF_G4_BATCH").as_deref() != Ok("0"))
2065}
2066
2067/// q8_2f MoE experts on the wgpu token and batch graphs (`CMF_MOE_Q82=0`
2068/// keeps them off the graphs: every op on its own, the 0.8.15 route).
2069pub(crate) fn moe_q82_graph_on() -> bool {
2070    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2071    *ON.get_or_init(|| std::env::var("CMF_MOE_Q82").as_deref() != Ok("0"))
2072}
2073
2074thread_local! {
2075    static MIMO_Q8_SHORT: std::cell::Cell<bool> = const { std::cell::Cell::new(true) };
2076}
2077
2078pub(crate) fn mimo_q8_short_enabled() -> bool {
2079    MIMO_Q8_SHORT.with(std::cell::Cell::get)
2080}
2081
2082/// Diagnostic A/B switch for row-exact short q8 graph kernels.
2083#[doc(hidden)]
2084pub fn mimo_q8_short_scope<R>(enabled: bool, f: impl FnOnce() -> R) -> R {
2085    struct Restore(bool);
2086    impl Drop for Restore {
2087        fn drop(&mut self) {
2088            MIMO_Q8_SHORT.with(|v| v.set(self.0));
2089        }
2090    }
2091    let _restore = Restore(MIMO_Q8_SHORT.with(|v| v.replace(enabled)));
2092    f()
2093}
2094
2095/// Batched graph over a span whose first absolute layer is `layer_base`.
2096#[allow(clippy::too_many_arguments)]
2097pub fn forward_batch_graph_at(
2098    model: &Arc<CmfModel>,
2099    kv_id: u64,
2100    layer_base: usize,
2101    layers: &[GraphLayer],
2102    invf: &[f32],
2103    h: &mut [f32],
2104    nh: usize,
2105    nkv: usize,
2106    hd: usize,
2107    rd: usize,
2108    hidden: usize,
2109    inter: usize,
2110    positions: &[usize],
2111    cap: usize,
2112    gemma: bool,
2113    eps: f32,
2114    attn_scale: f32,
2115    k: usize,
2116    // Per-layer sealed O(1) device views. An empty slice means the ordinary
2117    // exact-KV path; otherwise it must have one entry per graph layer.
2118    o1: &[Option<Vec<crate::nystrom::O1DeviceView<'_>>>],
2119    o1_epoch: u64,
2120    spec: Option<SpecTail<'_>>,
2121    // Device-prefix mode (plain prefill only): Some = when the whole stack
2122    // does not fit the weight budget, run the leading layers that do — the
2123    // same prefix rule as the token graph — leave the boundary hidden in
2124    // `h` and report the count here; the caller runs the rest on the host.
2125    // None = all layers or a decline, as before.
2126    layers_run: Option<&mut usize>,
2127) -> BatchGraphOutcome {
2128    match backend() {
2129        #[cfg(feature = "gpu")]
2130        Backend::Wgpu => crate::gpu_wgpu::forward_batch_graph_at(
2131            model, kv_id, layer_base, layers, invf, h, nh, nkv, hd, rd, hidden, inter, positions,
2132            cap, gemma, eps, attn_scale, k, o1, o1_epoch, spec, layers_run,
2133        ),
2134        #[allow(unreachable_patterns)]
2135        _ => {
2136            let _ = (o1, o1_epoch, spec, layers_run);
2137            BatchGraphOutcome::Declined
2138        }
2139    }
2140}
2141
2142/// After a partial speculative acceptance: restore every GDN layer's device
2143/// state to the snapshot after batch position `slot`. `base_pos` is the
2144/// absolute position of the first verify row and `expected_layers` makes the
2145/// restore all-or-nothing across the model's recurrent layers. wgpu only.
2146pub fn gdn_spec_restore(kv_id: u64, slot: usize, base_pos: usize, expected_layers: usize) -> bool {
2147    #[cfg(feature = "gpu")]
2148    if backend() == Backend::Wgpu {
2149        return crate::gpu_wgpu::gdn_spec_restore(kv_id, slot, base_pos, expected_layers);
2150    }
2151    #[allow(unreachable_code)]
2152    {
2153        let _ = (kv_id, slot, base_pos, expected_layers);
2154        false
2155    }
2156}
2157
2158/// Re-point one exact-attention device mirror after a speculative round has
2159/// discarded unaccepted rows. The rows beyond `stored` remain allocated and
2160/// are overwritten by the next append; only the logical cursor moves. This
2161/// is the wgpu twin of Metal's existing mirror cursor helper and keeps the
2162/// MTP graph's speculative/device cache coherent with its real anchor.
2163pub fn graph_kv_set_stored(kv_id: u64, layer: usize, stored: usize) -> bool {
2164    #[cfg(feature = "gpu")]
2165    if backend() == Backend::Wgpu {
2166        return crate::gpu_wgpu::kv_mirror_set_stored(kv_id, layer, stored);
2167    }
2168    #[cfg(target_os = "macos")]
2169    if backend() == Backend::Metal {
2170        crate::gpu_metal::kv_mirror_set_stored(kv_id, layer, stored);
2171        return true;
2172    }
2173    false
2174}
2175
2176/// Rows the wgpu token graph's exact-attention mirror holds for one layer
2177/// (None: no wgpu mirror). Metal keeps its owner cache current per token
2178/// and reports None here.
2179pub fn graph_kv_stored(_kv_id: u64, _layer: usize) -> Option<usize> {
2180    #[cfg(feature = "gpu")]
2181    if backend() == Backend::Wgpu {
2182        return crate::gpu_wgpu::kv_mirror_stored(_kv_id, _layer);
2183    }
2184    None
2185}
2186
2187/// Does the wgpu token graph hold a device-resident recurrent state for
2188/// this layer (one the host `linear_state` has not seen)?
2189pub fn graph_state_resident(_kv_id: u64, _layer: usize) -> bool {
2190    #[cfg(feature = "gpu")]
2191    if backend() == Backend::Wgpu {
2192        return crate::gpu_wgpu::graph_state_resident(_kv_id, _layer);
2193    }
2194    false
2195}
2196
2197/// Copy rows back from the wgpu token graph's K/V mirrors in one submit:
2198/// for each `(layer, from, to)` the K and V rows `[from..to)`, position-major
2199/// (`[(to − from) × nkv × hd]` each).
2200pub fn graph_kv_read_rows(
2201    _kv_id: u64,
2202    _reqs: &[(usize, usize, usize)],
2203    _nkv: usize,
2204    _hd: usize,
2205) -> Option<Vec<(Vec<f32>, Vec<f32>)>> {
2206    #[cfg(feature = "gpu")]
2207    if backend() == Backend::Wgpu {
2208        return crate::gpu_wgpu::kv_mirror_read_rows(_kv_id, _reqs, _nkv, _hd);
2209    }
2210    None
2211}
2212
2213/// Rows `[from, to)` of one wgpu exact-attention mirror in the host
2214/// cache's layout (V zero-padded to `hd`), whatever the mirror's geometry
2215/// (narrow V, a sliding layer's ring). The third value is the first
2216/// position actually read: a ring returns zeros below it. None: no wgpu
2217/// mirror holding those rows at (nkv, hd).
2218pub fn graph_kv_pull_host(
2219    _kv_id: u64,
2220    _layer: usize,
2221    _from: usize,
2222    _to: usize,
2223    _nkv: usize,
2224    _hd: usize,
2225) -> Option<(Vec<f32>, Vec<f32>, usize)> {
2226    #[cfg(feature = "gpu")]
2227    if backend() == Backend::Wgpu {
2228        return crate::gpu_wgpu::kv_mirror_pull_host(_kv_id, _layer, _from, _to, _nkv, _hd);
2229    }
2230    None
2231}
2232
2233/// Drop the wgpu token graph's device K/V mirror for a pipeline.
2234pub fn graph_kv_reset(_kv_id: u64) {
2235    #[cfg(feature = "gpu")]
2236    if backend() == Backend::Wgpu {
2237        crate::gpu_wgpu::kv_mirror_reset(_kv_id);
2238        crate::gpu_wgpu::embryo_graph_reset(_kv_id);
2239    }
2240}
2241
2242/// Ternary (q1t) BASE matvec on the GPU — fills `out` with the base dot; the
2243/// caller adds the sparse overlay on the CPU. Metal only for now (wgpu q1t not
2244/// yet written → CPU fallback).
2245pub fn q1t_matvec(
2246    model: &Arc<CmfModel>,
2247    idx: usize,
2248    xs: &[f32],
2249    rows: usize,
2250    cols: usize,
2251    out: &mut [f32],
2252) -> bool {
2253    match backend() {
2254        #[cfg(target_os = "macos")]
2255        Backend::Metal => {
2256            if metal_q1t_enabled() {
2257                crate::gpu_metal::q1t_matvec(model, idx, xs, rows, cols, out)
2258            } else {
2259                false
2260            }
2261        }
2262        #[cfg(feature = "gpu")]
2263        Backend::Wgpu => crate::gpu_wgpu::q1t_matvec(model, idx, xs, rows, cols, out),
2264        Backend::None => false,
2265    }
2266}
2267
2268/// q4_block matvec on the GPU — wgpu only (Metal drives q4_block through the
2269/// whole-token graph, not a standalone matvec).
2270#[allow(unused_variables)]
2271pub fn q4b_matvec(
2272    model: &Arc<CmfModel>,
2273    idx: usize,
2274    xs: &[f32],
2275    rows: usize,
2276    cols: usize,
2277    out: &mut [f32],
2278) -> bool {
2279    match backend() {
2280        #[cfg(target_os = "macos")]
2281        Backend::Metal => false,
2282        #[cfg(feature = "gpu")]
2283        Backend::Wgpu => crate::gpu_wgpu::q4b_matvec(model, idx, xs, rows, cols, out),
2284        Backend::None => false,
2285    }
2286}
2287
2288/// q1t batched GEMM (prefill) — base + overlay on-device (Metal simdgroup or
2289/// wgpu register-blocked).
2290pub fn q1t_matmat(
2291    model: &Arc<CmfModel>,
2292    idx: usize,
2293    xs: &[f32],
2294    b: usize,
2295    rows: usize,
2296    cols: usize,
2297    out: &mut [f32],
2298) -> bool {
2299    match backend() {
2300        #[cfg(target_os = "macos")]
2301        // Batched prefill and single-token decode are both enabled. On the
2302        // real 14.8B Q1T model prefill PPL was within 0.3% of CPU (7.942 vs
2303        // 7.966), and the alignment-safe decode kernel reached 3.52e-6 max_rel.
2304        Backend::Metal => crate::gpu_metal::q1t_matmat(model, idx, xs, b, rows, cols, out),
2305        #[cfg(feature = "gpu")]
2306        Backend::Wgpu => crate::gpu_wgpu::q1t_matmat(model, idx, xs, b, rows, cols, out),
2307        Backend::None => false,
2308    }
2309}
2310
2311/// Native Metal Q1T switch. Enabled by default after the byte-packed Q1T
2312/// fields were changed to alignment-safe loads; keep an explicit emergency
2313/// fallback for device/driver diagnostics.
2314#[cfg(target_os = "macos")]
2315pub(crate) fn metal_q1t_enabled() -> bool {
2316    std::env::var("CMF_METAL_Q1T")
2317        .map(|v| v != "0" && !v.eq_ignore_ascii_case("off"))
2318        .unwrap_or(true)
2319}
2320
2321/// Batched q1 GEMM (prefill). wgpu only — Metal has its own block path.
2322pub fn q1_matmat(
2323    model: &Arc<CmfModel>,
2324    idx: usize,
2325    xs: &[f32],
2326    b: usize,
2327    rows: usize,
2328    cols: usize,
2329    out: &mut [f32],
2330) -> bool {
2331    match backend() {
2332        #[cfg(feature = "gpu")]
2333        Backend::Wgpu => crate::gpu_wgpu::q1_matmat(model, idx, xs, b, rows, cols, out),
2334        #[allow(unused_variables)]
2335        _ => false,
2336    }
2337}
2338
2339/// Batched q1g GEMM (prefill). wgpu only, like `q1_matmat`.
2340pub fn q1g_matmat(
2341    model: &Arc<CmfModel>,
2342    idx: usize,
2343    xs: &[f32],
2344    b: usize,
2345    rows: usize,
2346    cols: usize,
2347    out: &mut [f32],
2348) -> bool {
2349    match backend() {
2350        #[cfg(feature = "gpu")]
2351        Backend::Wgpu => crate::gpu_wgpu::q1g_matmat(model, idx, xs, b, rows, cols, out),
2352        #[allow(unused_variables)]
2353        _ => false,
2354    }
2355}
2356
2357/// Contention kill for the wide imagegen GEMM/FFN paths: one grossly
2358/// slow op under a work-proportional budget (fair-device ops are
2359/// ≤~100 ms even at 1024px) means another process owns the device —
2360/// verdicts are per-process, so CPU for the rest of this one.
2361static MM_KILL: AtomicBool = AtomicBool::new(false);
2362pub(crate) fn mm_killed() -> bool {
2363    MM_KILL.load(Ordering::Relaxed)
2364}
2365pub(crate) fn mm_kill() {
2366    MM_KILL.store(true, Ordering::Relaxed);
2367}
2368
2369/// Consecutive over-budget ops. ONE slow op is not contention: on a
2370/// 24 GB Mac running the 25.7 GB fl2va file the first ops after the
2371/// prompt encode page their weights in from the SSD and take seconds —
2372/// a field report (hololabs, HF discussion #2) had to neuter the kill
2373/// to keep the denoise on the GPU, and then measured 48 s/step where the
2374/// CPU fallback took >60. Contention is persistent; a page-in is not.
2375static MM_STRIKES: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
2376const MM_STRIKES_TO_KILL: u32 = 3;
2377/// Whether the kill is armed at all. A one-shot phase whose slowness is
2378/// expected and not contention — the video prompt encoder streaming
2379/// 12 GB off the SSD on a 24 GB Mac (HF discussion #4: users had to
2380/// gut `mm_kill` to keep the denoise loop on the GPU) — disarms it and
2381/// re-arms it when the phase is over; strikes taken meanwhile are
2382/// forgotten.
2383static MM_ARMED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
2384
2385/// Disarm / re-arm the contention kill around a phase whose GEMMs are
2386/// slow for reasons that are not another process (see `MM_ARMED`).
2387pub fn mm_kill_arm(on: bool) {
2388    MM_ARMED.store(on, Ordering::Relaxed);
2389    if on {
2390        MM_STRIKES.store(0, Ordering::Relaxed);
2391    }
2392}
2393
2394/// The contention verdict for one wide op: `el` against its
2395/// work-proportional `budget`. `exempt` marks ops whose time is not
2396/// evidence — the cold probe, or a weight that was not resident before
2397/// the call and rode in with it. Kills after `MM_STRIKES_TO_KILL`
2398/// consecutive strikes; a within-budget op clears the count.
2399/// `CMF_MM_KILL=0` disables the kill entirely (the device is trusted).
2400pub(crate) fn mm_budget_check(
2401    what: &str,
2402    el: std::time::Duration,
2403    budget: std::time::Duration,
2404    exempt: bool,
2405) {
2406    if el <= budget {
2407        MM_STRIKES.store(0, Ordering::Relaxed);
2408        return;
2409    }
2410    if exempt || !MM_ARMED.load(Ordering::Relaxed) {
2411        return;
2412    }
2413    static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2414    let on = *ON.get_or_init(|| std::env::var("CMF_MM_KILL").as_deref() != Ok("0"));
2415    let n = MM_STRIKES.fetch_add(1, Ordering::Relaxed) + 1;
2416    if !on {
2417        tracing::info!(
2418            "gpu {what} took {el:?} (budget {budget:?}) — over budget, CMF_MM_KILL=0 keeps the device"
2419        );
2420        return;
2421    }
2422    if n >= MM_STRIKES_TO_KILL {
2423        tracing::warn!(
2424            "gpu {what} took {el:?} (budget {budget:?}), {n} in a row — \
2425             device contended, CPU for the rest of the process (CMF_MM_KILL=0 to override)"
2426        );
2427        mm_kill();
2428    } else {
2429        tracing::info!(
2430            "gpu {what} took {el:?} (budget {budget:?}) — strike {n} of {MM_STRIKES_TO_KILL}"
2431        );
2432    }
2433}
2434
2435/// Fused DiT SwiGLU FFN on the device: g=X·W1ᵀ, u=X·W3ᵀ, silu(g)·u,
2436/// Causal chunk attention on the device: `b` queries against `s0 + b`
2437/// cached keys. wgpu only — Metal's chunk graph keeps attention inside
2438/// the resident block and never calls out.
2439#[allow(unused_variables, clippy::too_many_arguments)]
2440pub fn chunk_attend(
2441    q: &[f32],
2442    k: &[&[f32]],
2443    v: &[&[f32]],
2444    b: usize,
2445    s0: usize,
2446    nh: usize,
2447    nkv: usize,
2448    hd: usize,
2449    scale: f32,
2450    out: &mut [f32],
2451) -> bool {
2452    match backend() {
2453        #[cfg(feature = "gpu")]
2454        Backend::Wgpu => crate::gpu_wgpu::chunk_attend(q, k, v, b, s0, nh, nkv, hd, scale, out),
2455        #[allow(unreachable_patterns)]
2456        _ => false,
2457    }
2458}
2459
2460/// `chunk_attend` under a sliding window (`window` keys, the query's own
2461/// included). wgpu only; see `chunk_attend_windowed`.
2462#[allow(unused_variables, clippy::too_many_arguments)]
2463pub fn chunk_attend_win(
2464    q: &[f32],
2465    k: &[&[f32]],
2466    v: &[&[f32]],
2467    b: usize,
2468    s0: usize,
2469    nh: usize,
2470    nkv: usize,
2471    hd: usize,
2472    scale: f32,
2473    window: usize,
2474    out: &mut [f32],
2475) -> bool {
2476    match backend() {
2477        #[cfg(feature = "gpu")]
2478        Backend::Wgpu => {
2479            crate::gpu_wgpu::chunk_attend_win(q, k, v, b, s0, nh, nkv, hd, scale, window, out)
2480        }
2481        #[allow(unreachable_patterns)]
2482        _ => false,
2483    }
2484}
2485
2486/// Several projections of one host activation, read back in one trip:
2487/// `out` = [P0 | P1 | …], each `idxs` entry (tensor, rows). wgpu only.
2488#[allow(unused_variables)]
2489pub fn gemm_many_keep(
2490    model: &Arc<CmfModel>,
2491    idxs: &[(usize, usize)],
2492    xs: &[f32],
2493    b: usize,
2494    cols: usize,
2495    out: &mut [f32],
2496) -> bool {
2497    match backend() {
2498        #[cfg(feature = "gpu")]
2499        Backend::Wgpu => crate::gpu_wgpu::gemm_many_keep(model, idxs, xs, b, cols, out),
2500        #[allow(unreachable_patterns)]
2501        _ => false,
2502    }
2503}
2504
2505thread_local! {
2506    /// Set by the pipeline around a layer of Spark-X2.5's batched prefill:
2507    /// its int8 matrix-unit GEMMs may take the `zi_mm` tile, and the
2508    /// projections that read one activation share its upload (wgpu).
2509    static PREFILL_FAST_GEMM: Cell<bool> = const { Cell::new(false) };
2510}
2511
2512/// Restores the previous flag on drop.
2513pub struct PrefillFastGemmGuard(bool);
2514
2515impl Drop for PrefillFastGemmGuard {
2516    fn drop(&mut self) {
2517        PREFILL_FAST_GEMM.with(|c| c.set(self.0));
2518    }
2519}
2520
2521/// Let this thread's int8 matrix-unit GEMMs take the `zi_mm` tile for the
2522/// guard's lifetime (bit-identical to the 64x64 kernel; see
2523/// `gpu_wgpu::zi_gemm_route`).
2524pub fn enter_prefill_fast_gemm() -> PrefillFastGemmGuard {
2525    PrefillFastGemmGuard(PREFILL_FAST_GEMM.with(|c| c.replace(true)))
2526}
2527
2528/// Whether `enter_prefill_fast_gemm` is in force on this thread.
2529pub fn prefill_fast_gemm() -> bool {
2530    PREFILL_FAST_GEMM.with(|c| c.get())
2531}
2532
2533/// The largest finite f16. An operand the matrix-unit attention casts to
2534/// f16 must stay within it, or the cast overflows to inf.
2535pub const F16_MAX: f32 = 65504.0;
2536
2537/// max |x| over `xs`, or +inf when an entry is inf or NaN: the value the
2538/// f16 operand guards compare with `F16_MAX`. Sixteen independent lanes
2539/// and a flag, so it vectorizes (a single `fold` chain does not).
2540pub fn abs_max_or_inf(xs: &[f32]) -> f32 {
2541    let mut acc = [0f32; 16];
2542    let mut bad = [false; 16];
2543    let mut it = xs.chunks_exact(16);
2544    for c in &mut it {
2545        for ((a, f), &v) in acc.iter_mut().zip(bad.iter_mut()).zip(c) {
2546            let v = v.abs();
2547            // NaN compares false both ways: flagged, never taken.
2548            *f |= !(v < f32::INFINITY);
2549            *a = if v > *a { v } else { *a };
2550        }
2551    }
2552    let mut m = 0f32;
2553    for &v in it.remainder() {
2554        let v = v.abs();
2555        if !(v < f32::INFINITY) {
2556            return f32::INFINITY;
2557        }
2558        m = m.max(v);
2559    }
2560    if bad.iter().any(|&f| f) {
2561        return f32::INFINITY;
2562    }
2563    acc.iter().fold(m, |m, &a| m.max(a))
2564}
2565
2566/// The decode graph's device K/V mirror a prefill chunk of one layer also
2567/// writes to (wgpu): `kv_id` is the pipeline's graph id, `layer` the
2568/// virtual layer index, `limit` the cache's `max_seq_len` — the same key
2569/// and capacity ceiling the token graph uses, so the mirror the prefill
2570/// fills is the one the first decode token reads.
2571#[derive(Clone, Copy, Debug, PartialEq, Eq)]
2572pub struct PrefillMirror {
2573    pub kv_id: u64,
2574    pub layer: usize,
2575    pub limit: usize,
2576}
2577
2578thread_local! {
2579    /// Set by the pipeline around one layer's batched attention (see
2580    /// `enter_prefill_mirror`); every other caller of the batched
2581    /// attention (MTP heads, tests) sees None and keeps the host upload.
2582    static PREFILL_MIRROR: Cell<Option<PrefillMirror>> = const { Cell::new(None) };
2583}
2584
2585/// Restores the previous target on drop.
2586pub struct PrefillMirrorGuard(Option<PrefillMirror>);
2587
2588impl Drop for PrefillMirrorGuard {
2589    fn drop(&mut self) {
2590        PREFILL_MIRROR.with(|c| c.set(self.0));
2591    }
2592}
2593
2594/// Name the device mirror the current layer's prefill chunk appends to
2595/// and attends against, for the guard's lifetime.
2596pub fn enter_prefill_mirror(t: PrefillMirror) -> PrefillMirrorGuard {
2597    PrefillMirrorGuard(PREFILL_MIRROR.with(|c| c.replace(Some(t))))
2598}
2599
2600/// The mirror `enter_prefill_mirror` named on this thread, if any.
2601pub fn prefill_mirror() -> Option<PrefillMirror> {
2602    PREFILL_MIRROR.with(|c| c.get())
2603}
2604
2605/// `chunk_attend_win` against the decode graph's device mirror instead of
2606/// an upload of the whole prefix: the host rows the mirror lacks (the
2607/// chunk's own `b`, or more after a host-only chunk) are appended first,
2608/// then the attention binds the mirror rows in place. `s0` is the chunk's
2609/// first position and `base` the position of host row 0
2610/// (`LayerKvCache::base`: past 0 on a trimmed sliding tail). `ring` is the
2611/// layer's window as the mirror geometry (Some on every sliding layer),
2612/// `window` the window the softmax masks with (0 while it masks nothing).
2613/// `operand_max` = max |x| over `q` and every K/V row the attend reads
2614/// (`abs_max_or_inf`; +inf when unknown): past `F16_MAX` the f32 kernels
2615/// run instead of the matrix units. wgpu only; false = nothing attended
2616/// (the caller uploads as before).
2617#[allow(unused_variables, clippy::too_many_arguments)]
2618pub fn chunk_attend_mirror(
2619    t: PrefillMirror,
2620    cpu_k: &[Vec<f32>],
2621    cpu_v: &[Vec<f32>],
2622    base: usize,
2623    q: &[f32],
2624    b: usize,
2625    s0: usize,
2626    nh: usize,
2627    nkv: usize,
2628    hd: usize,
2629    scale: f32,
2630    ring: Option<usize>,
2631    window: usize,
2632    operand_max: f32,
2633    out: &mut [f32],
2634) -> bool {
2635    match backend() {
2636        #[cfg(feature = "gpu")]
2637        // QKᵀ and P·V on the matrix units where the device has them (f16
2638        // operands, f32 accumulators — not bit-identical to the f32
2639        // kernels: Spark-X2.5 4B q8_2f wiki ppl 10.677 -> 10.672);
2640        // `CMF_PREFILL_ATTN_COOP=0` keeps the f32 ones (A/B).
2641        Backend::Wgpu => crate::gpu_wgpu::chunk_attend_mirror(
2642            t.kv_id,
2643            t.layer,
2644            t.limit,
2645            cpu_k,
2646            cpu_v,
2647            base,
2648            q,
2649            b,
2650            s0,
2651            nh,
2652            nkv,
2653            hd,
2654            scale,
2655            ring,
2656            window,
2657            std::env::var("CMF_PREFILL_ATTN_COOP").as_deref() != Ok("0"),
2658            operand_max,
2659            out,
2660        ),
2661        #[allow(unreachable_patterns)]
2662        _ => false,
2663    }
2664}
2665
2666/// `chunk_attend_mirror`, then the head gate (`gains`, b × nh, computed on
2667/// the host) and the O projection `wo` on the card: the attention output
2668/// never comes home. `out` = [b][hidden]. The card scans the O operand
2669/// itself, which is the host path's product bit for bit only while max
2670/// |gated output| <= 1000: the caller vouches for that (Spark-X2.5's
2671/// prefill, max |v| <= 990). wgpu only; false = nothing written to `out`
2672/// (the caller attends and projects as before).
2673#[allow(unused_variables, clippy::too_many_arguments)]
2674pub fn chunk_attend_mirror_wo(
2675    t: PrefillMirror,
2676    cpu_k: &[Vec<f32>],
2677    cpu_v: &[Vec<f32>],
2678    base: usize,
2679    q: &[f32],
2680    b: usize,
2681    s0: usize,
2682    nh: usize,
2683    nkv: usize,
2684    hd: usize,
2685    scale: f32,
2686    ring: Option<usize>,
2687    window: usize,
2688    operand_max: f32,
2689    gains: Option<&[f32]>,
2690    wo: (&Arc<CmfModel>, usize),
2691    hidden: usize,
2692    out: &mut [f32],
2693) -> bool {
2694    match backend() {
2695        #[cfg(feature = "gpu")]
2696        Backend::Wgpu => crate::gpu_wgpu::chunk_attend_mirror_wo(
2697            t.kv_id,
2698            t.layer,
2699            t.limit,
2700            cpu_k,
2701            cpu_v,
2702            base,
2703            q,
2704            b,
2705            s0,
2706            nh,
2707            nkv,
2708            hd,
2709            scale,
2710            ring,
2711            window,
2712            std::env::var("CMF_PREFILL_ATTN_COOP").as_deref() != Ok("0"),
2713            operand_max,
2714            gains,
2715            wo,
2716            hidden,
2717            out,
2718        ),
2719        #[allow(unreachable_patterns)]
2720        _ => false,
2721    }
2722}
2723
2724/// Whether the active backend's chunk attend can apply a sliding window.
2725pub fn chunk_attend_windowed() -> bool {
2726    match backend() {
2727        #[cfg(feature = "gpu")]
2728        Backend::Wgpu => true,
2729        #[allow(unreachable_patterns)]
2730        _ => false,
2731    }
2732}
2733
2734/// Fused QKV projection: one upload of the normed chunk, three GEMMs,
2735/// one readback of Q|K|V back to back. Metal has no twin yet — its
2736/// chunk graph keeps the whole layer resident and never surfaces QKV.
2737#[allow(unused_variables, clippy::too_many_arguments)]
2738pub fn q4t_qkv(
2739    model: &Arc<CmfModel>,
2740    wq: usize,
2741    wk: usize,
2742    wv: usize,
2743    xs: &[f32],
2744    b: usize,
2745    cols: usize,
2746    rq: usize,
2747    rk: usize,
2748    rv: usize,
2749    out: &mut [f32],
2750) -> bool {
2751    match backend() {
2752        #[cfg(feature = "gpu")]
2753        Backend::Wgpu => crate::gpu_wgpu::q4t_qkv(model, wq, wk, wv, xs, b, cols, rq, rk, rv, out),
2754        #[allow(unreachable_patterns)]
2755        _ => false,
2756    }
2757}
2758
2759/// y=·W2ᵀ — one command buffer, only X and Y cross the CPU boundary.
2760#[allow(unused_variables, clippy::too_many_arguments)]
2761/// SwiGLU FFN with a row-packed [gate|up] fc1 (MiniMax-H3's DiT), run
2762/// end to end on the device. wgpu only: Metal keeps the host loop until
2763/// its own packed kernel exists.
2764#[allow(clippy::too_many_arguments, unused_variables)]
2765pub fn q4tp_ffn_packed(
2766    model: &Arc<CmfModel>,
2767    w1: usize,
2768    w2: usize,
2769    xs: &[f32],
2770    b: usize,
2771    hidden: usize,
2772    inter: usize,
2773    bias: Option<&[f32]>,
2774    out: &mut [f32],
2775) -> bool {
2776    match backend() {
2777        #[cfg(feature = "gpu")]
2778        Backend::Wgpu => {
2779            crate::gpu_wgpu::ffn_packed(model, w1, w2, xs, b, hidden, inter, bias, out)
2780        }
2781        #[allow(unreachable_patterns)]
2782        _ => false,
2783    }
2784}
2785
2786pub fn q4tp_ffn(
2787    model: &Arc<CmfModel>,
2788    w1: usize,
2789    w3: usize,
2790    w2: usize,
2791    xs: &[f32],
2792    b: usize,
2793    hidden: usize,
2794    inter: usize,
2795    out: &mut [f32],
2796) -> bool {
2797    match backend() {
2798        #[cfg(target_os = "macos")]
2799        Backend::Metal => crate::gpu_metal::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2800        #[cfg(feature = "gpu")]
2801        Backend::Wgpu => crate::gpu_wgpu::q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2802        #[allow(unreachable_patterns)]
2803        _ => false,
2804    }
2805}
2806
2807/// `q4tp_ffn` (`q4tp` = true) / `q4t_ffn` with the activation named:
2808/// SiLU takes the plain entry points (every backend), the exact GELU of
2809/// Spark-X2.5 has a wgpu arm only — other backends decline it.
2810#[allow(clippy::too_many_arguments, unused_variables)]
2811pub fn q4_ffn_act(
2812    model: &Arc<CmfModel>,
2813    w1: usize,
2814    w3: usize,
2815    w2: usize,
2816    xs: &[f32],
2817    b: usize,
2818    hidden: usize,
2819    inter: usize,
2820    q4tp: bool,
2821    act: GraphAct,
2822    out: &mut [f32],
2823) -> bool {
2824    match act {
2825        GraphAct::Silu if q4tp => q4tp_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2826        GraphAct::Silu => q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2827        GraphAct::GeluErf | GraphAct::GeluTanh => match backend() {
2828            #[cfg(feature = "gpu")]
2829            Backend::Wgpu => crate::gpu_wgpu::q4_ffn_act(
2830                model,
2831                w1,
2832                w3,
2833                w2,
2834                xs,
2835                b,
2836                hidden,
2837                inter,
2838                q4tp,
2839                act.code(),
2840                out,
2841            ),
2842            #[allow(unreachable_patterns)]
2843            _ => false,
2844        },
2845    }
2846}
2847
2848/// The LLM prefill's dense FFN for separate gate/up/down weights in any
2849/// codec with a device GEMM (int8, q4tp, or a mix of them): the panels
2850/// stay on the card between the projections. wgpu only; other backends
2851/// decline and the caller keeps its per-GEMM path.
2852#[allow(clippy::too_many_arguments, unused_variables)]
2853pub fn ffn_act_keep(
2854    model: &Arc<CmfModel>,
2855    w1: usize,
2856    w3: usize,
2857    w2: usize,
2858    xs: &[f32],
2859    b: usize,
2860    hidden: usize,
2861    inter: usize,
2862    act: GraphAct,
2863    out: &mut [f32],
2864) -> bool {
2865    match backend() {
2866        #[cfg(feature = "gpu")]
2867        Backend::Wgpu => crate::gpu_wgpu::ffn_act_keep(
2868            model,
2869            w1,
2870            w3,
2871            w2,
2872            xs,
2873            b,
2874            hidden,
2875            inter,
2876            act.code(),
2877            out,
2878        ),
2879        #[allow(unreachable_patterns)]
2880        _ => false,
2881    }
2882}
2883
2884/// Qwen Image's exact two-projection tanh-GELU FFN.  The WGPU arm keeps the
2885/// intermediate on the device; other backends decline so the caller retains
2886/// its bounded CPU path.  `bias_in` is applied before GELU and `bias_out`
2887/// after the second projection, matching the official transformer.
2888#[allow(clippy::too_many_arguments, unused_variables)]
2889pub fn q4tp_gelu_ffn(
2890    model: &Arc<CmfModel>,
2891    w_in: usize,
2892    w_out: usize,
2893    xs: &[f32],
2894    b: usize,
2895    hidden: usize,
2896    inter: usize,
2897    bias_in: &[f32],
2898    bias_out: &[f32],
2899    out: &mut [f32],
2900) -> bool {
2901    match backend() {
2902        #[cfg(feature = "gpu")]
2903        Backend::Wgpu => crate::gpu_wgpu::q4tp_gelu_ffn(
2904            model, w_in, w_out, xs, b, hidden, inter, bias_in, bias_out, out,
2905        ),
2906        #[allow(unreachable_patterns)]
2907        _ => false,
2908    }
2909}
2910
2911pub fn q4t_ffn(
2912    model: &Arc<CmfModel>,
2913    w1: usize,
2914    w3: usize,
2915    w2: usize,
2916    xs: &[f32],
2917    b: usize,
2918    hidden: usize,
2919    inter: usize,
2920    out: &mut [f32],
2921) -> bool {
2922    match backend() {
2923        #[cfg(target_os = "macos")]
2924        Backend::Metal => crate::gpu_metal::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2925        #[cfg(feature = "gpu")]
2926        Backend::Wgpu => crate::gpu_wgpu::q4t_ffn(model, w1, w3, w2, xs, b, hidden, inter, out),
2927        #[allow(unreachable_patterns)]
2928        _ => false,
2929    }
2930}
2931
2932/// One whole modulated DiT block for `dit_block`: geometry, norm
2933/// weights, AdaLN scale/gate vectors (gates pre-tanh'd), a per-token
2934/// f32 RoPE cos/sin table, and the directory indices of the seven
2935/// q4t projections. `x` is in-out `[n, hidden]`.
2936pub struct DitBlockArgs<'a> {
2937    pub n: usize,
2938    pub hidden: usize,
2939    pub inter: usize,
2940    pub nh: usize,
2941    pub nkv: usize,
2942    pub hd: usize,
2943    pub eps: f32,
2944    pub rope_cos: &'a [f32],
2945    pub rope_sin: &'a [f32],
2946    pub norm1: &'a [f32],
2947    pub norm2: &'a [f32],
2948    pub ffn_norm1: &'a [f32],
2949    pub ffn_norm2: &'a [f32],
2950    pub norm_q: &'a [f32],
2951    pub norm_k: &'a [f32],
2952    pub s_msa: &'a [f32],
2953    pub gate_msa: &'a [f32],
2954    pub s_mlp: &'a [f32],
2955    pub gate_mlp: &'a [f32],
2956    pub wq: usize,
2957    pub wk: usize,
2958    pub wv: usize,
2959    pub wo: usize,
2960    pub w1: usize,
2961    pub w3: usize,
2962    pub w2: usize,
2963    /// The projections' layout: q4tp (ladder scales) vs plain q4_tiled.
2964    /// The recommended Lumina file is q4tp, and a backend that only
2965    /// knows q4t must decline rather than decode with the wrong reader.
2966    pub q4tp: bool,
2967    /// The hidden state is already on the device from the previous block,
2968    /// so `x` need not be uploaded.
2969    pub resident_in: bool,
2970    /// Leave the result on the device instead of reading it back. The DiT
2971    /// loop does not touch `x` between blocks, so 27 of every 28 readbacks
2972    /// were moving 19 MB across PCIe and stalling on it for nothing.
2973    pub resident_out: bool,
2974}
2975
2976/// Can the selected backend keep the DiT's hidden state on the device
2977/// between blocks? Only the wgpu whole-block path; the Metal entry takes
2978/// and returns host memory every call.
2979pub fn dit_chain_supported() -> bool {
2980    #[cfg(feature = "gpu")]
2981    {
2982        return matches!(backend(), Backend::Wgpu) && fused_dit_block_available();
2983    }
2984    #[allow(unreachable_code)]
2985    false
2986}
2987
2988/// Pull the resident hidden state back to the host. For the caller that
2989/// chained blocks and then hit one the device declined.
2990pub fn dit_state_fetch(_x: &mut [f32]) -> bool {
2991    #[cfg(feature = "gpu")]
2992    {
2993        if matches!(backend(), Backend::Wgpu) {
2994            return crate::gpu_wgpu::dit_state_fetch(_x);
2995        }
2996    }
2997    false
2998}
2999
3000/// One whole modulated DiT block on the device — norms, qkv, RoPE,
3001/// attention, residuals and the SwiGLU FFN in a single command
3002/// buffer; only `x` crosses the CPU boundary (in and out).
3003#[allow(unused_variables)]
3004/// The DiT's three projections in one submission (wgpu only; the
3005/// Metal path fuses the whole block instead). False = the caller keeps
3006/// its three separate calls.
3007#[allow(unused_variables, clippy::too_many_arguments)]
3008pub fn dit_qkv(
3009    model: &Arc<CmfModel>,
3010    wq: usize,
3011    wk: usize,
3012    wv: usize,
3013    xs: &[f32],
3014    b: usize,
3015    hidden: usize,
3016    qrows: usize,
3017    kvrows: usize,
3018    q_out: &mut [f32],
3019    k_out: &mut [f32],
3020    v_out: &mut [f32],
3021) -> bool {
3022    match backend() {
3023        #[cfg(feature = "gpu")]
3024        Backend::Wgpu => crate::gpu_wgpu::q4tp_qkv(
3025            model, wq, wk, wv, xs, b, hidden, qrows, kvrows, q_out, k_out, v_out,
3026        ),
3027        #[allow(unreachable_patterns)]
3028        _ => false,
3029    }
3030}
3031
3032/// The Qwen Image double-stream attention half.  The WGPU implementation
3033/// keeps the six Q/K/V projections, the stream join, qk-norm/RoPE, joint
3034/// attention, and both output projections on the device; the caller only
3035/// supplies the two normalized streams and receives the two projected
3036/// streams.  A backend or codec that cannot satisfy the full contract
3037/// returns `false` before changing either output, so the native host path
3038/// remains the portable fallback.
3039pub struct QwenImageAttentionArgs<'a> {
3040    pub image: &'a [f32],
3041    pub text: &'a [f32],
3042    pub image_tokens: usize,
3043    pub text_tokens: usize,
3044    pub heads: usize,
3045    pub head_dim: usize,
3046    pub image_q: usize,
3047    pub image_k: usize,
3048    pub image_v: usize,
3049    pub text_q: usize,
3050    pub text_k: usize,
3051    pub text_v: usize,
3052    pub image_out: usize,
3053    pub text_out: usize,
3054    pub image_q_norm: &'a [f32],
3055    pub image_k_norm: &'a [f32],
3056    pub text_q_norm: &'a [f32],
3057    pub text_k_norm: &'a [f32],
3058    pub image_cos: &'a [f32],
3059    pub image_sin: &'a [f32],
3060    pub text_cos: &'a [f32],
3061    pub text_sin: &'a [f32],
3062    pub image_q_bias: &'a [f32],
3063    pub image_k_bias: &'a [f32],
3064    pub image_v_bias: &'a [f32],
3065    pub text_q_bias: &'a [f32],
3066    pub text_k_bias: &'a [f32],
3067    pub text_v_bias: &'a [f32],
3068    pub image_out_bias: &'a [f32],
3069    pub text_out_bias: &'a [f32],
3070    pub image_proj: &'a mut [f32],
3071    pub text_proj: &'a mut [f32],
3072}
3073
3074/// The per-layer controls and Q4TP directory indices used by the native
3075/// Qwen block.  Keeping this descriptor separate from the stream buffers
3076/// lets a whole transformer forward reuse one explicit device state without
3077/// a global scratch slot or a hidden context label.
3078#[allow(clippy::too_many_fields)]
3079pub struct QwenImageChainBlock<'a> {
3080    pub image_mod: &'a [f32],
3081    pub text_mod: &'a [f32],
3082    pub image_q: usize,
3083    pub image_k: usize,
3084    pub image_v: usize,
3085    pub text_q: usize,
3086    pub text_k: usize,
3087    pub text_v: usize,
3088    pub image_out: usize,
3089    pub text_out: usize,
3090    pub image_q_norm: &'a [f32],
3091    pub image_k_norm: &'a [f32],
3092    pub text_q_norm: &'a [f32],
3093    pub text_k_norm: &'a [f32],
3094    pub image_q_bias: &'a [f32],
3095    pub image_k_bias: &'a [f32],
3096    pub image_v_bias: &'a [f32],
3097    pub text_q_bias: &'a [f32],
3098    pub text_k_bias: &'a [f32],
3099    pub text_v_bias: &'a [f32],
3100    pub image_out_bias: &'a [f32],
3101    pub text_out_bias: &'a [f32],
3102    pub image_attn_gate: &'a [f32],
3103    pub text_attn_gate: &'a [f32],
3104    pub image_mlp_in: usize,
3105    pub image_mlp_out: usize,
3106    pub text_mlp_in: usize,
3107    pub text_mlp_out: usize,
3108    pub image_mlp_in_bias: &'a [f32],
3109    pub image_mlp_out_bias: &'a [f32],
3110    pub text_mlp_in_bias: &'a [f32],
3111    pub text_mlp_out_bias: &'a [f32],
3112}
3113
3114/// Complete Qwen Image transformer block contract. The first norm/mod
3115/// panels are supplied by the native caller; the WGPU arm keeps both streams
3116/// resident through QKV, QK/RoPE, joint attention, output projections, both
3117/// gated residuals, and the exact tanh-GELU MLPs. A backend that cannot
3118/// satisfy the whole graph returns `false` without changing either output.
3119#[allow(clippy::too_many_fields)]
3120pub struct QwenImageBlockArgs<'a> {
3121    /// Raw stream state is read for the first gated residual and overwritten
3122    /// with the block's final state after the one readback.
3123    pub image: &'a mut [f32],
3124    pub text: &'a mut [f32],
3125    pub image_norm: &'a [f32],
3126    pub text_norm: &'a [f32],
3127    pub image_tokens: usize,
3128    pub text_tokens: usize,
3129    pub heads: usize,
3130    pub head_dim: usize,
3131    pub image_cos: &'a [f32],
3132    pub image_sin: &'a [f32],
3133    pub text_cos: &'a [f32],
3134    pub text_sin: &'a [f32],
3135    pub image_q: usize,
3136    pub image_k: usize,
3137    pub image_v: usize,
3138    pub text_q: usize,
3139    pub text_k: usize,
3140    pub text_v: usize,
3141    pub image_out: usize,
3142    pub text_out: usize,
3143    pub image_q_norm: &'a [f32],
3144    pub image_k_norm: &'a [f32],
3145    pub text_q_norm: &'a [f32],
3146    pub text_k_norm: &'a [f32],
3147    pub image_q_bias: &'a [f32],
3148    pub image_k_bias: &'a [f32],
3149    pub image_v_bias: &'a [f32],
3150    pub text_q_bias: &'a [f32],
3151    pub text_k_bias: &'a [f32],
3152    pub text_v_bias: &'a [f32],
3153    pub image_out_bias: &'a [f32],
3154    pub text_out_bias: &'a [f32],
3155    pub image_attn_gate: &'a [f32],
3156    pub text_attn_gate: &'a [f32],
3157    pub image_mlp_in: usize,
3158    pub image_mlp_out: usize,
3159    pub text_mlp_in: usize,
3160    pub text_mlp_out: usize,
3161    pub image_mlp_in_bias: &'a [f32],
3162    pub image_mlp_out_bias: &'a [f32],
3163    pub text_mlp_in_bias: &'a [f32],
3164    pub text_mlp_out_bias: &'a [f32],
3165    pub image_mlp_mod: &'a [f32],
3166    pub text_mlp_mod: &'a [f32],
3167    pub image_mlp_gate: &'a [f32],
3168    pub text_mlp_gate: &'a [f32],
3169}
3170
3171/// Explicit whole-forward Qwen state contract.  The WGPU backend uploads the
3172/// two initial streams once, encodes a bounded number of complete blocks per
3173/// submission, and reads the final state once.  `blocks` is immutable for the
3174/// call, while the two stream slices receive only the final readback.
3175pub struct QwenImageChainArgs<'a> {
3176    pub image: &'a mut [f32],
3177    pub text: &'a mut [f32],
3178    pub image_tokens: usize,
3179    pub text_tokens: usize,
3180    pub heads: usize,
3181    pub head_dim: usize,
3182    pub image_cos: &'a [f32],
3183    pub image_sin: &'a [f32],
3184    pub text_cos: &'a [f32],
3185    pub text_sin: &'a [f32],
3186    pub blocks: &'a [QwenImageChainBlock<'a>],
3187}
3188
3189#[allow(unused_variables)]
3190pub fn qwen_image_attention(
3191    model: &Arc<CmfModel>,
3192    args: &mut QwenImageAttentionArgs<'_>,
3193) -> bool {
3194    match backend() {
3195        #[cfg(feature = "gpu")]
3196        Backend::Wgpu => crate::gpu_wgpu::qwen_image_attention(model, args),
3197        #[allow(unreachable_patterns)]
3198        _ => false,
3199    }
3200}
3201
3202#[allow(unused_variables)]
3203pub fn qwen_image_block(model: &Arc<CmfModel>, args: &mut QwenImageBlockArgs<'_>) -> bool {
3204    match backend() {
3205        #[cfg(feature = "gpu")]
3206        Backend::Wgpu => crate::gpu_wgpu::qwen_image_block(model, args),
3207        #[allow(unreachable_patterns)]
3208        _ => false,
3209    }
3210}
3211
3212/// Keep all Qwen transformer blocks on the selected WGPU device, with only
3213/// bounded chunk submissions and one final readback.  Other backends decline
3214/// so the native caller can use its exact portable block loop.
3215#[allow(unused_variables)]
3216pub fn qwen_image_chain(model: &Arc<CmfModel>, args: &mut QwenImageChainArgs<'_>) -> bool {
3217    match backend() {
3218        #[cfg(feature = "gpu")]
3219        Backend::Wgpu => crate::gpu_wgpu::qwen_image_chain(model, args),
3220        #[allow(unreachable_patterns)]
3221        _ => false,
3222    }
3223}
3224
3225/// The Qwen Image second sub-block on WGPU: affine-free LayerNorm,
3226/// shift/scale modulation, Q4TP input projection, exact tanh-GELU, output
3227/// projection, bias and gated residual.  `data` is updated in place after a
3228/// single final readback.  Backends/codecs that cannot keep this chain on the
3229/// device return `false` before changing `data`, leaving the caller's
3230/// portable per-op path intact.
3231#[allow(unused_variables, clippy::too_many_arguments)]
3232pub fn qwen_image_mlp_inplace(
3233    model: &Arc<CmfModel>,
3234    w_in: usize,
3235    w_out: usize,
3236    data: &mut [f32],
3237    batch: usize,
3238    hidden: usize,
3239    inter: usize,
3240    bias_in: &[f32],
3241    bias_out: &[f32],
3242    modulation: &[f32],
3243    gate: &[f32],
3244) -> bool {
3245    match backend() {
3246        #[cfg(feature = "gpu")]
3247        Backend::Wgpu => crate::gpu_wgpu::qwen_image_mlp_inplace(
3248            model,
3249            w_in,
3250            w_out,
3251            data,
3252            batch,
3253            hidden,
3254            inter,
3255            bias_in,
3256            bias_out,
3257            modulation,
3258            gate,
3259        ),
3260        #[allow(unreachable_patterns)]
3261        _ => false,
3262    }
3263}
3264
3265/// Is a FUSED whole-block device path on offer? The batched-CFG shape
3266/// (two sequences in one tall batch) and the fused block (one sequence,
3267/// one command buffer) are alternatives, and the caller picks.
3268pub fn fused_dit_block_available() -> bool {
3269    #[cfg(target_os = "macos")]
3270    {
3271        matches!(backend(), Backend::Metal) && fused_block_trusted()
3272    }
3273    #[cfg(not(target_os = "macos"))]
3274    {
3275        false
3276    }
3277}
3278
3279pub fn dit_block(model: &Arc<CmfModel>, a: &DitBlockArgs, x: &mut [f32]) -> bool {
3280    dit_block_seg(model, a, &[a.n], x)
3281}
3282
3283/// The same block over a CONCATENATION of independent sequences:
3284/// attention per segment, everything position-wise batched. wgpu only —
3285/// the Metal path takes the single-sequence entry above.
3286pub fn dit_block_seg(
3287    model: &Arc<CmfModel>,
3288    a: &DitBlockArgs,
3289    segs: &[usize],
3290    x: &mut [f32],
3291) -> bool {
3292    match backend() {
3293        #[cfg(target_os = "macos")]
3294        Backend::Metal if segs.len() <= 1 => crate::gpu_metal::dit_block(model, a, x),
3295        // The wgpu whole-block path. What it buys is host round trips —
3296        // six a block become one — so it defaults ON where those cost
3297        // real time (a discrete card across PCIe) and OFF on unified
3298        // memory, where the per-op path shares the same pages and the
3299        // fusion measured slightly slower on an M4. `CMF_DIT_FUSED=1`
3300        // forces it anywhere, `=0` forbids it.
3301        #[cfg(feature = "gpu")]
3302        Backend::Wgpu
3303            if match std::env::var("CMF_DIT_FUSED").ok().as_deref() {
3304                Some("0") => false,
3305                Some(_) => true,
3306                None => crate::gpu_wgpu::discrete_active(),
3307            } =>
3308        {
3309            crate::gpu_wgpu::dit_block_seg(model, a, segs, x)
3310        }
3311        #[allow(unreachable_patterns)]
3312        _ => false,
3313    }
3314}
3315
3316/// One VAE resnet block for `vae_resnet`: norm/conv weights and the
3317/// channel/shape geometry. `shortcut` is the 1×1 projection (w, b, k)
3318/// when in/out channels differ.
3319pub struct VaeResnetArgs<'a> {
3320    pub groups: usize,
3321    pub ic: usize,
3322    pub oc: usize,
3323    pub h: usize,
3324    pub w: usize,
3325    pub n1w: &'a [f32],
3326    pub n1b: &'a [f32],
3327    pub c1w: &'a [f32],
3328    pub c1b: &'a [f32],
3329    pub c1k: usize,
3330    pub n2w: &'a [f32],
3331    pub n2b: &'a [f32],
3332    pub c2w: &'a [f32],
3333    pub c2b: &'a [f32],
3334    pub c2k: usize,
3335    pub shortcut: Option<(&'a [f32], &'a [f32], usize)>,
3336}
3337
3338/// One whole VAE resnet block on the device (norm+silu → conv ×2 →
3339/// shortcut → add, one command buffer).
3340#[allow(unused_variables)]
3341pub fn vae_resnet(a: &VaeResnetArgs, x: &[f32], out: &mut [f32]) -> bool {
3342    match backend() {
3343        #[cfg(target_os = "macos")]
3344        Backend::Metal => crate::gpu_metal::vae_resnet(a, x, out),
3345        _ => false,
3346    }
3347}
3348
3349/// Nearest-2× upsample fused with the following conv — the small
3350/// pre-upsample image is what crosses the CPU boundary.
3351#[allow(unused_variables, clippy::too_many_arguments)]
3352pub fn vae_upsample_conv(
3353    w: &[f32],
3354    bias: &[f32],
3355    x: &[f32],
3356    ic: usize,
3357    oc: usize,
3358    h: usize,
3359    w_img: usize,
3360    k: usize,
3361    out: &mut [f32],
3362) -> bool {
3363    match backend() {
3364        #[cfg(target_os = "macos")]
3365        Backend::Metal => crate::gpu_metal::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
3366        #[cfg(feature = "gpu")]
3367        Backend::Wgpu => crate::gpu_wgpu::vae_upsample_conv(w, bias, x, ic, oc, h, w_img, k, out),
3368        #[allow(unreachable_patterns)]
3369        _ => false,
3370    }
3371}
3372
3373/// VAE conv2d on the device (implicit GEMM — the CPU path pays for a
3374/// multi-GB im2col matrix at high resolutions).
3375#[allow(unused_variables, clippy::too_many_arguments)]
3376pub fn vae_conv2d(
3377    w: &[f32],
3378    bias: &[f32],
3379    x: &[f32],
3380    ic: usize,
3381    oc: usize,
3382    h: usize,
3383    w_img: usize,
3384    k: usize,
3385    out: &mut [f32],
3386) -> bool {
3387    match backend() {
3388        #[cfg(target_os = "macos")]
3389        Backend::Metal => crate::gpu_metal::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
3390        #[cfg(feature = "gpu")]
3391        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d(w, bias, x, ic, oc, h, w_img, k, out),
3392        #[allow(unreachable_patterns)]
3393        _ => false,
3394    }
3395}
3396
3397/// DiT full bidirectional attention on the device (all heads:
3398/// scores GEMM → row softmax → P·V → panel unstack, one command
3399/// buffer). Head-major inputs; out is [n, nh·hd].
3400#[allow(unused_variables, clippy::too_many_arguments)]
3401/// Attention from an interleaved qkv panel, splitting into head-major
3402/// planes ON the device. wgpu only; `false` elsewhere so the caller
3403/// keeps its host repack.
3404#[allow(unused_variables)]
3405#[allow(clippy::too_many_arguments)]
3406/// qkv projection + attention with the panel never leaving the card.
3407/// wgpu only; `false` elsewhere and the caller keeps its host chain.
3408#[allow(clippy::too_many_arguments, unused_variables)]
3409pub fn dit_qkv_attention(
3410    model: &Arc<CmfModel>,
3411    qkv_idx: usize,
3412    xn: &[f32],
3413    n: usize,
3414    hidden: usize,
3415    nh: usize,
3416    hd: usize,
3417    scale: f32,
3418    nr: (&[f32], &[f32], &[f32], f32),
3419    out: &mut [f32],
3420) -> bool {
3421    match backend() {
3422        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3423        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attention(
3424            model, qkv_idx, xn, n, hidden, nh, hd, scale, nr, out,
3425        ),
3426        #[allow(unreachable_patterns)]
3427        _ => false,
3428    }
3429}
3430
3431/// The whole attention half of a DiT block on the card: qkv GEMM,
3432/// attention, output projection. Only `proj` comes home.
3433#[allow(clippy::too_many_arguments)]
3434pub fn dit_qkv_attn_out(
3435    model: &Arc<CmfModel>,
3436    qkv_idx: usize,
3437    out_idx: usize,
3438    xn: &[f32],
3439    n: usize,
3440    hidden: usize,
3441    nh: usize,
3442    hd: usize,
3443    scale: f32,
3444    nr: (&[f32], &[f32], &[f32], f32),
3445    proj: &mut [f32],
3446) -> bool {
3447    match backend() {
3448        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3449        Backend::Wgpu => crate::gpu_wgpu::dit_qkv_attn_out(
3450            model, qkv_idx, out_idx, xn, n, hidden, nh, hd, scale, nr, proj,
3451        ),
3452        #[allow(unreachable_patterns)]
3453        _ => false,
3454    }
3455}
3456
3457/// The VAE decoder's attention half on the card. Only `proj` returns.
3458#[allow(clippy::too_many_arguments)]
3459pub fn vae_qkv_attn_out(
3460    model: &Arc<CmfModel>,
3461    qkv_idx: usize,
3462    out_idx: usize,
3463    xn: &[f32],
3464    n: usize,
3465    dim: usize,
3466    nh: usize,
3467    hd: usize,
3468    scale: f32,
3469    angles: &[f32],
3470    eps: f32,
3471    qkv_bias: &[f32],
3472    proj: &mut [f32],
3473) -> bool {
3474    match backend() {
3475        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3476        Backend::Wgpu => crate::gpu_wgpu::vae_qkv_attn_out(
3477            model, qkv_idx, out_idx, xn, n, dim, nh, hd, scale, angles, eps, qkv_bias, proj,
3478        ),
3479        #[allow(unreachable_patterns)]
3480        _ => false,
3481    }
3482}
3483
3484#[allow(clippy::too_many_arguments)]
3485pub fn vae_attention_packed(
3486    qkv: &[f32],
3487    nh: usize,
3488    n: usize,
3489    hd: usize,
3490    scale: f32,
3491    angles: &[f32],
3492    eps: f32,
3493    out: &mut [f32],
3494) -> bool {
3495    vae_attention_packed_layout(qkv, nh, n, hd, scale, angles, eps, out, 1)
3496}
3497
3498#[allow(clippy::too_many_arguments)]
3499pub fn vae_attention_packed_layout(
3500    qkv: &[f32],
3501    nh: usize,
3502    n: usize,
3503    hd: usize,
3504    scale: f32,
3505    angles: &[f32],
3506    eps: f32,
3507    out: &mut [f32],
3508    layout: u32,
3509) -> bool {
3510    match backend() {
3511        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3512        Backend::Wgpu => crate::gpu_wgpu::vae_attention_packed_layout(
3513            qkv, nh, n, hd, scale, angles, eps, out, layout,
3514        ),
3515        #[allow(unreachable_patterns)]
3516        _ => false,
3517    }
3518}
3519
3520#[allow(clippy::too_many_arguments)]
3521pub fn dit_split_only(
3522    qkv: &[f32],
3523    nh: usize,
3524    n: usize,
3525    hd: usize,
3526    layout: u32,
3527    norm: Option<(&[f32], f32)>,
3528    out_q: &mut [f32],
3529) -> bool {
3530    match backend() {
3531        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3532        Backend::Wgpu => crate::gpu_wgpu::dit_split_only(qkv, nh, n, hd, layout, norm, out_q),
3533        #[allow(unreachable_patterns)]
3534        _ => false,
3535    }
3536}
3537
3538/// The backend's f32 NT GEMM: `y[n×m] = x[n×k] · wᵀ[m×k]`. Tensor
3539/// cores where the card has them. Refuses under `CMF_BAKE_GPU=0` or
3540/// strict f32, and for jobs below n·k·m = 4M, where the round trip
3541/// costs more than the arithmetic saves.
3542/// `gemm_nt_f32` whose `w` is known to change every call (an
3543/// accumulation over fresh activations, not a weight): it skips the
3544/// resident ledger and its per-call fingerprint of the whole operand.
3545pub fn gemm_nt_f32_transient(
3546    x: &[f32],
3547    w: &[f32],
3548    y: &mut [f32],
3549    n: usize,
3550    k: usize,
3551    m: usize,
3552) -> bool {
3553    match backend() {
3554        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3555        Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32_transient(x, w, y, n, k, m),
3556        #[allow(unreachable_patterns)]
3557        _ => false,
3558    }
3559}
3560
3561pub fn gemm_nt_f32(x: &[f32], w: &[f32], y: &mut [f32], n: usize, k: usize, m: usize) -> bool {
3562    match backend() {
3563        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3564        Backend::Wgpu => crate::gpu_wgpu::gemm_nt_f32(x, w, y, n, k, m),
3565        #[allow(unreachable_patterns)]
3566        _ => false,
3567    }
3568}
3569
3570/// Music-3's FFN chain resident on the device — two GEMMs and the GLU
3571/// between them with no host round trip. `false` = refused, host runs.
3572#[allow(clippy::too_many_arguments)]
3573pub fn music3_ffn(
3574    model: &std::sync::Arc<CmfModel>,
3575    idx_in: usize,
3576    idx_out: usize,
3577    h: &[f32],
3578    bias_in: &[f32],
3579    n: usize,
3580    hs: usize,
3581    inter: usize,
3582    out: &mut [f32],
3583) -> bool {
3584    match backend() {
3585        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3586        Backend::Wgpu => {
3587            crate::gpu_wgpu::music3_ffn(model, idx_in, idx_out, h, bias_in, n, hs, inter, out)
3588        }
3589        #[allow(unreachable_patterns)]
3590        _ => false,
3591    }
3592}
3593
3594/// A 1D convolution as a GEMM whose column matrix is expanded on the
3595/// device instead of being built, transposed and uploaded by the host.
3596/// `yt` comes back `[out_n x oc]`. `false` = refused, caller runs host.
3597#[allow(clippy::too_many_arguments)]
3598pub fn conv1d_gemm(
3599    x: &[f32],
3600    w: &[f32],
3601    ic: usize,
3602    oc: usize,
3603    n: usize,
3604    k: usize,
3605    pad: usize,
3606    dil: usize,
3607    out_n: usize,
3608    yt: &mut [f32],
3609) -> bool {
3610    match backend() {
3611        #[cfg(target_os = "macos")]
3612        Backend::Metal => crate::gpu_metal::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
3613        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3614        Backend::Wgpu => crate::gpu_wgpu::conv1d_gemm(x, w, ic, oc, n, k, pad, dil, out_n, yt),
3615        #[allow(unreachable_patterns)]
3616        _ => false,
3617    }
3618}
3619
3620/// The convolution as a GEMM on the matrix units. `false` = refused.
3621#[allow(clippy::too_many_arguments)]
3622pub fn vae_conv2d_coop(
3623    w: &[f32],
3624    bias: Option<&[f32]>,
3625    x: &[f32],
3626    ic: usize,
3627    oc: usize,
3628    h: usize,
3629    wi: usize,
3630    k: usize,
3631    out: &mut [f32],
3632) -> bool {
3633    match backend() {
3634        #[cfg(all(feature = "gpu", not(target_os = "macos")))]
3635        Backend::Wgpu => crate::gpu_wgpu::vae_conv2d_coop(w, bias, x, ic, oc, h, wi, k, out),
3636        #[allow(unreachable_patterns)]
3637        _ => false,
3638    }
3639}
3640
3641pub fn dit_attention_packed(
3642    qkv: &[f32],
3643    nh: usize,
3644    n: usize,
3645    hd: usize,
3646    scale: f32,
3647    // (rope angles, q norm weights, k norm weights, eps) when the device
3648    // should apply qk-norm and RoPE itself; None when the host already did.
3649    nr: Option<(&[f32], &[f32], &[f32], f32)>,
3650    out: &mut [f32],
3651) -> bool {
3652    match backend() {
3653        // wgpu carries the only implementation, and it is not
3654        // platform-specific: `CMF_GPU=wgpu` on macOS runs it over Metal
3655        // like anywhere else. It used to be compiled out here on macOS,
3656        // which made the call a silent `false` — and the caller's
3657        // `assert!` turned that refusal into a panic on every
3658        // `cortiq animate` this platform ever ran.
3659        #[cfg(feature = "gpu")]
3660        Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed(qkv, nh, n, hd, scale, nr, out),
3661        #[allow(unreachable_patterns)]
3662        _ => false,
3663    }
3664}
3665
3666/// Whether `dit_attention_packed` has an implementation on the backend
3667/// that is actually selected.
3668///
3669/// The caller has to know BEFORE it skips the host qk-norm: deferring
3670/// the norm to a device that then refuses leaves q/k unnormalized with
3671/// no way back. Native Metal has no packed kernel, so on macOS this is
3672/// false unless `CMF_GPU=wgpu` picked the other backend.
3673pub fn dit_attention_packed_available() -> bool {
3674    #[allow(unreachable_patterns)]
3675    match backend() {
3676        #[cfg(feature = "gpu")]
3677        Backend::Wgpu => crate::gpu_wgpu::dit_attention_packed_ready(),
3678        _ => false,
3679    }
3680}
3681
3682pub fn dit_attention(
3683    qh: &[f32],
3684    kh: &[f32],
3685    vh: &[f32],
3686    nh: usize,
3687    nkv: usize,
3688    n: usize,
3689    hd: usize,
3690    scale: f32,
3691    out: &mut [f32],
3692) -> bool {
3693    match backend() {
3694        #[cfg(target_os = "macos")]
3695        Backend::Metal => crate::gpu_metal::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
3696        #[cfg(feature = "gpu")]
3697        Backend::Wgpu => crate::gpu_wgpu::dit_attention(qh, kh, vh, nh, nkv, n, hd, scale, out),
3698        #[allow(unreachable_patterns)]
3699        _ => false,
3700    }
3701}
3702
3703/// GPTQ calibration: a zeroed `cols × cols` Hessian accumulator on the
3704/// device (wgpu only), or None — the caller keeps the host fold then.
3705pub fn gram_alloc(cols: usize) -> Option<u64> {
3706    #[cfg(feature = "gpu")]
3707    if backend() == Backend::Wgpu {
3708        return crate::gpu_wgpu::gram::alloc(cols);
3709    }
3710    let _ = cols;
3711    None
3712}
3713
3714/// `H[id] += XᵀX` over `b` rows of `xs` (`[b·cols]`).
3715pub fn gram_add(id: u64, xs: &[f32], b: usize) -> bool {
3716    #[cfg(feature = "gpu")]
3717    if backend() == Backend::Wgpu {
3718        return crate::gpu_wgpu::gram::add(id, xs, b);
3719    }
3720    let _ = (id, xs, b);
3721    false
3722}
3723
3724/// `H[id] += XᵀX` over `b` rows of a device-resident f32 buffer.
3725#[cfg(feature = "gpu")]
3726pub fn gram_add_dev(id: u64, x: &wgpu::Buffer, b: usize) -> bool {
3727    if backend() == Backend::Wgpu {
3728        return crate::gpu_wgpu::gram::add_dev(id, x, b);
3729    }
3730    false
3731}
3732
3733/// The accumulated Hessian as a full symmetric f64 matrix (frees it).
3734pub fn gram_take(id: u64) -> Option<Vec<f64>> {
3735    #[cfg(feature = "gpu")]
3736    if backend() == Backend::Wgpu {
3737        return crate::gpu_wgpu::gram::take(id);
3738    }
3739    let _ = id;
3740    None
3741}
3742
3743/// GPTQ fold of the device Hessian `h_id` on the device: the upper Cholesky
3744/// factor of `(H + λ·mean(diag)·I)⁻¹` stays there under the returned id and
3745/// the Hessian is freed. None (no wgpu device, it does not fit, it does not
3746/// factor): the Hessian is left for `gram_take`.
3747pub fn gptq_fold_dev(h_id: u64, n: usize, lambda: f64) -> Option<u64> {
3748    #[cfg(feature = "gpu")]
3749    if backend() == Backend::Wgpu {
3750        return crate::gpu_wgpu::gptq_dev::fold(h_id, n, lambda);
3751    }
3752    let _ = (h_id, n, lambda);
3753    None
3754}
3755
3756/// (dead channels, raw H diagonal, damping used) of a device fold.
3757pub fn gptq_fold_info(id: u64) -> Option<(Vec<bool>, Vec<f32>, f64)> {
3758    #[cfg(feature = "gpu")]
3759    if backend() == Backend::Wgpu {
3760        return crate::gpu_wgpu::gptq_dev::info(id);
3761    }
3762    let _ = id;
3763    None
3764}
3765
3766/// A device fold's operator read back (n×n f32, upper).
3767pub fn gptq_fold_read(id: u64) -> Option<Vec<f32>> {
3768    #[cfg(feature = "gpu")]
3769    if backend() == Backend::Wgpu {
3770        return crate::gpu_wgpu::gptq_dev::read_u(id);
3771    }
3772    let _ = id;
3773    None
3774}
3775
3776/// Free a device fold.
3777pub fn gptq_fold_free(id: u64) {
3778    #[cfg(feature = "gpu")]
3779    if backend() == Backend::Wgpu {
3780        crate::gpu_wgpu::gptq_dev::free(id);
3781    }
3782    let _ = id;
3783}
3784
3785/// The q4tp GPTQ sweep on the device (see `gpu_wgpu::gptq_dev::sweep_q4tp`).
3786#[allow(clippy::too_many_arguments)]
3787pub fn gptq_sweep_q4tp_dev(
3788    id: u64,
3789    w: &[f32],
3790    rows: usize,
3791    tab: &[f32],
3792    lost: &[f32],
3793    nib: &mut [u8],
3794    codes: &mut [u32],
3795) -> bool {
3796    #[cfg(feature = "gpu")]
3797    if backend() == Backend::Wgpu {
3798        return crate::gpu_wgpu::gptq_dev::sweep_q4tp(id, w, rows, tab, lost, nib, codes);
3799    }
3800    let _ = (id, w, rows, tab, lost, nib, codes);
3801    false
3802}
3803
3804/// The q2tp GPTQ sweep ("placed" rule) on the device.
3805pub fn gptq_sweep_q2tp_dev(
3806    id: u64,
3807    w: &[f32],
3808    rows: usize,
3809    tab: &[f32],
3810    hw: &[f32],
3811    chunks: &mut [u8],
3812    codes: &mut [u32],
3813) -> bool {
3814    #[cfg(feature = "gpu")]
3815    if backend() == Backend::Wgpu {
3816        return crate::gpu_wgpu::gptq_dev::sweep_q2tp(id, w, rows, tab, hw, chunks, codes);
3817    }
3818    let _ = (id, w, rows, tab, hw, chunks, codes);
3819    false
3820}
3821
3822/// A device Hessian read back as a full symmetric f64 matrix, left on the
3823/// device.
3824pub fn gram_read(id: u64) -> Option<Vec<f64>> {
3825    #[cfg(feature = "gpu")]
3826    if backend() == Backend::Wgpu {
3827        return crate::gpu_wgpu::gram::read(id);
3828    }
3829    let _ = id;
3830    None
3831}
3832
3833/// Free every device Hessian (an aborted calibration pass).
3834pub fn gram_clear() {
3835    #[cfg(feature = "gpu")]
3836    if backend() == Backend::Wgpu {
3837        crate::gpu_wgpu::gram::clear();
3838    }
3839}
3840
3841/// Batched q4t GEMM on the device (imagegen DiT prefill shapes).
3842/// Metal: q4t_mul_mm decodes the mmap-resident tiles inside the
3843/// GEMM's K loop. wgpu (Vulkan/DX12 → NVIDIA/AMD/Intel/Adreno/Mali):
3844/// the register-blocked WGSL twin, weights cached in VRAM.
3845#[allow(unused_variables)]
3846pub fn q4tp_matmat(
3847    model: &Arc<CmfModel>,
3848    idx: usize,
3849    xs: &[f32],
3850    b: usize,
3851    rows: usize,
3852    cols: usize,
3853    out: &mut [f32],
3854) -> bool {
3855    match backend() {
3856        #[cfg(target_os = "macos")]
3857        Backend::Metal => crate::gpu_metal::q4tp_matmat(model, idx, xs, b, rows, cols, out),
3858        #[cfg(feature = "gpu")]
3859        Backend::Wgpu => crate::gpu_wgpu::q4tp_matmat(model, idx, xs, b, rows, cols, out),
3860        #[allow(unreachable_patterns)]
3861        _ => false,
3862    }
3863}
3864
3865/// The same over a two-bit weight plane. Native Metal uses the dedicated
3866/// q2tp tile; unsupported shapes return false and preserve the host fallback.
3867pub fn q2tp_matmat(
3868    model: &Arc<CmfModel>,
3869    idx: usize,
3870    xs: &[f32],
3871    b: usize,
3872    rows: usize,
3873    cols: usize,
3874    out: &mut [f32],
3875) -> bool {
3876    match backend() {
3877        #[cfg(target_os = "macos")]
3878        Backend::Metal => crate::gpu_metal::q2tp_matmat(model, idx, xs, b, rows, cols, out),
3879        #[cfg(feature = "gpu")]
3880        Backend::Wgpu => crate::gpu_wgpu::q2tp_matmat(model, idx, xs, b, rows, cols, out),
3881        #[allow(unreachable_patterns)]
3882        _ => false,
3883    }
3884}
3885
3886/// Descriptor-aware q2tp GEMM. The affine center is selected only for a
3887/// validated q2tp_affine target; the raw dtype16 payload remains unchanged.
3888pub fn q2tp_affine_matmat(
3889    model: &Arc<CmfModel>,
3890    idx: usize,
3891    xs: &[f32],
3892    b: usize,
3893    rows: usize,
3894    cols: usize,
3895    out: &mut [f32],
3896) -> bool {
3897    match backend() {
3898        #[cfg(target_os = "macos")]
3899        Backend::Metal => crate::gpu_metal::q2tp_affine_matmat(model, idx, xs, b, rows, cols, out),
3900        #[cfg(feature = "gpu")]
3901        Backend::Wgpu => crate::gpu_wgpu::q2tp_affine_matmat(model, idx, xs, b, rows, cols, out),
3902        #[allow(unreachable_patterns)]
3903        _ => false,
3904    }
3905}
3906
3907/// Binary low-rank (`blr`) product on the device: `out[b·rows] = xs[b·cols] ·
3908/// Wᵀ` through the two ±1 stages (V then U). `b = 1` is the decode GEMV.
3909/// False = not taken (no wgpu backend — Metal has no blr kernel yet — or a
3910/// shape outside the kernel's contract): the caller runs the CPU kernels.
3911pub fn blr_matmat(t: &crate::blr::BlrTensor, xs: &[f32], b: usize, out: &mut [f32]) -> bool {
3912    match backend() {
3913        #[cfg(feature = "gpu")]
3914        Backend::Wgpu => crate::gpu_wgpu::blr_matmat(t, xs, b, out),
3915        #[allow(unreachable_patterns)]
3916        _ => {
3917            let _ = (t, xs, b, out);
3918            false
3919        }
3920    }
3921}
3922
3923/// `mxfp4` matvec on the device: the wgpu kernel (`gpu_wgpu::mxfp4`).
3924/// Native Metal has no mxfp4 kernel and answers false, as does any shape or
3925/// budget the device path refuses — the CPU kernels run then.
3926pub fn mxfp4_matvec(
3927    model: &Arc<CmfModel>,
3928    idx: usize,
3929    xs: &[f32],
3930    rows: usize,
3931    cols: usize,
3932    out: &mut [f32],
3933) -> bool {
3934    mxfp4_matmat(model, idx, xs, 1, rows, cols, out)
3935}
3936
3937/// Batched `mxfp4` matvec on the device (`xs` `[b, cols]` → `out`
3938/// `[b, rows]`, up to eight tokens per workgroup). false = CPU fallback.
3939pub fn mxfp4_matmat(
3940    model: &Arc<CmfModel>,
3941    idx: usize,
3942    xs: &[f32],
3943    b: usize,
3944    rows: usize,
3945    cols: usize,
3946    out: &mut [f32],
3947) -> bool {
3948    match backend() {
3949        #[cfg(feature = "gpu")]
3950        Backend::Wgpu => crate::gpu_wgpu::mxfp4::matmat(model, idx, xs, b, rows, cols, out),
3951        #[allow(unreachable_patterns)]
3952        _ => {
3953            let _ = (model, idx, xs, b, rows, cols, out);
3954            false
3955        }
3956    }
3957}
3958
3959/// Single-token q2tp matvec through the ordinary (center=1.5) WGSL kernel.
3960pub fn q2tp_matvec(
3961    model: &Arc<CmfModel>,
3962    idx: usize,
3963    xs: &[f32],
3964    rows: usize,
3965    cols: usize,
3966    out: &mut [f32],
3967) -> bool {
3968    match backend() {
3969        #[cfg(target_os = "macos")]
3970        Backend::Metal => crate::gpu_metal::q2tp_matvec(model, idx, xs, rows, cols, out),
3971        #[cfg(feature = "gpu")]
3972        Backend::Wgpu => crate::gpu_wgpu::q2tp_matvec(model, idx, xs, rows, cols, out),
3973        #[allow(unreachable_patterns)]
3974        _ => false,
3975    }
3976}
3977
3978/// Single-token q2tp matvec with the explicit affine center=1 descriptor
3979/// operator. This is kept separate from ordinary q2tp to make accidental
3980/// center changes impossible at a call site.
3981pub fn q2tp_affine_matvec(
3982    model: &Arc<CmfModel>,
3983    idx: usize,
3984    xs: &[f32],
3985    rows: usize,
3986    cols: usize,
3987    out: &mut [f32],
3988) -> bool {
3989    match backend() {
3990        #[cfg(target_os = "macos")]
3991        Backend::Metal => crate::gpu_metal::q2tp_affine_matvec(model, idx, xs, rows, cols, out),
3992        #[cfg(feature = "gpu")]
3993        Backend::Wgpu => crate::gpu_wgpu::q2tp_affine_matvec(model, idx, xs, rows, cols, out),
3994        #[allow(unreachable_patterns)]
3995        _ => false,
3996    }
3997}
3998
3999/// Single-token q4tp matvec on the device — the lm_head class. Through the
4000/// DEDICATED matvec kernel: the batched GEMM at b=1 measured 11.73 ms
4001/// against the host's 9.51 on the release head, so the route that was
4002/// supposed to save eleven milliseconds a token lost its own probe instead.
4003pub fn q4tp_matvec(
4004    model: &Arc<CmfModel>,
4005    idx: usize,
4006    xs: &[f32],
4007    rows: usize,
4008    cols: usize,
4009    out: &mut [f32],
4010) -> bool {
4011    match backend() {
4012        #[cfg(target_os = "macos")]
4013        Backend::Metal => crate::gpu_metal::q4tp_matvec_for_test(model, idx, xs, rows, cols, out),
4014        #[cfg(feature = "gpu")]
4015        Backend::Wgpu => crate::gpu_wgpu::q4tp_matvec(model, idx, xs, rows, cols, out),
4016        #[allow(unreachable_patterns)]
4017        _ => false,
4018    }
4019}
4020
4021/// Single-token q4_tiled matvec on the device — the lm_head class (a
4022/// q4t checkpoint's head is its biggest host matvec, exactly like the
4023/// q4tp twin above). wgpu holds q4t_mv pipelines only inside the graph
4024/// encoder — the standalone arm stays an honest refusal until a
4025/// discrete-GPU q4t model reaches the bench.
4026pub fn q4t_matvec(
4027    model: &Arc<CmfModel>,
4028    idx: usize,
4029    xs: &[f32],
4030    rows: usize,
4031    cols: usize,
4032    out: &mut [f32],
4033) -> bool {
4034    match backend() {
4035        #[cfg(target_os = "macos")]
4036        Backend::Metal => crate::gpu_metal::q4t_matvec_for_test(model, idx, xs, rows, cols, out),
4037        #[allow(unreachable_patterns)]
4038        _ => false,
4039    }
4040}
4041
4042pub fn q4t_matmat(
4043    model: &Arc<CmfModel>,
4044    idx: usize,
4045    xs: &[f32],
4046    b: usize,
4047    rows: usize,
4048    cols: usize,
4049    out: &mut [f32],
4050) -> bool {
4051    match backend() {
4052        #[cfg(target_os = "macos")]
4053        Backend::Metal => crate::gpu_metal::q4t_matmat(model, idx, xs, b, rows, cols, out),
4054        #[cfg(feature = "gpu")]
4055        Backend::Wgpu => crate::gpu_wgpu::q4t_matmat(model, idx, xs, b, rows, cols, out),
4056        #[allow(unreachable_patterns)]
4057        _ => false,
4058    }
4059}
4060
4061/// Whole-block token-graph types re-exported from the Metal backend.
4062#[cfg(target_os = "macos")]
4063pub use crate::gpu_metal::{
4064    AttnDeviceParams, AttnGpuLayer, GdnGpuCfg, GdnGpuLayer, GpuMoe, GraphDims, MetalFfn,
4065    O1AttnParams, TokenGraph, kv_mirror_drop, kv_mirror_read_last, kv_mirror_take_imp,
4066};
4067
4068/// A BLOCK of consecutive q1 GDN layers in one submission (Metal only).
4069#[cfg(target_os = "macos")]
4070pub fn gdn_block(
4071    model: &Arc<CmfModel>,
4072    layers: &[GdnGpuLayer],
4073    states: &mut [&mut [f32]],
4074    cfg: &GdnGpuCfg,
4075    h: &mut [f32],
4076) -> bool {
4077    match backend() {
4078        Backend::Metal => crate::gpu_metal::gdn_block(model, layers, states, cfg, h),
4079        _ => false,
4080    }
4081}
4082
4083/// A layer's MoE-FFN in one submission (amortizing the dispatch cost).
4084#[allow(unused_variables)]
4085pub fn moe_block(model: &Arc<CmfModel>, jobs: &[MoeJob], out: &mut [f32]) -> bool {
4086    match backend() {
4087        // The Metal block applies SiLU only: any other activation is an
4088        // honest refusal (the caller keeps its host path).
4089        #[cfg(target_os = "macos")]
4090        Backend::Metal if jobs.iter().any(|j| j.act != 0) => false,
4091        #[cfg(target_os = "macos")]
4092        Backend::Metal => crate::gpu_metal::moe_block(model, jobs, out),
4093        #[cfg(feature = "gpu")]
4094        Backend::Wgpu => crate::gpu_wgpu::moe_block(model, jobs, out),
4095        Backend::None => false,
4096    }
4097}
4098
4099/// Independent matvecs of one input in a single submission (GDN projections).
4100#[allow(unused_variables)]
4101pub fn matvec_batch(model: &Arc<CmfModel>, jobs: &[BatchJob], out: &mut [&mut [f32]]) -> bool {
4102    match backend() {
4103        #[cfg(target_os = "macos")]
4104        Backend::Metal => crate::gpu_metal::matvec_batch(model, jobs, out),
4105        #[cfg(feature = "gpu")]
4106        Backend::Wgpu => crate::gpu_wgpu::matvec_batch(model, jobs, out),
4107        Backend::None => false,
4108    }
4109}
4110
4111// ── Whole-token wgpu graph race (generation granularity) ─────────────
4112// On integrated/mobile adapters the graph is neither trusted nor banned
4113// a priori — it RACES the normal path: generations alternate arms (the
4114// normal path first — known-good UX — then the graph), per-token wall
4115// times accumulate per arm, and once both arms have enough steady
4116// samples the faster one wins for the process. Arm switches happen ONLY
4117// at generation boundaries (`kv_cache.clear()` resets state), so the
4118// device KV mirror and the CPU cache never diverge mid-sequence. The
4119// single exception is the first-token bail: the very first decode token
4120// of a graph generation may be discarded and recomputed on the CPU
4121// path (the prompt KV is CPU-owned at that point, so this is safe) —
4122// a tiled mobile GPU that drains its pipeline at every barrier turns
4123// the ~300-dispatch graph into seconds per token (field report: 0.2
4124// tok/s vs 15 on the CPU), and one token is all it takes to see that.
4125static GRAPH_RACE_STATE: AtomicU8 = AtomicU8::new(0); // 0 racing, 1 graph won, 2 normal won
4126static GRAPH_RACE_FLIP: AtomicU32 = AtomicU32::new(0);
4127static GRAPH_RACE_ARM_GRAPH: AtomicU8 = AtomicU8::new(0); // this generation's arm
4128static GRAPH_RACE_TOK: AtomicU32 = AtomicU32::new(0); // token index within the generation
4129static GRAPH_NS: [AtomicU64; 2] = [AtomicU64::new(0), AtomicU64::new(0)]; // [normal, graph]
4130static GRAPH_N: [AtomicU32; 2] = [AtomicU32::new(0), AtomicU32::new(0)];
4131
4132/// Steady per-token samples per arm before the race decides.
4133const GRAPH_RACE_SAMPLES: u32 = 4;
4134
4135// A graph that cannot be built for THIS model will never build: the
4136// refusal is a property of the weights, not of the moment. Retrying it
4137// per token is not free — the builder walks every layer and asks each
4138// tensor for a graph view before giving up at layer 0 — and on an
4139// Adreno 642L that retry cost 3x: forcing the graph on a model it
4140// refuses measured 0.3 tok/s against 0.905 for the per-op path it falls
4141// back to. Remembered once, the fallback runs at its own speed.
4142//
4143// The verdict is kept PER PIPELINE (`Pipeline::graph_refused` /
4144// `mark_graph_refused`), not in a process-wide flag: several pipelines
4145// of one file share a process in `serve` (backbone slots + skill lanes
4146// loaded mid-traffic), and a refusal in one lane — or the reset a new
4147// lane used to issue — must not move another lane's running sequence
4148// between the device and the host (R4/NF-2). Callers must NOT report
4149// transient refusals (an unsealed o1 state during prefill, a softcap).
4150
4151/// Called at every generation start (fresh KV). Applies a pending
4152/// verdict and picks this generation's arm while racing.
4153pub fn graph_race_begin_generation() {
4154    // One generation has now compiled whatever this model needs; keep it
4155    // for the next process. Once per run: the blob does not grow after
4156    // the pipelines exist, and the write is megabytes against the ~200 s
4157    // of compiling it saves on the device that needed this.
4158    #[cfg(feature = "gpu")]
4159    {
4160        // Save once, at the start of the SECOND generation: the first
4161        // has dispatched, so there is something to keep, and nothing is
4162        // saved before any work (the driver compiles at first use, not
4163        // at pipeline creation — the context comes up in 1.5 s while the
4164        // compiling costs minutes).
4165        //
4166        // Flushing again on 4, 8, 16 … was tried on the theory that a
4167        // chat turn compiles shapes the first one did not. It buys
4168        // nothing: a fresh app process still spent 49.0 s, then 58.7,
4169        // then 61.3 on its first answer with the backoff in place. One
4170        // flush it is.
4171        static FLUSHED: std::sync::Once = std::sync::Once::new();
4172        static FIRST: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(true);
4173        if FIRST.swap(false, Ordering::Relaxed) {
4174            // Nothing dispatched yet.
4175        } else {
4176            FLUSHED.call_once(crate::gpu_wgpu::pipeline_cache_flush);
4177        }
4178    }
4179    GRAPH_RACE_TOK.store(0, Ordering::Relaxed);
4180    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
4181        return;
4182    }
4183    let (gn, cn) = (
4184        GRAPH_N[1].load(Ordering::Relaxed),
4185        GRAPH_N[0].load(Ordering::Relaxed),
4186    );
4187    if gn >= GRAPH_RACE_SAMPLES && cn >= GRAPH_RACE_SAMPLES {
4188        let g_avg = GRAPH_NS[1].load(Ordering::Relaxed) / gn as u64;
4189        let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
4190        let verdict = if g_avg < c_avg { 1 } else { 2 };
4191        GRAPH_RACE_STATE.store(verdict, Ordering::Relaxed);
4192        tracing::info!(
4193            "wgpu graph race: graph {:.2} ms/tok vs normal {:.2} ms/tok -> {}",
4194            g_avg as f64 / 1e6,
4195            c_avg as f64 / 1e6,
4196            if verdict == 1 { "graph" } else { "normal path" }
4197        );
4198        return;
4199    }
4200    let flip = GRAPH_RACE_FLIP.fetch_add(1, Ordering::Relaxed);
4201    GRAPH_RACE_ARM_GRAPH.store((flip % 2 == 1) as u8, Ordering::Relaxed);
4202}
4203
4204/// Should this decode token try the graph? `trusted` (discrete adapter,
4205/// explicit env, or a GDN hybrid whose state lives on the device) skips
4206/// the race entirely.
4207pub fn graph_race_use_graph(trusted: bool) -> bool {
4208    if trusted {
4209        return true;
4210    }
4211    match GRAPH_RACE_STATE.load(Ordering::Relaxed) {
4212        1 => true,
4213        2 => false,
4214        _ => GRAPH_RACE_ARM_GRAPH.load(Ordering::Relaxed) == 1,
4215    }
4216}
4217
4218/// First decode token of a racing graph generation: hopeless already?
4219/// (>4x the normal path's per-token average AND over a second.) Settles
4220/// the race immediately; the caller discards the graph result and
4221/// recomputes this token on the normal path.
4222pub fn graph_race_first_token_hopeless(dur: std::time::Duration) -> bool {
4223    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
4224        return false;
4225    }
4226    let first = GRAPH_RACE_TOK.load(Ordering::Relaxed) == 0;
4227    let cn = GRAPH_N[0].load(Ordering::Relaxed);
4228    if !first || cn == 0 {
4229        return false;
4230    }
4231    let c_avg = GRAPH_NS[0].load(Ordering::Relaxed) / cn as u64;
4232    let ns = dur.as_nanos() as u64;
4233    if ns > 1_000_000_000 && ns > 4 * c_avg {
4234        GRAPH_RACE_STATE.store(2, Ordering::Relaxed);
4235        tracing::info!(
4236            "wgpu graph race: first graph token {:.0} ms vs normal {:.2} ms/tok — hopeless, normal path wins",
4237            ns as f64 / 1e6,
4238            c_avg as f64 / 1e6
4239        );
4240        return true;
4241    }
4242    false
4243}
4244
4245/// Record one decode-token wall time for the racing arm. The first
4246/// token of each generation is discarded (KV-mirror upload / cold
4247/// caches on the graph arm; cold mmap on the normal arm).
4248pub fn graph_race_record(used_graph: bool, dur: std::time::Duration) {
4249    if GRAPH_RACE_STATE.load(Ordering::Relaxed) != 0 {
4250        return;
4251    }
4252    let tok = GRAPH_RACE_TOK.fetch_add(1, Ordering::Relaxed);
4253    if tok == 0 {
4254        return;
4255    }
4256    let i = used_graph as usize;
4257    GRAPH_NS[i].fetch_add(dur.as_nanos() as u64, Ordering::Relaxed);
4258    GRAPH_N[i].fetch_add(1, Ordering::Relaxed);
4259}
4260
4261/// Bounded-cost content fingerprint for the backends' pointer-keyed device
4262/// caches: FNV over the whole slice up to 4 KiB, over 64 spread 64-byte
4263/// windows (plus the length) above. An address-keyed hit must also prove
4264/// the bytes are still the ones it uploaded — the allocator reuses heap
4265/// and mmap addresses freely, so a reloaded model or a re-dequantized
4266/// layer lands where the old bytes were — and sampling keeps that proof at
4267/// ~a microsecond even for a 126 MB matrix. Real replacements (another
4268/// model's tensor, an Adam-updated master) differ densely, so a 4 KiB
4269/// spread cannot miss them.
4270pub(crate) fn fp_bytes(data: &[u8]) -> u64 {
4271    #[inline]
4272    fn fnv(mut h: u64, bytes: &[u8]) -> u64 {
4273        let (chunks, tail) = bytes.split_at(bytes.len() & !7);
4274        for c in chunks.chunks_exact(8) {
4275            h ^= u64::from_le_bytes(c.try_into().unwrap());
4276            h = h.wrapping_mul(0x100_0000_01b3);
4277        }
4278        for &b in tail {
4279            h ^= b as u64;
4280            h = h.wrapping_mul(0x100_0000_01b3);
4281        }
4282        h
4283    }
4284    let mut h = 0xcbf2_9ce4_8422_2325u64 ^ (data.len() as u64);
4285    if data.len() <= 4096 {
4286        return fnv(h, data);
4287    }
4288    let step = (data.len() - 64) / 63;
4289    for i in 0..64 {
4290        h = fnv(h, &data[i * step..i * step + 64]);
4291    }
4292    h
4293}
4294
4295/// `fp_bytes` over an f32 slice without a bytemuck dependency (the Metal
4296/// backend builds with no GPU feature flags).
4297pub(crate) fn fp_f32(data: &[f32]) -> u64 {
4298    let bytes = unsafe { std::slice::from_raw_parts(data.as_ptr() as *const u8, data.len() * 4) };
4299    fp_bytes(bytes)
4300}
4301
4302#[cfg(test)]
4303mod fp_tests {
4304    use super::fp_bytes;
4305
4306    /// The pointer-keyed caches survive on `fp_bytes` telling two different
4307    /// tensors apart at a reused address. Its sampling must therefore see a
4308    /// change ANYWHERE — head, tail, and the stretches between windows are
4309    /// the places a cheaper hash would go blind.
4310    #[test]
4311    fn fp_bytes_sees_a_change_anywhere_in_a_sampled_slice() {
4312        let n = 1 << 20; // 1 MiB — far above the 4 KiB full-hash threshold
4313        let base: Vec<u8> = (0..n).map(|i| (i * 31 + 7) as u8).collect();
4314        let h0 = fp_bytes(&base);
4315        assert_eq!(h0, fp_bytes(&base), "fingerprint must be deterministic");
4316        // A DENSE change (every requantized/redequantized tensor is one)
4317        // must flip the fingerprint no matter how the windows fall.
4318        let mut dense = base.clone();
4319        for b in dense.iter_mut() {
4320            *b = b.wrapping_add(1);
4321        }
4322        assert_ne!(
4323            h0,
4324            fp_bytes(&dense),
4325            "a fully different tensor slipped through"
4326        );
4327        // Length participates: the same prefix at a shorter length is a
4328        // different key AND a different fingerprint.
4329        assert_ne!(h0, fp_bytes(&base[..n - 64]));
4330        // Below the threshold the hash is exact: a single flipped byte in
4331        // a norm-sized vector must be seen.
4332        let mut small = vec![3u8; 4096];
4333        let hs = fp_bytes(&small);
4334        small[2048] ^= 1;
4335        assert_ne!(hs, fp_bytes(&small), "full hash missed a one-byte change");
4336        // And the sampled windows land within bounds on awkward sizes.
4337        for n in [4097usize, 5000, 64 * 64, 1 << 16] {
4338            let v = vec![9u8; n];
4339            let _ = fp_bytes(&v); // must not panic on window math
4340        }
4341    }
4342}
4343
4344/// Hand the card back after a bake: drop its resident weights, planes and
4345/// pools so the ordinary engine (the runtime gate, a serve that follows)
4346/// starts from a clean budget. No-op off the wgpu backend.
4347pub fn bake_release() {
4348    #[cfg(feature = "gpu")]
4349    crate::gpu_wgpu::bake_release();
4350}
4351
4352/// Strict-f32 for the bake's GEMMs (phase A mask training): the mask
4353/// selects neurons by a gradient signal, and f16 operand rounding on
4354/// that signal closes the wrong ones. No-op off the wgpu backend.
4355pub fn bake_precision_strict(on: bool) {
4356    #[cfg(feature = "gpu")]
4357    crate::gpu_wgpu::bake_precision_strict(on);
4358    #[cfg(not(feature = "gpu"))]
4359    let _ = on;
4360}
4361
4362/// CMF_GRAPH_HOSTPROF=1: how a graph token's wall splits between the
4363/// host encoding the command stream and the tail the GPU still owes
4364/// after encode. Fifteen GPU-side suspects measured null while the
4365/// bench counted 17.7k allocations a token — this is the instrument
4366/// that says whether the thief was on the host all along.
4367pub fn hostprof_encode_done(t0: std::time::Instant) {
4368    use std::sync::atomic::{AtomicU64, Ordering};
4369    static ENC: AtomicU64 = AtomicU64::new(0);
4370    static N: AtomicU64 = AtomicU64::new(0);
4371    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
4372        return;
4373    }
4374    ENC.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
4375    let n = N.fetch_add(1, Ordering::Relaxed) + 1;
4376    if n % 100 == 0 {
4377        eprintln!(
4378            "hostprof: encode {:.2} ms/token over {n} tokens",
4379            ENC.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
4380        );
4381    }
4382}
4383
4384pub fn hostprof_total(t0: std::time::Instant) {
4385    use std::sync::atomic::{AtomicU64, Ordering};
4386    static TOT: AtomicU64 = AtomicU64::new(0);
4387    static N: AtomicU64 = AtomicU64::new(0);
4388    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
4389        return;
4390    }
4391    TOT.fetch_add(t0.elapsed().as_nanos() as u64, Ordering::Relaxed);
4392    let n = N.fetch_add(1, Ordering::Relaxed) + 1;
4393    if n % 100 == 0 {
4394        eprintln!(
4395            "hostprof: total {:.2} ms/token over {n} tokens",
4396            TOT.load(Ordering::Relaxed) as f64 / n as f64 / 1e6
4397        );
4398    }
4399}
4400
4401/// Per-stage host-encode accumulator for the Metal token loop
4402/// (CMF_GRAPH_HOSTPROF=1). Stage 0 = GDN-run encode; everything else
4403/// falls out by subtraction from hostprof's encode total.
4404pub fn stageprof(stage: u32, dt: std::time::Duration) {
4405    use std::sync::atomic::{AtomicU64, Ordering};
4406    static NS: [AtomicU64; 4] = [
4407        AtomicU64::new(0),
4408        AtomicU64::new(0),
4409        AtomicU64::new(0),
4410        AtomicU64::new(0),
4411    ];
4412    static N: AtomicU64 = AtomicU64::new(0);
4413    if std::env::var("CMF_GRAPH_HOSTPROF").as_deref() != Ok("1") {
4414        return;
4415    }
4416    NS[stage as usize % 4].fetch_add(dt.as_nanos() as u64, Ordering::Relaxed);
4417    if stage == 1 {
4418        let n = N.fetch_add(1, Ordering::Relaxed) + 1;
4419        if n % 200 == 0 {
4420            eprintln!(
4421                "stageprof: planning {:.2} ms/tok | gdn-item {:.2} ms/tok | attn-item {:.2} ms/tok ({n} tok)",
4422                NS[1].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
4423                NS[2].load(Ordering::Relaxed) as f64 / n as f64 / 1e6,
4424                NS[3].load(Ordering::Relaxed) as f64 / n as f64 / 1e6
4425            );
4426        }
4427    }
4428}
4429
4430/// Active weight bytes dispatched so far (Metal decode path); 0 where
4431/// the backend does not count. The honest floor's numerator.
4432pub fn weight_bytes_dispatched() -> u64 {
4433    let mut total = 0u64;
4434    #[cfg(target_os = "macos")]
4435    {
4436        total += crate::gpu_metal::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
4437    }
4438    #[cfg(feature = "gpu")]
4439    {
4440        total += crate::gpu_wgpu::WEIGHT_BYTES.load(std::sync::atomic::Ordering::Relaxed);
4441    }
4442    total
4443}
4444
4445/// The per-stage split of `weight_bytes_dispatched`:
4446/// [misc, dense-ffn, moe, attn, gdn, head].
4447pub fn weight_bytes_by() -> [u64; 6] {
4448    #[cfg(target_os = "macos")]
4449    {
4450        let mut o = [0u64; 6];
4451        for (i, a) in crate::gpu_metal::WEIGHT_BYTES_BY.iter().enumerate() {
4452            o[i] = a.load(std::sync::atomic::Ordering::Relaxed);
4453        }
4454        return o;
4455    }
4456    #[allow(unreachable_code)]
4457    [0; 6]
4458}
4459
4460#[cfg(test)]
4461mod f16_guard_tests {
4462    use super::*;
4463
4464    /// The f16 guard's maximum: the largest magnitude through the lanes and
4465    /// the remainder, +inf for an inf or a NaN anywhere.
4466    #[test]
4467    fn abs_max_or_inf_is_the_magnitude_maximum() {
4468        let mut xs: Vec<f32> = (0..37).map(|i| (i as f32 - 18.0) * 0.5).collect();
4469        assert_eq!(abs_max_or_inf(&xs), 9.0);
4470        xs[36] = -70000.0; // the remainder
4471        assert_eq!(abs_max_or_inf(&xs), 70000.0);
4472        xs[3] = f32::NAN; // a lane
4473        assert_eq!(abs_max_or_inf(&xs), f32::INFINITY);
4474        xs[3] = 0.0;
4475        xs[35] = f32::NEG_INFINITY;
4476        assert_eq!(abs_max_or_inf(&xs), f32::INFINITY);
4477        assert_eq!(abs_max_or_inf(&[]), 0.0);
4478        assert_eq!(abs_max_or_inf(&[-0.0, -1e-30]), 1e-30);
4479    }
4480}
4481
4482#[cfg(test)]
4483mod probe_warmup_tests {
4484    use super::*;
4485    use std::time::Duration;
4486
4487    fn ms(v: f64) -> Duration {
4488        Duration::from_nanos((v * 1e6) as u64)
4489    }
4490
4491    /// The bug this pins, measured on an A100: the first device call for
4492    /// a class compiles its pipeline, was timed at 117.01 ms against the
4493    /// host's 3.19, and sent `gemm-nt` to the CPU for the whole process —
4494    /// which ran a 27B bake on 2.6 cores with the card idle.
4495    #[test]
4496    fn one_cold_first_sample_does_not_lose_the_class() {
4497        let p = Probe::new();
4498        // First device sample is the pipeline build. Then the truth.
4499        probe_record_into(&p, "gemm-nt", None, true, ms(117.01));
4500        probe_record_into(&p, "gemm-nt", None, true, ms(1.1));
4501        probe_record_into(&p, "gemm-nt", None, true, ms(1.0));
4502        probe_record_into(&p, "gemm-nt", None, false, ms(3.19));
4503        probe_record_into(&p, "gemm-nt", None, false, ms(3.20));
4504        assert_eq!(
4505            p.state.load(Ordering::Relaxed),
4506            1,
4507            "the device is 3x faster once warm and must win"
4508        );
4509    }
4510
4511    /// The warm-up must not become a way to never decide, and must not
4512    /// underflow: a blind decrement at zero wraps a u32 to its maximum
4513    /// and mutes the arm for the life of the process.
4514    #[test]
4515    fn the_warmup_is_spent_once_and_never_underflows() {
4516        let p = Probe::new();
4517        for _ in 0..8 {
4518            probe_record_into(&p, "matmat", None, true, ms(10.0));
4519        }
4520        assert_eq!(p.gpu_burn.load(Ordering::Relaxed), 0, "spent, not wrapped");
4521        assert_eq!(
4522            p.gpu_n.load(Ordering::Relaxed),
4523            7,
4524            "one sample burned, the rest counted"
4525        );
4526    }
4527
4528    /// A device path that always refuses records no timing, so without
4529    /// counting the refusals the class can never reach a verdict. On an
4530    /// M4 with LFM2.5-2.6B `ffn` was still undecided after 9000 calls,
4531    /// alternating arms and paying a failed device attempt on half of
4532    /// them.
4533    #[test]
4534    fn a_class_whose_device_always_declines_settles_on_the_host() {
4535        let _probe_guard = probe_test_guard();
4536        // A class no other test in this file touches: `probe_note_decline`
4537        // works on the process-wide probes by design, and the tests in
4538        // this binary share them.
4539        let c = OpClass::MatmatWide;
4540        let p = &PROBES[c as usize];
4541        p.state.store(0, Ordering::Relaxed);
4542        p.declines.store(0, Ordering::Relaxed);
4543        for _ in 0..(PROBE_DECLINE_LIMIT - 1) {
4544            probe_note_decline(c);
4545        }
4546        assert_eq!(
4547            p.state.load(Ordering::Relaxed),
4548            0,
4549            "one short of the limit is still a question, not an answer"
4550        );
4551        probe_note_decline(c);
4552        assert_eq!(p.state.load(Ordering::Relaxed), 2, "settled on the host");
4553        assert!(matches!(probe_arm(c), ProbeArm::Cpu));
4554        p.state.store(0, Ordering::Relaxed);
4555        p.declines.store(0, Ordering::Relaxed);
4556    }
4557
4558    /// A genuinely slower device still loses — the warm-up removes an
4559    /// artefact, it does not put a thumb on the scale.
4560    #[test]
4561    fn a_slow_device_still_loses_after_the_warmup() {
4562        let p = Probe::new();
4563        for _ in 0..4 {
4564            probe_record_into(&p, "matvec", None, true, ms(40.0));
4565        }
4566        for _ in 0..4 {
4567            probe_record_into(&p, "matvec", None, false, ms(2.0));
4568        }
4569        assert_eq!(p.state.load(Ordering::Relaxed), 2, "host wins on merit");
4570    }
4571}
4572
4573/// Scratch/weight lifetime for a synchronous image-pipeline stage. Declare
4574/// this before the stage model so the model drops before cache collection.
4575pub(crate) struct ImageStageGuard {
4576    #[cfg(target_os = "macos")]
4577    metal: Option<crate::gpu_metal::ImageStageGuard>,
4578    #[cfg(feature = "gpu")]
4579    wgpu: crate::gpu_wgpu::ImageStageGuard,
4580}
4581
4582pub(crate) fn image_stage_scope() -> ImageStageGuard {
4583    ImageStageGuard {
4584        #[cfg(target_os = "macos")]
4585        metal: if matches!(backend(), Backend::Metal) {
4586            Some(crate::gpu_metal::image_stage_scope())
4587        } else {
4588            None
4589        },
4590        #[cfg(feature = "gpu")]
4591        wgpu: crate::gpu_wgpu::image_stage_scope(),
4592    }
4593}
4594
4595impl ImageStageGuard {
4596    pub(crate) fn track_model(&mut self, uid: u64) {
4597        #[cfg(target_os = "macos")]
4598        if let Some(metal) = &mut self.metal {
4599            metal.track_model(uid);
4600        }
4601        #[cfg(feature = "gpu")]
4602        self.wgpu.track_model(uid);
4603        #[cfg(not(target_os = "macos"))]
4604        let _ = uid;
4605    }
4606}
4607
4608// ════════════════════════════════════════════════════════════════════
4609// Z-Image-Turbo device contract (WP0 scaffold, plan §2.1). APPEND-ONLY.
4610//
4611// Owner of the contract: the WP1 lead. The backends implement it in their
4612// own child modules — `gpu_wgpu/zimage.rs` (WP2) and `gpu_metal/zimage.rs`
4613// (WP3) — and never edit the parent files. New fields are added only as
4614// `Option<…>` with agreed semantics; existing fields never change meaning.
4615//
4616// Convention (the same as every `gpu::*` entry): `false` = "not handled",
4617// nothing observable was changed, and the caller runs the CPU path
4618// (`zimage::ZImageDit::step_cpu` etc.), which is the bit-level reference.
4619//
4620// Sequence order everywhere is diffusers' [img rows…, cap rows…], with
4621// padded lengths n_img_p = ceil32(n_img) and n_cap_p = ceil32(L).
4622// ════════════════════════════════════════════════════════════════════
4623
4624// ───────────────────── Qwen-Image-2.1 device contract ─────────────────────
4625
4626/// Geometry of the Qwen-Image-2.1 denoiser.
4627#[derive(Clone, Copy, Debug, PartialEq)]
4628pub struct Qi21Geom {
4629    pub hidden: usize,
4630    pub nh: usize,
4631    pub hd: usize,
4632    pub inter: usize,
4633    pub in_ch: usize,
4634    pub eps: f32,
4635}
4636
4637/// One block's weights: tensor indices of `to_q, to_k, to_v, to_out.0,
4638/// gate_layer, proj (up), out (down)` and the per-head q/k norm weights.
4639pub struct Qi21BlockRef<'a> {
4640    pub w: [usize; 7],
4641    pub norm_q: &'a [f32],
4642    pub norm_k: &'a [f32],
4643}
4644
4645/// Everything the device needs to build a program and run its prefix.
4646pub struct Qi21PrefillArgs<'a> {
4647    pub model: &'a Arc<CmfModel>,
4648    pub geom: Qi21Geom,
4649    pub blocks: &'a [Qi21BlockRef<'a>],
4650    /// `img_in` [hidden, 64] and `proj_out` [64, hidden], f32.
4651    pub img_in: &'a [f32],
4652    pub proj_out: &'a [f32],
4653    pub key: u64,
4654    /// Prefix rows after `txt_in` / `img_in`, `[lp, hidden]`.
4655    pub x: &'a [f32],
4656    pub lp: usize,
4657    /// RoPE cos/sin of the prefix rows and of the target rows, `[rows, 64]`.
4658    pub rope_p: (&'a [f32], &'a [f32]),
4659    pub rope_t: (&'a [f32], &'a [f32]),
4660    /// Keys each prefix row sees (a leading run; non-decreasing).
4661    pub vis: &'a [u32],
4662    /// The t = 0 modulation `[s1|g1|s2|g2]`.
4663    pub mods0: &'a [f32],
4664    /// Target rows.
4665    pub n: usize,
4666}
4667
4668/// Build the program `a.key` and run its prefix on the device.
4669#[allow(unused_variables)]
4670pub fn qi21_prefill(a: &Qi21PrefillArgs) -> bool {
4671    match backend() {
4672        #[cfg(target_os = "macos")]
4673        Backend::Metal => crate::gpu_metal::qi21::prefill(a),
4674        #[cfg(feature = "gpu")]
4675        Backend::Wgpu => crate::gpu_wgpu::qi21::prefill(a),
4676        #[allow(unreachable_patterns)]
4677        _ => false,
4678    }
4679}
4680
4681/// One denoiser call of program `key`: `xtok [n, 64]` → `out [n, 64]`.
4682#[allow(unused_variables)]
4683pub fn qi21_step(key: u64, xtok: &[f32], mods: &[f32], fs: &[f32], out: &mut [f32]) -> bool {
4684    match backend() {
4685        #[cfg(target_os = "macos")]
4686        Backend::Metal => crate::gpu_metal::qi21::step(key, xtok, mods, fs, out),
4687        #[cfg(feature = "gpu")]
4688        Backend::Wgpu => crate::gpu_wgpu::qi21::step(key, xtok, mods, fs, out),
4689        #[allow(unreachable_patterns)]
4690        _ => false,
4691    }
4692}
4693
4694/// Drop program `key`. The wgpu module is released directly (module-local
4695/// state only), so a release never brings a device up.
4696#[allow(unused_variables)]
4697pub fn qi21_release_key(key: u64) {
4698    #[cfg(target_os = "macos")]
4699    if matches!(backend(), Backend::Metal) {
4700        crate::gpu_metal::qi21::release_key(key);
4701    }
4702    #[cfg(feature = "gpu")]
4703    crate::gpu_wgpu::qi21::release_key(key);
4704}
4705
4706/// Drop the denoiser's device state (see `qi21_release_key`), and the
4707/// resident VAE decoder's weights.
4708pub fn qi21_release() {
4709    #[cfg(target_os = "macos")]
4710    if matches!(backend(), Backend::Metal) {
4711        crate::gpu_metal::qi21::release();
4712    }
4713    #[cfg(feature = "gpu")]
4714    {
4715        crate::gpu_wgpu::qi21::release();
4716        crate::gpu_wgpu::qi21_vae::release();
4717    }
4718}
4719
4720/// One conv of the Qwen-Image-2.1 VAE decoder: host f32 weights
4721/// `[co][ci][k][k]` and bias `[co]` (k = 1 or 3, stride 1, same padding).
4722#[derive(Clone, Copy)]
4723pub struct Qi21VaeConvRef<'a> {
4724    pub w: &'a [f32],
4725    pub b: &'a [f32],
4726    pub ci: usize,
4727    pub co: usize,
4728    pub k: usize,
4729}
4730
4731/// A residual block: `conv2(φ(conv1(φ(x)))) + x` (through the 1×1
4732/// shortcut when ci ≠ co), φ = RMS_norm over channels (x/‖x‖·√C·γ), SiLU.
4733pub struct Qi21VaeResRef<'a> {
4734    pub g1: &'a [f32],
4735    pub c1: Qi21VaeConvRef<'a>,
4736    pub g2: &'a [f32],
4737    pub c2: Qi21VaeConvRef<'a>,
4738    pub shortcut: Option<Qi21VaeConvRef<'a>>,
4739}
4740
4741/// An up block: its resnets, then (all but the last block) nearest-2× +
4742/// the 3×3 conv, plus the DupUp shortcut of the block's input (`in_dim` →
4743/// `out_dim` channels, temporal factor `ft`).
4744pub struct Qi21VaeUpRef<'a> {
4745    pub resnets: Vec<Qi21VaeResRef<'a>>,
4746    pub up: Option<(Qi21VaeConvRef<'a>, usize)>,
4747    pub in_dim: usize,
4748    pub out_dim: usize,
4749}
4750
4751/// The Qwen-Image-2.1 VAE decoder (`qwen_image21_vae.rs`) for a resident
4752/// device decode.
4753pub struct Qi21VaeDecodeArgs<'a> {
4754    /// Identity of the weights: a backend caches its planes by it.
4755    pub key: u64,
4756    pub post_quant: Qi21VaeConvRef<'a>,
4757    pub conv_in: Qi21VaeConvRef<'a>,
4758    pub mid_res: [Qi21VaeResRef<'a>; 2],
4759    /// The mid attention: RMS_norm γ, `to_qkv` (1×1, 3c outputs: q, k, v),
4760    /// `proj` (1×1); single head, residual.
4761    pub attn_gamma: &'a [f32],
4762    pub attn_qkv: Qi21VaeConvRef<'a>,
4763    pub attn_proj: Qi21VaeConvRef<'a>,
4764    pub ups: Vec<Qi21VaeUpRef<'a>>,
4765    pub norm_out: &'a [f32],
4766    pub conv_out: Qi21VaeConvRef<'a>,
4767}
4768
4769/// Resident VAE decode: `z` = raw latent planes `[z_dim, h·w]` → `out`
4770/// `[out_channels, 16h·16w]` (before the clamp). `false` = not handled,
4771/// `out` untouched: the per-conv path runs.
4772#[allow(unused_variables)]
4773pub fn qi21_vae_decode(a: &Qi21VaeDecodeArgs, z: &[f32], h: usize, w: usize, out: &mut [f32]) -> bool {
4774    match backend() {
4775        #[cfg(feature = "gpu")]
4776        Backend::Wgpu => crate::gpu_wgpu::qi21_vae::decode(a, z, h, w, out),
4777        #[allow(unreachable_patterns)]
4778        _ => false,
4779    }
4780}
4781
4782/// One Z-Image transformer block's device inputs (noise refiner, context
4783/// refiner or main layer — all share this shape). Weights are tensor
4784/// indices into `model.tensors` (diffusers names under `dit.`); the codec
4785/// is whatever the container holds (F16/Bf16/Q8Row/Q8_2f/Q4TiledP…), and a
4786/// backend that cannot expand a codec declines (returns `false`).
4787/// Norm vectors are f32 host slices that live as long as the caller's
4788/// `ZImageDit`; a backend may cache them by pointer (they do not change).
4789#[derive(Clone, Copy)]
4790pub struct ZBlockRef<'a> {
4791    /// `attention.to_q/to_k/to_v/to_out.0.weight`, each [hidden, hidden].
4792    pub wq: usize,
4793    pub wk: usize,
4794    pub wv: usize,
4795    pub wo: usize,
4796    /// `feed_forward.w1` (gate) / `w3` (up) [inter, hidden], `w2` (down)
4797    /// [hidden, inter]. FFN = w2(silu(w1·x) ⊙ w3·x).
4798    pub w1: usize,
4799    pub w3: usize,
4800    pub w2: usize,
4801    /// `attention_norm1` / `attention_norm2`, [hidden] (plain-w RMSNorm).
4802    pub norm1: &'a [f32],
4803    pub norm2: &'a [f32],
4804    /// `ffn_norm1` / `ffn_norm2`, [hidden].
4805    pub ffn_norm1: &'a [f32],
4806    pub ffn_norm2: &'a [f32],
4807    /// `attention.norm_q` / `norm_k`, [hd] (per-head RMSNorm before RoPE).
4808    pub norm_q: &'a [f32],
4809    pub norm_k: &'a [f32],
4810}
4811
4812/// Z-Image geometry. Turbo: hidden 3840, nh 30 (MHA, no GQA), hd 128,
4813/// inter 10240, eps 1e-5 (all RMSNorms incl. qk-norm), final_eps 1e-6
4814/// (the affine-free final LayerNorm), patch_dim 64 (2×2×16).
4815#[derive(Clone, Copy, Debug, PartialEq)]
4816pub struct ZGeom {
4817    pub hidden: usize,
4818    pub nh: usize,
4819    pub hd: usize,
4820    pub inter: usize,
4821    pub eps: f32,
4822    pub final_eps: f32,
4823    pub patch_dim: usize,
4824}
4825
4826/// Once per (prompt, resolution). The backend uploads/caches what it needs
4827/// keyed by `key`; weight planes are keyed by the MODEL (not by `key`) and
4828/// survive across prompts until `zimage_release`.
4829pub struct ZPrepareArgs<'a> {
4830    pub model: &'a Arc<CmfModel>,
4831    pub geom: ZGeom,
4832    /// Caller-chosen identity of this (prompt, resolution) state; every
4833    /// `ZStepArgs` of the same image carries the same key.
4834    pub key: u64,
4835    /// Image tokens (H/16 · W/16), padded count ceil32(n_img), caption
4836    /// padded count ceil32(L). S = n_img_p + n_cap_p.
4837    pub n_img: usize,
4838    pub n_img_p: usize,
4839    pub n_cap_p: usize,
4840    /// The patch grid (H/16, W/16); n_img = grid.0 · grid.1. Row-major
4841    /// token order `hp·grid.1 + wp`.
4842    pub grid: (usize, usize),
4843    /// [n_cap_p, hidden], ALREADY context-refined (host or device).
4844    pub cap: &'a [f32],
4845    /// Noise-refiner RoPE: [n_img_p · hd/2] cos, sin (complex-interleaved
4846    /// pairs, hd/2 angles per token).
4847    pub rope_img: (&'a [f32], &'a [f32]),
4848    /// Main-layer RoPE: [(n_img_p + n_cap_p) · hd/2], rows ordered [img, cap].
4849    pub rope_joint: (&'a [f32], &'a [f32]),
4850    /// `all_x_embedder.2-1.weight` [hidden, 64], `.bias` [hidden],
4851    /// `x_pad_token` [hidden] (replaces rows ≥ n_img after the embed).
4852    pub x_emb_w: &'a [f32],
4853    pub x_emb_b: &'a [f32],
4854    pub x_pad: &'a [f32],
4855    /// `all_final_layer.2-1.linear.weight` [64, hidden], `.bias` [64].
4856    pub final_w: &'a [f32],
4857    pub final_b: &'a [f32],
4858    /// 2 noise-refiner blocks (image rows only) and 30 main layers.
4859    pub noise_refiner: &'a [ZBlockRef<'a>],
4860    pub layers: &'a [ZBlockRef<'a>],
4861    /// OPTIONAL (backends may ignore): the modulation of EVERY step of this
4862    /// image, [steps][(2+30)·4·hidden] in the `ZStepArgs::mods` layout, and
4863    /// [steps][hidden] final scales, so a backend can upload them once per
4864    /// image and index them by `ZStepArgs::step`. `ZStepArgs::mods` is still
4865    /// always supplied and is authoritative.
4866    pub mods_all: Option<&'a [f32]>,
4867    pub final_scale_all: Option<&'a [f32]>,
4868    /// OPTIONAL (B2): the CFG negative item. When `Some`, the backend
4869    /// prepares ONE batch-2 program under `key` — item 0 is this prompt,
4870    /// item 1 the negative — and every `ZStepArgs` of that key must carry
4871    /// `out_neg`. A backend without batch 2 returns `false` (the caller
4872    /// then prepares the two items separately or runs the CPU path).
4873    pub neg: Option<ZNegArgs<'a>>,
4874}
4875
4876/// The negative (unconditional) item of a CFG pair: its own refined
4877/// caption, padded caption length and joint RoPE table (the image ids sit
4878/// at axis-0 position L_p+1, so both tables depend on the item's L_p).
4879pub struct ZNegArgs<'a> {
4880    /// [n_cap_p, hidden], context-refined.
4881    pub cap: &'a [f32],
4882    pub n_cap_p: usize,
4883    /// [n_img_p · hd/2] cos, sin (noise refiner) of this item.
4884    pub rope_img: (&'a [f32], &'a [f32]),
4885    /// [(n_img_p + n_cap_p) · hd/2] cos, sin, rows [img, cap].
4886    pub rope_joint: (&'a [f32], &'a [f32]),
4887}
4888
4889/// Once per denoising step.
4890pub struct ZStepArgs<'a> {
4891    /// The `ZPrepareArgs::key` this step belongs to. A key the backend has
4892    /// not prepared → `false`.
4893    pub key: u64,
4894    /// Step index into the schedule (0..steps); selects the row of
4895    /// `ZPrepareArgs::mods_all` when a backend uses it.
4896    pub step: usize,
4897    /// [n_img_p, 64] patchified latent, inner order (dy·2+dx)·16+c. Rows
4898    /// ≥ n_img are copies of the last row; the backend replaces them with
4899    /// `x_pad` after the embed.
4900    pub x_tok: &'a [f32],
4901    /// Per block (noise_refiner then layers) the RAW chunks
4902    /// [scale_msa, gate_msa, scale_mlp, gate_mlp] of Linear(temb) (no SiLU
4903    /// before it), [(2+30)·4·hidden]. The backend applies (1+s) and tanh(g).
4904    pub mods: &'a [f32],
4905    /// [hidden] = 1 + Linear(SiLU(temb)) — already includes the +1.
4906    pub final_scale: &'a [f32],
4907    /// [n_img, 64]: the model output v (before the pipeline's negation),
4908    /// image rows only, patchified order.
4909    pub out: &'a mut [f32],
4910    /// [n_img, 64]: the negative item's v — required (and only valid) for
4911    /// a key prepared with `ZPrepareArgs::neg`. Both items see `x_tok`.
4912    pub out_neg: Option<&'a mut [f32]>,
4913}
4914
4915/// Prepare the per-(prompt, resolution) device state. Backends: wgpu →
4916/// `gpu_wgpu::zimage::prepare` (WP2), Metal → `gpu_metal::zimage::prepare`
4917/// (WP3).
4918#[allow(unused_variables)]
4919pub fn zimage_prepare(a: &ZPrepareArgs) -> bool {
4920    match backend() {
4921        #[cfg(target_os = "macos")]
4922        Backend::Metal => crate::gpu_metal::zimage::prepare(a),
4923        #[cfg(feature = "gpu")]
4924        Backend::Wgpu => crate::gpu_wgpu::zimage::prepare(a),
4925        #[allow(unreachable_patterns)]
4926        _ => false,
4927    }
4928}
4929
4930/// One full DiT forward on the device: x_embed → pad rows → noise refiner
4931/// ×2 → concat [img, cap] → 30 layers → final LayerNorm·scale → Linear →
4932/// image rows into `a.out`.
4933#[allow(unused_variables)]
4934pub fn zimage_step(a: &mut ZStepArgs) -> bool {
4935    match backend() {
4936        #[cfg(target_os = "macos")]
4937        Backend::Metal => crate::gpu_metal::zimage::step(a),
4938        #[cfg(feature = "gpu")]
4939        Backend::Wgpu => crate::gpu_wgpu::zimage::step(a),
4940        #[allow(unreachable_patterns)]
4941        _ => false,
4942    }
4943}
4944
4945/// OPTIONAL (B2): build the backend's weight planes for the per-step blocks
4946/// and the context refiner ahead of `zimage_prepare`, so the caller can
4947/// overlap the upload with the (CPU) text encoder. `false` = not done;
4948/// `zimage_prepare` builds whatever is missing either way.
4949#[allow(unused_variables)]
4950pub fn zimage_preload(
4951    model: &Arc<CmfModel>,
4952    geom: &ZGeom,
4953    noise_refiner: &[ZBlockRef],
4954    layers: &[ZBlockRef],
4955    context_refiner: &[ZBlockRef],
4956) -> bool {
4957    match backend() {
4958        #[cfg(target_os = "macos")]
4959        Backend::Metal => {
4960            crate::gpu_metal::zimage::preload(model, geom, noise_refiner, layers, context_refiner)
4961        }
4962        #[cfg(feature = "gpu")]
4963        Backend::Wgpu => crate::gpu_wgpu::zimage::preload(model, geom, noise_refiner, layers, context_refiner),
4964        #[allow(unreachable_patterns)]
4965        _ => false,
4966    }
4967}
4968
4969/// Persist the driver's compiled pipelines after a Z-Image generation (the
4970/// chain's kernels are built at first use, after the context came up), so
4971/// the next process skips the compile. Best-effort, no-op off wgpu.
4972pub fn zimage_flush_pipelines() {
4973    #[cfg(feature = "gpu")]
4974    if matches!(backend(), Backend::Wgpu) {
4975        crate::gpu_wgpu::pipeline_cache_flush();
4976    }
4977}
4978
4979/// OPTIONAL (B2): bring the device up and compile the Z-Image kernels, on
4980/// a helper thread at the start of a generation (the context and the
4981/// compiles cost ~1 s cold, beside the host-side loading). `false` = no
4982/// device path here.
4983pub fn zimage_warmup() -> bool {
4984    match backend() {
4985        #[cfg(target_os = "macos")]
4986        Backend::Metal => crate::gpu_metal::zimage::warmup(),
4987        #[cfg(feature = "gpu")]
4988        Backend::Wgpu => crate::gpu_wgpu::zimage::warmup(),
4989        #[allow(unreachable_patterns)]
4990        _ => false,
4991    }
4992}
4993
4994/// OPTIONAL (B2): upload the resident VAE's weights and compile its
4995/// kernels ahead of `vae_decode_chain` (the caller runs it on a helper
4996/// thread while the DiT steps keep the device busy). `false` = not done.
4997#[allow(unused_variables)]
4998pub fn vae_prewarm(a: &crate::vae::VaeChainArgs) -> bool {
4999    match backend() {
5000        #[cfg(target_os = "macos")]
5001        Backend::Metal => crate::gpu_metal::zimage::vae_prewarm(a),
5002        #[cfg(feature = "gpu")]
5003        Backend::Wgpu => crate::gpu_wgpu::zimage::vae_prewarm(a),
5004        #[allow(unreachable_patterns)]
5005        _ => false,
5006    }
5007}
5008
5009/// Drop the Z-Image DiT device state (planes, prepared programs) but keep
5010/// the VAE chain (B2: the generator frees the DiT before decoding).
5011pub fn zimage_release_dit() {
5012    #[cfg(target_os = "macos")]
5013    crate::gpu_metal::zimage::release_dit();
5014    #[cfg(feature = "gpu")]
5015    crate::gpu_wgpu::zimage::release_dit();
5016}
5017
5018/// Drop every Z-Image device resource (planes, prepared states, VAE chain
5019/// buffers): stage change or process end. Calls each compiled backend's
5020/// release directly, without `backend()`, so it never brings a device up;
5021/// the child modules' `release` must touch module-local state only.
5022pub fn zimage_release() {
5023    #[cfg(target_os = "macos")]
5024    crate::gpu_metal::zimage::release();
5025    #[cfg(feature = "gpu")]
5026    crate::gpu_wgpu::zimage::release();
5027}
5028
5029/// Optional device context refiner: the same block math with scale = 0 and
5030/// gate = 1 (unmodulated): x += norm2(attn(norm1(x))); x += ffn_norm2(ffn(
5031/// ffn_norm1(x))). `cap` is [n_cap_p, hidden] in/out (the cap_embedder
5032/// output with pad rows already = cap_pad_token); `rope_cap` is
5033/// [n_cap_p · hd/2] cos, sin. `false` = untouched, run the CPU refiner.
5034#[allow(unused_variables)]
5035pub fn zimage_refine_caption(
5036    model: &Arc<CmfModel>,
5037    geom: &ZGeom,
5038    blocks: &[ZBlockRef],
5039    rope_cap: (&[f32], &[f32]),
5040    cap: &mut [f32],
5041) -> bool {
5042    match backend() {
5043        #[cfg(target_os = "macos")]
5044        Backend::Metal => {
5045            crate::gpu_metal::zimage::refine_caption(model, geom, blocks, rope_cap, cap)
5046        }
5047        #[cfg(feature = "gpu")]
5048        Backend::Wgpu => {
5049            crate::gpu_wgpu::zimage::refine_caption(model, geom, blocks, rope_cap, cap)
5050        }
5051        #[allow(unreachable_patterns)]
5052        _ => false,
5053    }
5054}
5055
5056/// Resident Flux-VAE decode (the whole decoder on the device, one latent
5057/// upload, one RGB readback). `a` comes from `VaeDecoder::chain_args()`.
5058/// `z` is [latent_channels, h, w] ALREADY de-normalised
5059/// (z/scaling_factor + shift_factor — the conv_in input); `out` is
5060/// [3, 8h, 8w], the raw decoder output (≈[-1, 1], before x/2+0.5).
5061#[allow(unused_variables)]
5062pub fn vae_decode_chain(
5063    a: &crate::vae::VaeChainArgs,
5064    z: &[f32],
5065    h: usize,
5066    w: usize,
5067    out: &mut [f32],
5068) -> bool {
5069    match backend() {
5070        #[cfg(target_os = "macos")]
5071        Backend::Metal => crate::gpu_metal::zimage::vae_decode_chain(a, z, h, w, out),
5072        #[cfg(feature = "gpu")]
5073        Backend::Wgpu => crate::gpu_wgpu::zimage::vae_decode_chain(a, z, h, w, out),
5074        #[allow(unreachable_patterns)]
5075        _ => false,
5076    }
5077}