1use crate::context::GpuContext;
50use crate::executor::{ComputeOp, GpuOpError};
51use wgpu::util::DeviceExt;
52
53const WORKGROUP_X: u32 = 16;
55const WORKGROUP_Y: u32 = 16;
56
57pub struct MatmulNtInput<'a> {
63 pub x: &'a [f32],
64 pub w: &'a [f32],
65 pub m: usize,
66 pub k: usize,
67 pub n: usize,
68}
69
70pub struct MatmulNt;
72
73impl ComputeOp for MatmulNt {
74 type Input<'a> = MatmulNtInput<'a>;
75 type Output = Vec<f32>;
76
77 fn compute_gpu(
78 &self,
79 ctx: &GpuContext,
80 input: &Self::Input<'_>,
81 ) -> Result<Self::Output, GpuOpError> {
82 matmul_nt_gpu(ctx, input.x, input.w, input.m, input.k, input.n)
83 }
84
85 fn compute_cpu(&self, input: &Self::Input<'_>) -> Self::Output {
86 matmul_nt_cpu(input.x, input.w, input.m, input.k, input.n)
87 }
88}
89
90#[must_use]
98pub fn matmul_nt_cpu(x: &[f32], w: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
99 let mut y = vec![0f32; m * n];
100 if x.len() < m * k || w.len() < n * k {
101 return y;
102 }
103 for i in 0..m {
104 let x_row = &x[i * k..i * k + k];
105 for j in 0..n {
106 let w_row = &w[j * k..j * k + k];
107 let mut acc = 0f32;
108 for t in 0..k {
109 acc += x_row[t] * w_row[t];
110 }
111 y[i * n + j] = acc;
112 }
113 }
114 y
115}
116
117pub fn matmul_nt_gpu(
124 ctx: &GpuContext,
125 x: &[f32],
126 w: &[f32],
127 m: usize,
128 k: usize,
129 n: usize,
130) -> Result<Vec<f32>, GpuOpError> {
131 if x.len() != m * k {
132 return Err(GpuOpError::InvalidInput(format!(
133 "x has {} elements, expected m*k = {}*{} = {}",
134 x.len(),
135 m,
136 k,
137 m * k
138 )));
139 }
140 if w.len() != n * k {
141 return Err(GpuOpError::InvalidInput(format!(
142 "w has {} elements, expected n*k = {}*{} = {}",
143 w.len(),
144 n,
145 k,
146 n * k
147 )));
148 }
149 if m == 0 || n == 0 || k == 0 {
152 return Ok(vec![0f32; m * n]);
153 }
154
155 let device = ctx.device();
156 let queue = ctx.queue();
157 let out_bytes = (m * n * std::mem::size_of::<f32>()) as wgpu::BufferAddress;
158
159 let limit = device.limits().max_storage_buffer_binding_size;
169 let biggest = [
170 (m * k * std::mem::size_of::<f32>()) as u64,
171 (n * k * std::mem::size_of::<f32>()) as u64,
172 out_bytes,
173 ]
174 .into_iter()
175 .max()
176 .unwrap_or(0);
177 if biggest > limit {
178 return Err(GpuOpError::InvalidInput(format!(
179 "matmul {m}x{k}x{n} needs a {biggest}-byte storage binding but this \
180 adapter's max_storage_buffer_binding_size is {limit}; falling back to CPU"
181 )));
182 }
183
184 let x_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
185 label: Some("matmul_nt.x"),
186 contents: bytemuck::cast_slice(x),
187 usage: wgpu::BufferUsages::STORAGE,
188 });
189 let w_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
190 label: Some("matmul_nt.w"),
191 contents: bytemuck::cast_slice(w),
192 usage: wgpu::BufferUsages::STORAGE,
193 });
194 let dims: [u32; 4] = [m as u32, k as u32, n as u32, 0];
197 let dims_buf = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
198 label: Some("matmul_nt.dims"),
199 contents: bytemuck::cast_slice(&dims),
200 usage: wgpu::BufferUsages::UNIFORM,
201 });
202 let y_buf = device.create_buffer(&wgpu::BufferDescriptor {
203 label: Some("matmul_nt.y"),
204 size: out_bytes,
205 usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
206 mapped_at_creation: false,
207 });
208 let staging = device.create_buffer(&wgpu::BufferDescriptor {
209 label: Some("matmul_nt.staging"),
210 size: out_bytes,
211 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
212 mapped_at_creation: false,
213 });
214
215 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
216 label: Some("matmul_nt.wgsl"),
217 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/matmul_nt.wgsl").into()),
218 });
219 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
220 label: Some("matmul_nt.pipeline"),
221 layout: None,
222 module: &shader,
223 entry_point: Some("main"),
224 compilation_options: wgpu::PipelineCompilationOptions::default(),
225 cache: None,
226 });
227
228 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
229 label: Some("matmul_nt.bind_group"),
230 layout: &pipeline.get_bind_group_layout(0),
231 entries: &[
232 wgpu::BindGroupEntry { binding: 0, resource: x_buf.as_entire_binding() },
233 wgpu::BindGroupEntry { binding: 1, resource: w_buf.as_entire_binding() },
234 wgpu::BindGroupEntry { binding: 2, resource: y_buf.as_entire_binding() },
235 wgpu::BindGroupEntry { binding: 3, resource: dims_buf.as_entire_binding() },
236 ],
237 });
238
239 let mut encoder =
240 device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("matmul_nt") });
241 {
242 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
243 label: Some("matmul_nt.pass"),
244 timestamp_writes: None,
245 });
246 pass.set_pipeline(&pipeline);
247 pass.set_bind_group(0, &bind_group, &[]);
248 pass.dispatch_workgroups(
249 (m as u32).div_ceil(WORKGROUP_X),
250 (n as u32).div_ceil(WORKGROUP_Y),
251 1,
252 );
253 }
254 encoder.copy_buffer_to_buffer(&y_buf, 0, &staging, 0, out_bytes);
255 queue.submit(Some(encoder.finish()));
256
257 let slice = staging.slice(..);
258 let (tx, rx) = std::sync::mpsc::channel();
259 slice.map_async(wgpu::MapMode::Read, move |res| {
260 let _ = tx.send(res);
261 });
262 device
263 .poll(wgpu::PollType::wait_indefinitely())
264 .map_err(|e| GpuOpError::Backend(format!("device poll failed: {e:?}")))?;
265 rx.recv()
266 .map_err(|e| GpuOpError::Backend(format!("map callback dropped: {e}")))?
267 .map_err(|e| GpuOpError::Backend(format!("buffer map failed: {e:?}")))?;
268
269 let data = slice
270 .get_mapped_range()
271 .map_err(|e| GpuOpError::Backend(format!("get_mapped_range failed: {e:?}")))?;
272 let result: Vec<f32> = bytemuck::cast_slice(&data).to_vec();
273 drop(data);
274 staging.unmap();
275
276 Ok(result)
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282 use crate::Executor;
283
284 fn fill(n: usize, seed: f32) -> Vec<f32> {
287 (0..n).map(|i| (i as f32 * 0.37 + seed).sin()).collect()
288 }
289
290 #[test]
291 fn cpu_matches_a_hand_computed_product() {
292 let y = matmul_nt_cpu(&[1.0, 2.0, 3.0, 4.0], &[5.0, 6.0, 7.0, 8.0], 2, 2, 2);
296 assert_eq!(y, vec![17.0, 23.0, 39.0, 53.0]);
297 }
298
299 #[test]
302 fn cpu_handles_non_square_shapes() {
303 let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
305 let w: Vec<f32> = (1..=12).map(|v| v as f32).collect();
306 let y = matmul_nt_cpu(&x, &w, 2, 3, 4);
307 assert_eq!(y.len(), 8);
308 assert_eq!(y[0], 14.0);
310 assert_eq!(y[7], 167.0);
311 }
312
313 #[test]
314 fn gpu_rejects_ragged_inputs_instead_of_guessing() {
315 let Ok(ctx) = crate::GpuContext::new() else {
316 eprintln!("skipped: no GPU on this machine");
317 return;
318 };
319 let err = matmul_nt_gpu(&ctx, &[1.0, 2.0], &[1.0, 2.0], 2, 2, 1).unwrap_err();
320 assert!(matches!(err, GpuOpError::InvalidInput(_)), "got {err:?}");
321 }
322
323 #[test]
330 fn gpu_matches_cpu_at_real_transformer_shapes() {
331 let Ok(ctx) = crate::GpuContext::new() else {
332 eprintln!("skipped: no GPU on this machine");
333 return;
334 };
335 for &(m, k, n) in &[(33usize, 960usize, 960usize), (1, 960, 2560), (33, 2560, 960), (1, 64, 64)] {
339 let x = fill(m * k, 0.1);
340 let w = fill(n * k, 0.7);
341 let gpu = matmul_nt_gpu(&ctx, &x, &w, m, k, n).expect("gpu matmul");
342 let cpu = matmul_nt_cpu(&x, &w, m, k, n);
343 assert_eq!(gpu.len(), cpu.len(), "shape {m}x{k}x{n}");
344
345 let mut worst = 0f32;
346 for (g, c) in gpu.iter().zip(&cpu) {
347 worst = worst.max((g - c).abs());
348 }
349 let scale = cpu.iter().fold(0f32, |a, v| a.max(v.abs())).max(1e-6);
350 assert!(
351 worst / scale < 1e-4,
352 "shape {m}x{k}x{n}: GPU and CPU disagree, worst {worst} (relative {})",
353 worst / scale
354 );
355 }
356 }
357
358 #[test]
361 fn the_executor_cascade_agrees_with_the_forced_cpu_path() {
362 let (m, k, n) = (8usize, 64usize, 32usize);
363 let x = fill(m * k, 0.3);
364 let w = fill(n * k, 0.9);
365 let input = MatmulNtInput { x: &x, w: &w, m, k, n };
366
367 let cascade = Executor::new().run(&MatmulNt, &input);
368 let cpu_only = Executor::cpu_only().run(&MatmulNt, &input);
369 assert_eq!(cascade.len(), cpu_only.len());
370 let worst = cascade
371 .iter()
372 .zip(&cpu_only)
373 .fold(0f32, |acc, (a, b)| acc.max((a - b).abs()));
374 let scale = cpu_only.iter().fold(0f32, |a, v| a.max(v.abs())).max(1e-6);
375 assert!(worst / scale < 1e-4, "cascade disagreed with CPU: worst {worst}");
376 }
377}
378
379#[cfg(test)]
380mod limit_tests {
381 use super::*;
382
383 #[test]
391 fn an_oversized_weight_falls_back_instead_of_panicking() {
392 let Ok(ctx) = crate::GpuContext::new() else {
393 eprintln!("skipped: no GPU on this machine");
394 return;
395 };
396 let limit = ctx.device().limits().max_storage_buffer_binding_size as usize;
397 let k = 960usize;
398 let n = limit / (k * std::mem::size_of::<f32>()) + 1;
401
402 let x = vec![0.0f32; k];
407 let w = vec![0.0f32; n * k];
408 let err = matmul_nt_gpu(&ctx, &x, &w, 1, k, n).expect_err("must refuse, not panic");
409 assert!(
410 matches!(err, GpuOpError::InvalidInput(ref m) if m.contains("max_storage_buffer_binding_size")),
411 "expected a binding-size refusal, got {err:?}"
412 );
413
414 let out = crate::Executor::new().run(&MatmulNt, &MatmulNtInput { x: &x, w: &w, m: 1, k, n });
416 assert_eq!(out.len(), n);
417 }
418}