Skip to main content

cortiq_engine/
gpu.rs

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