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::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/// covers a `(16 × 16)` tile (matching the kernel's `@workgroup_size`).
545///
546/// # GPU-memory layout (all row-major)
547///
548/// | buffer | shape | element type |
549/// |--------|-------|--------------|
550/// | `a` | M × K | `f32` |
551/// | `b` | K × N | `f32` |
552/// | `c` | M × N | `f32` (output) |
553///
554/// # Errors
555///
556/// Returns `GpuError::ExecutionFailed` if the queue submission fails.
557pub fn dispatch_cooperative_gemm(
558 ctx: &GpuContext,
559 pipeline: &wgpu::ComputePipeline,
560 a: &wgpu::Buffer,
561 b: &wgpu::Buffer,
562 c: &wgpu::Buffer,
563 dim: CoopMatrixDim,
564) -> GpuResult<()> {
565 let device = ctx.device();
566 let queue = ctx.queue();
567
568 // Build the uniform dims buffer (M, N, K, _pad).
569 let dims_data: [u32; 4] = [dim.m, dim.n, dim.k, 0];
570 let dims_bytes: &[u8] = bytemuck::cast_slice(&dims_data);
571 let dims_buf = device.create_buffer(&wgpu::BufferDescriptor {
572 label: Some("coop_matrix_dims_uniform"),
573 size: (dims_bytes.len() as u64).max(16),
574 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
575 mapped_at_creation: false,
576 });
577 queue.write_buffer(&dims_buf, 0, dims_bytes);
578
579 // Derive the bind-group layout from the pipeline.
580 let bind_group_layout = pipeline.get_bind_group_layout(0);
581
582 let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
583 label: Some("coop_matrix_gemm_bind_group"),
584 layout: &bind_group_layout,
585 entries: &[
586 wgpu::BindGroupEntry {
587 binding: 0,
588 resource: a.as_entire_binding(),
589 },
590 wgpu::BindGroupEntry {
591 binding: 1,
592 resource: b.as_entire_binding(),
593 },
594 wgpu::BindGroupEntry {
595 binding: 2,
596 resource: c.as_entire_binding(),
597 },
598 wgpu::BindGroupEntry {
599 binding: 3,
600 resource: dims_buf.as_entire_binding(),
601 },
602 ],
603 });
604
605 // Tile size matches the shader's @workgroup_size.
606 const TILE: u32 = 16;
607 let wg_x = dim.m.div_ceil(TILE);
608 let wg_y = dim.n.div_ceil(TILE);
609
610 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
611 label: Some("coop_matrix_gemm_encoder"),
612 });
613
614 {
615 let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
616 label: Some("coop_matrix_gemm_pass"),
617 timestamp_writes: None,
618 });
619 pass.set_pipeline(pipeline);
620 pass.set_bind_group(0, &bind_group, &[]);
621 pass.dispatch_workgroups(wg_x, wg_y, 1);
622 }
623
624 queue.submit(std::iter::once(encoder.finish()));
625
626 Ok(())
627}
628
629// ─────────────────────────────────────────────────────────────────────────────
630// Tests
631// ─────────────────────────────────────────────────────────────────────────────
632
633#[cfg(test)]
634mod tests {
635 use super::*;
636
637 #[test]
638 fn test_component_type_as_wgsl() {
639 assert_eq!(CoopMatrixComponentType::F16.as_wgsl(), "f16");
640 assert_eq!(CoopMatrixComponentType::F32.as_wgsl(), "f32");
641 assert_eq!(CoopMatrixComponentType::I8.as_wgsl(), "i32");
642 assert_eq!(CoopMatrixComponentType::U8.as_wgsl(), "u32");
643 }
644
645 #[test]
646 fn test_component_type_byte_size() {
647 assert_eq!(CoopMatrixComponentType::F16.byte_size(), 2);
648 assert_eq!(CoopMatrixComponentType::F32.byte_size(), 4);
649 assert_eq!(CoopMatrixComponentType::I8.byte_size(), 4);
650 assert_eq!(CoopMatrixComponentType::U8.byte_size(), 4);
651 }
652
653 #[test]
654 fn test_dim_default() {
655 let d = CoopMatrixDim::default();
656 assert_eq!(d.m, 16);
657 assert_eq!(d.n, 16);
658 assert_eq!(d.k, 16);
659 }
660
661 #[test]
662 fn test_gemm_config_default_workgroup_size() {
663 let cfg = CoopMatrixGemmConfig::default();
664 assert_eq!(cfg.workgroup_size, (16, 16, 1));
665 }
666
667 #[test]
668 fn test_make_gemm_wgsl_contains_subgroup_matrix() {
669 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
670 assert!(
671 src.contains("subgroupMatrix"),
672 "shader source must reference subgroupMatrix (as comment/string): \n{src}"
673 );
674 }
675
676 #[test]
677 fn test_make_gemm_wgsl_contains_workgroup_var() {
678 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
679 assert!(
680 src.contains("var<workgroup>"),
681 "tiled kernel must declare var<workgroup> shared memory"
682 );
683 }
684
685 #[test]
686 fn test_make_gemm_wgsl_emits_compute_entry() {
687 let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
688 assert!(src.contains("@compute @workgroup_size"));
689 }
690
691 #[test]
692 fn test_make_gemm_wgsl_fallback_is_simpler() {
693 let src = make_gemm_wgsl_fallback(&CoopMatrixGemmConfig::default());
694 // Fallback must not use shared memory.
695 assert!(
696 !src.contains("var<workgroup>"),
697 "fallback must not declare var<workgroup>"
698 );
699 // But it must still be a valid compute shader.
700 assert!(src.contains("@compute"));
701 }
702}