#![allow(missing_docs)]
use cubecl::prelude::*;
use cubecl_utils_rs::prelude::*;
use crate::prelude::*;
const GRAM_BM: u32 = 64;
const GRAM_BK: u32 = 8;
const GRAM_RT: u32 = 4;
const GRAM_TN: u32 = GRAM_BM / GRAM_RT;
const GRAM_THREADS: u32 = GRAM_TN * GRAM_TN;
const GRAM_SMEM_STRIDE: u32 = GRAM_BM + 1;
const GRAM_TARGET_CUBES: usize = 2048;
const GRAM_MAX_CHUNKS: usize = 256;
const GRAM_MIN_ROWS_PER_CHUNK: usize = 1024;
const GRAM_MAX_PARTIAL_BYTES: usize = 64 * 1024 * 1024;
const GRAM_REDUCE_WG: u32 = 256;
fn gram_chunks(n: usize, d: usize, elem_bytes: usize) -> usize {
let tiles = d.div_ceil(GRAM_BM as usize);
let triangular = tiles * (tiles + 1) / 2;
if triangular >= GRAM_TARGET_CUBES {
return 1;
}
let wanted = GRAM_TARGET_CUBES.div_ceil(triangular.max(1));
let by_rows = (n / GRAM_MIN_ROWS_PER_CHUNK).max(1);
let by_memory = (GRAM_MAX_PARTIAL_BYTES / (d * d * elem_bytes).max(1)).max(1);
wanted.min(by_rows).min(by_memory).clamp(1, GRAM_MAX_CHUNKS)
}
#[cube(launch_unchecked)]
pub fn gram_symmetric<F: Float>(
a: &Tensor<F>,
out: &mut Tensor<F>,
n: u32,
d: u32,
rows_per_chunk: u32,
) {
if CUBE_POS_Y < CUBE_POS_X {
terminate!();
}
let tid = UNIT_POS_X;
let tm = tid / GRAM_TN;
let tn = tid % GRAM_TN;
let i0 = CUBE_POS_X * GRAM_BM;
let j0 = CUBE_POS_Y * GRAM_BM;
let r_start = CUBE_POS_Z * rows_per_chunk;
let mut r_end = r_start + rows_per_chunk;
if r_end > n {
r_end = n;
}
let mut sa = Shared::<[F]>::new_slice((GRAM_BK * GRAM_SMEM_STRIDE) as usize);
let mut sb = Shared::<[F]>::new_slice((GRAM_BK * GRAM_SMEM_STRIDE) as usize);
let mut acc = Array::<F>::new((GRAM_RT * GRAM_RT) as usize);
#[unroll]
for t in 0..GRAM_RT * GRAM_RT {
acc[t as usize] = F::new(0.0_f32);
}
let stage = GRAM_BK * GRAM_BM;
let mut r = r_start;
while r < r_end {
let mut li = tid;
while li < stage {
let m = li / GRAM_BK;
let k = li % GRAM_BK;
let row = r + k;
let in_row = row < r_end;
let dst = k * GRAM_SMEM_STRIDE + m;
if in_row && i0 + m < d {
sa[dst as usize] = a[((i0 + m) * n + row) as usize];
} else {
sa[dst as usize] = F::new(0.0_f32);
}
if in_row && j0 + m < d {
sb[dst as usize] = a[((j0 + m) * n + row) as usize];
} else {
sb[dst as usize] = F::new(0.0_f32);
}
li += GRAM_THREADS;
}
sync_cube();
let abase = tm * GRAM_RT;
let bbase = tn * GRAM_RT;
#[unroll]
for k in 0..GRAM_BK {
let koff = k * GRAM_SMEM_STRIDE;
let mut av = Array::<F>::new(GRAM_RT as usize);
let mut bv = Array::<F>::new(GRAM_RT as usize);
#[unroll]
for t in 0..GRAM_RT {
av[t as usize] = sa[(koff + abase + t) as usize];
bv[t as usize] = sb[(koff + bbase + t) as usize];
}
#[unroll]
for p in 0..GRAM_RT {
#[unroll]
for q in 0..GRAM_RT {
acc[(p * GRAM_RT + q) as usize] += av[p as usize] * bv[q as usize];
}
}
}
sync_cube();
r += GRAM_BK;
}
let base = CUBE_POS_Z * d * d;
let on_diagonal = CUBE_POS_X == CUBE_POS_Y;
#[unroll]
for p in 0..GRAM_RT {
#[unroll]
for q in 0..GRAM_RT {
let gi = i0 + tm * GRAM_RT + p;
let gj = j0 + tn * GRAM_RT + q;
if gi < d && gj < d {
let v = acc[(p * GRAM_RT + q) as usize];
out[(base + gi * d + gj) as usize] = v;
if !on_diagonal {
out[(base + gj * d + gi) as usize] = v;
}
}
}
}
}
#[cube(launch_unchecked)]
pub fn gram_reduce<F: Float>(partials: &Tensor<F>, g: &mut Tensor<F>, total: u32, n_chunks: u32) {
let idx = (CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X) * GRAM_REDUCE_WG + UNIT_POS_X;
if idx >= total {
terminate!();
}
let mut acc = F::new(0.0_f32);
let mut c = 0u32;
while c < n_chunks {
acc += partials[(c * total + idx) as usize];
c += 1u32;
}
g[idx as usize] = acc;
}
pub fn gram_aat<F>(
client: &Client,
a: &GpuTensor<F>,
g: &GpuTensor<F>,
n: usize,
d: usize,
) -> Result<(), BixverseErrors>
where
F: Float + cubecl::CubeElement,
{
let limits = GpuLimits::from_client(client);
let n_chunks = gram_chunks(n, d, size_of::<F>());
let rows_per_chunk = n.div_ceil(n_chunks) as u32;
let tiles = (d as u32).div_ceil(GRAM_BM);
let partials = (n_chunks > 1)
.then(|| GpuTensor::<F>::empty(vec![n_chunks, d, d], client))
.transpose()?;
let target = partials.as_ref().unwrap_or(g);
let count = checked_cube_count("gram_symmetric", tiles, tiles, n_chunks as u32, &limits)?;
unsafe {
gram_symmetric::launch_unchecked::<F>(
client,
count,
CubeDim::new_1d(GRAM_THREADS),
a.clone().into_tensor_arg(),
target.clone().into_tensor_arg(),
n as u32,
d as u32,
rows_per_chunk,
);
}
if let Some(partials) = partials {
let total = (d * d) as u32;
let (gx, gy) = grid_2d(total.div_ceil(GRAM_REDUCE_WG), &limits)?;
let count = checked_cube_count("gram_reduce", gx, gy, 1, &limits)?;
unsafe {
gram_reduce::launch_unchecked::<F>(
client,
count,
CubeDim::new_1d(GRAM_REDUCE_WG),
partials.clone().into_tensor_arg(),
g.clone().into_tensor_arg(),
total,
n_chunks as u32,
);
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use cubecl::wgpu::WgpuDevice;
fn try_device() -> Option<WgpuDevice> {
let device = WgpuDevice::default();
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cubecl::Device::from(device.clone()).client();
}))
.ok()
.map(|_| device)
}
fn build_a(n: usize, d: usize) -> Vec<f32> {
(0..n * d)
.map(|t| {
let i = t / n;
let r = t % n;
((i * 31 + r * 17) % 23) as f32 * 0.1 - 1.1
})
.collect()
}
fn gram_host(a: &[f32], n: usize, d: usize) -> Vec<f32> {
let mut out = vec![0.0f32; d * d];
for i in 0..d {
for j in 0..d {
let mut acc = 0.0f32;
for r in 0..n {
acc += a[i * n + r] * a[j * n + r];
}
out[i * d + j] = acc;
}
}
out
}
fn run(n: usize, d: usize) -> (Vec<f32>, Vec<f32>) {
let device = try_device().expect("no device");
let client = cubecl::Device::from(device.clone()).client();
let a_host = build_a(n, d);
let a = GpuTensor::<f32>::from_slice(&a_host, vec![d, n], &client).unwrap();
let g = GpuTensor::<f32>::empty(vec![d, d], &client).unwrap();
gram_aat::<f32>(&client, &a, &g, n, d).unwrap();
let got = g.read(&client).unwrap();
(got, gram_host(&a_host, n, d))
}
fn assert_close(got: &[f32], want: &[f32], d: usize, tol: f32) {
assert!(
got.iter().any(|v| v.abs() > 1e-6),
"output is all zeros, the GPU did no work"
);
for i in 0..d {
for j in 0..d {
let (a, b) = (got[i * d + j], want[i * d + j]);
assert!(
(a - b).abs() <= tol * b.abs().max(1.0),
"({i},{j}): got {a} want {b}"
);
}
}
}
#[test]
fn test_gram_aat_single_tile() {
if try_device().is_none() {
return;
}
let (n, d) = (200, 64);
let (got, want) = run(n, d);
assert_close(&got, &want, d, 1e-4);
}
#[test]
fn test_gram_aat_ragged_tails() {
if try_device().is_none() {
return;
}
let (n, d) = (137, 150);
let (got, want) = run(n, d);
assert_close(&got, &want, d, 1e-4);
}
#[test]
fn test_gram_aat_symmetric_bitwise() {
if try_device().is_none() {
return;
}
let d = 150;
let (got, _) = run(137, d);
for i in 0..d {
for j in 0..d {
assert_eq!(
got[i * d + j].to_bits(),
got[j * d + i].to_bits(),
"asymmetric at ({i},{j})"
);
}
}
}
#[test]
#[cfg(feature = "large-test")]
fn test_gram_aat_split_k_path() {
if try_device().is_none() {
return;
}
let (n, d) = (40_000, 48);
assert!(
gram_chunks(n, d, size_of::<f32>()) > 1,
"this shape was supposed to take the split-K arm"
);
let (got, want) = run(n, d);
assert_close(&got, &want, d, 1e-3);
}
#[test]
fn test_gram_chunks_each_bound_binds() {
let bytes = size_of::<f32>();
assert_eq!(gram_chunks(500, 8_000, bytes), 1);
assert_eq!(gram_chunks(1_000_000, 8_000, bytes), 1);
assert_eq!(gram_chunks(500, 64, bytes), 1);
assert_eq!(gram_chunks(2_000, 2_000, bytes), 1);
assert_eq!(gram_chunks(1_000_000, 4_000, bytes), 1);
assert_eq!(gram_chunks(50_000, 2_000, bytes), 4);
assert_eq!(gram_chunks(1_000_000, 100, bytes), GRAM_MAX_CHUNKS);
}
#[test]
fn test_gram_reduce_grid_within_dispatch_limit() {
let (max_x, max_y, _) = (
u32::from(u16::MAX),
u32::from(u16::MAX),
u32::from(u16::MAX),
);
assert!((20_000u32 * 20_000).div_ceil(GRAM_REDUCE_WG) > max_x);
for d in [4_096u32, 20_000, 32_768] {
let blocks = (d * d).div_ceil(GRAM_REDUCE_WG);
let (gx, gy) = grid_2d_limited(blocks, max_x.min(max_y)).unwrap();
assert!(gx <= max_x && gy <= max_y, "d = {d}");
assert!(gx as u64 * gy as u64 >= blocks as u64, "d = {d} uncovered");
}
}
#[test]
fn test_gram_shared_memory_within_budget() {
let Some(device) = try_device() else { return };
let client = cubecl::Device::from(device.clone()).client();
let budget = client.properties().hardware.max_shared_memory_size;
let footprint = 2 * GRAM_BK as usize * GRAM_SMEM_STRIDE as usize * size_of::<f32>();
assert!(
footprint <= budget,
"staging needs {footprint} B, device allows {budget} B"
);
assert!(
budget / footprint >= 2,
"only {} resident workgroup(s) at {footprint} B against a {budget} B budget",
budget / footprint
);
}
}