use std::sync::Arc;
use crate::context::GpuContext;
use crate::error::GpuResult;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoopMatrixComponentType {
F16,
F32,
I8,
U8,
}
impl CoopMatrixComponentType {
pub fn as_wgsl(self) -> &'static str {
match self {
Self::F16 => "f16",
Self::F32 => "f32",
Self::I8 => "i32",
Self::U8 => "u32",
}
}
pub fn byte_size(self) -> u32 {
match self {
Self::F16 => 2,
Self::F32 => 4,
Self::I8 => 4,
Self::U8 => 4,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CoopMatrixUse {
A,
B,
Accumulator,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CoopMatrixDim {
pub m: u32,
pub n: u32,
pub k: u32,
}
impl Default for CoopMatrixDim {
fn default() -> Self {
Self {
m: 16,
n: 16,
k: 16,
}
}
}
#[derive(Debug, Clone)]
pub struct CoopMatrixDescriptor {
pub component_type: CoopMatrixComponentType,
pub use_kind: CoopMatrixUse,
pub rows: u32,
pub cols: u32,
pub stride: u32,
}
#[derive(Debug, Clone)]
pub struct CoopMatrixGemmConfig {
pub dim: CoopMatrixDim,
pub a_type: CoopMatrixComponentType,
pub b_type: CoopMatrixComponentType,
pub accum_type: CoopMatrixComponentType,
pub workgroup_size: (u32, u32, u32),
}
impl Default for CoopMatrixGemmConfig {
fn default() -> Self {
Self {
dim: CoopMatrixDim::default(),
a_type: CoopMatrixComponentType::F32,
b_type: CoopMatrixComponentType::F32,
accum_type: CoopMatrixComponentType::F32,
workgroup_size: (16, 16, 1),
}
}
}
pub fn supports_cooperative_matrix(ctx: &GpuContext) -> bool {
let features = ctx.device().features();
features.contains(wgpu::Features::EXPERIMENTAL_COOPERATIVE_MATRIX)
|| features.contains(wgpu::Features::SUBGROUP)
}
pub fn max_cooperative_matrix_dim(ctx: &GpuContext) -> Option<CoopMatrixDim> {
if supports_cooperative_matrix(ctx) {
Some(CoopMatrixDim::default())
} else {
None
}
}
pub fn make_gemm_wgsl(config: &CoopMatrixGemmConfig) -> String {
let (wg_x, wg_y, _wg_z) = config.workgroup_size;
let tile_m = config.dim.m;
let tile_n = config.dim.n;
let tile_k = config.dim.k;
let tile_a_size = tile_m * tile_k; let tile_b_size = tile_k * tile_n; let accum_type = config.accum_type.as_wgsl();
format!(
r#"// Cooperative-matrix (workgroup-tiled) GEMM kernel.
// Future hardware path note: subgroupMatrixLoad / subgroupMatrixStore /
// subgroupMatrixMultiplyAccumulate builtins would replace the shared-memory
// path below when wgpu enables the cooperative-matrix extension.
struct MatrixDims {{
M: u32,
N: u32,
K: u32,
_pad: u32,
}}
@group(0) @binding(0) var<storage, read> mat_a: array<{accum_type}>;
@group(0) @binding(1) var<storage, read> mat_b: array<{accum_type}>;
@group(0) @binding(2) var<storage, read_write> mat_c: array<{accum_type}>;
@group(0) @binding(3) var<uniform> dims: MatrixDims;
// Workgroup-shared tile storage (TILE_M × TILE_K and TILE_K × TILE_N).
var<workgroup> tile_a: array<{accum_type}, {tile_a_size}u>;
var<workgroup> tile_b: array<{accum_type}, {tile_b_size}u>;
@compute @workgroup_size({wg_x}, {wg_y}, 1)
fn gemm_main(
@builtin(global_invocation_id) gid: vec3<u32>,
@builtin(local_invocation_id) lid: vec3<u32>,
@builtin(workgroup_id) wgid: vec3<u32>,
) {{
let row: u32 = gid.x;
let col: u32 = gid.y;
let local_row: u32 = lid.x;
let local_col: u32 = lid.y;
var acc: {accum_type} = 0.0;
let num_tiles: u32 = (dims.K + {tile_k}u - 1u) / {tile_k}u;
for (var t: u32 = 0u; t < num_tiles; t = t + 1u) {{
// ── Collaborative tile load ─────────────────────────────────────
// Each thread loads one element of tile_a and one element of tile_b.
// tile_a: shape [TILE_M, TILE_K], row-major.
let a_global_row: u32 = wgid.x * {tile_m}u + local_row;
let a_global_col: u32 = t * {tile_k}u + local_col;
if (a_global_row < dims.M && a_global_col < dims.K) {{
tile_a[local_row * {tile_k}u + local_col] = mat_a[a_global_row * dims.K + a_global_col];
}} else {{
tile_a[local_row * {tile_k}u + local_col] = 0.0;
}}
// tile_b: shape [TILE_K, TILE_N], row-major.
let b_global_row: u32 = t * {tile_k}u + local_row;
let b_global_col: u32 = wgid.y * {tile_n}u + local_col;
if (b_global_row < dims.K && b_global_col < dims.N) {{
tile_b[local_row * {tile_n}u + local_col] = mat_b[b_global_row * dims.N + b_global_col];
}} else {{
tile_b[local_row * {tile_n}u + local_col] = 0.0;
}}
// Ensure all threads have finished writing the tiles before reading.
workgroupBarrier();
// ── Partial dot product ─────────────────────────────────────────
for (var k: u32 = 0u; k < {tile_k}u; k = k + 1u) {{
acc = acc + tile_a[local_row * {tile_k}u + k] * tile_b[k * {tile_n}u + local_col];
}}
// Ensure tile reads are complete before the next iteration overwrites.
workgroupBarrier();
}}
// Write output only for threads within the logical matrix bounds.
if (row < dims.M && col < dims.N) {{
mat_c[row * dims.N + col] = acc;
}}
}}
"#,
accum_type = accum_type,
tile_a_size = tile_a_size,
tile_b_size = tile_b_size,
tile_m = tile_m,
tile_n = tile_n,
tile_k = tile_k,
wg_x = wg_x,
wg_y = wg_y,
)
}
pub fn make_gemm_wgsl_fallback(config: &CoopMatrixGemmConfig) -> String {
let (wg_x, wg_y, _wg_z) = config.workgroup_size;
let accum_type = config.accum_type.as_wgsl();
format!(
r#"// Naive (non-tiled) GEMM fallback kernel.
// Each thread independently accumulates a full dot product for its C element.
struct MatrixDims {{
M: u32,
N: u32,
K: u32,
_pad: u32,
}}
@group(0) @binding(0) var<storage, read> mat_a: array<{accum_type}>;
@group(0) @binding(1) var<storage, read> mat_b: array<{accum_type}>;
@group(0) @binding(2) var<storage, read_write> mat_c: array<{accum_type}>;
@group(0) @binding(3) var<uniform> dims: MatrixDims;
@compute @workgroup_size({wg_x}, {wg_y}, 1)
fn gemm_main(
@builtin(global_invocation_id) gid: vec3<u32>,
) {{
let row: u32 = gid.x;
let col: u32 = gid.y;
if (row >= dims.M || col >= dims.N) {{
return;
}}
var acc: {accum_type} = 0.0;
for (var k: u32 = 0u; k < dims.K; k = k + 1u) {{
acc = acc + mat_a[row * dims.K + k] * mat_b[k * dims.N + col];
}}
mat_c[row * dims.N + col] = acc;
}}
"#,
accum_type = accum_type,
wg_x = wg_x,
wg_y = wg_y,
)
}
pub fn build_cooperative_matrix_gemm_pipeline(
ctx: &GpuContext,
config: &CoopMatrixGemmConfig,
) -> GpuResult<Arc<wgpu::ComputePipeline>> {
let wgsl = make_gemm_wgsl(config);
let device = ctx.device();
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("coop_matrix_gemm_shader"),
source: wgpu::ShaderSource::Wgsl(wgsl.into()),
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("coop_matrix_gemm_bgl"),
entries: &[
wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: true },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Storage { read_only: false },
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 3,
visibility: wgpu::ShaderStages::COMPUTE,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("coop_matrix_gemm_pipeline_layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
label: Some("coop_matrix_gemm_pipeline"),
layout: Some(&pipeline_layout),
module: &shader_module,
entry_point: Some("gemm_main"),
compilation_options: wgpu::PipelineCompilationOptions::default(),
cache: None,
});
Ok(Arc::new(pipeline))
}
pub fn dispatch_cooperative_gemm(
ctx: &GpuContext,
pipeline: &wgpu::ComputePipeline,
a: &wgpu::Buffer,
b: &wgpu::Buffer,
c: &wgpu::Buffer,
dim: CoopMatrixDim,
) -> GpuResult<()> {
let device = ctx.device();
let queue = ctx.queue();
let dims_data: [u32; 4] = [dim.m, dim.n, dim.k, 0];
let dims_bytes: &[u8] = bytemuck::cast_slice(&dims_data);
let dims_buf = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("coop_matrix_dims_uniform"),
size: (dims_bytes.len() as u64).max(16),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
queue.write_buffer(&dims_buf, 0, dims_bytes);
let bind_group_layout = pipeline.get_bind_group_layout(0);
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("coop_matrix_gemm_bind_group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: a.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: b.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: c.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: dims_buf.as_entire_binding(),
},
],
});
const TILE: u32 = 16;
let wg_x = dim.m.div_ceil(TILE);
let wg_y = dim.n.div_ceil(TILE);
let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("coop_matrix_gemm_encoder"),
});
{
let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
label: Some("coop_matrix_gemm_pass"),
timestamp_writes: None,
});
pass.set_pipeline(pipeline);
pass.set_bind_group(0, &bind_group, &[]);
pass.dispatch_workgroups(wg_x, wg_y, 1);
}
queue.submit(std::iter::once(encoder.finish()));
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_component_type_as_wgsl() {
assert_eq!(CoopMatrixComponentType::F16.as_wgsl(), "f16");
assert_eq!(CoopMatrixComponentType::F32.as_wgsl(), "f32");
assert_eq!(CoopMatrixComponentType::I8.as_wgsl(), "i32");
assert_eq!(CoopMatrixComponentType::U8.as_wgsl(), "u32");
}
#[test]
fn test_component_type_byte_size() {
assert_eq!(CoopMatrixComponentType::F16.byte_size(), 2);
assert_eq!(CoopMatrixComponentType::F32.byte_size(), 4);
assert_eq!(CoopMatrixComponentType::I8.byte_size(), 4);
assert_eq!(CoopMatrixComponentType::U8.byte_size(), 4);
}
#[test]
fn test_dim_default() {
let d = CoopMatrixDim::default();
assert_eq!(d.m, 16);
assert_eq!(d.n, 16);
assert_eq!(d.k, 16);
}
#[test]
fn test_gemm_config_default_workgroup_size() {
let cfg = CoopMatrixGemmConfig::default();
assert_eq!(cfg.workgroup_size, (16, 16, 1));
}
#[test]
fn test_make_gemm_wgsl_contains_subgroup_matrix() {
let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
assert!(
src.contains("subgroupMatrix"),
"shader source must reference subgroupMatrix (as comment/string): \n{src}"
);
}
#[test]
fn test_make_gemm_wgsl_contains_workgroup_var() {
let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
assert!(
src.contains("var<workgroup>"),
"tiled kernel must declare var<workgroup> shared memory"
);
}
#[test]
fn test_make_gemm_wgsl_emits_compute_entry() {
let src = make_gemm_wgsl(&CoopMatrixGemmConfig::default());
assert!(src.contains("@compute @workgroup_size"));
}
#[test]
fn test_make_gemm_wgsl_fallback_is_simpler() {
let src = make_gemm_wgsl_fallback(&CoopMatrixGemmConfig::default());
assert!(
!src.contains("var<workgroup>"),
"fallback must not declare var<workgroup>"
);
assert!(src.contains("@compute"));
}
}