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