Skip to main content

qualia_core_db/inference/inference_bench/
toggles.rs

1//! Runtime-config toggles read by the real inference path (decode / prefill /
2//! attention / GEMM selection). Each is a process-global flag with a `set_*` /
3//! `*_enabled` pair; env vars override where documented. Also the GEMM backend
4//! selector. Pure code motion — behaviour unchanged.
5
6use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering};
7use std::sync::Mutex;
8
9// ── Decode budget override ────────────────────────────────────────────────────
10// A bounded, fixed decode-token count gives stable, comparable tok/s. The real
11// decode loop reads this once per call; 0 = use the production `DECODE_TOKEN_BUDGET`.
12
13static DECODE_BUDGET_OVERRIDE: AtomicU32 = AtomicU32::new(0);
14
15/// Set a fixed decode-token budget for benchmarking (0 = production default).
16#[inline]
17pub fn set_decode_budget_override(n: u32) {
18    DECODE_BUDGET_OVERRIDE.store(n, Ordering::Relaxed);
19}
20
21/// When budget override is active, ignore EOS so A/B runs a fixed token count
22/// (prevents early-stop from inflating/deflating tok/s on short prompts).
23#[inline]
24pub fn decode_budget_fixed_tokens() -> bool {
25    DECODE_BUDGET_OVERRIDE.load(Ordering::Relaxed) > 0
26}
27
28/// Current decode-budget override (0 = none).
29#[inline]
30pub fn decode_budget_override() -> u32 {
31    DECODE_BUDGET_OVERRIDE.load(Ordering::Relaxed)
32}
33
34// ── Wall-clock inference timeout override (batch / overnight jobs) ────────────
35// 0 = use production `INFERENCE_TIMEOUT_MS` (30s interactive). Batch profile may
36// raise this to hours for multi-system health differential analysis overnight.
37static INFERENCE_TIMEOUT_OVERRIDE_MS: AtomicU64 = AtomicU64::new(0);
38
39/// Set wall-clock decode timeout in ms (0 = production default 30_000).
40#[inline]
41pub fn set_inference_timeout_override_ms(ms: u64) {
42    INFERENCE_TIMEOUT_OVERRIDE_MS.store(ms, Ordering::Relaxed);
43}
44
45/// Effective timeout: override if non-zero, else env `QUALIA_INFERENCE_TIMEOUT_MS`, else 30s.
46#[inline]
47pub fn inference_timeout_ms() -> u64 {
48    let o = INFERENCE_TIMEOUT_OVERRIDE_MS.load(Ordering::Relaxed);
49    if o > 0 {
50        return o;
51    }
52    if let Ok(s) = std::env::var("QUALIA_INFERENCE_TIMEOUT_MS") {
53        if let Ok(v) = s.parse::<u64>() {
54            if v > 0 {
55                return v;
56            }
57        }
58    }
59    30_000
60}
61
62// ── A1a GPU top-k toggle (D18) ────────────────────────────────────────────────
63// Default ON after widening the output gate and adding the allocation-free top-1 path. The decode
64// loop reads only block winners from the GPU instead of full vocabulary chunks when no sieve mask is
65// active.
66// Set `QUALIA_LLM_GPU_TOPK=0` to force the full-logit argmax fallback.
67static GPU_TOPK: AtomicBool = AtomicBool::new(true);
68
69/// Enable/disable the GPU top-k decode path (`QUALIA_LLM_GPU_TOPK`).
70#[inline]
71pub fn set_gpu_topk(on: bool) {
72    GPU_TOPK.store(on, Ordering::Relaxed);
73}
74
75/// Whether the GPU top-k decode path is active. The env var overrides the flag in BOTH directions
76/// (`0`/`false` → off, `1`/`true` → on); otherwise the process default (ON) applies.
77#[inline]
78pub fn gpu_topk_enabled() -> bool {
79    match std::env::var("QUALIA_LLM_GPU_TOPK").ok().as_deref() {
80        Some("0") | Some("false") => false,
81        Some("1") | Some("true") => true,
82        _ => GPU_TOPK.load(Ordering::Relaxed),
83    }
84}
85
86// ── A1b ternary-FFN toggle (D3/D7) ────────────────────────────────────────────
87// Additive, default-OFF: when a `.q42` ternary container is booted, routes its FFN
88// GEMMs through the resident 2-bit GPU kernel (`TernaryFfnResident`). OFF runs the
89// SAME ternary weights via the CPU oracle — so ON-vs-OFF isolates the GPU-kernel win
90// on identical weights, and ternary-container-vs-Q8 (a0) is the headline FFN number.
91static TERNARY_FFN: AtomicBool = AtomicBool::new(false);
92
93/// Enable/disable the resident 2-bit GPU ternary-FFN path (`QUALIA_LLM_TERNARY_FFN`).
94#[inline]
95pub fn set_ternary_ffn(on: bool) {
96    TERNARY_FFN.store(on, Ordering::Relaxed);
97}
98
99/// Whether the GPU ternary-FFN path is active (atomic flag OR the env var). When false, ternary
100/// FFN GEMMs fall back to the CPU oracle (correct, slower) — the toggle's OFF baseline.
101#[inline]
102pub fn ternary_ffn_enabled() -> bool {
103    TERNARY_FFN.load(Ordering::Relaxed)
104        || matches!(
105            std::env::var("QUALIA_LLM_TERNARY_FFN").ok().as_deref(),
106            Some("1") | Some("true")
107        )
108}
109
110// Native attention projection split: K/V matmuls run through cooperative GEMV, then the attention
111// shader consumes the projected rows via `proj_row_stride` only for RoPE + KV-cache writes. Default
112// ON after the no-readback fused K/V path replaced the old diagnostic readback implementation.
113// Set `QUALIA_LLM_PREPROJECT_ATTN=0` to force the legacy in-attention projection path.
114static ATTN_PREPROJECT: AtomicBool = AtomicBool::new(true);
115
116#[inline]
117pub fn set_attention_preproject(on: bool) {
118    ATTN_PREPROJECT.store(on, Ordering::Relaxed);
119}
120
121#[inline]
122pub fn attention_preproject_enabled() -> bool {
123    // W5b Phase 4b: the dict-coded write happens in `write_kv_head` (the attention pass); the fused
124    // pre-projection bypasses it, so force it off in dict mode.
125    if kv_dict_enabled() {
126        return false;
127    }
128    match std::env::var("QUALIA_LLM_PREPROJECT_ATTN").ok().as_deref() {
129        Some("0") | Some("false") => false,
130        Some("1") | Some("true") => true,
131        _ => ATTN_PREPROJECT.load(Ordering::Relaxed),
132    }
133}
134
135// Native attention tail fusion: Q-attention writes its output to a GPU buffer, and o_proj consumes
136// that buffer directly after K and V are both present in the KV cache. Default ON (native): removes
137// one submit->wait round-trip per layer while preserving token identity against the proven readback
138// path. Set `QUALIA_LLM_FUSE_ATTN_O=0` to force the older Q-readback + o_proj path.
139static ATTN_O_FUSE: AtomicBool = AtomicBool::new(true);
140
141#[inline]
142pub fn set_attention_o_fuse(on: bool) {
143    ATTN_O_FUSE.store(on, Ordering::Relaxed);
144}
145
146#[inline]
147pub fn attention_o_fuse_enabled() -> bool {
148    // W5b Phase 4b: the fused Q+O tail reads K/V from the cache on the GPU path; keep the plain
149    // read_k/read_v (dict-aware) path in dict mode.
150    if kv_dict_enabled() {
151        return false;
152    }
153    match std::env::var("QUALIA_LLM_FUSE_ATTN_O").ok().as_deref() {
154        Some("0") | Some("false") => false,
155        Some("1") | Some("true") => true,
156        _ => ATTN_O_FUSE.load(Ordering::Relaxed),
157    }
158}
159
160// ── Phase 2: resident weights toggle ──────────────────────────────────────────
161// Default ON (native). Each layer's q/k/v/o/gate/up/down weight is uploaded to its own resident
162// VRAM buffer once (keyed by the GGUF tensor byte_offset) and reused every token, instead of
163// re-`write_buffer`ing the (up to ~50 MB for a 3B FFN tensor) weight into the shared GEMM buffer
164// on every GEMM, every token. For a 3B F16 model that re-upload is ~5 GB/token of PCIe traffic —
165// the decode bottleneck. Set `QUALIA_LLM_RESIDENT_WEIGHTS=0` to force the per-token re-upload (the
166// A/B OFF baseline) — useful for measuring the win or on VRAM-constrained GPUs.
167static RESIDENT_WEIGHTS: AtomicBool = AtomicBool::new(true);
168
169/// Enable/disable the resident per-tensor weight buffers (`QUALIA_LLM_RESIDENT_WEIGHTS`).
170#[inline]
171pub fn set_resident_weights(on: bool) {
172    RESIDENT_WEIGHTS.store(on, Ordering::Relaxed);
173}
174
175/// Whether native GEMM should bind resident per-tensor weight buffers (upload-once) rather than
176/// re-uploading the weight every token. Env forces either direction; otherwise the atomic flag.
177#[inline]
178pub fn resident_weights_enabled() -> bool {
179    match std::env::var("QUALIA_LLM_RESIDENT_WEIGHTS").ok().as_deref() {
180        Some("0") | Some("false") => false,
181        Some("1") | Some("true") => true,
182        _ => RESIDENT_WEIGHTS.load(Ordering::Relaxed),
183    }
184}
185
186// ── Resident-token decode toggle (single fence per token) ─────────────────────
187// Default ON (native). Keeps the hidden state in VRAM for the WHOLE token: all 32 layers
188// (RMSNorm/residuals as GPU elem ops) + output norm + chunked logits top-1 are encoded into ONE
189// command submit with ONE blocking fence and a ~400 B candidate readback — replacing the legacy
190// ~107 submit→wait round-trips/token (measured ~24% pure fence time on SmolLM2-360M, A2000,
191// Vulkan). Any per-model ineligibility (unsupported quant, no resident logits, sieve mask, CPU
192// attention) falls back to the legacy per-layer path. `QUALIA_LLM_RESIDENT_DECODE=0` forces the
193// legacy path (the A/B baseline + the differential-test comparator).
194static RESIDENT_DECODE: AtomicBool = AtomicBool::new(true);
195
196/// Enable/disable the resident-token single-fence decode (`QUALIA_LLM_RESIDENT_DECODE`).
197#[inline]
198pub fn set_resident_decode(on: bool) {
199    RESIDENT_DECODE.store(on, Ordering::Relaxed);
200}
201
202/// Whether native decode should run the GPU-resident single-fence token path.
203#[inline]
204pub fn resident_decode_enabled() -> bool {
205    match std::env::var("QUALIA_LLM_RESIDENT_DECODE").ok().as_deref() {
206        Some("0") | Some("false") => false,
207        Some("1") | Some("true") => true,
208        _ => RESIDENT_DECODE.load(Ordering::Relaxed),
209    }
210}
211
212// ── W3: resident single-fence-per-chunk prefill toggle ────────────────────────
213// Default ON (verified). When on (and the model is eligible — GPU-eligible weights, coop GEMV, no
214// active sparse-attention route), each prefill chunk of ≤PREFILL_CHUNK_SIZE prompt tokens populates
215// the KV cache in ONE command submit / ONE fence (all 32 layers batched + resident hidden state)
216// instead of the legacy per-layer + per-token Q/FFN loop (~640 submit→wait round-trips for a 10-token
217// prompt). Delivers TTFT and the batched-forward primitive W6a-verify needs; on a fast discrete GPU
218// the steady-state win is latent (prefill is compute-bound), the fence win lands on
219// edge/mobile/under-load. Passed the `a3a` gate on SmolLM2-360M Q8 (A2000): the batched RMSNorm
220// reduces in the same sequential order as the legacy CPU path, so the KV it writes is BYTE-IDENTICAL
221// → decode-identical, with the int8 KV cache both ON and OFF. Any ineligibility falls back to the
222// legacy `dispatch_prefill_chunk` path unchanged. `QUALIA_LLM_RESIDENT_PREFILL=0` forces legacy.
223static RESIDENT_PREFILL: AtomicBool = AtomicBool::new(true);
224
225/// Enable/disable the resident single-fence-per-chunk prefill path (`QUALIA_LLM_RESIDENT_PREFILL`).
226#[inline]
227pub fn set_resident_prefill(on: bool) {
228    RESIDENT_PREFILL.store(on, Ordering::Relaxed);
229}
230
231/// Whether native prefill should run the GPU-resident single-fence-per-chunk arena.
232#[inline]
233pub fn resident_prefill_enabled() -> bool {
234    match std::env::var("QUALIA_LLM_RESIDENT_PREFILL").ok().as_deref() {
235        Some("0") | Some("false") => false,
236        Some("1") | Some("true") => true,
237        _ => RESIDENT_PREFILL.load(Ordering::Relaxed),
238    }
239}
240
241// ── W6a: prompt-lookup speculative decode toggle (ADR 0010) ───────────────────
242// Default ON (ADR 0010, directed by Timothy). When on (and no sieve/sampler/route is active), the
243// decode loop drafts the next few tokens by n-gram prompt-lookup, verifies them in ONE batched
244// forward (`verify_draft_batch`), and emits the longest greedily-agreeing prefix + the model's own
245// correction token. Output matches ordinary decode everywhere except rare genuine near-ties (the a1a
246// phenomenon: the model is ambivalent, both tokens equally valid, and a ULP-level difference between
247// the batched and single-token forwards flips the pick). The win is pure latency on repetitive /
248// quoting / structured / code text (several tokens per forward, measured ~3–12×); on novel text it
249// drafts little and costs ~nothing.
250//
251// This is the MODE SWITCH. Change modes three ways: (1) env `QUALIA_LLM_SPEC_DECODE=0` (off) / `=1`
252// (on) at launch — the desktop/daemon reads it; (2) `set_spec_decode(bool)` at runtime (the UI /
253// host calls this); (3) `spec_decode_enabled()` to read the effective mode. The env var, when set,
254// overrides the runtime flag in BOTH directions. See ADR 0010 for the rationale.
255static SPEC_DECODE: AtomicBool = AtomicBool::new(true);
256
257/// Enable/disable prompt-lookup speculative decode (`QUALIA_LLM_SPEC_DECODE`). Runtime mode switch —
258/// the desktop UI / host calls this to flip between exact single-token decode and speculative decode.
259#[inline]
260pub fn set_spec_decode(on: bool) {
261    SPEC_DECODE.store(on, Ordering::Relaxed);
262}
263
264/// Whether the decode loop should run prompt-lookup speculative decode (the effective mode: env var
265/// wins if set, else the runtime flag). Read this to reflect the current mode in a UI.
266#[inline]
267pub fn spec_decode_enabled() -> bool {
268    match std::env::var("QUALIA_LLM_SPEC_DECODE").ok().as_deref() {
269        Some("0") | Some("false") => false,
270        Some("1") | Some("true") => true,
271        _ => SPEC_DECODE.load(Ordering::Relaxed),
272    }
273}
274
275// ── W5a: int8 KV cache toggle ─────────────────────────────────────────────────
276// Default ON (verified). When on (and head_dim % 4 == 0), the KV cache is stored as packed int8
277// lanes + one f32 scale per (slot, kv_head) instead of f32 — ~3.8× less KV memory (80→21 MiB @
278// ctx 1024) and ~3.8× less attention memory bandwidth (the decode bottleneck). Passed the gate on
279// SmolLM2-360M Q8: ΔPPL +0.05% (≪ the 5% bar), coherent, Vulkan-parity tok/s (see `w5a_int8_kv`).
280// Read once at model load (`ensure_kv_cache`), so set it BEFORE the model loads. Models with
281// head_dim not a multiple of 4 transparently fall back to the f32 layout. `QUALIA_LLM_KV_INT8=0`
282// forces the f32 KV cache (the A/B baseline).
283static KV_INT8: AtomicBool = AtomicBool::new(true);
284
285/// Enable/disable the int8 KV cache (`QUALIA_LLM_KV_INT8`).
286#[inline]
287pub fn set_kv_int8(on: bool) {
288    KV_INT8.store(on, Ordering::Relaxed);
289}
290
291/// Whether the KV cache should be int8-quantized.
292#[inline]
293pub fn kv_int8_enabled() -> bool {
294    match std::env::var("QUALIA_LLM_KV_INT8").ok().as_deref() {
295        Some("0") | Some("false") => false,
296        Some("1") | Some("true") => true,
297        _ => KV_INT8.load(Ordering::Relaxed),
298    }
299}
300
301// ── W5b Phase 4b: sparse-dictionary KV cache ──────────────────────────────────
302// Store each KV vector as its k-sparse dictionary code instead of f32/int8, reconstructing in the
303// attention shader on read (~3-4× smaller than int8). Read once at model load (`ensure_kv_cache`); a
304// certified dictionary must be installed (`kv_dict_runtime::load_certified`) whose head_dim matches the
305// model, else the layout transparently falls back. Default OFF — it trades memory for reconstruct
306// compute, so it only wins on memory-bound / long-context targets (see the Phase 4b plan).
307static KV_DICT: AtomicBool = AtomicBool::new(false);
308
309/// Enable/disable the sparse-dictionary KV cache (`QUALIA_LLM_KV_DICT`).
310#[inline]
311pub fn set_kv_dict(on: bool) {
312    KV_DICT.store(on, Ordering::Relaxed);
313}
314
315/// Whether the KV cache should use the installed sparse dictionary.
316#[inline]
317pub fn kv_dict_enabled() -> bool {
318    match std::env::var("QUALIA_LLM_KV_DICT").ok().as_deref() {
319        Some("0") | Some("false") => false,
320        Some("1") | Some("true") => true,
321        _ => KV_DICT.load(Ordering::Relaxed),
322    }
323}
324
325// ── W2: exact sampler config ──────────────────────────────────────────────────
326// Process-global sampler config, read ONCE at decode start (like the decode budget). `None` ⇒
327// greedy argmax (the pre-W2 default; a1a/a1c/a1d byte-identical). `Some(cfg)` with cfg.temperature
328// > 0 activates the CPU sampling chain in `crate::sampler`. Set per-request by the host/MCP layer.
329static SAMPLER_CONFIG: Mutex<Option<crate::sampler::SamplerConfig>> = Mutex::new(None);
330
331/// Install the decode sampler config (`None` restores greedy argmax).
332#[inline]
333pub fn set_sampler_config(cfg: Option<crate::sampler::SamplerConfig>) {
334    if let Ok(mut g) = SAMPLER_CONFIG.lock() {
335        // A greedy config is equivalent to None — normalize so the decode loop can skip the
336        // full-logits readback entirely when nothing non-greedy is requested.
337        *g = cfg.filter(|c| !c.is_greedy());
338    }
339}
340
341/// The active decode sampler config, if a non-greedy one is installed.
342#[inline]
343pub fn sampler_config() -> Option<crate::sampler::SamplerConfig> {
344    SAMPLER_CONFIG.lock().ok().and_then(|g| *g)
345}
346
347// ── Phase 3: FFN fusion toggle ────────────────────────────────────────────────
348// Default ON (native). Runs the whole pre-norm FFN — gate GEMM, up GEMM, GPU SiLU·mul,
349// down GEMM — in ONE command submit with intermediates kept in VRAM, so a layer costs ONE
350// submit→wait round-trip instead of three (the gate/up/down readbacks + CPU SiLU·mul between
351// them). Requires resident weights (it binds resident weight buffers); falls back to the
352// per-GEMM path when resident is off or a tensor is GPU-ineligible. `QUALIA_LLM_FFN_FUSION=0`
353// forces the per-GEMM path (the A/B OFF baseline).
354static FFN_FUSION: AtomicBool = AtomicBool::new(true);
355
356/// Enable/disable the fused single-submit FFN path (`QUALIA_LLM_FFN_FUSION`).
357#[inline]
358pub fn set_ffn_fusion(on: bool) {
359    FFN_FUSION.store(on, Ordering::Relaxed);
360}
361
362/// Whether the native FFN should run fused (one submit/layer) rather than three GEMM round-trips.
363#[inline]
364pub fn ffn_fusion_enabled() -> bool {
365    match std::env::var("QUALIA_LLM_FFN_FUSION").ok().as_deref() {
366        Some("0") | Some("false") => false,
367        Some("1") | Some("true") => true,
368        _ => FFN_FUSION.load(Ordering::Relaxed),
369    }
370}
371
372/// Set when a resident decode plan was built with `fused_ffn.wgsl` in the mega-pass (T-A1).
373static FFN_FUSION_IN_RESIDENT: AtomicBool = AtomicBool::new(false);
374
375#[inline]
376pub fn set_ffn_fusion_in_resident(on: bool) {
377    FFN_FUSION_IN_RESIDENT.store(on, Ordering::Relaxed);
378}
379
380/// True when the last-built resident plan wires fused FFN expansion (not just the flag).
381#[inline]
382pub fn ffn_fusion_in_resident() -> bool {
383    FFN_FUSION_IN_RESIDENT.load(Ordering::Relaxed)
384}
385
386// ── FFN quant → f16 promotion (opt-in; bandwidth vs dequant trade-off) ─
387// Default OFF. Microbench on small GEMMs favoured f16, but full Llama-3.2-3B on
388// A2000 12GB measured **slower** with FFN f16 (~2.1 tok/s) than Q4_K SoA (~2.6)
389// — 4× weight traffic outweighs dequant savings when memory-bound. Opt in with
390// `QUALIA_LLM_FFN_F16=1` on higher-bandwidth GPUs / smaller FFN dims.
391static FFN_F16: AtomicBool = AtomicBool::new(false);
392
393/// Enable/disable FFN quant→f16 promotion at resident-plan build (`QUALIA_LLM_FFN_F16`).
394#[inline]
395pub fn set_ffn_f16(on: bool) {
396    FFN_F16.store(on, Ordering::Relaxed);
397}
398
399/// Whether FFN weights should be promoted to f16 in VRAM for decode/prefill GEMV.
400#[inline]
401pub fn ffn_f16_enabled() -> bool {
402    match std::env::var("QUALIA_LLM_FFN_F16").ok().as_deref() {
403        Some("0") | Some("false") => false,
404        Some("1") | Some("true") => true,
405        _ => FFN_F16.load(Ordering::Relaxed),
406    }
407}
408
409// ── 0.0.21: cooperative GEMV kernel toggle ────────────────────────────────────
410// Default ON (native), verified. Routes native GEMV work through the cooperative
411// one-workgroup-per-row kernel (`coop_gemv`: coalesced reads + per-thread dequant +
412// shared-memory reduction) instead of the naive 1-thread/row `main`. The fused FFN path also selects
413// this cooperative entry point for gate/up/down GEMMs, so decode keeps one FFN readback per layer
414// while using the faster row reducer. The naive GEMV is the measured decode bottleneck
415// (compute/ALU-bound: uncoalesced strided reads + serial accumulate; it also makes Q4_K slower than
416// F16). `QUALIA_LLM_COOP_GEMV=0` forces the naive kernel (the A/B OFF baseline).
417// Earlier A2000 / Llama-3.2-3B-F16 per-GEMM verification: 2.39→3.22 tok/s (+35% over naive).
418static COOP_GEMV: AtomicBool = AtomicBool::new(true);
419
420/// Enable/disable the cooperative GEMV decode path (`QUALIA_LLM_COOP_GEMV`).
421#[inline]
422pub fn set_coop_gemv(on: bool) {
423    COOP_GEMV.store(on, Ordering::Relaxed);
424}
425
426/// Whether native GEMM should run the cooperative `coop_gemv` kernel rather than the naive
427/// 1-thread/row `main`. Env forces either direction; otherwise the atomic flag (default OFF).
428#[inline]
429pub fn coop_gemv_enabled() -> bool {
430    match std::env::var("QUALIA_LLM_COOP_GEMV").ok().as_deref() {
431        Some("0") | Some("false") => false,
432        Some("1") | Some("true") => true,
433        _ => COOP_GEMV.load(Ordering::Relaxed),
434    }
435}
436
437/// Rows per workgroup for multi-row coop GEMV (`coop_gemv_mr` in fused_transformer.wgsl).
438/// Must stay in lock-step with WGSL `COOP_ROWS`.
439pub const COOP_GEMV_ROWS: u32 = 8;
440
441/// Workgroup count for coop GEMV dispatch: `ceil(n_out / COOP_GEMV_ROWS)`.
442#[inline]
443pub fn coop_gemv_workgroups(n_out: u32) -> u32 {
444    n_out.div_ceil(COOP_GEMV_ROWS).max(1)
445}
446
447// ── W8: coopmat (tensor-core) GEMM selection seam ─────────────────────────────
448// Default OFF. The forge already has the self-activating coopmat path
449// (`wgsl_forge::gemm_f32_tc` → `coopmat_usable()` runtime probe): on wgpu 29.0.3 the WGSL coopmat
450// multiply returns zeros (#9741), so `coopmat_usable()` is `false` and the tier stays dormant; it
451// self-activates the moment a wgpu release / soft-fork carries the fix. This toggle is the
452// INFERENCE-side seam: when on AND coopmat is genuinely usable AND the matmul dims fit the 8×8×8 tile
453// (m,n,k multiples of 8 — so batched prefill, not the m=1 decode GEMV), the GEMM backend selector
454// reports `Coopmat`. Until the inference-side coopmat kernel is wired (which needs the wgpu fix), an
455// eligible `Coopmat` selection logs its readiness and falls back to `CoopGemv` — the plumbing is ready
456// and visible, self-activating with the forge. `QUALIA_LLM_COOPMAT=1` arms it.
457static COOPMAT_GEMM: AtomicBool = AtomicBool::new(false);
458
459/// Arm/disarm the coopmat (tensor-core) GEMM selection seam (`QUALIA_LLM_COOPMAT`).
460#[inline]
461pub fn set_coopmat_gemm(on: bool) {
462    COOPMAT_GEMM.store(on, Ordering::Relaxed);
463}
464
465/// Whether the coopmat GEMM seam is armed (env forces either direction; else the flag). Being armed
466/// does not mean coopmat runs — see [`coopmat_gemm_usable`], which also requires the hardware probe.
467#[inline]
468pub fn coopmat_gemm_enabled() -> bool {
469    match std::env::var("QUALIA_LLM_COOPMAT").ok().as_deref() {
470        Some("0") | Some("false") => false,
471        Some("1") | Some("true") => true,
472        _ => COOPMAT_GEMM.load(Ordering::Relaxed),
473    }
474}
475
476/// Whether coopmat is BOTH armed and genuinely usable on this device right now (the forge runtime
477/// probe passes — i.e. the wgpu #9741 fix is present). `false` on wgpu 29.0.3. Feature-guarded: with
478/// `wgsl-forge` off there is no probe, so coopmat is never usable.
479#[inline]
480pub fn coopmat_gemm_usable() -> bool {
481    if !coopmat_gemm_enabled() {
482        return false;
483    }
484    #[cfg(all(feature = "wgsl-forge", not(target_arch = "wasm32")))]
485    {
486        crate::wgsl_forge::coopmat_usable()
487    }
488    #[cfg(not(all(feature = "wgsl-forge", not(target_arch = "wasm32"))))]
489    {
490        false
491    }
492}
493
494/// GEMM backend the inference layer selects for a given matmul shape.
495#[derive(Debug, Clone, Copy, PartialEq, Eq)]
496pub enum GemmBackend {
497    /// Naive 1-thread/row `main` kernel (the correctness floor / A-B baseline).
498    Naive,
499    /// Cooperative one-workgroup-per-row `coop_gemv` (coalesced reads + shared-mem reduce).
500    CoopGemv,
501    /// WGSL cooperative-matrix (tensor-core) tile — selected only when armed, usable, and the dims
502    /// fit the 8×8×8 tile. Self-activates via the forge when wgpu #9741 lands.
503    Coopmat,
504}
505
506/// Select the GEMM backend for an `m×k×n` matmul: coopmat when armed+usable and all dims are 8-mult
507/// (the tensor-core tile — batched prefill, not the m=1 decode GEMV); else cooperative GEMV when
508/// enabled; else naive. Pure + total, so it is unit-tested without a GPU.
509#[inline]
510pub fn select_gemm_backend(m: usize, k: usize, n: usize) -> GemmBackend {
511    let tile_fits = m % 8 == 0 && k % 8 == 0 && n % 8 == 0 && m.min(k).min(n) > 0;
512    if coopmat_gemm_usable() && tile_fits {
513        GemmBackend::Coopmat
514    } else if coop_gemv_enabled() {
515        GemmBackend::CoopGemv
516    } else {
517        GemmBackend::Naive
518    }
519}
520
521// ── 0.0.21: resident-activation decode toggle ─────────────────────────────────
522// readback happens, after the final layer — replacing the legacy 2 readbacks/layer (each forced by
523// ── #48 correctness path: CPU attention reference ─────────────────────────────
524// Route native attention through the wasm-proven CPU SDPA (`cpu_attention_pass`) instead of the
525// GPU attention shader (whose output is currently unbounded). Correct-but-slower; opt-in.
526static CPU_ATTENTION: AtomicBool = AtomicBool::new(false);
527
528/// Enable/disable the native CPU-attention reference path (`QUALIA_LLM_CPU_ATTENTION`).
529#[inline]
530pub fn set_cpu_attention(on: bool) {
531    CPU_ATTENTION.store(on, Ordering::Relaxed);
532}
533
534/// Whether native attention should use the CPU reference.
535///
536/// **Default OFF** (use the GPU attention path) — as of #49 the GPU path also honors `norm_weight`
537/// for prefill K/V and produces coherent output, and it is faster. The CPU SDPA reference remains
538/// available as a correctness fallback / cross-check via `QUALIA_LLM_CPU_ATTENTION=1` or
539/// [`set_cpu_attention`].
540#[inline]
541pub fn cpu_attention_enabled() -> bool {
542    CPU_ATTENTION.load(Ordering::Relaxed)
543        || matches!(
544            std::env::var("QUALIA_LLM_CPU_ATTENTION").ok().as_deref(),
545            Some("1") | Some("true")
546        )
547}