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::{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}