#![allow(missing_docs)]
use cubecl::prelude::*;
use cubecl_utils_rs::prelude::*;
use crate::gpu::linalg::gram::gram_reduce;
use crate::prelude::*;
const SG_BM: u32 = 128;
const SG_BN: u32 = 16;
const SG_BK: u32 = 8;
const SG_RT_M: u32 = 4;
const SG_RT_N: u32 = 4;
const SG_TN: u32 = SG_BN / SG_RT_N;
const SG_TM: u32 = SG_BM / SG_RT_M;
pub const SG_THREADS: u32 = SG_TM * SG_TN;
const SG_TARGET_CUBES: usize = 2048;
const SG_MAX_CHUNKS: usize = 256;
const SG_MIN_LEN_PER_CHUNK: usize = 512;
#[cfg(test)]
const SG_MAX_PARTIAL_ELEMS: usize = 2 * SG_TARGET_CUBES * (SG_BM * SG_BN) as usize;
const SG_REDUCE_WG: u32 = 256;
pub fn skinny_chunks(rows: usize, k: usize, len: usize) -> usize {
let tiles = rows.div_ceil(SG_BM as usize) * k.div_ceil(SG_BN as usize);
if tiles >= SG_TARGET_CUBES {
return 1;
}
let wanted = SG_TARGET_CUBES.div_ceil(tiles.max(1));
let by_len = (len / SG_MIN_LEN_PER_CHUNK).max(1);
wanted.min(by_len).clamp(1, SG_MAX_CHUNKS)
}
pub fn skinny_partial_elems(rows: usize, k: usize, len: usize) -> usize {
let chunks = skinny_chunks(rows, k, len);
if chunks > 1 { chunks * rows * k } else { 0 }
}
#[cube(launch_unchecked)]
pub fn skinny_gemm_partial<F: Float>(
a: &Tensor<F>,
b: &Tensor<F>,
out: &mut Tensor<F>,
rows: u32,
k: u32,
len: u32,
len_per_chunk: u32,
n_col_blocks: u32,
#[comptime] a_transposed: bool,
) {
let row_block = CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X;
let col_block = CUBE_POS_Z % n_col_blocks;
let chunk = CUBE_POS_Z / n_col_blocks;
let i0 = row_block * SG_BM;
let j0 = col_block * SG_BN;
let mut sa = SharedMemory::<F>::new((SG_BK * SG_BM) as usize);
let mut sb = SharedMemory::<F>::new((SG_BK * SG_BN) as usize);
if i0 >= rows {
terminate!();
}
let t_start = chunk * len_per_chunk;
let mut t_end = t_start + len_per_chunk;
if t_end > len {
t_end = len;
}
let tid = UNIT_POS_X;
let tm = tid / SG_TN;
let tn = tid % SG_TN;
let zero = F::new(0.0_f32);
let mut acc = Array::<F>::new((SG_RT_M * SG_RT_N) as usize);
#[unroll]
for p in 0..SG_RT_M {
#[unroll]
for q in 0..SG_RT_N {
acc[(p * SG_RT_N + q) as usize] = zero;
}
}
let mut t = t_start;
while t < t_end {
let mut li = tid;
while li < SG_BK * SG_BM {
let kk = if a_transposed { li / SG_BM } else { li % SG_BK };
let mm = if a_transposed { li % SG_BM } else { li / SG_BK };
let tt = t + kk;
let rr = i0 + mm;
let mut v = zero;
if tt < t_end && rr < rows {
if a_transposed {
v = a[(tt * rows + rr) as usize];
} else {
v = a[(rr * len + tt) as usize];
}
}
sa[(kk * SG_BM + mm) as usize] = v;
li += SG_THREADS;
}
let mut lj = tid;
while lj < SG_BK * SG_BN {
let kk = lj / SG_BN;
let nn = lj % SG_BN;
let tt = t + kk;
let cc = j0 + nn;
let mut v = zero;
if tt < t_end && cc < k {
v = b[(tt * k + cc) as usize];
}
sb[lj as usize] = v;
lj += SG_THREADS;
}
sync_cube();
#[unroll]
for kk in 0..SG_BK {
let arow = kk * SG_BM + tm * SG_RT_M;
let brow = kk * SG_BN + tn * SG_RT_N;
let mut av = Array::<F>::new(SG_RT_M as usize);
let mut bv = Array::<F>::new(SG_RT_N as usize);
#[unroll]
for p in 0..SG_RT_M {
av[p as usize] = sa[(arow + p) as usize];
}
#[unroll]
for q in 0..SG_RT_N {
bv[q as usize] = sb[(brow + q) as usize];
}
#[unroll]
for p in 0..SG_RT_M {
#[unroll]
for q in 0..SG_RT_N {
acc[(p * SG_RT_N + q) as usize] += av[p as usize] * bv[q as usize];
}
}
}
sync_cube();
t += SG_BK;
}
let base = chunk * rows * k;
#[unroll]
for p in 0..SG_RT_M {
let rr = i0 + tm * SG_RT_M + p;
if rr < rows {
#[unroll]
for q in 0..SG_RT_N {
let cc = j0 + tn * SG_RT_N + q;
if cc < k {
out[(base + rr * k + cc) as usize] = acc[(p * SG_RT_N + q) as usize];
}
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn skinny_gemm<R, F>(
client: &ComputeClient<R>,
a: &GpuTensor<R, F>,
b: &GpuTensor<R, F>,
c: &GpuTensor<R, F>,
partials: &GpuTensor<R, F>,
rows: usize,
k: usize,
len: usize,
a_transposed: bool,
) -> Result<(), BixverseErrors>
where
R: Runtime,
F: Float + cubecl::CubeElement,
{
let limits = GpuLimits::from_client(client);
fits_shared_memory(
"skinny_gemm_partial",
(SG_BK * (SG_BM + SG_BN)) as usize * size_of::<F>(),
&limits,
)?;
let n_chunks = skinny_chunks(rows, k, len);
let len_per_chunk = len.div_ceil(n_chunks) as u32;
let n_col_blocks = (k as u32).div_ceil(SG_BN);
let row_blocks = (rows as u32).div_ceil(SG_BM);
let split = n_chunks > 1;
if split {
let need = n_chunks * rows * k;
if partials.len() < need {
return Err(BixverseErrors::GpuBindingTooLarge {
buffer: "skinny_gemm partials",
bytes: need * size_of::<F>(),
limit: partials.len() * size_of::<F>(),
});
}
}
let target = if split { partials } else { c };
let (gx, gy) = grid_2d(row_blocks, &limits)?;
let count = checked_cube_count(
"skinny_gemm_partial",
gx,
gy,
n_col_blocks * n_chunks as u32,
&limits,
)?;
macro_rules! dispatch {
($transposed:expr) => {
unsafe {
skinny_gemm_partial::launch_unchecked::<F, R>(
client,
count,
CubeDim::new_1d(SG_THREADS),
a.clone().into_tensor_arg(),
b.clone().into_tensor_arg(),
target.clone().into_tensor_arg(),
rows as u32,
k as u32,
len as u32,
len_per_chunk,
n_col_blocks,
$transposed,
);
}
};
}
if a_transposed {
dispatch!(true);
} else {
dispatch!(false);
}
if split {
let total = (rows * k) as u32;
let (rx, ry) = grid_2d(total.div_ceil(SG_REDUCE_WG), &limits)?;
let reduce_count = checked_cube_count("gram_reduce", rx, ry, 1, &limits)?;
unsafe {
gram_reduce::launch_unchecked::<F, R>(
client,
reduce_count,
CubeDim::new_1d(SG_REDUCE_WG),
partials.clone().into_tensor_arg(),
c.clone().into_tensor_arg(),
total,
n_chunks as u32,
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl::wgpu::{WgpuDevice, WgpuRuntime};
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::DefaultDevice;
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
WgpuRuntime::client(&device);
}))
.ok()
.map(|_| device)
}
fn build(len: usize, salt: usize) -> Vec<f32> {
(0..len)
.map(|t| (((t * 31 + salt * 17) % 23) as f32) * 0.1 - 1.1)
.collect()
}
fn host_gemm(
a: &[f32],
b: &[f32],
rows: usize,
k: usize,
len: usize,
a_transposed: bool,
) -> Vec<f32> {
let mut out = vec![0f32; rows * k];
for r in 0..rows {
for c in 0..k {
let mut acc = 0f32;
for t in 0..len {
let av = if a_transposed {
a[t * rows + r]
} else {
a[r * len + t]
};
acc += av * b[t * k + c];
}
out[r * k + c] = acc;
}
}
out
}
fn assert_close(got: &[f32], want: &[f32], tol: f32) {
assert_eq!(got.len(), want.len(), "length mismatch");
assert!(
got.iter().any(|v| v.abs() > 1e-6),
"output is all zeros, the GPU did no work"
);
for (i, (&a, &b)) in got.iter().zip(want.iter()).enumerate() {
assert!(
(a - b).abs() <= tol * b.abs().max(1.0),
"index {i}: got {a} want {b}"
);
}
}
fn run(rows: usize, k: usize, len: usize, a_transposed: bool) -> (Vec<f32>, Vec<f32>) {
let device = try_device().expect("no device");
let client = WgpuRuntime::client(&device);
let a_host = build(rows * len, 1);
let b_host = build(len * k, 2);
let a_shape = if a_transposed {
vec![len, rows]
} else {
vec![rows, len]
};
let a = GpuTensor::<WgpuRuntime, f32>::from_slice(&a_host, a_shape, &client).unwrap();
let b = GpuTensor::<WgpuRuntime, f32>::from_slice(&b_host, vec![len, k], &client).unwrap();
let c = GpuTensor::<WgpuRuntime, f32>::empty(vec![rows, k], &client).unwrap();
let elems = skinny_partial_elems(rows, k, len).max(1);
let partials = GpuTensor::<WgpuRuntime, f32>::empty(vec![elems], &client).unwrap();
skinny_gemm::<WgpuRuntime, f32>(&client, &a, &b, &c, &partials, rows, k, len, a_transposed)
.unwrap();
let got = c.read(&client).unwrap();
(got, host_gemm(&a_host, &b_host, rows, k, len, a_transposed))
}
#[test]
fn test_skinny_gemm_ragged_tails() {
if try_device().is_none() {
return;
}
for transposed in [false, true] {
let (got, want) = run(200, 22, 37, transposed);
assert_close(&got, &want, 1e-4);
}
}
#[test]
fn test_skinny_gemm_split_k_path() {
if try_device().is_none() {
return;
}
let (rows, k, len) = (64usize, 8usize, 4_096usize);
assert!(
skinny_chunks(rows, k, len) > 1,
"this shape was supposed to take the split-K arm"
);
for transposed in [false, true] {
let (got, want) = run(rows, k, len, transposed);
assert_close(&got, &want, 1e-3);
}
}
#[test]
fn test_skinny_gemm_single_chunk_path() {
if try_device().is_none() {
return;
}
let (rows, k, len) = (4_096usize, 128usize, 64usize);
assert_eq!(
skinny_chunks(rows, k, len),
1,
"this shape was supposed to skip the split-K arm"
);
let (got, want) = run(rows, k, len, false);
assert_close(&got, &want, 1e-4);
}
#[test]
fn test_skinny_gemm_rank_one() {
if try_device().is_none() {
return;
}
let (got, want) = run(300, 1, 71, true);
assert_close(&got, &want, 1e-4);
}
#[test]
fn test_skinny_gemm_rejects_a_short_partials_buffer() {
let Some(device) = try_device() else {
return;
};
let client = WgpuRuntime::client(&device);
let (rows, k, len) = (64usize, 8usize, 4_096usize);
assert!(skinny_chunks(rows, k, len) > 1);
let a = GpuTensor::<WgpuRuntime, f32>::empty(vec![rows, len], &client).unwrap();
let b = GpuTensor::<WgpuRuntime, f32>::empty(vec![len, k], &client).unwrap();
let c = GpuTensor::<WgpuRuntime, f32>::empty(vec![rows, k], &client).unwrap();
let partials = GpuTensor::<WgpuRuntime, f32>::empty(vec![1], &client).unwrap();
assert!(matches!(
skinny_gemm::<WgpuRuntime, f32>(&client, &a, &b, &c, &partials, rows, k, len, false),
Err(BixverseErrors::GpuBindingTooLarge { .. })
));
}
#[test]
fn test_skinny_gemm_shared_memory_within_budget() {
let footprint = (SG_BK * (SG_BM + SG_BN)) as usize * size_of::<f32>();
assert!(footprint <= 32 * 1024, "over the shared-memory floor");
assert!(
32 * 1024 / footprint >= 2,
"only one workgroup would be resident"
);
assert_eq!(SG_THREADS, 128);
assert_eq!(SG_BM % SG_RT_M, 0);
assert_eq!(SG_BN % SG_RT_N, 0);
}
#[test]
fn test_skinny_gemm_grid_within_dispatch_limit() {
let (max_x, max_y, max_z) = WgpuRuntime::max_cube_count();
let cap = max_x.min(max_y);
assert!((16_000_000u32).div_ceil(SG_BM) > max_x);
for rows in [1_000_000usize, 16_000_000, 100_000_000] {
let blocks = (rows as u32).div_ceil(SG_BM);
let (gx, gy) = grid_2d_limited(blocks, cap).unwrap();
assert!(gx <= max_x && gy <= max_y);
assert!(gx as u64 * gy as u64 >= blocks as u64, "grid misses work");
}
let max_col_blocks = 128u32.div_ceil(SG_BN);
assert!(max_col_blocks * SG_MAX_CHUNKS as u32 <= max_z);
}
#[test]
fn test_skinny_chunks_each_bound_binds() {
assert_eq!(skinny_chunks(1_000_000, 128, 4_096), 1);
assert!(skinny_chunks(128, 16, 1_000_000) > 1);
assert_eq!(skinny_chunks(128, 16, 600), 1);
assert_eq!(skinny_chunks(128, 16, 1_000_000_000), SG_MAX_CHUNKS);
for &(rows, k) in &[
(1usize, 1usize),
(128, 16),
(500, 10),
(3_000, 30),
(50_000, 30),
(200_000, 128),
(1_000_000, 64),
] {
for &len in &[600usize, 4_096, 1_000_000, 1_000_000_000] {
let elems = skinny_partial_elems(rows, k, len);
assert!(
elems <= SG_MAX_PARTIAL_ELEMS,
"{rows}x{k} over {len}: partials {elems} above the implied ceiling"
);
}
}
}
#[test]
fn test_skinny_partial_elems_is_zero_when_unsplit() {
assert_eq!(skinny_partial_elems(1_000_000, 128, 4_096), 0);
let (rows, k, len) = (128usize, 16usize, 1_000_000usize);
assert_eq!(
skinny_partial_elems(rows, k, len),
skinny_chunks(rows, k, len) * rows * k
);
}
}