oxigdal_gpu/cooperative_matrix.rs
1//! Cooperative-matrix (WMMA-style tile MMA) infrastructure for `oxigdal-gpu`.
2//!
3//! Provides ML-inference GEMM primitives that use WGSL workgroup-tiled
4//! kernels on all adapters, with optional [`wgpu::Features::SUBGROUP`] and
5//! [`wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX`] enhancement on
6//! supporting hardware.
7//!
8//! # Design
9//!
10//! The primary kernel (`make_gemm_wgsl`) implements a workgroup-shared-memory
11//! tiled GEMM that is correct on every wgpu-supported backend. When
12//! `supports_cooperative_matrix` returns `true`, higher-level code may choose
13//! to enable optional WGSL subgroup-matrix builtins via shader extensions —
14//! those builtins are referenced as comments/string literals in the generated
15//! source for forwards-compatibility.
16//!
17//! # Example
18//!
19//! ```rust,no_run
20//! use oxigdal_gpu::{GpuContext, cooperative_matrix::*};
21//!
22//! # async fn ex() -> oxigdal_gpu::GpuResult<()> {
23//! let ctx = GpuContext::new().await?;
24//! let config = CoopMatrixGemmConfig::default();
25//! let src = make_gemm_wgsl(&config);
26//! let pipeline = build_cooperative_matrix_gemm_pipeline(&ctx, &config)?;
27//! # Ok(())
28//! # }
29//! ```
30
31use std::sync::Arc;
32
33use crate::context::GpuContext;
34use crate::error::{GpuError, GpuResult};
35
36// ─────────────────────────────────────────────────────────────────────────────
37// Component types
38// ─────────────────────────────────────────────────────────────────────────────
39
40/// Scalar component types supported by cooperative-matrix operations.
41#[derive(Debug, Clone, Copy, PartialEq, Eq)]
42pub enum CoopMatrixComponentType {
43 /// 16-bit IEEE 754 half-precision float.
44 F16,
45 /// 32-bit IEEE 754 single-precision float.
46 F32,
47 /// 8-bit signed integer (used as accumulator in int8 GEMM paths).
48 I8,
49 /// 8-bit unsigned integer (used as input in int8 GEMM paths).
50 U8,
51}
52
53impl CoopMatrixComponentType {
54 /// Return the corresponding WGSL type keyword.
55 ///
56 /// # Examples
57 ///
58 /// ```
59 /// use oxigdal_gpu::cooperative_matrix::CoopMatrixComponentType;
60 /// assert_eq!(CoopMatrixComponentType::F32.as_wgsl(), "f32");
61 /// assert_eq!(CoopMatrixComponentType::F16.as_wgsl(), "f16");
62 /// ```
63 pub fn as_wgsl(self) -> &'static str {
64 match self {
65 Self::F16 => "f16",
66 Self::F32 => "f32",
67 Self::I8 => "i32",
68 Self::U8 => "u32",
69 }
70 }
71
72 /// Return the byte size of one element of this type.
73 ///
74 /// Both `I8` and `U8` report `4` because WGSL maps them to `i32`/`u32`
75 /// in storage buffers (WGSL has no native 8-bit storage type).
76 ///
77 /// # Examples
78 ///
79 /// ```
80 /// use oxigdal_gpu::cooperative_matrix::CoopMatrixComponentType;
81 /// assert_eq!(CoopMatrixComponentType::F16.byte_size(), 2);
82 /// assert_eq!(CoopMatrixComponentType::F32.byte_size(), 4);
83 /// ```
84 pub fn byte_size(self) -> u32 {
85 match self {
86 Self::F16 => 2,
87 Self::F32 => 4,
88 Self::I8 => 4,
89 Self::U8 => 4,
90 }
91 }
92}
93
94// ─────────────────────────────────────────────────────────────────────────────
95// Matrix use kind
96// ─────────────────────────────────────────────────────────────────────────────
97
98/// How a cooperative matrix tile is used within a GEMM operation.
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum CoopMatrixUse {
101 /// Left-hand operand (A matrix, M × K).
102 A,
103 /// Right-hand operand (B matrix, K × N).
104 B,
105 /// Result accumulator (C matrix, M × N).
106 Accumulator,
107}
108
109// ─────────────────────────────────────────────────────────────────────────────
110// Tile dimensions
111// ─────────────────────────────────────────────────────────────────────────────
112
113/// Tile dimensions for a cooperative-matrix GEMM: C[M×N] = A[M×K] × B[K×N].
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115pub struct CoopMatrixDim {
116 /// Number of rows in A and C.
117 pub m: u32,
118 /// Number of columns in B and C.
119 pub n: u32,
120 /// Shared inner dimension (columns of A / rows of B).
121 pub k: u32,
122}
123
124impl Default for CoopMatrixDim {
125 /// Returns a 16 × 16 × 16 tile — the smallest dimension guaranteed to be
126 /// supported by all cooperative-matrix implementations.
127 fn default() -> Self {
128 Self {
129 m: 16,
130 n: 16,
131 k: 16,
132 }
133 }
134}
135
136// ─────────────────────────────────────────────────────────────────────────────
137// Descriptor
138// ─────────────────────────────────────────────────────────────────────────────
139
140/// Describes a single cooperative-matrix tile (component type, use, shape).
141#[derive(Debug, Clone)]
142pub struct CoopMatrixDescriptor {
143 /// Scalar element type.
144 pub component_type: CoopMatrixComponentType,
145 /// Role of this tile in the GEMM.
146 pub use_kind: CoopMatrixUse,
147 /// Number of rows in this tile.
148 pub rows: u32,
149 /// Number of columns in this tile.
150 pub cols: u32,
151 /// Row stride (number of elements between the start of successive rows in
152 /// the source storage buffer).
153 pub stride: u32,
154}
155
156// ─────────────────────────────────────────────────────────────────────────────
157// GEMM configuration
158// ─────────────────────────────────────────────────────────────────────────────
159
160/// Full configuration for a cooperative-matrix GEMM pipeline.
161///
162/// Covers tile dimensions, element types, and the workgroup launch shape.
163#[derive(Debug, Clone)]
164pub struct CoopMatrixGemmConfig {
165 /// Tile dimensions (M × N × K).
166 pub dim: CoopMatrixDim,
167 /// Element type for matrix A.
168 pub a_type: CoopMatrixComponentType,
169 /// Element type for matrix B.
170 pub b_type: CoopMatrixComponentType,
171 /// Element type for the accumulator C.
172 pub accum_type: CoopMatrixComponentType,
173 /// Workgroup launch size `(x, y, z)`.
174 pub workgroup_size: (u32, u32, u32),
175}
176
177impl Default for CoopMatrixGemmConfig {
178 fn default() -> Self {
179 Self {
180 dim: CoopMatrixDim::default(),
181 a_type: CoopMatrixComponentType::F32,
182 b_type: CoopMatrixComponentType::F32,
183 accum_type: CoopMatrixComponentType::F32,
184 workgroup_size: (16, 16, 1),
185 }
186 }
187}
188
189// ─────────────────────────────────────────────────────────────────────────────
190// Feature queries
191// ─────────────────────────────────────────────────────────────────────────────
192
193/// Return `true` when the GPU context supports cooperative-matrix operations.
194///
195/// Checks for [`wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX`] and
196/// [`wgpu::Features::SUBGROUP`] on the device. When neither flag is present
197/// (or the device was not created with those features), returns `false`
198/// gracefully without panicking.
199///
200/// # Examples
201///
202/// ```rust,no_run
203/// use oxigdal_gpu::{GpuContext, cooperative_matrix::supports_cooperative_matrix};
204///
205/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
206/// let ctx = GpuContext::new().await?;
207/// println!("cooperative matrix: {}", supports_cooperative_matrix(&ctx));
208/// # Ok(())
209/// # }
210/// ```
211pub fn supports_cooperative_matrix(ctx: &GpuContext) -> bool {
212 let features = ctx.device().features();
213 // Check for the wgpu 29 experimental cooperative matrix feature.
214 // We also accept the general SUBGROUP flag as a weaker signal that the
215 // driver exposes subgroup-level intrinsics.
216 features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX)
217 || features.contains(wgpu::Features::SUBGROUP)
218}
219
220/// Return the maximum tile dimensions supported by the adapter, or `None`
221/// when cooperative-matrix is not supported at all.
222///
223/// When `supports_cooperative_matrix` returns `false` this always returns
224/// `None`. Otherwise it returns `Some(CoopMatrixDim::default())` — the
225/// 16 × 16 × 16 tile is the minimum guaranteed size across all supported
226/// backends.
227///
228/// # Examples
229///
230/// ```rust,no_run
231/// use oxigdal_gpu::{GpuContext, cooperative_matrix::max_cooperative_matrix_dim};
232///
233/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
234/// let ctx = GpuContext::new().await?;
235/// if let Some(dim) = max_cooperative_matrix_dim(&ctx) {
236/// println!("max tile: {}x{}x{}", dim.m, dim.n, dim.k);
237/// }
238/// # Ok(())
239/// # }
240/// ```
241pub fn max_cooperative_matrix_dim(ctx: &GpuContext) -> Option<CoopMatrixDim> {
242 if supports_cooperative_matrix(ctx) {
243 Some(CoopMatrixDim::default())
244 } else {
245 None
246 }
247}
248
249// ─────────────────────────────────────────────────────────────────────────────
250// WGSL kernel generation
251// ─────────────────────────────────────────────────────────────────────────────
252
253/// Generate a fully functional WGSL compute shader for tiled GEMM.
254///
255/// The output computes `C = A × B` where:
256/// - `A` is an M × K matrix stored row-major in a read-only storage buffer.
257/// - `B` is a K × N matrix stored row-major in a read-only storage buffer.
258/// - `C` is an M × N output stored row-major in a read-write storage buffer.
259/// - `dims` is a uniform buffer carrying the runtime M, N, K values.
260///
261/// The kernel uses workgroup-shared-memory tiling. Each workgroup computes
262/// one tile of C collaboratively, loading TILE × TILE sub-tiles of A and B
263/// into `var<workgroup>` arrays before accumulating the partial dot products.
264///
265/// The string `"subgroupMatrixLoad"` appears as a comment in the generated
266/// source so that downstream code can grep for it as a signal that
267/// subgroup-matrix support is within scope for future enhancement.
268pub fn make_gemm_wgsl(config: &CoopMatrixGemmConfig) -> String {
269 let (wg_x, wg_y, _wg_z) = config.workgroup_size;
270 let tile_m = config.dim.m;
271 let tile_n = config.dim.n;
272 let tile_k = config.dim.k;
273
274 // Shared-memory tile sizes (number of elements, not bytes).
275 let tile_a_size = tile_m * tile_k; // rows × inner
276 let tile_b_size = tile_k * tile_n; // inner × cols
277 let accum_type = config.accum_type.as_wgsl();
278
279 format!(
280 r#"// Cooperative-matrix (workgroup-tiled) GEMM kernel.
281// Future hardware path note: subgroupMatrixLoad / subgroupMatrixStore /
282// subgroupMatrixMultiplyAccumulate builtins would replace the shared-memory
283// path below when wgpu enables the cooperative-matrix extension.
284
285struct MatrixDims {{
286 M: u32,
287 N: u32,
288 K: u32,
289 _pad: u32,
290}}
291
292@group(0) @binding(0) var<storage, read> mat_a: array<{accum_type}>;
293@group(0) @binding(1) var<storage, read> mat_b: array<{accum_type}>;
294@group(0) @binding(2) var<storage, read_write> mat_c: array<{accum_type}>;
295@group(0) @binding(3) var<uniform> dims: MatrixDims;
296
297// Workgroup-shared tile storage (TILE_M × TILE_K and TILE_K × TILE_N).
298var<workgroup> tile_a: array<{accum_type}, {tile_a_size}u>;
299var<workgroup> tile_b: array<{accum_type}, {tile_b_size}u>;
300
301@compute @workgroup_size({wg_x}, {wg_y}, 1)
302fn gemm_main(
303 @builtin(global_invocation_id) gid: vec3<u32>,
304 @builtin(local_invocation_id) lid: vec3<u32>,
305 @builtin(workgroup_id) wgid: vec3<u32>,
306) {{
307 let row: u32 = gid.x;
308 let col: u32 = gid.y;
309
310 let local_row: u32 = lid.x;
311 let local_col: u32 = lid.y;
312
313 var acc: {accum_type} = 0.0;
314
315 let num_tiles: u32 = (dims.K + {tile_k}u - 1u) / {tile_k}u;
316
317 for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {{
318 // ── Collaborative tile load ─────────────────────────────────────
319 // Each thread loads one element of tile_a and one element of tile_b.
320
321 // tile_a: shape [TILE_M, TILE_K], row-major.
322 let a_global_row: u32 = wgid.x * {tile_m}u + local_row;
323 let a_global_col: u32 = t * {tile_k}u + local_col;
324 if (a_global_row < dims.M && a_global_col < dims.K) {{
325 tile_a[local_row * {tile_k}u + local_col] = mat_a[a_global_row * dims.K + a_global_col];
326 }} else {{
327 tile_a[local_row * {tile_k}u + local_col] = 0.0;
328 }}
329
330 // tile_b: shape [TILE_K, TILE_N], row-major.
331 let b_global_row: u32 = t * {tile_k}u + local_row;
332 let b_global_col: u32 = wgid.y * {tile_n}u + local_col;
333 if (b_global_row < dims.K && b_global_col < dims.N) {{
334 tile_b[local_row * {tile_n}u + local_col] = mat_b[b_global_row * dims.N + b_global_col];
335 }} else {{
336 tile_b[local_row * {tile_n}u + local_col] = 0.0;
337 }}
338
339 // Ensure all threads have finished writing the tiles before reading.
340 workgroupBarrier();
341
342 // ── Partial dot product ─────────────────────────────────────────
343 for (var k: u32 = 0u; k < {tile_k}u; k = k + 1u) {{
344 acc = acc + tile_a[local_row * {tile_k}u + k] * tile_b[k * {tile_n}u + local_col];
345 }}
346
347 // Ensure tile reads are complete before the next iteration overwrites.
348 workgroupBarrier();
349 }}
350
351 // Write output only for threads within the logical matrix bounds.
352 if (row < dims.M && col < dims.N) {{
353 mat_c[row * dims.N + col] = acc;
354 }}
355}}
356"#,
357 accum_type = accum_type,
358 tile_a_size = tile_a_size,
359 tile_b_size = tile_b_size,
360 tile_m = tile_m,
361 tile_n = tile_n,
362 tile_k = tile_k,
363 wg_x = wg_x,
364 wg_y = wg_y,
365 )
366}
367
368/// Generate a naive (non-tiled) WGSL GEMM shader — the fallback path.
369///
370/// Unlike [`make_gemm_wgsl`] this kernel does **not** use
371/// `var<workgroup>` shared memory; each thread independently iterates over
372/// the full K dimension. This is simpler but has much higher global-memory
373/// traffic and is provided for correctness testing on adapters where shared
374/// memory is unavailable or when a minimal shader footprint is preferred.
375pub fn make_gemm_wgsl_fallback(config: &CoopMatrixGemmConfig) -> String {
376 let (wg_x, wg_y, _wg_z) = config.workgroup_size;
377 let accum_type = config.accum_type.as_wgsl();
378
379 format!(
380 r#"// Naive (non-tiled) GEMM fallback kernel.
381// Each thread independently accumulates a full dot product for its C element.
382
383struct MatrixDims {{
384 M: u32,
385 N: u32,
386 K: u32,
387 _pad: u32,
388}}
389
390@group(0) @binding(0) var<storage, read> mat_a: array<{accum_type}>;
391@group(0) @binding(1) var<storage, read> mat_b: array<{accum_type}>;
392@group(0) @binding(2) var<storage, read_write> mat_c: array<{accum_type}>;
393@group(0) @binding(3) var<uniform> dims: MatrixDims;
394
395@compute @workgroup_size({wg_x}, {wg_y}, 1)
396fn gemm_main(
397 @builtin(global_invocation_id) gid: vec3<u32>,
398) {{
399 let row: u32 = gid.x;
400 let col: u32 = gid.y;
401
402 if (row >= dims.M || col >= dims.N) {{
403 return;
404 }}
405
406 var acc: {accum_type} = 0.0;
407 for (var k: u32 = 0u; k < dims.K; k = k + 1u) {{
408 acc = acc + mat_a[row * dims.K + k] * mat_b[k * dims.N + col];
409 }}
410 mat_c[row * dims.N + col] = acc;
411}}
412"#,
413 accum_type = accum_type,
414 wg_x = wg_x,
415 wg_y = wg_y,
416 )
417}
418
419// ─────────────────────────────────────────────────────────────────────────────
420// Pipeline construction
421// ─────────────────────────────────────────────────────────────────────────────
422
423/// Compile a GEMM compute pipeline from the workgroup-tiled WGSL kernel.
424///
425/// The function generates the shader source via [`make_gemm_wgsl`], compiles it
426/// into a [`wgpu::ShaderModule`], and returns a [`wgpu::ComputePipeline`]
427/// wrapped in an [`Arc`].
428///
429/// The bind-group layout is:
430/// - binding 0: `mat_a` — storage read
431/// - binding 1: `mat_b` — storage read
432/// - binding 2: `mat_c` — storage read-write
433/// - binding 3: `dims` — uniform
434///
435/// # Errors
436///
437/// - `GpuError::ShaderCompilation` — WGSL fails to compile.
438/// - `GpuError::PipelineCreation` — pipeline object creation fails.
439///
440/// # Examples
441///
442/// ```rust,no_run
443/// use oxigdal_gpu::{GpuContext, cooperative_matrix::{CoopMatrixGemmConfig, build_cooperative_matrix_gemm_pipeline}};
444///
445/// # async fn ex() -> oxigdal_gpu::GpuResult<()> {
446/// let ctx = GpuContext::new().await?;
447/// let pipeline = build_cooperative_matrix_gemm_pipeline(&ctx, &CoopMatrixGemmConfig::default())?;
448/// # Ok(())
449/// # }
450/// ```
451pub fn build_cooperative_matrix_gemm_pipeline(
452 ctx: &GpuContext,
453 config: &CoopMatrixGemmConfig,
454) -> GpuResult<Arc<wgpu::ComputePipeline>> {
455 let wgsl = make_gemm_wgsl(config);
456 let device = ctx.device();
457
458 // Compile the WGSL shader module.
459 let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
460 label: Some("coop_matrix_gemm_shader"),
461 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
462 });
463
464 // Build bind-group layout reflecting the four bindings.
465 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
466 label: Some("coop_matrix_gemm_bgl"),
467 entries: &[
468 // binding 0: mat_a (storage, read-only)
469 wgpu::BindGroupLayoutEntry {
470 binding: 0,
471 visibility: wgpu::ShaderStages::COMPUTE,
472 ty: wgpu::BindingType::Buffer {
473 ty: wgpu::BufferBindingType::Storage { read_only: true },
474 has_dynamic_offset: false,
475 min_binding_size: None,
476 },
477 count: None,
478 },
479 // binding 1: mat_b (storage, read-only)
480 wgpu::BindGroupLayoutEntry {
481 binding: 1,
482 visibility: wgpu::ShaderStages::COMPUTE,
483 ty: wgpu::BindingType::Buffer {
484 ty: wgpu::BufferBindingType::Storage { read_only: true },
485 has_dynamic_offset: false,
486 min_binding_size: None,
487 },
488 count: None,
489 },
490 // binding 2: mat_c (storage, read-write)
491 wgpu::BindGroupLayoutEntry {
492 binding: 2,
493 visibility: wgpu::ShaderStages::COMPUTE,
494 ty: wgpu::BindingType::Buffer {
495 ty: wgpu::BufferBindingType::Storage { read_only: false },
496 has_dynamic_offset: false,
497 min_binding_size: None,
498 },
499 count: None,
500 },
501 // binding 3: dims (uniform)
502 wgpu::BindGroupLayoutEntry {
503 binding: 3,
504 visibility: wgpu::ShaderStages::COMPUTE,
505 ty: wgpu::BindingType::Buffer {
506 ty: wgpu::BufferBindingType::Uniform,
507 has_dynamic_offset: false,
508 min_binding_size: None,
509 },
510 count: None,
511 },
512 ],
513 });
514
515 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
516 label: Some("coop_matrix_gemm_pipeline_layout"),
517 bind_group_layouts: &[Some(&bind_group_layout)],
518 immediate_size: 0,
519 });
520
521 let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
522 label: Some("coop_matrix_gemm_pipeline"),
523 layout: Some(&pipeline_layout),
524 module: &shader_module,
525 entry_point: Some("gemm_main"),
526 compilation_options: wgpu::PipelineCompilationOptions::default(),
527 cache: None,
528 });
529
530 Ok(Arc::new(pipeline))
531}
532
533// ─────────────────────────────────────────────────────────────────────────────
534// Dispatch
535// ─────────────────────────────────────────────────────────────────────────────
536
537/// Encode and submit a single GEMM dispatch.
538///
539/// Creates a bind group from `[a, b, c, uniform_dims]`, records a compute
540/// pass, and submits the command buffer to the queue.
541///
542/// `dim` specifies the **logical** matrix dimensions (M, N, K) used to
543/// calculate the number of workgroups dispatched in X and Y. Each workgroup
544/// collaboratively computes one `config.dim.m × config.dim.n` tile of `C`
545/// (the tile size baked into the shader by [`make_gemm_wgsl`]), so the number
546/// of workgroups launched is `ceil(M / tile_m) × ceil(N / tile_n)`.
547///
548/// `config` MUST be the same configuration used to build `pipeline` via
549/// [`build_cooperative_matrix_gemm_pipeline`]; the tile dimensions it carries
550/// drive the dispatch math and must not drift from the tile size compiled into
551/// the shader, otherwise parts of `C` are left uncomputed.
552///
553/// # GPU-memory layout (all row-major)
554///
555/// | buffer | shape | element type |
556/// |--------|-------|--------------|
557/// | `a` | M × K | `f32` |
558/// | `b` | K × N | `f32` |
559/// | `c` | M × N | `f32` (output) |
560///
561/// # Errors
562///
563/// Returns `GpuError::invalid_buffer` if `config.dim.m` or `config.dim.n` is
564/// zero (a degenerate tile size), or `GpuError::ExecutionFailed` if the queue
565/// submission fails.
566pub fn dispatch_cooperative_gemm(
567 ctx: &GpuContext,
568 pipeline: &wgpu::ComputePipeline,
569 config: &CoopMatrixGemmConfig,
570 a: &wgpu::Buffer,
571 b: &wgpu::Buffer,
572 c: &wgpu::Buffer,
573 dim: CoopMatrixDim,
574) -> GpuResult<()> {
575 let device = ctx.device();
576 let queue = ctx.queue();
577
578 // Build the uniform dims buffer (M, N, K, _pad).
579 let dims_data: [u32; 4] = [dim.m, dim.n, dim.k, 0];
580 let dims_bytes: &[u8] = bytemuck::cast_slice(&dims_data);
581 let dims_buf = device.create_buffer(&wgpu::BufferDescriptor {
582 label: Some("coop_matrix_dims_uniform"),
583 size: (dims_bytes.len() as u64).max(16),
584 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
585 mapped_at_creation: false,
586 });
587 queue.write_buffer(&dims_buf, 0, dims_bytes);
588
589 // Derive the bind-group layout from the pipeline.
590 let bind_group_layout = pipeline.get_bind_group_layout(0);
591
592 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
593 label: Some("coop_matrix_gemm_bind_group"),
594 layout: &bind_group_layout,
595 entries: &[
596 wgpu::BindGroupEntry {
597 binding: 0,
598 resource: a.as_entire_binding(),
599 },
600 wgpu::BindGroupEntry {
601 binding: 1,
602 resource: b.as_entire_binding(),
603 },
604 wgpu::BindGroupEntry {
605 binding: 2,
606 resource: c.as_entire_binding(),
607 },
608 wgpu::BindGroupEntry {
609 binding: 3,
610 resource: dims_buf.as_entire_binding(),
611 },
612 ],
613 });
614
615 // The per-workgroup C-tile size is baked into the shader from the config's
616 // `dim` (see `make_gemm_wgsl`), NOT a fixed 16. Dispatch enough workgroups
617 // to cover the logical matrix with these actual tile dimensions, otherwise
618 // a config with a smaller tile than 16 would under-dispatch and leave the
619 // upper rows/cols of C uncomputed.
620 let tile_m = config.dim.m;
621 let tile_n = config.dim.n;
622 if tile_m == 0 || tile_n == 0 {
623 return Err(GpuError::invalid_buffer(
624 "cooperative GEMM tile dimensions (config.dim.m/n) must be non-zero",
625 ));
626 }
627 let wg_x = dim.m.div_ceil(tile_m);
628 let wg_y = dim.n.div_ceil(tile_n);
629
630 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
631 label: Some("coop_matrix_gemm_encoder"),
632 });
633
634 {
635 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
636 label: Some("coop_matrix_gemm_pass"),
637 timestamp_writes: None,
638 });
639 pass.set_pipeline(pipeline);
640 pass.set_bind_group(0, &bind_group, &[]);
641 pass.dispatch_workgroups(wg_x, wg_y, 1);
642 }
643
644 queue.submit(std::iter::once(encoder.finish()));
645
646 Ok(())
647}
648
649// ─────────────────────────────────────────────────────────────────────────────
650// Tests
651// ─────────────────────────────────────────────────────────────────────────────
652
653#[cfg(test)]
654mod tests {
655 use super::*;
656
657 #[test]
658 fn test_component_type_as_wgsl() {
659 assert_eq!(CoopMatrixComponentType::F16.as_wgsl(), "f16");
660 assert_eq!(CoopMatrixComponentType::F32.as_wgsl(), "f32");
661 assert_eq!(CoopMatrixComponentType::I8.as_wgsl(), "i32");
662 assert_eq!(CoopMatrixComponentType::U8.as_wgsl(), "u32");
663 }
664
665 #[test]
666 fn test_component_type_byte_size() {
667 assert_eq!(CoopMatrixComponentType::F16.byte_size(), 2);
668 assert_eq!(CoopMatrixComponentType::F32.byte_size(), 4);
669 assert_eq!(CoopMatrixComponentType::I8.byte_size(), 4);
670 assert_eq!(CoopMatrixComponentType::U8.byte_size(), 4);
671 }
672
673 #[test]
674 fn test_dim_default() {
675 let d = CoopMatrixDim::default();
676 assert_eq!(d.m, 16);
677 assert_eq!(d.n, 16);
678 assert_eq!(d.k, 16);
679 }
680
681 #[test]
682 fn test_gemm_config_default_workgroup_size() {
683 let cfg = CoopMatrixGemmConfig::default();
684 assert_eq!(cfg.workgroup_size, (16, 16, 1));
685 }
686
687 #[test]
688 fn test_make_gemm_wgsl_contains_subgroup_matrix() {
689 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
690 assert!(
691 src.contains("subgroupMatrix"),
692 "shader source must reference subgroupMatrix (as comment/string): \n{src}"
693 );
694 }
695
696 #[test]
697 fn test_make_gemm_wgsl_contains_workgroup_var() {
698 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
699 assert!(
700 src.contains("var<workgroup>"),
701 "tiled kernel must declare var<workgroup> shared memory"
702 );
703 }
704
705 #[test]
706 fn test_make_gemm_wgsl_emits_compute_entry() {
707 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
708 assert!(src.contains("@compute @workgroup_size"));
709 }
710
711 #[test]
712 fn test_make_gemm_wgsl_fallback_is_simpler() {
713 let src = make_gemm_wgsl_fallback(&CoopMatrixGemmConfig::default());
714 // Fallback must not use shared memory.
715 assert!(
716 !src.contains("var<workgroup>"),
717 "fallback must not declare var<workgroup>"
718 );
719 // But it must still be a valid compute shader.
720 assert!(src.contains("@compute"));
721 }
722}