qualia_core_db/wgsl_forge/dispatch.rs
1//! Capability-aware "best path on this machine" compute dispatcher, keystoned on GEMM.
2//!
3//! The forge proves and tunes individual kernels; this module is the layer that, at
4//! runtime, picks the **best compute path actually available on this machine** for a
5//! given call, while keeping a CPU floor so the call is *never* broken. All backends
6//! are present in the code; which one activates is decided by the machine's probed
7//! [`ComputeCaps`], not by the call site.
8//!
9//! # Why f32 and f64 take different best-paths
10//!
11//! WGSL has **no `f64`** — only `f32`/`f16`/`i32`/`u32`. So the GPU best-path for
12//! single precision is the certified WGSL GEMM (via [`ForgeRuntime`]); but for
13//! *double* precision there is no WGSL analogue at all. Native `f64` on the GPU has
14//! to come from **CUDA/PTX**, which has a real `double` type and `fma.rn.f64`. That
15//! is the whole reason [`gemm_f32`] and [`gemm_f64`] resolve to different backends:
16//!
17//! | dtype | best path (if available) | floor (always present) |
18//! |-------|---------------------------------------------|------------------------|
19//! | f32 | WGSL GEMM ([`ForgeRuntime::gemm`]) | [`gemm_cpu`] (f32) |
20//! | f64 | native CUDA-f64 → df64-WGSL (double-single) | [`gemm_cpu_f64`] |
21//!
22//! ## f64 on every GPU: the 3-tier chain (native CUDA → df64-WGSL → CPU)
23//!
24//! `f64` now has a GPU path on **every** machine, not just NVIDIA. The chain in
25//! [`gemm_f64`] is three tiers:
26//!
27//! 1. **native CUDA-f64** ([`gemm_f64_cuda`]) — exact double via PTX `fma.rn.f64`,
28//! NVIDIA only (`cuda` feature + a CUDA device).
29//! 2. **df64 / double-single WGSL** ([`gemm_f64_df64`]) — *emulated* double on any
30//! other wgpu adapter (AMD, Intel, Apple, mobile). Each `f64` is a hi/lo pair of
31//! `f32` and the accumulation uses error-free transforms (Dekker/TwoSum/TwoProd),
32//! giving ~44–48 effective mantissa bits — well beyond a single `f32`'s 24. The
33//! kernel is the raw WGSL [`GEMM_DF64_WGSL`](super::emit::GEMM_DF64_WGSL).
34//! 3. **CPU floor** ([`gemm_cpu_f64`]) — exact double, always present, never broken.
35//!
36//! So a non-NVIDIA GPU can get real f64 *acceleration* (tier 2) instead of dropping
37//! straight to the CPU — **but only where the adapter's WGSL float arithmetic preserves
38//! the df64 error-free transforms.** Many drivers (incl. the naga→SPIR-V→NVIDIA-Vulkan
39//! path) reassociate floats (`c - (c - a)` → `a`, `fma(x,y,-(x*y))` → `0`), which
40//! silently collapses df64 to f32 precision. WGSL exposes no portable way to forbid
41//! that, so tier 2 is gated on a runtime precision probe ([`df64_usable`]): df64 runs
42//! only where it actually delivers ~double precision; elsewhere the chain uses native
43//! CUDA (if present) or the exact CPU floor — never a degraded df64 masquerading as f64.
44//! (GEMV's f64 chain is `CUDA-f64 → CPU` — the df64 path is GEMM-only today.)
45
46use std::sync::{Mutex, OnceLock};
47
48use super::execute::WgpuComputeContext;
49use super::oracle::{dft_cpu, gemm_cpu, gemv_cpu};
50use super::ForgeError;
51use super::ForgeRuntime;
52
53/// Prepend `CUDA_PATH/bin/x64` (and `bin`) to `PATH` so cudarc can dlopen NVRTC.
54/// CUDA 12+/13 ships `nvrtc64_*.dll` under `bin\x64`, not `bin` — without this,
55/// `gemm_f32_tc` always soft-falls to plain f32 even when the toolkit is installed.
56/// Idempotent; safe to call from any thread (best-effort env mutation).
57pub fn ensure_cuda_runtime_path() {
58 static ONCE: OnceLock<()> = OnceLock::new();
59 ONCE.get_or_init(|| {
60 let cuda = match std::env::var_os("CUDA_PATH") {
61 Some(p) => std::path::PathBuf::from(p),
62 None => {
63 // Common Windows default when CUDA_PATH is unset.
64 let guess = std::path::PathBuf::from(
65 r"C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v13.3",
66 );
67 if guess.is_dir() {
68 guess
69 } else {
70 return;
71 }
72 }
73 };
74 let mut prepend = Vec::new();
75 let x64 = cuda.join("bin").join("x64");
76 let bin = cuda.join("bin");
77 if x64.is_dir() {
78 prepend.push(x64);
79 }
80 if bin.is_dir() {
81 prepend.push(bin);
82 }
83 if prepend.is_empty() {
84 return;
85 }
86 let old = std::env::var_os("PATH").unwrap_or_default();
87 let mut parts: Vec<std::path::PathBuf> = prepend;
88 parts.extend(std::env::split_paths(&old));
89 if let Ok(joined) = std::env::join_paths(parts) {
90 // SAFETY: process-global PATH update before any cudarc dlopen; single-threaded init.
91 std::env::set_var("PATH", joined);
92 log::info!(
93 "cuda_path|ensured|{}\\bin\\x64 prepended for NVRTC",
94 cuda.display()
95 );
96 }
97 });
98}
99
100/// Problem-size threshold (in `m * n * k` multiply-adds) below which GEMM stays on
101/// the CPU regardless of available accelerators. Small GEMMs are dominated by
102/// dispatch/transfer overhead, so the GPU path only earns its keep above this size.
103/// `1 << 15` (32768 FMAs, e.g. a 32×32×32 GEMM) is a conservative crossover that
104/// keeps the hand-checked unit tests (well below it) firmly on the CPU floor.
105pub const GEMM_GPU_THRESHOLD: usize = 1 << 15;
106
107/// Probed compute capabilities of *this* machine. Every flag reflects what was
108/// actually constructible at probe time, not what the build was compiled with — a
109/// `cuda`-feature build on a machine with no NVIDIA device still reports
110/// `cuda == false`.
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
112pub struct ComputeCaps {
113 /// A wgpu adapter could be acquired (the WGSL GPU path is available).
114 pub wgpu: bool,
115 /// A CUDA device could be acquired (the native-f64 GPU path is available).
116 /// Always `false` unless built with the `cuda` feature.
117 pub cuda: bool,
118 /// The wgpu adapter advertises cooperative-matrix (tensor-core) support.
119 /// Read from the probe context's constraints; `false` when `wgpu` is `false`.
120 pub coopmat: bool,
121 /// The wgpu adapter advertises ray-query (RT-core) support. Read from the probe
122 /// context's constraints; `false` when `wgpu` is `false`.
123 pub rt: bool,
124}
125
126/// Probe size for the throwaway capability contexts. Small — these contexts are
127/// built only to answer "can this backend initialise on this machine?" and are then
128/// dropped; no real workload runs on them.
129const PROBE_CAPACITY_BYTES: usize = 4 << 20;
130
131static CAPS: OnceLock<ComputeCaps> = OnceLock::new();
132
133/// The probed [`ComputeCaps`] for this machine, computed once and cached for the
134/// process lifetime. The probe never panics: each backend is tried with
135/// `..::new(_).is_ok()`, and any failure (no adapter, no driver, no device) is
136/// simply recorded as the corresponding flag being `false`.
137///
138/// `coopmat`/`rt` are taken from the wgpu probe context's
139/// [`AdapterConstraints`](super::AdapterConstraints) (the same flags the tuner uses
140/// to prune tensor-core / ray-query kernels), so they are honest hardware bits, not
141/// build-time assumptions.
142pub fn caps() -> ComputeCaps {
143 *CAPS.get_or_init(probe_caps)
144}
145
146fn probe_caps() -> ComputeCaps {
147 // Make NVRTC discoverable before the CUDA probe (CUDA 13: bin\x64).
148 ensure_cuda_runtime_path();
149 // wgpu: try Vulkan first for coopmat (NVIDIA Vulkan exposes VK_KHR_cooperative_matrix
150 // even when DX12 doesn't expose the equivalent). Falls back to default backend if no
151 // coopmat adapter is found.
152 let (wgpu, coopmat, rt) = match WgpuComputeContext::new_for_coopmat(PROBE_CAPACITY_BYTES) {
153 Ok(ctx) => (
154 true,
155 ctx.constraints.supports_coopmat,
156 ctx.constraints.supports_rt_cores,
157 ),
158 Err(_) => (false, false, false),
159 };
160
161 let cuda = probe_cuda();
162
163 ComputeCaps {
164 wgpu,
165 cuda,
166 coopmat,
167 rt,
168 }
169}
170
171/// CUDA availability probe. Behind the `cuda` feature: try to build a throwaway
172/// [`CudaComputeContext`](super::execute::CudaComputeContext); a missing toolkit /
173/// device degrades to `Err` (the backend is built with `fallback-dynamic-loading`),
174/// which we map to `false`. Without the `cuda` feature this is unconditionally
175/// `false`.
176#[cfg(feature = "cuda")]
177fn probe_cuda() -> bool {
178 use super::execute::CudaComputeContext;
179 CudaComputeContext::new(PROBE_CAPACITY_BYTES).is_ok()
180}
181
182#[cfg(not(feature = "cuda"))]
183fn probe_cuda() -> bool {
184 false
185}
186
187/// Process-wide shared [`ForgeRuntime`] for the WGSL GEMM path.
188///
189/// Building a `ForgeRuntime` acquires a wgpu device/queue and slab, which is
190/// expensive, so the dispatcher caches a single instance and reuses it across calls.
191/// It lives behind `Mutex<Option<_>>` in a `OnceLock`: the `OnceLock` makes the
192/// cell itself one-time-initialised, the `Mutex` serialises the `&mut self`
193/// `ForgeRuntime::gemm` call (the GPU context is not `Sync`-shareable for concurrent
194/// dispatch). `Option` lets a failed/again-unavailable runtime be retried lazily
195/// without poisoning the slot permanently.
196static FORGE_RT: OnceLock<Mutex<Option<ForgeRuntime>>> = OnceLock::new();
197
198fn forge_rt_cell() -> &'static Mutex<Option<ForgeRuntime>> {
199 FORGE_RT.get_or_init(|| Mutex::new(None))
200}
201
202/// Best-path single-precision dense GEMM: row-major `C[M×N] = A[M×K] · B[K×N]`.
203///
204/// Path selection:
205/// 1. **WGSL GPU** — when [`caps().wgpu`](caps) is set *and* the problem is at least
206/// [`GEMM_GPU_THRESHOLD`] FMAs, run the certified GEMM via the shared
207/// [`ForgeRuntime`]. If the runtime cannot be built or the dispatch errors at
208/// runtime, the error is **not** propagated — the call falls through to the CPU
209/// floor so it is never broken.
210/// 2. **CPU floor** — otherwise (no GPU, sub-threshold, or GPU fell through) compute
211/// on the CPU via [`gemm_cpu`].
212///
213/// `a` must have `m * k` elements, `b` must have `k * n`; both row-major. Returns
214/// `m * n` row-major elements. Dimension/length mismatches are the only hard errors.
215pub fn gemm_f32(
216 m: usize,
217 k: usize,
218 n: usize,
219 a: &[f32],
220 b: &[f32],
221) -> Result<Vec<f32>, ForgeError> {
222 validate_dims(m, k, n, a.len(), b.len())?;
223
224 let work = m.saturating_mul(n).saturating_mul(k);
225 if caps().wgpu && work >= GEMM_GPU_THRESHOLD {
226 if let Some(out) = gemm_f32_gpu(m, k, n, a, b) {
227 return Ok(out);
228 }
229 // GPU path was eligible but failed at runtime — fall through to the CPU
230 // floor rather than propagating, so the call is never broken.
231 }
232
233 Ok(gemm_cpu(a, b, m, k, n))
234}
235
236/// Run the f32 GEMM through the shared [`ForgeRuntime`], returning `None` on any
237/// runtime failure (runtime un-buildable now, or dispatch error) so the caller can
238/// fall through to the CPU floor. Never propagates a GPU error.
239fn gemm_f32_gpu(m: usize, k: usize, n: usize, a: &[f32], b: &[f32]) -> Option<Vec<f32>> {
240 let cell = forge_rt_cell();
241 let mut guard = cell.lock().ok()?;
242 if guard.is_none() {
243 // Size the slab generously enough for the inputs+output of a typical GEMM;
244 // ForgeRuntime allocates transiently per call within this capacity.
245 match ForgeRuntime::new(64 * 1024 * 1024, None) {
246 Ok(rt) => *guard = Some(rt),
247 Err(_) => return None,
248 }
249 }
250 let rt = guard.as_mut()?;
251 rt.gemm(a, b, m, k, n).ok()
252}
253
254/// Best-path double-precision dense GEMM: row-major `C[M×N] = A[M×K] · B[K×N]`, all
255/// `f64`.
256///
257/// # The 3-tier f64 chain ("best f64 path on every machine")
258///
259/// WGSL has no native `f64`, so double precision on the GPU is reached two different
260/// ways depending on the hardware; this is the whole reason `gemm_f64` resolves
261/// through three tiers rather than the single accelerator arm of [`gemm_f32`]:
262///
263/// | tier | path | when |
264/// |------|-----------------------------------|---------------------------------------------------------|
265/// | 1 | **native CUDA-f64** ([`gemm_f64_cuda`], NVIDIA only) | [`caps().cuda`](caps) and ≥ [`GEMM_GPU_THRESHOLD`] FMAs |
266/// | 2 | **df64 / double-single WGSL** ([`gemm_f64_df64`], any other GPU) | [`caps().wgpu`](caps) and ≥ [`GEMM_GPU_THRESHOLD`] FMAs |
267/// | 3 | **CPU floor** ([`gemm_cpu_f64`]) | otherwise, or if every eligible accelerator errors |
268///
269/// Tier 1 is *exact* double (native `double` + `fma.rn.f64`). Tier 2 emulates each
270/// `f64` as a hi/lo pair of `f32` with error-free transforms (~44–48 effective
271/// mantissa bits, well beyond a single `f32`'s 24) — so a non-NVIDIA GPU (AMD,
272/// Intel, Apple, mobile) now gets real f64 *acceleration* instead of dropping
273/// straight to the CPU. On any accelerator runtime error the call falls through to
274/// the next tier (errors are **never** propagated), so it is never broken. The CUDA
275/// arm is compiled in only under the `cuda` feature; the df64 arm is always present
276/// (it needs only a wgpu adapter).
277///
278/// `a` must have `m * k` elements, `b` must have `k * n`; both row-major. Returns
279/// `m * n` row-major elements.
280pub fn gemm_f64(
281 m: usize,
282 k: usize,
283 n: usize,
284 a: &[f64],
285 b: &[f64],
286) -> Result<Vec<f64>, ForgeError> {
287 validate_dims(m, k, n, a.len(), b.len())?;
288
289 let work = m.saturating_mul(n).saturating_mul(k);
290
291 // Tier 1: native CUDA-f64 (exact double) on an NVIDIA device.
292 #[cfg(feature = "cuda")]
293 {
294 if caps().cuda && work >= GEMM_GPU_THRESHOLD {
295 if let Ok(out) = gemm_f64_cuda(m, k, n, a, b) {
296 return Ok(out);
297 }
298 // CUDA path was eligible but errored — fall through to the next tier.
299 }
300 }
301
302 // Tier 2: df64 (double-single) emulated-f64 in WGSL — but ONLY on adapters whose
303 // WGSL float semantics actually preserve the error-free transforms. Many drivers
304 // (incl. the naga->SPIR-V->NVIDIA-Vulkan path) reassociate floats, collapsing the
305 // df64 residuals to ~0 (f32 precision); `df64_usable()` probes for that at runtime
306 // so we never return f32-precision results dressed up as f64 — we drop to the CPU
307 // floor (exact f64) instead.
308 if caps().wgpu && work >= GEMM_GPU_THRESHOLD && df64_usable() {
309 if let Ok(out) = gemm_f64_df64(m, k, n, a, b) {
310 return Ok(out);
311 }
312 // df64 path was eligible but errored — fall through to the CPU floor.
313 }
314
315 // Tier 3: CPU floor (always present, never broken).
316 Ok(gemm_cpu_f64(a, b, m, k, n))
317}
318
319/// Runtime probe: does this adapter's WGSL float arithmetic preserve the df64
320/// error-free transforms (genuine ~double precision), or does the driver reassociate
321/// floats and silently collapse df64 to f32? Measured once, then cached.
322///
323/// df64 (double-single) is correct only where each f32 `+`/`-`/`*` rounds per IEEE
324/// without reassociation. Some drivers — notably the naga->SPIR-V->NVIDIA-Vulkan path
325/// on this hardware — algebraically simplify `c - (c - a)` to `a` and `fma(x,y,-(x*y))`
326/// to `0`, which destroys the residual (lo) terms. WGSL exposes no portable way to
327/// forbid that, so we MEASURE it: run a tiny df64 GEMM whose exact f64 result differs
328/// from f32 by ~1e-7, and accept df64 only if it lands within f64 tolerance. On
329/// adapters that fail the probe, the f64 chain uses native CUDA (if present) or the
330/// exact CPU floor — never a degraded df64.
331fn df64_usable() -> bool {
332 static USABLE: OnceLock<bool> = OnceLock::new();
333 *USABLE.get_or_init(|| {
334 if !caps().wgpu {
335 return false;
336 }
337 // 8x8x8 with low-mantissa-bit perturbations: the exact f64 result differs from
338 // an f32 evaluation by ~1e-7, so only a working df64 lands within 1e-9.
339 let n = 8usize;
340 let a: Vec<f64> = (0..n * n)
341 .map(|i| 1.0 + (i as f64) * 1.0e-7 + 1.0e-9)
342 .collect();
343 let b: Vec<f64> = (0..n * n)
344 .map(|i| 1.0 - (i as f64) * 1.0e-7 + 3.0e-10)
345 .collect();
346 let cpu = gemm_cpu_f64(&a, &b, n, n, n);
347 match gemm_f64_df64(n, n, n, &a, &b) {
348 Ok(df) => df.iter().zip(&cpu).all(|(d, c)| (d - c).abs() <= 1.0e-9),
349 Err(_) => false,
350 }
351 })
352}
353
354/// Runtime probe: does this adapter's WGSL **cooperative-matrix** (tensor-core) multiply
355/// actually compute, or does it return zeros? Measured once, then cached — the f32 mirror
356/// of [`df64_usable`].
357///
358/// The coopmat kernels are correct and naga-validated, but on wgpu 29.0.3 the
359/// `coopMultiplyAdd` is a no-op that returns all-zeros (gfx-rs/wgpu #9741: coopmat emits
360/// Device-scope SPIR-V memory ops invalid unless `vulkanMemoryModelDeviceScope` is
361/// auto-enabled at device creation — fixed on `main` after 29.0.3, the newest crates.io
362/// release). So we never *assume* coopmat works from the advertised feature bit: we MEASURE
363/// it by running a tiny 8×8×8 coopmat GEMM whose exact result is non-zero (all-ones inputs
364/// → every output `= 8.0`) and accepting coopmat only if the result matches. On 29.0.3 this
365/// returns `false` (zeros); it returns `true` automatically once a wgpu release (or the
366/// [`docs/WGPU_UPSTREAM_TRACKING.md`] soft-fork) carries the fix. Gated first on
367/// [`caps().wgpu`](caps) and [`caps().coopmat`](caps) so non-coopmat adapters never dispatch.
368pub fn coopmat_usable() -> bool {
369 static USABLE: OnceLock<bool> = OnceLock::new();
370 *USABLE.get_or_init(|| {
371 let c = caps();
372 if !c.wgpu || !c.coopmat {
373 return false;
374 }
375 // 8×8×8, all-ones: exact C[i][j] = sum_{0..8} 1*1 = 8.0. A working coopmat lands on
376 // 8.0; the #9741 no-op returns 0.0, so the tolerance check rejects it.
377 let n = 8usize;
378 let a = vec![1.0f32; n * n];
379 let b = vec![1.0f32; n * n];
380 match gemm_f32_tc_coopmat_unchecked(n, n, n, &a, &b) {
381 Ok(out) => out.len() == n * n && out.iter().all(|&v| (v - 8.0).abs() <= 1.0e-3),
382 Err(_) => false,
383 }
384 })
385}
386
387/// **Cooperative-matrix (tensor-core) f32 GEMM on a wgpu adapter**: row-major
388/// `C[m×n] = A[m×k]·B[k×n]`, all `f32`, computed by the tiled coopmat kernel
389/// ([`matmul_tc_wgsl_tiled`](super::emit::matmul_tc_wgsl_tiled)). `m`, `n`, `k` must be
390/// non-zero multiples of 8 (the 8×8×8 cooperative-matrix tile).
391///
392/// This is the *portable* tensor-core path — it needs only a coopmat-capable wgpu adapter
393/// (no CUDA), so it covers NVIDIA/AMD/Intel/Apple alike once the driver computes coopmat.
394/// **It is dormant on wgpu 29.0.3** (the multiply returns zeros, #9741); callers gate on
395/// [`coopmat_usable`] so it is invoked only where it actually computes.
396///
397/// Mechanics mirror [`gemm_f64_df64`]: a transient [`WgpuComputeContext`], `a`(0,
398/// [`StorageRead`]) / `b`(1, [`StorageRead`]) / zeroed `c`(2, [`StorageReadWrite`]) /
399/// `dims=[m,n,k]`(3, [`StorageRead`]), compile the tiled kernel, dispatch one workgroup
400/// (== one subgroup, `@workgroup_size(32)`) per 8×8 output tile (`num_tiles = (m/8)·(n/8)`),
401/// read back `c`.
402///
403/// [`StorageRead`]: super::execute::BindingUsage::StorageRead
404/// [`StorageReadWrite`]: super::execute::BindingUsage::StorageReadWrite
405pub fn gemm_f32_tc_coopmat(
406 m: usize,
407 k: usize,
408 n: usize,
409 a: &[f32],
410 b: &[f32],
411) -> Result<Vec<f32>, ForgeError> {
412 validate_dims(m, k, n, a.len(), b.len())?;
413 if !coopmat_usable() {
414 return Err(ForgeError::GpuUnavailable(
415 "cooperative-matrix multiply failed its runtime correctness oracle".to_string(),
416 ));
417 }
418 gemm_f32_tc_coopmat_unchecked(m, k, n, a, b)
419}
420
421/// Internal cooperative-matrix dispatch used only by the one-time correctness
422/// oracle and by the fail-closed public wrapper after that oracle passes.
423fn gemm_f32_tc_coopmat_unchecked(
424 m: usize,
425 k: usize,
426 n: usize,
427 a: &[f32],
428 b: &[f32],
429) -> Result<Vec<f32>, ForgeError> {
430 use super::emit::{matmul_tc_wgsl_tiled, MATMUL_TC_TILED_ENTRY};
431 use super::execute::{BindingUsage, QualiaCompute, WgpuPipeline};
432 use super::Schedule;
433
434 if m == 0 || n == 0 || k == 0 || m % 8 != 0 || n % 8 != 0 || k % 8 != 0 {
435 return Err(ForgeError::GpuValidation(format!(
436 "gemm_f32_tc_coopmat: m={m}, n={n}, k={k} must be non-zero multiples of 8 (coopmat tile)"
437 )));
438 }
439 validate_dims(m, k, n, a.len(), b.len())?;
440
441 let element_count = m.checked_mul(n).ok_or_else(|| {
442 ForgeError::GpuValidation("m*n overflow in gemm_f32_tc_coopmat".to_string())
443 })?;
444 let capacity = (element_count.saturating_mul(8)).max(4 << 20);
445 let mut ctx = WgpuComputeContext::new_for_coopmat(capacity)?;
446
447 let view_a =
448 ctx.allocate_and_write(bytemuck::cast_slice(a), 0, 0, BindingUsage::StorageRead)?;
449 let view_b =
450 ctx.allocate_and_write(bytemuck::cast_slice(b), 1, 0, BindingUsage::StorageRead)?;
451 let zeros = vec![0.0f32; element_count];
452 let view_c = ctx.allocate_and_write(
453 bytemuck::cast_slice(&zeros),
454 2,
455 0,
456 BindingUsage::StorageReadWrite,
457 )?;
458 let dims: [u32; 3] = [m as u32, n as u32, k as u32];
459 let view_dims =
460 ctx.allocate_and_write(bytemuck::cast_slice(&dims), 3, 0, BindingUsage::StorageRead)?;
461
462 let buffers = vec![view_a, view_b, view_c, view_dims];
463 let src = matmul_tc_wgsl_tiled();
464 let pipeline = WgpuPipeline::compile(&ctx, &src, MATMUL_TC_TILED_ENTRY)?;
465 // One workgroup (== one subgroup, @workgroup_size(32)) per 8×8 output tile.
466 let num_tiles = (m / 8) * (n / 8);
467 let schedule = Schedule {
468 workgroup_size: 32,
469 ..Default::default()
470 };
471 pipeline.dispatch(&buffers, &schedule, num_tiles * 32)?;
472 let mut out = ctx.read_buffer_f32(&view_c)?;
473 out.truncate(element_count);
474 Ok(out)
475}
476
477/// Native CUDA double-precision GEMM. Builds a transient
478/// [`CudaComputeContext`](super::execute::CudaComputeContext), uploads `a` (binding
479/// 0) / `b` (binding 1) / a zeroed `c` (binding 2) and the `dims = [m, n, k]` u32
480/// storage buffer (binding 3), compiles
481/// [`GEMM_F64_SRC`](super::emit::cuda_c::GEMM_F64_SRC) via NVRTC, dispatches one
482/// thread per output element (`element_count = m * n`), and reads back the `c`
483/// buffer as `f64`. This is the exact-double path WGSL cannot provide.
484#[cfg(feature = "cuda")]
485fn gemm_f64_cuda(
486 m: usize,
487 k: usize,
488 n: usize,
489 a: &[f64],
490 b: &[f64],
491) -> Result<Vec<f64>, ForgeError> {
492 use super::emit::cuda_c::{GEMM_F64_ENTRY, GEMM_F64_SRC};
493 use super::execute::{CudaComputeContext, CudaPipeline, QualiaCompute};
494 use super::Schedule;
495
496 let mut ctx = CudaComputeContext::new(64 * 1024 * 1024)?;
497
498 let element_count = m * n;
499 let view_a = ctx.allocate_and_write(bytemuck::cast_slice(a), 0, 0)?;
500 let view_b = ctx.allocate_and_write(bytemuck::cast_slice(b), 1, 0)?;
501 let zeros = vec![0.0f64; element_count];
502 let view_c = ctx.allocate_and_write(bytemuck::cast_slice(&zeros), 2, 0)?;
503 // dims = [m, n, k] as u32, written as a storage buffer (binding 3) — no by-value
504 // uniform, matching compile_cuda_c_source's pointer-only binding ABI.
505 let dims: [u32; 3] = [m as u32, n as u32, k as u32];
506 let view_dims = ctx.allocate_and_write(bytemuck::cast_slice(&dims), 3, 0)?;
507
508 let buffers = vec![view_a, view_b, view_c, view_dims];
509 let pipeline =
510 CudaPipeline::compile_cuda_c_source(&ctx, GEMM_F64_SRC, GEMM_F64_ENTRY, &[0, 1, 2, 3])?;
511 let schedule = Schedule {
512 workgroup_size: 64,
513 ..Default::default()
514 };
515 pipeline.dispatch(&buffers, &schedule, element_count)?;
516 let mut out = ctx.read_buffer_f64(&view_c)?;
517 out.truncate(element_count);
518 Ok(out)
519}
520
521/// **f32-faithful tensor-core GEMM** — row-major `C[m×n] = A[m×k]·B[k×n]`, f32 in/out,
522/// computed to **full f32 accuracy**. This is the entry point for callers who want
523/// tensor-core throughput *without* trading precision.
524///
525/// Selection (accurate paths only, with a correct floor):
526/// 1. **WGSL coopmat** ([`gemm_f32_tc_coopmat`]) — the *portable* wgpu tensor-core path.
527/// It is genuinely **f32** (an 8×8×8 f32 cooperative-matrix tile), so it belongs on the
528/// accurate path. Gated on [`coopmat_usable`]: on wgpu ≤30 the coopmat multiply returns
529/// zeros on adapters that don't compute it (e.g. this machine's DX12 backend, #9741), so
530/// the probe is `false` and this tier stays dormant, self-activating the moment an adapter
531/// computes coopmat correctly (see [`docs/WGPU_UPSTREAM_TRACKING.md`]). 8-multiple dims.
532/// 2. **plain f32 GEMM** ([`gemm_f32`]) — the always-correct full-f32 floor.
533///
534/// **The lossy f16 CUDA WMMA tier is deliberately NOT here** — it lives in
535/// [`gemm_f32_tc_reduced`], the explicit reduced-precision opt-in. A function named
536/// `gemm_f32_tc` must not silently return f16-precision results (this was the
537/// `stage4_forge_gemm` selector bug: on a CUDA machine with coopmat dormant, the f16 tier
538/// fired and produced ~1.2 absolute error against an f32-accuracy expectation).
539pub fn gemm_f32_tc(
540 m: usize,
541 k: usize,
542 n: usize,
543 a: &[f32],
544 b: &[f32],
545) -> Result<Vec<f32>, ForgeError> {
546 validate_dims(m, k, n, a.len(), b.len())?;
547
548 // Tier 1: WGSL coopmat — the *portable* f32 wgpu tensor-core path, gated on the runtime
549 // probe `coopmat_usable()`. Dormant where the adapter doesn't compute coopmat (#9741);
550 // self-activates once it does. Requires 8-multiple dims (the 8×8×8 tile). f32-accurate.
551 if caps().wgpu
552 && caps().coopmat
553 && m % 8 == 0
554 && n % 8 == 0
555 && k % 8 == 0
556 && m.min(n).min(k) > 0
557 && coopmat_usable()
558 {
559 if let Ok(out) = gemm_f32_tc_coopmat(m, k, n, a, b) {
560 return Ok(out);
561 }
562 // Coopmat path eligible but errored — fall through to the exact floor.
563 }
564
565 // Floor: full-f32 GEMM. No lossy f16 tier on the f32-faithful path.
566 gemm_f32(m, k, n, a, b)
567}
568
569/// **Reduced-precision tensor-core GEMM** — the explicit opt-in for callers that are
570/// precision-tolerant (LLM matmuls are already f16-tolerant) and want maximum tensor-core
571/// throughput. Row-major `C[m×n] = A[m×k]·B[k×n]`, f32 in/out, but the result **may be
572/// f16-precision** when the CUDA WMMA tier fires.
573///
574/// Selection (fastest tensor-core path on this machine, with a correct floor):
575/// 1. **WGSL coopmat** ([`gemm_f32_tc_coopmat`], f32) — still preferred when usable: it is
576/// both accurate *and* fast. Gated on [`coopmat_usable`]. 8-multiple dims.
577/// 2. **CUDA WMMA** ([`gemm_tc_cuda`]) — genuine NVIDIA tensor cores at **f16-input**
578/// precision, when `cuda` is available and `m,n,k` are multiples of 16. Carries tensor
579/// cores today; this is the tier that trades precision for throughput.
580/// 3. **plain f32 GEMM** ([`gemm_f32`]) — the always-correct floor.
581///
582/// Use this for LLM decode GEMV / TC microbenchmarks. Use [`gemm_f32_tc`] when you need f32
583/// accuracy.
584pub fn gemm_f32_tc_reduced(
585 m: usize,
586 k: usize,
587 n: usize,
588 a: &[f32],
589 b: &[f32],
590) -> Result<Vec<f32>, ForgeError> {
591 validate_dims(m, k, n, a.len(), b.len())?;
592 ensure_cuda_runtime_path();
593
594 // Tier 1: WGSL coopmat (f32) — accurate AND fast, preferred when the probe says it works.
595 if caps().wgpu
596 && caps().coopmat
597 && m % 8 == 0
598 && n % 8 == 0
599 && k % 8 == 0
600 && m.min(n).min(k) > 0
601 && coopmat_usable()
602 {
603 if let Ok(out) = gemm_f32_tc_coopmat(m, k, n, a, b) {
604 return Ok(out);
605 }
606 }
607
608 // Tier 2: CUDA WMMA (genuine NVIDIA tensor cores, f16-input precision — reduced).
609 // cudarc may *panic* (not Err) when NVRTC/CUDA DLLs are missing — catch that so
610 // the plain f32 floor always remains reachable (toolkit probe found this 2026-07-09).
611 #[cfg(feature = "cuda")]
612 {
613 if caps().cuda && m % 16 == 0 && n % 16 == 0 && k % 16 == 0 && m.min(n).min(k) > 0 {
614 let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
615 gemm_tc_cuda(m, k, n, a, b)
616 }));
617 match result {
618 Ok(Ok(out)) => return Ok(out),
619 Ok(Err(_)) | Err(_) => {
620 // Missing toolkit / NVRTC / launch failure → exact floor.
621 }
622 }
623 }
624 }
625 gemm_f32(m, k, n, a, b)
626}
627
628/// Capacity for the process-wide CUDA WMMA slab (weights + C tile + dims).
629#[cfg(feature = "cuda")]
630const CUDA_TC_SLAB_BYTES: usize = 64 * 1024 * 1024;
631
632/// Process-wide CUDA context for WMMA GEMM — avoids full driver re-init on every call.
633#[cfg(feature = "cuda")]
634static CUDA_TC_CTX: OnceLock<Mutex<Option<crate::wgsl_forge::execute::CudaComputeContext>>> =
635 OnceLock::new();
636
637#[cfg(feature = "cuda")]
638fn cuda_tc_ctx_cell() -> &'static Mutex<Option<crate::wgsl_forge::execute::CudaComputeContext>> {
639 CUDA_TC_CTX.get_or_init(|| Mutex::new(None))
640}
641
642/// **Tensor-core** GEMM via the tiled CUDA WMMA kernel: row-major `C[m×n] = A[m×k]·B[k×n]`,
643/// with `A`/`B` rounded to **f16** and accumulated in **f32** on NVIDIA tensor cores. This
644/// is the genuine reduced-precision tensor-core path — the throughput win that the plain
645/// f32 GEMM cannot get — exposed as an **opt-in** (`MatMul.tc`) because it trades f32
646/// precision for speed. `m`, `n`, `k` must be non-zero multiples of 16 (the WMMA tile);
647/// callers with other shapes pad or fall back to the plain path.
648///
649/// Uses a **persistent** CUDA context (process-wide) so hot calls do not re-init the
650/// driver. NVRTC compile is still per-first-use of the pipeline on that context (slab
651/// cleared between calls). Still host-round-trips dense f32 tiles — not a full
652/// llama.cpp-class Q4 decode lane (`InferenceMode::CudaTc` follow-up).
653///
654/// f32 inputs are converted to f16 bit patterns host-side and uploaded as `u16`; the
655/// `dims = [m, n, k]` storage buffer drives the kernel's tiling. Returns `m*n` f32 outputs.
656#[cfg(feature = "cuda")]
657pub fn gemm_tc_cuda(
658 m: usize,
659 k: usize,
660 n: usize,
661 a: &[f32],
662 b: &[f32],
663) -> Result<Vec<f32>, ForgeError> {
664 use crate::wgsl_forge::emit::cuda_c::{WMMA_GEMM_TILED_ENTRY, WMMA_GEMM_TILED_SRC};
665 use crate::wgsl_forge::execute::{CudaComputeContext, CudaPipeline, QualiaCompute};
666
667 if m == 0 || n == 0 || k == 0 || m % 16 != 0 || n % 16 != 0 || k % 16 != 0 {
668 return Err(ForgeError::GpuValidation(format!(
669 "gemm_tc_cuda: m={m}, n={n}, k={k} must be non-zero multiples of 16 (WMMA tile)"
670 )));
671 }
672 validate_dims(m, k, n, a.len(), b.len())?;
673 ensure_cuda_runtime_path();
674
675 let a_bits: Vec<u16> = a
676 .iter()
677 .map(|&x| half::f16::from_f32(x).to_bits())
678 .collect();
679 let b_bits: Vec<u16> = b
680 .iter()
681 .map(|&x| half::f16::from_f32(x).to_bits())
682 .collect();
683
684 let mut guard = cuda_tc_ctx_cell()
685 .lock()
686 .map_err(|_| ForgeError::GpuUnavailable("CUDA TC mutex poisoned".into()))?;
687 if guard.is_none() {
688 *guard = Some(CudaComputeContext::new(CUDA_TC_SLAB_BYTES)?);
689 log::info!("cuda_tc|context|initialized|slab={CUDA_TC_SLAB_BYTES}");
690 }
691 let ctx = guard.as_mut().unwrap();
692 ctx.clear_transient_allocations();
693
694 let view_a = ctx.allocate_and_write(bytemuck::cast_slice(&a_bits), 0, 0)?;
695 let view_b = ctx.allocate_and_write(bytemuck::cast_slice(&b_bits), 1, 0)?;
696 let zeros = vec![0.0f32; m * n];
697 let view_c = ctx.allocate_and_write(bytemuck::cast_slice(&zeros), 2, 0)?;
698 let dims: [u32; 3] = [m as u32, n as u32, k as u32];
699 let view_dims = ctx.allocate_and_write(bytemuck::cast_slice(&dims), 3, 0)?;
700
701 let buffers = vec![view_a, view_b, view_c, view_dims];
702 let num_tiles = (m / 16) * (n / 16);
703 let schedule = super::Schedule {
704 workgroup_size: 32,
705 ..Default::default()
706 };
707 // Cached NVRTC PTX (process-wide) + persistent context — only load_module+launch per call.
708 let pipeline = CudaPipeline::compile_cuda_c_source_cached(
709 ctx,
710 WMMA_GEMM_TILED_SRC,
711 WMMA_GEMM_TILED_ENTRY,
712 &[0, 1, 2, 3],
713 )?;
714 pipeline.dispatch(&buffers, &schedule, num_tiles * 32)?;
715 let mut out = ctx.read_buffer_f32(&view_c)?;
716 out.truncate(m * n);
717 ctx.clear_transient_allocations();
718 Ok(out)
719}
720
721/// Split one `f64` into a double-single (`df64`) hi/lo pair of `f32`. `hi` is the
722/// `f64` rounded to nearest `f32`; `lo` is the (exactly representable in `f32`)
723/// residual `v - hi`. Together the pair carries ~44–48 effective mantissa bits, far
724/// beyond a single `f32`'s 24. Inverse of [`df32_to_f64`].
725fn f64_to_df32(v: f64) -> [f32; 2] {
726 let hi = v as f32;
727 let lo = (v - hi as f64) as f32;
728 [hi, lo]
729}
730
731/// Recombine a double-single (`df64`) hi/lo `f32` pair back into an `f64`. The sum
732/// is exact in `f64` (both operands are `f32`-representable and `|lo| ≤ ½ ulp(hi)`),
733/// so this is the exact inverse of [`f64_to_df32`] up to the `f32` rounding of `hi`.
734fn df32_to_f64(hi: f32, lo: f32) -> f64 {
735 hi as f64 + lo as f64
736}
737
738/// Pack an `&[f64]` into a flat `Vec<f32>` of twice the length, hi/lo interleaved
739/// per element (`[hi0, lo0, hi1, lo1, …]`) — the df64 GEMM's input layout. Inverse
740/// of [`unpack_df32`].
741fn pack_df32(values: &[f64]) -> Vec<f32> {
742 let mut out = Vec::with_capacity(values.len() * 2);
743 for &v in values {
744 let [hi, lo] = f64_to_df32(v);
745 out.push(hi);
746 out.push(lo);
747 }
748 out
749}
750
751/// Unpack a flat `&[f32]` of hi/lo-interleaved df64 pairs (`[hi0, lo0, hi1, lo1, …]`)
752/// back into an `&[f64]` of half the length. Inverse of [`pack_df32`]. A trailing
753/// half-pair (odd length) is ignored.
754fn unpack_df32(packed: &[f32]) -> Vec<f64> {
755 packed
756 .chunks_exact(2)
757 .map(|pair| df32_to_f64(pair[0], pair[1]))
758 .collect()
759}
760
761/// Emulated double-precision (`df64` / double-single) dense GEMM on **any** wgpu
762/// adapter: row-major `C[M×N] = A[M×K] · B[K×N]`, all `f64`.
763///
764/// WGSL has no `f64`, so each double is carried as a hi/lo pair of `f32` and the
765/// accumulation runs with error-free transforms (Dekker/Knuth `two_prod`/`two_sum`)
766/// inside the raw kernel [`GEMM_DF64_WGSL`]. This is the portable f64-on-GPU path
767/// that complements the NVIDIA-only native-CUDA-f64 path: an AMD/Intel/Apple/mobile
768/// GPU gets real f64 acceleration here, at ~44–48 effective mantissa bits (vs a
769/// single f32's 24).
770///
771/// Mechanics (mirrors the raw-source path of
772/// [`crate::wgsl_forge::oracle::evaluate_coopmat_loadstore`]): build a transient
773/// [`WgpuComputeContext`], pack `a`→`2*M*K` f32 (binding 0, [`StorageRead`]) and
774/// `b`→`2*K*N` f32 (binding 1, [`StorageRead`]), allocate a zeroed `c` of `2*M*N`
775/// f32 (binding 2, [`StorageReadWrite`]) and `dims = [m, n, k]` as `u32` (binding 3,
776/// [`StorageRead`]), compile [`GEMM_DF64_WGSL`] / [`GEMM_DF64_ENTRY`], dispatch one
777/// invocation per output element (`element_count = m * n`, `workgroup_size = 64`),
778/// read back `c` as `2*M*N` f32 and unpack to `M*N` f64.
779///
780/// [`StorageRead`]: super::execute::BindingUsage::StorageRead
781/// [`StorageReadWrite`]: super::execute::BindingUsage::StorageReadWrite
782pub fn gemm_f64_df64(
783 m: usize,
784 k: usize,
785 n: usize,
786 a: &[f64],
787 b: &[f64],
788) -> Result<Vec<f64>, ForgeError> {
789 use super::emit::{GEMM_DF64_ENTRY, GEMM_DF64_WGSL};
790 use super::execute::{BindingUsage, QualiaCompute, WgpuPipeline};
791 use super::Schedule;
792
793 // Slab must hold a (2*M*K f32) + b (2*K*N f32) on the read slab and c (2*M*N f32)
794 // + dims (3 u32) on the out/read slabs. Size to M*N*16 bytes of headroom (>= the
795 // 2*M*N f32 = M*N*8-byte output, doubled), floored at 4 MiB so small GEMMs still
796 // fit comfortably alongside the inputs.
797 let element_count = m
798 .checked_mul(n)
799 .ok_or_else(|| ForgeError::GpuValidation("m*n overflow in gemm_f64_df64".to_string()))?;
800 let capacity = (element_count.saturating_mul(16)).max(4 << 20);
801 let mut ctx = WgpuComputeContext::new(capacity)?;
802
803 let a_packed = pack_df32(a); // 2*M*K f32
804 let b_packed = pack_df32(b); // 2*K*N f32
805 let view_a = ctx.allocate_and_write(
806 bytemuck::cast_slice(&a_packed),
807 0,
808 0,
809 BindingUsage::StorageRead,
810 )?;
811 let view_b = ctx.allocate_and_write(
812 bytemuck::cast_slice(&b_packed),
813 1,
814 0,
815 BindingUsage::StorageRead,
816 )?;
817 let zeros = vec![0.0f32; element_count * 2]; // 2*M*N f32
818 let view_c = ctx.allocate_and_write(
819 bytemuck::cast_slice(&zeros),
820 2,
821 0,
822 BindingUsage::StorageReadWrite,
823 )?;
824 // dims is a u32 storage buffer [m, n, k] (binding 3, StorageRead) — note the
825 // kernel reads dims[0]=m, dims[1]=n, dims[2]=k.
826 let dims: [u32; 3] = [m as u32, n as u32, k as u32];
827 let view_dims =
828 ctx.allocate_and_write(bytemuck::cast_slice(&dims), 3, 0, BindingUsage::StorageRead)?;
829
830 let buffers = vec![view_a, view_b, view_c, view_dims];
831 let pipeline = WgpuPipeline::compile(&ctx, GEMM_DF64_WGSL, GEMM_DF64_ENTRY)?;
832 // @workgroup_size(64); one invocation per output element. The Schedule's
833 // dispatch_workgroups computes ceil(element_count / 64) workgroups.
834 let schedule = Schedule {
835 workgroup_size: 64,
836 ..Default::default()
837 };
838 pipeline.dispatch(&buffers, &schedule, element_count)?;
839 let packed = ctx.read_buffer_f32(&view_c)?; // 2*M*N f32
840 Ok(unpack_df32(&packed))
841}
842
843/// CPU reference for the double-precision dense GEMM — the `f64` mirror of
844/// [`gemm_cpu`]: row-major `C[M×N] = A[M×K] · B[K×N]`,
845/// `C[i][j] = sum_{k<K} A[i*K + k] * B[k*N + j]`. The inner `kk` sum order matches
846/// the CUDA-f64 kernel so the two agree to f64 summation precision. This is the
847/// always-present f64 floor.
848pub fn gemm_cpu_f64(a: &[f64], b: &[f64], m: usize, k: usize, n: usize) -> Vec<f64> {
849 let mut c = vec![0.0f64; m * n];
850 for i in 0..m {
851 let a_row = i * k;
852 for j in 0..n {
853 let mut acc = 0.0f64;
854 for kk in 0..k {
855 acc += a[a_row + kk] * b[kk * n + j];
856 }
857 c[i * n + j] = acc;
858 }
859 }
860 c
861}
862
863/// All-pairs squared Euclidean distance `D[i][j] = ‖a_i − b_j‖²` between the rows of
864/// `a` (`n×p`, row-major) and `b` (`m×p`, row-major), returned row-major `n×m`.
865///
866/// This is the kernel under **k-means assignment**, the **GMM E-step**, and the
867/// **RBF-kernel Gram matrix** — the dominant cost when `n·m·p` is large. It is computed
868/// with the best path on this machine via the identity
869///
870/// ```text
871/// ‖a_i − b_j‖² = ‖a_i‖² + ‖b_j‖² − 2·(a_i · b_j)
872/// ```
873///
874/// where the cross-term Gram matrix `a · bᵀ` (`n×m`) is the dense product `A·Bᵀ` routed
875/// through [`gemm_f64`], so it inherits that function's CUDA-f64 / CPU-floor best-path
876/// selection and the [`GEMM_GPU_THRESHOLD`] crossover automatically. The per-row norms
877/// and the final combine are a linear-time CPU pass. Float cancellation can make a
878/// near-zero entry slightly negative; such entries are clamped to `0.0`.
879///
880/// Because it uses the `‖·‖²` identity (not a direct `Σ(a−b)²` loop), entries whose true
881/// distance is tiny *relative to the operands' norms* carry the usual catastrophic-
882/// cancellation error of that identity — fine for argmin-style clustering/kernels, which
883/// is what every caller does. [`pairwise_sq_dist_cpu_f64`] is the exact direct reference.
884///
885/// On any shape mismatch (`p == 0`, or a slice length that disagrees with `n`/`m`/`p`),
886/// or if the GEMM cross-term errors, it returns the exact CPU floor instead of failing.
887pub fn pairwise_sq_dist_f64(a: &[f64], b: &[f64], n: usize, m: usize, p: usize) -> Vec<f64> {
888 if p == 0 || a.len() != n * p || b.len() != m * p {
889 return pairwise_sq_dist_cpu_f64(a, b, n, m, p);
890 }
891
892 // Row norms ‖a_i‖² and ‖b_j‖².
893 let mut norm_a = vec![0.0_f64; n];
894 for (i, na) in norm_a.iter_mut().enumerate() {
895 let row = &a[i * p..i * p + p];
896 *na = row.iter().map(|&v| v * v).sum();
897 }
898 let mut norm_b = vec![0.0_f64; m];
899 for (j, nb) in norm_b.iter_mut().enumerate() {
900 let row = &b[j * p..j * p + p];
901 *nb = row.iter().map(|&v| v * v).sum();
902 }
903
904 // Cross term A·Bᵀ = [n×m]. Materialise Bᵀ (p×m, row-major) and route the dense
905 // product through the best-path GEMM: gemm_f64(m=n, k=p, n=m) computes A[n×p]·Bᵀ[p×m].
906 let mut bt = vec![0.0_f64; p * m];
907 for j in 0..m {
908 for d in 0..p {
909 bt[d * m + j] = b[j * p + d];
910 }
911 }
912 let cross = match gemm_f64(n, p, m, a, &bt) {
913 Ok(c) => c,
914 Err(_) => return pairwise_sq_dist_cpu_f64(a, b, n, m, p),
915 };
916
917 let mut out = vec![0.0_f64; n * m];
918 for i in 0..n {
919 for j in 0..m {
920 let d = norm_a[i] + norm_b[j] - 2.0 * cross[i * m + j];
921 out[i * m + j] = if d > 0.0 { d } else { 0.0 };
922 }
923 }
924 out
925}
926
927/// Exact direct reference for [`pairwise_sq_dist_f64`]: `D[i][j] = Σ_d (a[i][d] − b[j][d])²`
928/// computed without the `‖·‖²` identity, so there is no cancellation. Always on the CPU,
929/// always correct; this is the always-present floor and the differential oracle for the
930/// accelerated form. Returns a zero-filled `n×m` for any shape mismatch.
931pub fn pairwise_sq_dist_cpu_f64(a: &[f64], b: &[f64], n: usize, m: usize, p: usize) -> Vec<f64> {
932 let mut out = vec![0.0_f64; n * m];
933 if a.len() != n * p || b.len() != m * p {
934 return out;
935 }
936 for i in 0..n {
937 for j in 0..m {
938 let mut s = 0.0_f64;
939 for d in 0..p {
940 let diff = a[i * p + d] - b[j * p + d];
941 s += diff * diff;
942 }
943 out[i * m + j] = s;
944 }
945 }
946 out
947}
948
949/// Best-path single-precision dense GEMV: row-major `y[M] = A[M×N] · x[N]`.
950///
951/// Path selection (mirrors [`gemm_f32`]):
952/// 1. **WGSL GPU** — when [`caps().wgpu`](caps) is set *and* the problem is at least
953/// [`GEMM_GPU_THRESHOLD`] MACs (`m * n`), run the certified GEMV via the shared
954/// [`ForgeRuntime`]. A runtime build/dispatch failure is **not** propagated — the
955/// call falls through to the CPU floor so it is never broken.
956/// 2. **CPU floor** — otherwise compute on the CPU via [`gemv_cpu`].
957///
958/// `a` must have `m * n` elements (row-major) and `x` must have `n`. Returns `m`
959/// row elements. Dimension/length mismatches are the only hard errors.
960pub fn gemv_f32(m: usize, n: usize, a: &[f32], x: &[f32]) -> Result<Vec<f32>, ForgeError> {
961 validate_gemv_dims(m, n, a.len(), x.len())?;
962
963 let work = m.saturating_mul(n);
964 if caps().wgpu && work >= GEMM_GPU_THRESHOLD {
965 if let Some(out) = gemv_f32_gpu(m, n, a, x) {
966 return Ok(out);
967 }
968 // GPU path was eligible but failed at runtime — fall through to the CPU
969 // floor rather than propagating, so the call is never broken.
970 }
971
972 Ok(gemv_cpu(a, x, m, n))
973}
974
975/// Run the f32 GEMV through the shared [`ForgeRuntime`], returning `None` on any
976/// runtime failure (runtime un-buildable now, or dispatch error) so the caller can
977/// fall through to the CPU floor. Never propagates a GPU error.
978fn gemv_f32_gpu(m: usize, n: usize, a: &[f32], x: &[f32]) -> Option<Vec<f32>> {
979 let cell = forge_rt_cell();
980 let mut guard = cell.lock().ok()?;
981 if guard.is_none() {
982 match ForgeRuntime::new(64 * 1024 * 1024, None) {
983 Ok(rt) => *guard = Some(rt),
984 Err(_) => return None,
985 }
986 }
987 let rt = guard.as_mut()?;
988 rt.gemv(a, x, m, n).ok()
989}
990
991/// Best-path double-precision dense GEMV: row-major `y[M] = A[M×N] · x[N]`, all
992/// `f64`.
993///
994/// Path selection (see the module doc for *why* this differs from [`gemv_f32`] —
995/// WGSL has no `f64`):
996/// 1. **native CUDA-f64 GPU** — when [`caps().cuda`](caps) is set *and* the problem
997/// is at least [`GEMM_GPU_THRESHOLD`] MACs (`m * n`), run the native
998/// double-precision CUDA GEMV. On any runtime error the call falls through to the
999/// CPU floor (never propagated).
1000/// 2. **CPU floor** — otherwise compute on the CPU via [`gemv_cpu_f64`].
1001///
1002/// There is intentionally **no WGSL path here**: WGSL has no `f64`. Today the f64
1003/// chain is exactly **CUDA-f64 → CPU**.
1004///
1005/// `a` must have `m * n` elements (row-major) and `x` must have `n`. Returns `m`
1006/// row elements.
1007pub fn gemv_f64(m: usize, n: usize, a: &[f64], x: &[f64]) -> Result<Vec<f64>, ForgeError> {
1008 validate_gemv_dims(m, n, a.len(), x.len())?;
1009
1010 #[cfg(feature = "cuda")]
1011 {
1012 let work = m.saturating_mul(n);
1013 if caps().cuda && work >= GEMM_GPU_THRESHOLD {
1014 if let Ok(out) = gemv_f64_cuda(m, n, a, x) {
1015 return Ok(out);
1016 }
1017 // CUDA path was eligible but errored — fall through to the CPU floor.
1018 }
1019 }
1020
1021 Ok(gemv_cpu_f64(a, x, m, n))
1022}
1023
1024/// Native CUDA double-precision GEMV. Builds a transient
1025/// [`CudaComputeContext`](super::execute::CudaComputeContext), uploads `a` (binding
1026/// 0) / `x` (binding 1) / a zeroed `y` (binding 2) and the `dims = [m, n]` u32
1027/// storage buffer (binding 3), compiles
1028/// [`GEMV_F64_SRC`](super::emit::cuda_c::GEMV_F64_SRC) via NVRTC, dispatches one
1029/// thread per output row (`element_count = m`), and reads back the `y` buffer as
1030/// `f64`. This is the exact-double path WGSL cannot provide.
1031#[cfg(feature = "cuda")]
1032fn gemv_f64_cuda(m: usize, n: usize, a: &[f64], x: &[f64]) -> Result<Vec<f64>, ForgeError> {
1033 use super::emit::cuda_c::{GEMV_F64_ENTRY, GEMV_F64_SRC};
1034 use super::execute::{CudaComputeContext, CudaPipeline, QualiaCompute};
1035 use super::Schedule;
1036
1037 let mut ctx = CudaComputeContext::new(64 * 1024 * 1024)?;
1038
1039 let element_count = m;
1040 let view_a = ctx.allocate_and_write(bytemuck::cast_slice(a), 0, 0)?;
1041 let view_x = ctx.allocate_and_write(bytemuck::cast_slice(x), 1, 0)?;
1042 let zeros = vec![0.0f64; element_count];
1043 let view_y = ctx.allocate_and_write(bytemuck::cast_slice(&zeros), 2, 0)?;
1044 // dims = [m, n] as u32, written as a storage buffer (binding 3) — no by-value
1045 // uniform, matching compile_cuda_c_source's pointer-only binding ABI.
1046 let dims: [u32; 2] = [m as u32, n as u32];
1047 let view_dims = ctx.allocate_and_write(bytemuck::cast_slice(&dims), 3, 0)?;
1048
1049 let buffers = vec![view_a, view_x, view_y, view_dims];
1050 let pipeline =
1051 CudaPipeline::compile_cuda_c_source(&ctx, GEMV_F64_SRC, GEMV_F64_ENTRY, &[0, 1, 2, 3])?;
1052 let schedule = Schedule {
1053 workgroup_size: 64,
1054 ..Default::default()
1055 };
1056 pipeline.dispatch(&buffers, &schedule, element_count)?;
1057 let mut out = ctx.read_buffer_f64(&view_y)?;
1058 out.truncate(element_count);
1059 Ok(out)
1060}
1061
1062/// CPU reference for the double-precision dense GEMV — the `f64` mirror of
1063/// [`gemv_cpu`]: row-major `y[M] = A[M×N] · x[N]`,
1064/// `y[i] = sum_{j<N} A[i*N + j] * x[j]`. The inner `j` sum order matches the
1065/// CUDA-f64 kernel so the two agree to f64 summation precision. This is the
1066/// always-present f64 floor.
1067pub fn gemv_cpu_f64(a: &[f64], x: &[f64], m: usize, n: usize) -> Vec<f64> {
1068 let mut y = vec![0.0f64; m];
1069 for i in 0..m {
1070 let a_row = i * n;
1071 let mut acc = 0.0f64;
1072 for j in 0..n {
1073 acc += a[a_row + j] * x[j];
1074 }
1075 y[i] = acc;
1076 }
1077 y
1078}
1079
1080/// Best-path forward FFT: `out = DFT(in)` over `n = complex_interleaved.len()/2`
1081/// complex points, input and output interleaved f32 (`[re0, im0, re1, im1, …]`,
1082/// length `2*n`). The transform is **un-normalized** and uses the forward sign
1083/// convention `X[k] = Σ_j x[j] · e^{−2πi kj/N}`, identical on both paths.
1084///
1085/// # Why this differs from the GEMM/GEMV dispatch shape
1086///
1087/// Unlike [`gemm_f32`]/[`gemm_f64`], the FFT has **no CUDA/df64 arm** — the forge
1088/// only ships a *WGSL* radix-2 kernel today, so the accelerated path is
1089/// wgpu-only. There is therefore exactly one accelerator branch:
1090///
1091/// | path (in order) | when |
1092/// |------------------------------|-------------------------------------------------|
1093/// | WGSL forge ([`ForgeRuntime::fft`]) | `caps().wgpu` and `n` a power of two, `2 ≤ n ≤ 1024` |
1094/// | CPU floor ([`dft_cpu`]) | otherwise, or if the forge errors at runtime |
1095///
1096/// The CPU floor is the naive O(N²) DFT [`dft_cpu`] — always present, never
1097/// broken. The forge kernel runs ONE workgroup of `n` threads, which is why `n`
1098/// must be a power of two and `≤ 1024` (the single-workgroup cap); inputs outside
1099/// that window fall straight to the CPU floor. On any forge build/dispatch error
1100/// the call falls through to the CPU floor rather than propagating (mirrors
1101/// [`gemm_f32`]).
1102///
1103/// `complex_interleaved.len()` must be even (it is `2*n`); an odd length is the
1104/// only hard error.
1105pub fn fft_f32(complex_interleaved: &[f32]) -> Result<Vec<f32>, ForgeError> {
1106 if complex_interleaved.len() % 2 != 0 {
1107 return Err(ForgeError::GpuValidation(format!(
1108 "fft input must be interleaved complex (even length = 2*n); got {}",
1109 complex_interleaved.len()
1110 )));
1111 }
1112 let n = complex_interleaved.len() / 2;
1113
1114 // Accelerated path is WGSL-only and single-workgroup: power-of-two n in
1115 // [2, 1024]. (n == 1 is a trivial identity the CPU floor handles directly.)
1116 if caps().wgpu && n.is_power_of_two() && (2..=1024).contains(&n) {
1117 if let Some(out) = fft_f32_gpu(complex_interleaved) {
1118 return Ok(out);
1119 }
1120 // Forge path was eligible but failed at runtime — fall through to the CPU
1121 // floor rather than propagating, so the call is never broken.
1122 }
1123
1124 Ok(dft_cpu(complex_interleaved, n))
1125}
1126
1127/// Run the forward FFT through the shared [`ForgeRuntime`], returning `None` on
1128/// any runtime failure (runtime un-buildable now, or dispatch error) so the
1129/// caller can fall through to the CPU floor. Never propagates a GPU error.
1130/// Reuses the same process-wide [`forge_rt_cell`] as [`gemm_f32`]/[`gemv_f32`].
1131fn fft_f32_gpu(complex_interleaved: &[f32]) -> Option<Vec<f32>> {
1132 let cell = forge_rt_cell();
1133 let mut guard = cell.lock().ok()?;
1134 if guard.is_none() {
1135 match ForgeRuntime::new(64 * 1024 * 1024, None) {
1136 Ok(rt) => *guard = Some(rt),
1137 Err(_) => return None,
1138 }
1139 }
1140 let rt = guard.as_mut()?;
1141 rt.fft(complex_interleaved).ok()
1142}
1143
1144/// Shared dimension/length validation for both GEMV entry points: `a` is `m*n`
1145/// (row-major) and `x` is `n`.
1146fn validate_gemv_dims(m: usize, n: usize, a_len: usize, x_len: usize) -> Result<(), ForgeError> {
1147 if m == 0 || n == 0 {
1148 return Err(ForgeError::GpuValidation(
1149 "gemv requires m > 0 and n > 0".to_string(),
1150 ));
1151 }
1152 if a_len != m * n {
1153 return Err(ForgeError::GpuValidation(format!(
1154 "a must have m*n = {} elements; got {}",
1155 m * n,
1156 a_len
1157 )));
1158 }
1159 if x_len != n {
1160 return Err(ForgeError::GpuValidation(format!(
1161 "x must have n = {} elements; got {}",
1162 n, x_len
1163 )));
1164 }
1165 Ok(())
1166}
1167
1168/// Shared dimension/length validation for both GEMM entry points.
1169fn validate_dims(
1170 m: usize,
1171 k: usize,
1172 n: usize,
1173 a_len: usize,
1174 b_len: usize,
1175) -> Result<(), ForgeError> {
1176 if m == 0 || k == 0 || n == 0 {
1177 return Err(ForgeError::GpuValidation(
1178 "gemm requires m > 0, k > 0, and n > 0".to_string(),
1179 ));
1180 }
1181 if a_len != m * k {
1182 return Err(ForgeError::GpuValidation(format!(
1183 "a must have m*k = {} elements; got {}",
1184 m * k,
1185 a_len
1186 )));
1187 }
1188 if b_len != k * n {
1189 return Err(ForgeError::GpuValidation(format!(
1190 "b must have k*n = {} elements; got {}",
1191 k * n,
1192 b_len
1193 )));
1194 }
1195 Ok(())
1196}
1197
1198#[cfg(test)]
1199mod tests {
1200 use super::*;
1201
1202 /// The capability probe must never panic, on any machine, regardless of which
1203 /// backends are present. (It is also memoised, so this just calls it.)
1204 #[test]
1205 fn caps_probe_never_panics() {
1206 let c = caps();
1207 // A second call returns the same cached value.
1208 assert_eq!(c, caps());
1209 }
1210
1211 /// Non-GPU: a sub-threshold f32 GEMM is forced onto the CPU floor and must match
1212 /// the hand-checked 2×3 · 3×2 reference [58, 64, 139, 154].
1213 /// A=[[1,2,3],[4,5,6]], B=[[7,8],[9,10],[11,12]].
1214 #[test]
1215 fn gemm_f32_cpu_fallback_is_correct() {
1216 let a = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1217 let b = [7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
1218 // 2*2*3 = 12 FMAs, far below GEMM_GPU_THRESHOLD, so this is the CPU path
1219 // even on a GPU machine.
1220 let out = gemm_f32(2, 3, 2, &a, &b).expect("gemm_f32");
1221 assert_eq!(out, vec![58.0, 64.0, 139.0, 154.0]);
1222 }
1223
1224 /// Non-GPU: the f64 twin of the above, on the f64 CPU floor.
1225 #[test]
1226 fn gemm_f64_cpu_fallback_is_correct() {
1227 let a = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
1228 let b = [7.0f64, 8.0, 9.0, 10.0, 11.0, 12.0];
1229 let out = gemm_f64(2, 3, 2, &a, &b).expect("gemm_f64");
1230 assert_eq!(out, vec![58.0, 64.0, 139.0, 154.0]);
1231 }
1232
1233 /// Non-GPU / non-16-multiple: the opt-in tensor-core `gemm_f32_tc` falls through to the
1234 /// exact plain f32 floor (the 2×3·3×2 case is neither a 16-multiple nor on an
1235 /// accelerator), so it returns the hand-checked [58, 64, 139, 154] — proving the
1236 /// tensor-core path never breaks a call that can't use it.
1237 #[test]
1238 fn gemm_f32_tc_falls_to_plain_floor() {
1239 let a = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1240 let b = [7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
1241 let out = gemm_f32_tc(2, 3, 2, &a, &b).expect("gemm_f32_tc");
1242 assert_eq!(out, vec![58.0, 64.0, 139.0, 154.0]);
1243 }
1244
1245 /// The reduced-precision entry point ([`gemm_f32_tc_reduced`]) likewise returns the exact
1246 /// f32 floor for a call that can use no tensor-core tier (non-16-multiple / non-GPU), so
1247 /// the opt-in never breaks a plain call. On TC dims + a CUDA adapter it may instead return
1248 /// an f16-precision result — that reduced path is exercised at runtime (cuda_lane /
1249 /// microbench), not asserted for f32 accuracy here.
1250 #[test]
1251 fn gemm_f32_tc_reduced_falls_to_plain_floor() {
1252 let a = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1253 let b = [7.0f32, 8.0, 9.0, 10.0, 11.0, 12.0];
1254 let out = gemm_f32_tc_reduced(2, 3, 2, &a, &b).expect("gemm_f32_tc_reduced");
1255 assert_eq!(out, vec![58.0, 64.0, 139.0, 154.0]);
1256 }
1257
1258 /// The coopmat (WGSL tensor-core) probe is honest and memoised: it can only be `true`
1259 /// where the adapter actually advertises coopmat, it is `false` without one (or no GPU),
1260 /// and repeated calls agree. On wgpu ≤30 it is `false` even on a coopmat-capable adapter
1261 /// where the driver/backend doesn't compute coopmat (verified 2026-07-13: on wgpu 30 the
1262 /// DX12 backend still returns zeros, #9741) — so this never wrongly enables the path.
1263 #[test]
1264 fn coopmat_usable_respects_caps_and_is_cached() {
1265 let first = coopmat_usable();
1266 if !caps().coopmat {
1267 assert!(
1268 !first,
1269 "coopmat_usable must be false without a coopmat-capable adapter"
1270 );
1271 }
1272 // usable ⇒ the adapter advertises coopmat (never the other way on 29.0.3).
1273 assert!(!first || caps().coopmat);
1274 // Memoised: a second call returns the same verdict.
1275 assert_eq!(first, coopmat_usable());
1276 }
1277
1278 /// Wrong dims are a hard error on the coopmat GEMM (non-8-multiple / zero), so the
1279 /// executor/dispatcher never dispatches an ill-formed tile. Non-GPU safe (validates
1280 /// before touching the device).
1281 #[test]
1282 fn gemm_f32_tc_coopmat_rejects_non_8_multiples() {
1283 assert!(gemm_f32_tc_coopmat(8, 8, 12, &[0.0; 96], &[0.0; 96]).is_err());
1284 assert!(gemm_f32_tc_coopmat(0, 8, 8, &[], &[0.0; 64]).is_err());
1285 }
1286
1287 /// The delivered f32 tensor-core selector must match the exact CPU reference on every
1288 /// backend. A backend whose raw cooperative-matrix primitive fails the runtime oracle
1289 /// is required to use the exact GPU/CPU floor instead of returning corrupt output.
1290 #[test]
1291 #[serial_test::serial(gpu)]
1292 fn gemm_f32_tc_matches_cpu_reference_or_exact_fallback() {
1293 let (m, k, n) = (16usize, 16usize, 16usize);
1294 let a: Vec<f32> = (0..m * k).map(|i| ((i % 7) as f32) * 0.5 - 1.0).collect();
1295 let b: Vec<f32> = (0..k * n).map(|i| ((i % 5) as f32) * 0.25 + 0.1).collect();
1296 let got = gemm_f32_tc(m, k, n, &a, &b).expect("f32-faithful tensor-core selector");
1297 let want = crate::wgsl_forge::oracle::gemm_cpu(&a, &b, m, k, n);
1298 assert_eq!(got.len(), want.len());
1299 for (g, w) in got.iter().zip(&want) {
1300 assert!(
1301 (g - w).abs() <= 1.0e-3 + 1.0e-3 * w.abs(),
1302 "selected f32 path {g} vs cpu {w}"
1303 );
1304 }
1305 // Non-zero sanity: the broken DX12 primitive's all-zero output must never escape.
1306 assert!(got.iter().any(|&v| v.abs() > 1.0e-6));
1307
1308 if !coopmat_usable() {
1309 assert!(
1310 gemm_f32_tc_coopmat(m, k, n, &a, &b).is_err(),
1311 "the raw public coopmat API must fail closed after a failed oracle"
1312 );
1313 }
1314 }
1315
1316 /// Dimension mismatches are hard errors on both entry points.
1317 #[test]
1318 fn gemm_dim_mismatch_errors() {
1319 assert!(gemm_f32(2, 3, 2, &[1.0; 5], &[1.0; 6]).is_err());
1320 assert!(gemm_f64(2, 3, 2, &[1.0; 6], &[1.0; 5]).is_err());
1321 assert!(gemm_f32(0, 3, 2, &[], &[1.0; 6]).is_err());
1322 }
1323
1324 /// `gemm_cpu_f64` agrees with the f32 reference on small exact-integer inputs,
1325 /// pinning the f64 floor's layout/sum order independently of the dispatcher.
1326 #[test]
1327 fn gemm_cpu_f64_matches_hand_checked() {
1328 let a = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
1329 let b = [7.0f64, 8.0, 9.0, 10.0, 11.0, 12.0];
1330 assert_eq!(
1331 gemm_cpu_f64(&a, &b, 2, 3, 2),
1332 vec![58.0, 64.0, 139.0, 154.0]
1333 );
1334 }
1335
1336 /// Non-GPU: the df64 (double-single) host pack/unpack helpers round-trip a
1337 /// handful of `f64` values to ~1e-15. `f64_to_df32` splits a double into a hi/lo
1338 /// `f32` pair carrying ~44–48 mantissa bits; `df32_to_f64` recombines them. The
1339 /// residual is the `f32` rounding of `hi` refined by `lo`, far tighter than a
1340 /// single `f32` (~1e-7) — this pins the host side of the df64 path independently
1341 /// of any GPU.
1342 #[test]
1343 fn df64_pack_roundtrips() {
1344 let values = [
1345 0.0f64,
1346 1.0,
1347 -1.0,
1348 0.1,
1349 std::f64::consts::PI,
1350 -std::f64::consts::E,
1351 123.456_789,
1352 1.0 / 3.0,
1353 ];
1354 for &v in &values {
1355 let [hi, lo] = f64_to_df32(v);
1356 let back = df32_to_f64(hi, lo);
1357 assert!(
1358 (back - v).abs() <= 1.0e-15 * (1.0 + v.abs()),
1359 "df64 roundtrip {v} -> {back} (hi={hi}, lo={lo})"
1360 );
1361 }
1362 // And the flat-vector pack/unpack is the elementwise round-trip.
1363 let packed = pack_df32(&values);
1364 assert_eq!(packed.len(), values.len() * 2);
1365 let unpacked = unpack_df32(&packed);
1366 assert_eq!(unpacked.len(), values.len());
1367 for (u, v) in unpacked.iter().zip(values.iter()) {
1368 assert!((u - v).abs() <= 1.0e-15 * (1.0 + v.abs()), "{u} vs {v}");
1369 }
1370 }
1371
1372 /// Non-GPU: a sub-threshold f32 GEMV is forced onto the CPU floor and must match
1373 /// the hand-checked 2×3 · 3 reference [6, 15].
1374 /// A=[[1,2,3],[4,5,6]], x=[1,1,1].
1375 #[test]
1376 fn gemv_f32_cpu_fallback_is_correct() {
1377 let a = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
1378 let x = [1.0f32, 1.0, 1.0];
1379 // 2*3 = 6 MACs, far below GEMM_GPU_THRESHOLD, so this is the CPU path even on
1380 // a GPU machine.
1381 let out = gemv_f32(2, 3, &a, &x).expect("gemv_f32");
1382 assert_eq!(out, vec![6.0, 15.0]);
1383 }
1384
1385 /// Non-GPU: the f64 twin of the above, on the f64 CPU floor.
1386 #[test]
1387 fn gemv_f64_cpu_fallback_is_correct() {
1388 let a = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
1389 let x = [1.0f64, 1.0, 1.0];
1390 let out = gemv_f64(2, 3, &a, &x).expect("gemv_f64");
1391 assert_eq!(out, vec![6.0, 15.0]);
1392 }
1393
1394 /// Dimension mismatches are hard errors on both GEMV entry points.
1395 #[test]
1396 fn gemv_dim_mismatch_errors() {
1397 assert!(gemv_f32(2, 3, &[1.0; 5], &[1.0; 3]).is_err()); // a too short
1398 assert!(gemv_f64(2, 3, &[1.0; 6], &[1.0; 2]).is_err()); // x too short
1399 assert!(gemv_f32(0, 3, &[], &[1.0; 3]).is_err()); // m == 0
1400 }
1401
1402 /// `gemv_cpu_f64` agrees with the hand-checked reference on small exact-integer
1403 /// inputs, pinning the f64 floor's layout/sum order independently of the
1404 /// dispatcher.
1405 #[test]
1406 fn gemv_cpu_f64_matches_hand_checked() {
1407 let a = [1.0f64, 2.0, 3.0, 4.0, 5.0, 6.0];
1408 let x = [1.0f64, 1.0, 1.0];
1409 assert_eq!(gemv_cpu_f64(&a, &x, 2, 3), vec![6.0, 15.0]);
1410 }
1411
1412 /// `pairwise_sq_dist_cpu_f64` — the exact direct reference — on a hand-checked
1413 /// 2-point × 2-point, 2-D case. a=[[0,0],[1,1]], b=[[1,0],[0,1]]:
1414 /// ‖a0−b0‖²=1, ‖a0−b1‖²=1, ‖a1−b0‖²=1, ‖a1−b1‖²=1.
1415 #[test]
1416 fn pairwise_cpu_matches_hand_checked() {
1417 let a = [0.0f64, 0.0, 1.0, 1.0];
1418 let b = [1.0f64, 0.0, 0.0, 1.0];
1419 let d = pairwise_sq_dist_cpu_f64(&a, &b, 2, 2, 2);
1420 assert_eq!(d, vec![1.0, 1.0, 1.0, 1.0]);
1421 // A point's distance to itself is exactly 0.
1422 let self_d = pairwise_sq_dist_cpu_f64(&a, &a, 2, 2, 2);
1423 assert_eq!(self_d[0], 0.0);
1424 assert_eq!(self_d[3], 0.0);
1425 }
1426
1427 /// The best-path `pairwise_sq_dist_f64` (GEMM-identity form) must agree with the
1428 /// exact direct CPU reference within f64 tolerance. Sub-threshold here, so the
1429 /// cross-term GEMM takes its own CPU floor — exercising the identity arithmetic and
1430 /// the norm/combine pass on a GPU-less box. Deterministic data, no RNG.
1431 #[test]
1432 fn pairwise_identity_matches_direct_reference() {
1433 let (n, m, p) = (5usize, 4usize, 3usize);
1434 let mut a = vec![0.0f64; n * p];
1435 for i in 0..n {
1436 for d in 0..p {
1437 a[i * p + d] = ((i * 3 + d * 2) % 7) as f64 * 0.5 - 1.0;
1438 }
1439 }
1440 let mut b = vec![0.0f64; m * p];
1441 for j in 0..m {
1442 for d in 0..p {
1443 b[j * p + d] = ((j * 5 + d) % 6) as f64 * 0.25 - 0.5;
1444 }
1445 }
1446 let identity = pairwise_sq_dist_f64(&a, &b, n, m, p);
1447 let direct = pairwise_sq_dist_cpu_f64(&a, &b, n, m, p);
1448 assert_eq!(identity.len(), direct.len());
1449 for (id, dr) in identity.iter().zip(direct.iter()) {
1450 assert!((id - dr).abs() < 1e-9, "pairwise mismatch: {id} vs {dr}");
1451 }
1452 // All squared distances are non-negative (clamp holds).
1453 assert!(identity.iter().all(|&v| v >= 0.0));
1454 }
1455
1456 /// Shape mismatch is not a panic — it falls to the zero-filled CPU floor.
1457 #[test]
1458 fn pairwise_shape_mismatch_is_graceful() {
1459 // a has 5 elems but n*p = 2*3 = 6.
1460 let out = pairwise_sq_dist_f64(&[1.0; 5], &[1.0; 6], 2, 2, 3);
1461 assert_eq!(out.len(), 4);
1462 }
1463
1464 /// Either path: the forward FFT of a real unit impulse at index 0 (`x[0] = 1`,
1465 /// rest 0) has a flat spectrum — every bin is exactly `(1, 0)` — because
1466 /// `X[k] = Σ_j x[j] e^{−2πi kj/N} = x[0] = 1` for all `k`. This identity is
1467 /// exact regardless of whether the WGSL forge or the CPU DFT floor runs, so it
1468 /// validates `fft_f32` on a GPU-less box (CPU floor) and a GPU box (forge)
1469 /// alike. N=4 (a power of two ≤ 1024, so the forge path is eligible when a
1470 /// wgpu adapter is present).
1471 #[test]
1472 fn fft_cpu_fallback_matches_dft() {
1473 let n = 4usize;
1474 let mut signal = vec![0.0f32; 2 * n]; // interleaved (real, imag)
1475 signal[0] = 1.0; // unit impulse at j=0
1476 let spectrum = fft_f32(&signal).expect("fft_f32");
1477 assert_eq!(spectrum.len(), 2 * n);
1478 for k in 0..n {
1479 assert!(
1480 (spectrum[2 * k] - 1.0).abs() < 1e-4,
1481 "bin {k} real should be 1, got {}",
1482 spectrum[2 * k]
1483 );
1484 assert!(
1485 spectrum[2 * k + 1].abs() < 1e-4,
1486 "bin {k} imag should be 0, got {}",
1487 spectrum[2 * k + 1]
1488 );
1489 }
1490 }
1491
1492 /// An odd-length (not `2*n`) input is the only hard error on `fft_f32`.
1493 #[test]
1494 fn fft_f32_odd_length_errors() {
1495 assert!(fft_f32(&[1.0f32, 2.0, 3.0]).is_err());
1496 }
1497
1498 // ── GPU / CUDA end-to-end tests (require a real device; run by the orchestrator) ──
1499
1500 /// Deterministic xorshift fill in [-1, 1], so GPU and CPU see identical inputs.
1501 #[cfg(test)]
1502 fn det_f32(len: usize, seed: u64) -> Vec<f32> {
1503 let mut state = seed.max(1);
1504 let mut v = Vec::with_capacity(len);
1505 for _ in 0..len {
1506 state ^= state << 13;
1507 state ^= state >> 7;
1508 state ^= state << 17;
1509 let unit = (state as u32) as f32 / u32::MAX as f32;
1510 v.push(unit.mul_add(2.0, -1.0));
1511 }
1512 v
1513 }
1514
1515 /// Above-threshold f32 GEMM on the WGSL GPU path must match the CPU reference
1516 /// within f32 summation tolerance. m=k=n=64 → 262144 FMAs ≥ threshold.
1517 #[test]
1518 #[serial_test::serial(gpu)]
1519 fn gemm_f32_gpu_matches_cpu() {
1520 if !crate::wgsl_forge::test_gpu_available() {
1521 return;
1522 }
1523 let (m, k, n) = (64usize, 64, 64);
1524 let a = det_f32(m * k, 0x6745_4D4D_4633_3201);
1525 let b = det_f32(k * n, 0x6745_4D4D_4633_3202);
1526 let gpu = gemm_f32(m, k, n, &a, &b).expect("gemm_f32 gpu");
1527 let cpu = gemm_cpu(&a, &b, m, k, n);
1528 assert_eq!(gpu.len(), cpu.len());
1529 for (g, c) in gpu.iter().zip(cpu.iter()) {
1530 assert!((g - c).abs() <= 1.0e-3, "f32 GPU/CPU mismatch: {g} vs {c}");
1531 }
1532 }
1533
1534 /// Above-threshold f32 GEMV on the WGSL GPU path must match the CPU reference
1535 /// within f32 summation tolerance. m=n=256 → 65536 MACs ≥ threshold.
1536 #[test]
1537 #[serial_test::serial(gpu)]
1538 fn gemv_f32_gpu_matches_cpu() {
1539 if !crate::wgsl_forge::test_gpu_available() {
1540 return;
1541 }
1542 let (m, n) = (256usize, 256);
1543 let a = det_f32(m * n, 0x6745_4D56_4633_3201);
1544 let x = det_f32(n, 0x6745_4D56_4633_3202);
1545 let gpu = gemv_f32(m, n, &a, &x).expect("gemv_f32 gpu");
1546 let cpu = gemv_cpu(&a, &x, m, n);
1547 assert_eq!(gpu.len(), cpu.len());
1548 for (g, c) in gpu.iter().zip(cpu.iter()) {
1549 assert!((g - c).abs() <= 1.0e-3, "f32 GPU/CPU mismatch: {g} vs {c}");
1550 }
1551 }
1552
1553 /// Forward FFT on the WGSL forge path must match the naive DFT floor within
1554 /// f32-vs-(f64-accumulated)-DFT tolerance. N=256 (a power-of-two single
1555 /// workgroup); both fed the SAME deterministic interleaved signal so the GPU
1556 /// FFT and the CPU `dft_cpu` reference compute the identical transform.
1557 #[test]
1558 #[serial_test::serial(gpu)]
1559 fn fft_f32_gpu_matches_dft() {
1560 if !crate::wgsl_forge::test_gpu_available() {
1561 return;
1562 }
1563 let n = 256usize;
1564 // 2*n interleaved (real, imag) samples, deterministic and identical for
1565 // both paths.
1566 let signal = det_f32(2 * n, 0x4646_545F_4D54_4348);
1567 let gpu = fft_f32(&signal).expect("fft_f32 gpu");
1568 let cpu = dft_cpu(&signal, n);
1569 assert_eq!(gpu.len(), cpu.len());
1570 for (g, c) in gpu.iter().zip(cpu.iter()) {
1571 assert!((g - c).abs() <= 1.0e-2, "f32 FFT/DFT mismatch: {g} vs {c}");
1572 }
1573 }
1574
1575 /// Above-threshold f64 GEMM on the native CUDA path must match the f64 CPU
1576 /// reference to near-exact precision (native double, no emulation).
1577 #[cfg(feature = "cuda")]
1578 #[test]
1579 #[serial_test::serial(gpu)]
1580 fn gemm_f64_cuda_matches_cpu() {
1581 if !crate::wgsl_forge::test_cuda_available() {
1582 return;
1583 }
1584 let (m, k, n) = (64usize, 64, 64);
1585 let a: Vec<f64> = det_f32(m * k, 0x6745_4D4D_4636_3401)
1586 .into_iter()
1587 .map(|x| x as f64)
1588 .collect();
1589 let b: Vec<f64> = det_f32(k * n, 0x6745_4D4D_4636_3402)
1590 .into_iter()
1591 .map(|x| x as f64)
1592 .collect();
1593 let gpu = gemm_f64(m, k, n, &a, &b).expect("gemm_f64 cuda");
1594 let cpu = gemm_cpu_f64(&a, &b, m, k, n);
1595 assert_eq!(gpu.len(), cpu.len());
1596 for (g, c) in gpu.iter().zip(cpu.iter()) {
1597 assert!((g - c).abs() <= 1.0e-9, "f64 CUDA/CPU mismatch: {g} vs {c}");
1598 }
1599 }
1600
1601 /// Tiled tensor-core (WMMA) GEMM on the CUDA backend: f16-input / f32-accumulate, so it
1602 /// is graded against an f32 matmul of the SAME inputs rounded through f16 first (the
1603 /// reduced-precision contract). 64×64×64 = a 4×4 grid of output tiles, each looping 4
1604 /// K-tiles — this exercises the tiling orchestration, not just a single tile.
1605 #[cfg(feature = "cuda")]
1606 #[test]
1607 #[serial_test::serial(gpu)]
1608 fn gemm_tc_cuda_tiled_matches_f16_reference() {
1609 if !crate::wgsl_forge::test_cuda_available() {
1610 return;
1611 }
1612 let (m, k, n) = (64usize, 64, 64);
1613 // Small-magnitude data so f16 rounding error stays bounded over the K=64 sum.
1614 let a: Vec<f32> = det_f32(m * k, 0x574D_4D41_5449_4C45)
1615 .iter()
1616 .map(|&x| x * 0.5)
1617 .collect();
1618 let b: Vec<f32> = det_f32(k * n, 0x574D_4D41_5449_4C46)
1619 .iter()
1620 .map(|&x| x * 0.5)
1621 .collect();
1622 // Reference: f32 matmul of the f16-rounded inputs.
1623 let ar: Vec<f32> = a.iter().map(|&x| half::f16::from_f32(x).to_f32()).collect();
1624 let br: Vec<f32> = b.iter().map(|&x| half::f16::from_f32(x).to_f32()).collect();
1625 let expected = gemm_cpu(&ar, &br, m, k, n);
1626 let actual = gemm_tc_cuda(m, k, n, &a, &b).expect("gemm_tc_cuda");
1627 assert_eq!(actual.len(), expected.len());
1628 for (e, g) in expected.iter().zip(actual.iter()) {
1629 assert!(
1630 (e - g).abs() <= 5.0e-2 + 5.0e-2 * e.abs(),
1631 "WMMA tiled GEMM mismatch: cpu {e} vs gpu {g}"
1632 );
1633 }
1634 // Sanity: real output, not the all-zeros symptom of a broken tensor-core multiply.
1635 assert!(actual.iter().any(|&v| v.abs() > 1.0e-3));
1636 }
1637
1638 /// df64 (double-single) emulated-f64 GEMM is correct only on adapters that do NOT
1639 /// reassociate f32 arithmetic (which would collapse the error-free transforms to
1640 /// f32 precision). `df64_usable()` probes this at runtime. This test verifies the
1641 /// probe is HONEST and the public `gemm_f64` is correct on every adapter:
1642 /// - where the probe reports usable, the direct df64 GEMM is genuinely f64-precise;
1643 /// - regardless, `gemm_f64` lands within f64 tolerance via the best working path
1644 /// (df64 if usable, else native CUDA-f64, else the exact CPU floor).
1645 /// On the naga->SPIR-V->NVIDIA-Vulkan path here, the probe reports NOT usable (the
1646 /// driver reassociates floats), so df64 is correctly skipped and CUDA/CPU is used.
1647 #[test]
1648 #[serial_test::serial(gpu)]
1649 fn df64_precision_is_probed_and_honest() {
1650 if !crate::wgsl_forge::test_gpu_available() {
1651 return;
1652 }
1653 let (m, k, n) = (64usize, 64, 64);
1654 let a: Vec<f64> = det_f32(m * k, 0x6446_3634_4D4D_3401)
1655 .into_iter()
1656 .map(|x| x as f64)
1657 .collect();
1658 let b: Vec<f64> = det_f32(k * n, 0x6446_3634_4D4D_3402)
1659 .into_iter()
1660 .map(|x| x as f64)
1661 .collect();
1662 let cpu = gemm_cpu_f64(&a, &b, m, k, n);
1663
1664 if df64_usable() {
1665 // The probe says this adapter preserves the error-free transforms — so the
1666 // direct df64 GEMM MUST be genuinely f64-precise.
1667 let df = gemm_f64_df64(m, k, n, &a, &b).expect("df64 gpu");
1668 for (d, c) in df.iter().zip(cpu.iter()) {
1669 assert!(
1670 (d - c).abs() <= 1.0e-9,
1671 "df64 reported usable but imprecise: {d} vs {c}"
1672 );
1673 }
1674 } else {
1675 eprintln!(
1676 "df64 not usable on this adapter (driver reassociates floats) — \
1677 the f64 chain uses native CUDA or the exact CPU floor instead."
1678 );
1679 }
1680
1681 // The PUBLIC f64 entry point must be correct on every adapter, via whichever
1682 // tier actually works (df64 / CUDA-f64 / CPU).
1683 let chain = gemm_f64(m, k, n, &a, &b).expect("gemm_f64");
1684 for (g, c) in chain.iter().zip(cpu.iter()) {
1685 assert!(
1686 (g - c).abs() <= 1.0e-9,
1687 "gemm_f64 chain incorrect: {g} vs {c}"
1688 );
1689 }
1690 }
1691
1692 /// Above-threshold f64 GEMV on the native CUDA path must match the f64 CPU
1693 /// reference to near-exact precision (native double, no emulation). m=n=256.
1694 #[cfg(feature = "cuda")]
1695 #[test]
1696 #[serial_test::serial(gpu)]
1697 fn gemv_f64_cuda_matches_cpu() {
1698 if !crate::wgsl_forge::test_cuda_available() {
1699 return;
1700 }
1701 let (m, n) = (256usize, 256);
1702 let a: Vec<f64> = det_f32(m * n, 0x6745_4D56_4636_3401)
1703 .into_iter()
1704 .map(|v| v as f64)
1705 .collect();
1706 let x: Vec<f64> = det_f32(n, 0x6745_4D56_4636_3402)
1707 .into_iter()
1708 .map(|v| v as f64)
1709 .collect();
1710 let gpu = gemv_f64(m, n, &a, &x).expect("gemv_f64 cuda");
1711 let cpu = gemv_cpu_f64(&a, &x, m, n);
1712 assert_eq!(gpu.len(), cpu.len());
1713 for (g, c) in gpu.iter().zip(cpu.iter()) {
1714 assert!((g - c).abs() <= 1.0e-9, "f64 CUDA/CPU mismatch: {g} vs {c}");
1715 }
1716 }
1717}