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