Skip to main content

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}