Skip to main content

cortiq_engine/
gpu.rs

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