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}