use anyhow::{Context, ensure};
use cera::backend::wgpu::{GpuContext, shaders};
struct Shape {
label: &'static str,
m: u32,
k: u32,
n: u32,
}
const SHAPES: &[Shape] = &[
Shape {
label: "ffn_gate",
m: 4608,
k: 1024,
n: 512,
},
Shape {
label: "ffn_down",
m: 1024,
k: 4608,
n: 512,
},
Shape {
label: "conv_in",
m: 3072,
k: 1024,
n: 512,
},
Shape {
label: "attn_out",
m: 1024,
k: 1024,
n: 512,
},
];
struct Variant {
name: &'static str,
loader: &'static str,
src0_bytes: fn(u64) -> u64,
}
const VARIANTS: &[Variant] = &[
Variant {
name: "f32",
loader: "INIT_SRC0_SHMEM_FLOAT",
src0_bytes: |e| e * 4,
},
Variant {
name: "q4_0",
loader: "INIT_SRC0_SHMEM_Q4_0",
src0_bytes: |e| e / 32 * 18,
},
Variant {
name: "q8_0",
loader: "INIT_SRC0_SHMEM_Q8_0",
src0_bytes: |e| e / 32 * 34,
},
Variant {
name: "q4_k",
loader: "INIT_SRC0_SHMEM_Q4_K",
src0_bytes: |e| e / 256 * 144,
},
Variant {
name: "q6_k",
loader: "INIT_SRC0_SHMEM_Q6_K",
src0_bytes: |e| e / 256 * 210,
},
];
fn env_u32(key: &str, default: u32) -> anyhow::Result<u32> {
let Some(raw) = std::env::var_os(key) else {
return Ok(default);
};
let raw = raw.to_string_lossy();
let value: u32 = raw
.trim()
.parse()
.with_context(|| format!("{key}={raw:?} is not a u32"))?;
ensure!(value > 0, "{key} must be at least 1, got 0");
Ok(value)
}
fn fill_quant_bytes(len: usize) -> Vec<u8> {
let mut s = 0x12345678u32;
(0..len)
.map(|i| {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
let b = (s >> 16) as u8;
if i % 2 == 1 { b & 0xBF } else { b }
})
.collect()
}
fn fill_f32(len: usize) -> Vec<f32> {
let mut s = 0x9E3779B9u32;
(0..len)
.map(|_| {
s = s.wrapping_mul(1664525).wrapping_add(1013904223);
((s >> 16) as f32 / 32768.0) - 1.0
})
.collect()
}
struct Tile {
wg_m: u32,
wg_n: u32,
tile_m: u32,
tile_n: u32,
tile_k: u32,
}
impl Tile {
fn from_env() -> anyhow::Result<Self> {
Ok(Self {
wg_m: env_u32("WG_M", 16)?,
wg_n: env_u32("WG_N", 16)?,
tile_m: env_u32("TILE_M", 4)?,
tile_n: env_u32("TILE_N", 4)?,
tile_k: env_u32("TILE_K", 16)?,
})
}
fn defines(&self, v: &Variant) -> Vec<(String, String)> {
vec![
(
"SRC0_INNER_TYPE".into(),
if v.loader == "INIT_SRC0_SHMEM_FLOAT" {
"f32".into()
} else {
"u32".into()
},
),
(v.loader.into(), String::new()),
("WORKGROUP_SIZE_M".into(), format!("{}u", self.wg_m)),
("WORKGROUP_SIZE_N".into(), format!("{}u", self.wg_n)),
("TILE_M".into(), format!("{}u", self.tile_m)),
("TILE_N".into(), format!("{}u", self.tile_n)),
("TILE_K".into(), format!("{}u", self.tile_k)),
]
}
fn grid(&self, s: &Shape) -> (u32, u32, u32) {
(
s.m.div_ceil(self.wg_m * self.tile_m),
s.n.div_ceil(self.wg_n * self.tile_n),
1,
)
}
}
fn build(ctx: &GpuContext, src: &str, tile: &Tile, v: &Variant) -> wgpu::ComputePipeline {
let defs = tile.defines(v);
let refs: Vec<(&str, &str)> = defs
.iter()
.map(|(k, val)| (k.as_str(), val.as_str()))
.collect();
ctx.create_pipeline_with_defines(src, "main", v.name, &refs)
}
fn main() -> anyhow::Result<()> {
let ctx = GpuContext::new()?;
let experimental = std::env::var("SHADER").ok();
let source = match &experimental {
Some(path) => std::fs::read_to_string(path)
.with_context(|| format!("failed to read SHADER={path}"))?,
None => shaders::MUL_MAT_REG_TILE.to_string(),
};
let tile = Tile::from_env()?;
ensure!(
tile.tile_m == 4 && tile.tile_n == 4,
"TILE_M/TILE_N must be 4: mul_mat_reg_tile.wgsl hand-unrolls a 4x4 thread \
tile, and any other value computes only part of the dispatched tile \
(fast, and wrong) — got {}x{}",
tile.tile_m,
tile.tile_n,
);
ensure!(
tile.tile_k.is_multiple_of(8),
"TILE_K must be a multiple of 8: the Q4_0 loader stages 8 consecutive k \
per thread — got {}",
tile.tile_k,
);
let iters = env_u32("ITERS", 20)?;
let shmem = (tile.tile_k * (tile.wg_m * tile.tile_m + 4)
+ tile.tile_k * (tile.wg_n * tile.tile_n + 4))
* 4;
println!(
"adapter: {} ({})\ntile: WG_M={} WG_N={} TILE_M={} TILE_N={} TILE_K={} \
-> {} threads/wg, {}x{} out-tile/wg, shmem ~{:.1} KB{}\n",
ctx.adapter_name,
ctx.backend,
tile.wg_m,
tile.wg_n,
tile.tile_m,
tile.tile_n,
tile.tile_k,
tile.wg_m * tile.wg_n,
tile.wg_m * tile.tile_m,
tile.wg_n * tile.tile_n,
shmem as f64 / 1024.0,
experimental
.as_ref()
.map(|p| format!("\nshader: {p} (diffed vs production)"))
.unwrap_or_default(),
);
let only = std::env::var("ONLY").unwrap_or_default();
for v in VARIANTS
.iter()
.filter(|v| only.is_empty() || only.split(',').any(|o| o == v.name))
{
let pipeline = build(&ctx, &source, &tile, v);
let reference = experimental
.is_some()
.then(|| build(&ctx, shaders::MUL_MAT_REG_TILE, &tile, v));
for s in SHAPES {
let src0 = if v.loader == "INIT_SRC0_SHMEM_FLOAT" {
ctx.upload_f32(&fill_f32(s.m as usize * s.k as usize), "src0")
} else {
ctx.upload_storage(
&fill_quant_bytes((v.src0_bytes)(s.m as u64 * s.k as u64) as usize),
"src0",
)
};
let src1 = ctx.upload_f32(&fill_f32(s.n as usize * s.k as usize), "src1");
let params =
ctx.upload_storage(bytemuck::cast_slice(&[s.m, s.k, s.n, s.k, s.m]), "params");
let dst_len = s.n as u64 * s.m as u64;
let dispatch = |pipe: &wgpu::ComputePipeline, dst: &wgpu::Buffer, count: u32| {
let bg = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
label: None,
layout: &pipe.get_bind_group_layout(0),
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: src0.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: src1.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 2,
resource: dst.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 3,
resource: params.as_entire_binding(),
},
],
});
let grid = tile.grid(s);
let mut enc = ctx.device.create_command_encoder(&Default::default());
for _ in 0..count {
let mut pass = enc.begin_compute_pass(&Default::default());
pass.set_pipeline(pipe);
pass.set_bind_group(0, &bg, &[]);
pass.dispatch_workgroups(grid.0, grid.1, grid.2);
}
ctx.queue.submit(Some(enc.finish()));
ctx.device.poll(wgpu::Maintain::Wait);
};
let dst = ctx.create_storage_rw(dst_len * 4, "dst");
dispatch(&pipeline, &dst, 2); let t0 = std::time::Instant::now();
dispatch(&pipeline, &dst, iters);
let per_iter = t0.elapsed().as_secs_f64() / iters as f64;
let diff = reference.as_ref().map(|r| {
let dst_ref = ctx.create_storage_rw(dst_len * 4, "dst_ref");
dispatch(r, &dst_ref, 1);
let n = dst_len as usize;
let a = ctx.download_f32(&dst, n);
let b = ctx.download_f32(&dst_ref, n);
assert!(
a.iter().chain(&b).all(|x| x.is_finite()),
"{} {}: non-finite output — the diff validates nothing",
v.name,
s.label,
);
let scale = b.iter().fold(0f32, |acc, x| acc.max(x.abs())).max(1e-6);
a.iter()
.zip(&b)
.fold(0f32, |acc, (x, y)| acc.max((x - y).abs()))
/ scale
});
if let Some(d) = diff {
assert!(
d == 0.0,
"{} {}: rel_err {d:.3e} against the production kernel — the \
candidate computes something different",
v.name,
s.label,
);
}
let gflop = 2.0 * s.m as f64 * s.k as f64 * s.n as f64 / 1e9;
let grid = tile.grid(s);
println!(
"{:4} {:8} m={:5} k={:5} n={:4} grid={:4}x{:<3} {:8.3} ms {:8.1} GFLOP/s{}",
v.name,
s.label,
s.m,
s.k,
s.n,
grid.0,
grid.1,
per_iter * 1e3,
gflop / per_iter,
diff.map(|d| format!(" rel_err={d:.2e}"))
.unwrap_or_default(),
);
}
println!();
}
Ok(())
}