use crate::context::GpuContext;
use crate::executor::{ComputeOp, GpuOpError};
use wgpu::util::DeviceExt;
const WORKGROUP_X: u32 = 16;
const WORKGROUP_Y: u32 = 16;
pub struct MatmulNtInput<'a> {
pub x: &'a [f32],
pub w: &'a [f32],
pub m: usize,
pub k: usize,
pub n: usize,
}
pub struct MatmulNt;
impl ComputeOp for MatmulNt {
type Input<'a> = MatmulNtInput<'a>;
type Output = Vec<f32>;
fn compute_gpu(
&self,
ctx: &GpuContext,
input: &Self::Input<'_>,
) -> Result<Self::Output, GpuOpError> {
matmul_nt_gpu(ctx, input.x, input.w, input.m, input.k, input.n)
}
fn compute_cpu(&self, input: &Self::Input<'_>) -> Self::Output {
matmul_nt_cpu(input.x, input.w, input.m, input.k, input.n)
}
}
#[must_use]
pub fn matmul_nt_cpu(x: &[f32], w: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
let mut y = vec![0f32; m * n];
if x.len() < m * k || w.len() < n * k {
return y;
}
for i in 0..m {
let x_row = &x[i * k..i * k + k];
for j in 0..n {
let w_row = &w[j * k..j * k + k];
let mut acc = 0f32;
for t in 0..k {
acc += x_row[t] * w_row[t];
}
y[i * n + j] = acc;
}
}
y
}
pub fn matmul_nt_gpu(
ctx: &GpuContext,
x: &[f32],
w: &[f32],
m: usize,
k: usize,
n: usize,
) -> Result<Vec<f32>, GpuOpError> {
if x.len() != m * k {
return Err(GpuOpError::InvalidInput(format!(
"x has {} elements, expected m*k = {}*{} = {}",
x.len(),
m,
k,
m * k
)));
}
if w.len() != n * k {
return Err(GpuOpError::InvalidInput(format!(
"w has {} elements, expected n*k = {}*{} = {}",
w.len(),
n,
k,
n * k
)));
}
if m == 0 || n == 0 || k == 0 {
return Ok(vec![0f32; m * n]);
}
let device = ctx.device();
let queue = ctx.queue();
let out_bytes = (m * n * std::mem::size_of::<f32>()) as wgpu::BufferAddress;
let limit = device.limits().max_storage_buffer_binding_size;
let biggest = [
(m * k * std::mem::size_of::<f32>()) as u64,
(n * k * std::mem::size_of::<f32>()) as u64,
out_bytes,
]
.into_iter()
.max()
.unwrap_or(0);
if biggest > limit {
return Err(GpuOpError::InvalidInput(format!(
"matmul {m}x{k}x{n} needs a {biggest}-byte storage binding but this \
adapter's max_storage_buffer_binding_size is {limit}; falling back to CPU"
)));
}
let x_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("matmul_nt.x"),
contents: bytemuck::cast_slice(x),
usage: wgpu::BufferUsages::STORAGE,
});
let w_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("matmul_nt.w"),
contents: bytemuck::cast_slice(w),
usage: wgpu::BufferUsages::STORAGE,
});
let dims: [u32; 4] = [m as u32, k as u32, n as u32, 0];
let dims_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("matmul_nt.dims"),
contents: bytemuck::cast_slice(&dims),
usage: wgpu::BufferUsages::UNIFORM,
});
let y_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("matmul_nt.y"),
size: out_bytes,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
mapped_at_creation: false,
});
let staging = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("matmul_nt.staging"),
size: out_bytes,
usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
mapped_at_creation: false,
});
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("matmul_nt.wgsl"),
source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/matmul_nt.wgsl").into()),
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("matmul_nt.pipeline"),
layout: None,
module: &shader,
entry_point: Some("main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("matmul_nt.bind_group"),
layout: &pipeline.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry { binding: 0, resource: x_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 1, resource: w_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 2, resource: y_buf.as_entire_binding() },
wgpu::BindGroupEntry { binding: 3, resource: dims_buf.as_entire_binding() },
],
});
let mut encoder =
device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("matmul_nt") });
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("matmul_nt.pass"),
timestamp_writes: None,
});
pass.set_pipeline(&pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.dispatch_workgroups(
(m as u32).div_ceil(WORKGROUP_X),
(n as u32).div_ceil(WORKGROUP_Y),
1,
);
}
encoder.copy_buffer_to_buffer(&y_buf, 0, &staging, 0, out_bytes);
queue.submit(Some(encoder.finish()));
let slice = staging.slice(..);
let (tx, rx) = std::sync::mpsc::channel();
slice.map_async(wgpu::MapMode::Read, move |res| {
let _ = tx.send(res);
});
device
.poll(wgpu::PollType::wait_indefinitely())
.map_err(|e| GpuOpError::Backend(format!("device poll failed: {e:?}")))?;
rx.recv()
.map_err(|e| GpuOpError::Backend(format!("map callback dropped: {e}")))?
.map_err(|e| GpuOpError::Backend(format!("buffer map failed: {e:?}")))?;
let data = slice
.get_mapped_range()
.map_err(|e| GpuOpError::Backend(format!("get_mapped_range failed: {e:?}")))?;
let result: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
drop(data);
staging.unmap();
Ok(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Executor;
fn fill(n: usize, seed: f32) -> Vec<f32> {
(0..n).map(|i| (i as f32 * 0.37 + seed).sin()).collect()
}
#[test]
fn cpu_matches_a_hand_computed_product() {
let y = matmul_nt_cpu(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0], 2, 2, 2);
assert_eq!(y, vec![17.0, 23.0, 39.0, 53.0]);
}
#[test]
fn cpu_handles_non_square_shapes() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let w: Vec<f32> = (1..=12).map(|v| v as f32).collect();
let y = matmul_nt_cpu(&x, &w, 2, 3, 4);
assert_eq!(y.len(), 8);
assert_eq!(y[0], 14.0);
assert_eq!(y[7], 167.0);
}
#[test]
fn gpu_rejects_ragged_inputs_instead_of_guessing() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
let err = matmul_nt_gpu(&ctx, &[1.0, 2.0], &[1.0, 2.0], 2, 2, 1).unwrap_err();
assert!(matches!(err, GpuOpError::InvalidInput(_)), "got {err:?}");
}
#[test]
fn gpu_matches_cpu_at_real_transformer_shapes() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
for &(m, k, n) in &[(33usize, 960usize, 960usize), (1, 960, 2560), (33, 2560, 960), (1, 64, 64)] {
let x = fill(m * k, 0.1);
let w = fill(n * k, 0.7);
let gpu = matmul_nt_gpu(&ctx, &x, &w, m, k, n).expect("gpu matmul");
let cpu = matmul_nt_cpu(&x, &w, m, k, n);
assert_eq!(gpu.len(), cpu.len(), "shape {m}x{k}x{n}");
let mut worst = 0f32;
for (g, c) in gpu.iter().zip(&cpu) {
worst = worst.max((g - c).abs());
}
let scale = cpu.iter().fold(0f32, |a, v| a.max(v.abs())).max(1e-6);
assert!(
worst / scale < 1e-4,
"shape {m}x{k}x{n}: GPU and CPU disagree, worst {worst} (relative {})",
worst / scale
);
}
}
#[test]
fn the_executor_cascade_agrees_with_the_forced_cpu_path() {
let (m, k, n) = (8usize, 64usize, 32usize);
let x = fill(m * k, 0.3);
let w = fill(n * k, 0.9);
let input = MatmulNtInput { x: &x, w: &w, m, k, n };
let cascade = Executor::new().run(&MatmulNt, &input);
let cpu_only = Executor::cpu_only().run(&MatmulNt, &input);
assert_eq!(cascade.len(), cpu_only.len());
let worst = cascade
.iter()
.zip(&cpu_only)
.fold(0f32, |acc, (a, b)| acc.max((a - b).abs()));
let scale = cpu_only.iter().fold(0f32, |a, v| a.max(v.abs())).max(1e-6);
assert!(worst / scale < 1e-4, "cascade disagreed with CPU: worst {worst}");
}
}
#[cfg(test)]
mod limit_tests {
use super::*;
#[test]
fn an_oversized_weight_falls_back_instead_of_panicking() {
let Ok(ctx) = crate::GpuContext::new() else {
eprintln!("skipped: no GPU on this machine");
return;
};
let limit = ctx.device().limits().max_storage_buffer_binding_size as usize;
let k = 960usize;
let n = limit / (k * std::mem::size_of::<f32>()) + 1;
let x = vec![0.0f32; k];
let w = vec![0.0f32; n * k];
let err = matmul_nt_gpu(&ctx, &x, &w, 1, k, n).expect_err("must refuse, not panic");
assert!(
matches!(err, GpuOpError::InvalidInput(ref m) if m.contains("max_storage_buffer_binding_size")),
"expected a binding-size refusal, got {err:?}"
);
let out = crate::Executor::new().run(&MatmulNt, &MatmulNtInput { x: &x, w: &w, m: 1, k, n });
assert_eq!(out.len(), n);
}
}