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, 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() {
use crate::mul_mm::KINDS;
for kind in KINDS {
for (n_rows, cols, batch) in [(BM * 2, 128usize, 32), (BM + 7, 96, 37), (33, 64, 3)] {
let n_cols = cols.next_multiple_of(kind.block_elems);
let row_bytes = (n_cols / kind.block_elems) * kind.block_bytes;
let weights = crate::mul_mm_ref::fixtures::weights(kind, n_rows, n_cols, 4242);
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}");
}
}
}
}
}