use crate::gpu::{ensure_module_loaded_lazy, resident_cuda_weights, shared_device, CudaError};
use crate::mul_mm::{grid_dims, kernel_src, validate_shape, MulMmKind, THREADS};
pub fn launch_mul_mm(
kind: &MulMmKind,
weights: &[u8],
x_batch: &[f32],
n_rows: usize,
n_cols: usize,
batch: usize,
row_bytes: usize,
) -> Result<Vec<f32>, CudaError> {
use cudarc::driver::LaunchAsync;
validate_shape(
kind,
weights.len(),
x_batch.len(),
n_rows,
n_cols,
batch,
row_bytes,
)
.map_err(|e| CudaError::Unsupported(e.to_string()))?;
let dev = shared_device()?;
ensure_module_loaded_lazy(&dev, kind.module_name, kind.fn_name, || kernel_src(kind))?;
let func = dev
.get_func(kind.module_name, kind.fn_name)
.ok_or_else(|| {
CudaError::KernelCompile(format!(
"function '{}' not found after load_ptx",
kind.fn_name
))
})?;
let d_weights = resident_cuda_weights(&dev, weights)?;
let d_x = dev
.htod_copy(x_batch[..batch * n_cols].to_vec())
.map_err(|e| CudaError::Launch(format!("mul_mm activation upload: {e:?}")))?;
let mut d_out = dev
.alloc_zeros::<f32>(batch * n_rows)
.map_err(|e| CudaError::Launch(format!("mul_mm output alloc: {e:?}")))?;
let (grid_x, grid_y) = grid_dims(n_rows, batch);
let cfg = cudarc::driver::LaunchConfig {
grid_dim: (grid_x as u32, grid_y as u32, 1),
block_dim: (THREADS as u32, 1, 1),
shared_mem_bytes: 0,
};
unsafe {
func.launch(
cfg,
(
&d_weights.slice,
&d_x,
&mut d_out,
n_rows as i32,
n_cols as i32,
batch as i32,
row_bytes as i32,
),
)
.map_err(|e| CudaError::Launch(format!("kernel {}: {e:?}", kind.fn_name)))?;
}
let out = dev
.dtoh_sync_copy(&d_out)
.map_err(|e| CudaError::Launch(format!("mul_mm output download: {e:?}")))?;
drop(d_weights);
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mul_mm::{BM, Q4_0, Q8_0};
use crate::mul_mm_ref::mul_mm_reference;
#[test]
fn a_shape_the_kernel_cannot_do_is_refused_without_touching_the_device() {
let err = launch_mul_mm(&Q8_0, &[0; 68], &[0.0; 48], 2, 48, 1, 34).unwrap_err();
match err {
CudaError::Unsupported(msg) => {
assert!(msg.contains("K-tile"), "unhelpful refusal: {msg}");
}
other => panic!("expected a named refusal, got {other:?}"),
}
}
#[test]
#[ignore = "requires real CUDA hardware -- NEVER RUN: this kernel has never executed on a GPU. Run with --ignored on a CUDA-capable machine and record the result before any doc claims CUDA mul_mm works"]
fn launch_mul_mm_matches_the_scalar_twin() {
for (kind, weights_of) in [(&Q8_0, 0usize), (&Q4_0, 1usize)] {
for (n_rows, n_cols, batch) in [(BM * 2, 128, 32), (BM + 7, 96, 37), (33, 64, 3)] {
let row_bytes = (n_cols / kind.block_elems) * kind.block_bytes;
let weights = match weights_of {
0 => q8_0_weights(n_rows, n_cols),
_ => q4_0_weights(n_rows, n_cols),
};
let x: Vec<f32> = (0..batch * n_cols)
.map(|i| ((i as f32) * 0.019).cos())
.collect();
let want = mul_mm_reference(kind, &weights, &x, n_rows, n_cols, batch, row_bytes)
.expect("the twin must accept every shape the kernel accepts");
let got = launch_mul_mm(kind, &weights, &x, n_rows, n_cols, batch, row_bytes)
.expect("kernel launch must succeed on real CUDA hardware");
assert_eq!(got.len(), want.len());
for (i, (g, w)) in got.iter().zip(want.iter()).enumerate() {
let where_ = format!("{} {n_rows}x{n_cols}x{batch} element {i}", kind.name);
if w.is_nan() {
assert!(g.is_nan(), "{where_}: twin is NaN but GPU={g} is not");
continue;
}
let scale = w.abs().max(1.0);
assert!((g - w).abs() <= 1e-4 * scale, "{where_}: GPU={g} twin={w}");
}
}
}
}
fn q8_0_weights(n_rows: usize, n_cols: usize) -> Vec<u8> {
let mut out = Vec::new();
for r in 0..n_rows {
let row: Vec<f32> = (0..n_cols)
.map(|i| (((r * n_cols + i) as f32) * 0.037).sin())
.collect();
out.extend(ferrox_quant::quantize_q8_0(&row));
}
out
}
fn q4_0_weights(n_rows: usize, n_cols: usize) -> Vec<u8> {
let mut out = Vec::new();
let blocks = n_cols / 32;
let mut state = 12345u32;
for r in 0..n_rows {
for b in 0..blocks {
let scale = half::f16::from_f32(0.05 + ((r * blocks + b) % 13) as f32 * 0.01);
out.extend_from_slice(&scale.to_le_bytes());
for _ in 0..16 {
state = state.wrapping_mul(1103515245).wrapping_add(12345);
out.push((state >> 16) as u8);
}
}
}
out
}
}