#![allow(missing_docs)]
use cubecl::prelude::*;
use cubecl::server::Handle;
use cubecl::std::tensor::TensorHandle;
use cubek::matmul::definition::{MatmulElems, MatmulPrecision};
use cubek::matmul::launch::{Strategy, launch_ref};
use cubek::std::InputBinding;
use faer::linalg::triangular_solve::solve_upper_triangular_in_place;
use faer::{Mat, Side};
use cubecl_utils_rs::prelude::*;
use crate::prelude::*;
use crate::utils::faer_parallelism;
const GRAM_TILE: u32 = 16;
const GRAM_ROWS_STEP: u32 = 8;
const GRAM_MAX_CHUNKS: usize = 64;
const GRAM_MIN_ROWS_PER_CHUNK: usize = 1024;
fn gram_chunks(n: usize) -> usize {
(n / GRAM_MIN_ROWS_PER_CHUNK).clamp(1, GRAM_MAX_CHUNKS)
}
fn wrap_handle<R: Runtime>(
handle: &Handle,
shape: [usize; 2],
transposed: bool,
dtype: cubecl::ir::StorageType,
) -> TensorHandle<R> {
let strides = if transposed {
vec![1usize, shape[0]]
} else {
vec![shape[1], 1usize]
};
TensorHandle::new(handle.clone(), shape.to_vec(), strides, dtype)
}
#[allow(clippy::too_many_arguments)]
pub fn dense_gemm<R, MP>(
a_handle: &Handle,
a_logical_shape: [usize; 2],
a_transposed: bool,
b_handle: &Handle,
b_logical_shape: [usize; 2],
b_tranposed: bool,
c_handle: &Handle,
c_shape: [usize; 2],
strategy: Option<Strategy>,
client: &ComputeClient<R>,
) -> Result<(), BixverseErrors>
where
R: Runtime,
MP: MatmulPrecision,
{
let mut dtypes = MatmulElems::new_deprecated::<MP>();
let strategy = strategy.unwrap_or(Strategy::Auto);
let a_tensor = wrap_handle::<R>(a_handle, a_logical_shape, a_transposed, dtypes.lhs_global);
let b_tensor = wrap_handle::<R>(b_handle, b_logical_shape, b_tranposed, dtypes.rhs_global);
let c_tensor = wrap_handle::<R>(c_handle, c_shape, false, dtypes.acc_global);
launch_ref(
&strategy,
client,
InputBinding::Normal(a_tensor.binding(), dtypes.lhs_global),
InputBinding::Normal(b_tensor.binding(), dtypes.rhs_global),
c_tensor.binding(),
&mut dtypes,
)
.map_err(|e| BixverseErrors::GpuMatmul(format!("{e:?}")))?;
Ok(())
}
fn r_inverse_from_gram<T>(g_row_major: &[T], s: usize) -> Result<Vec<T>, BixverseErrors>
where
T: BixverseFloat,
{
let g = Mat::<T>::from_fn(s, s, |i, j| g_row_major[i * s + j]);
let chol = g.llt(Side::Lower)?;
let l = chol.L();
let r = l.transpose().to_owned();
let mut r_inv = Mat::<T>::identity(s, s);
solve_upper_triangular_in_place(r.as_ref(), r_inv.as_mut(), faer_parallelism());
let mut out = vec![T::zero(); s * s];
for i in 0..s {
for j in 0..s {
out[i * s + j] = r_inv[(i, j)];
}
}
Ok(out)
}
#[cube(launch_unchecked)]
pub fn gram_partial<F: Float>(
y: &Tensor<F>,
partials: &mut Tensor<F>,
n: u32,
s: u32,
rows_per_chunk: u32,
) {
let ti = UNIT_POS_X;
let tj = UNIT_POS_Y;
let i0 = CUBE_POS_X * GRAM_TILE;
let j0 = CUBE_POS_Y * GRAM_TILE;
let chunk = CUBE_POS_Z;
let r_start = chunk * rows_per_chunk;
let mut r_end = r_start + rows_per_chunk;
if r_end > n {
r_end = n;
}
let mut sa = SharedMemory::<F>::new((GRAM_ROWS_STEP * GRAM_TILE) as usize);
let mut sb = SharedMemory::<F>::new((GRAM_ROWS_STEP * GRAM_TILE) as usize);
let lid = tj * GRAM_TILE + ti;
let n_threads = GRAM_TILE * GRAM_TILE;
let stage = GRAM_ROWS_STEP * GRAM_TILE;
let mut acc = F::new(0.0);
let mut r = r_start;
while r < r_end {
let mut li = lid;
while li < stage {
let rr = li / GRAM_TILE;
let cc = li % GRAM_TILE;
let row = r + rr;
let in_row = row < r_end;
if in_row && i0 + cc < s {
sa[li as usize] = y[(row * s + i0 + cc) as usize];
} else {
sa[li as usize] = F::new(0.0);
}
if in_row && j0 + cc < s {
sb[li as usize] = y[(row * s + j0 + cc) as usize];
} else {
sb[li as usize] = F::new(0.0);
}
li += n_threads;
}
sync_cube();
#[unroll]
for rr in 0..GRAM_ROWS_STEP {
acc += sa[(rr * GRAM_TILE + ti) as usize] * sb[(rr * GRAM_TILE + tj) as usize];
}
sync_cube();
r += GRAM_ROWS_STEP;
}
let gi = i0 + ti;
let gj = j0 + tj;
if gi < s && gj < s {
partials[(chunk * s * s + gi * s + gj) as usize] = acc;
}
}
#[cube(launch_unchecked)]
pub fn gram_reduce<F: Float>(partials: &Tensor<F>, g: &mut Tensor<F>, s: u32, n_chunks: u32) {
let idx = ABSOLUTE_POS_X;
let total = s * s;
if idx >= total {
terminate!();
}
let mut acc = F::new(0.0);
let mut c = 0u32;
while c < n_chunks {
acc += partials[(c * total + idx) as usize];
c += 1u32;
}
g[idx as usize] = acc;
}
fn gram<R, T>(
client: &ComputeClient<R>,
y: &GpuTensor<R, T>,
g_scratch: &GpuTensor<R, T>,
gram_partials: &GpuTensor<R, T>,
n: usize,
s: usize,
) -> Result<(), BixverseErrors>
where
R: Runtime,
T: cubecl::prelude::Float + cubecl::CubeElement,
{
let n_chunks = gram_chunks(n);
let rows_per_chunk = n.div_ceil(n_chunks) as u32;
let tiles = (s as u32).div_ceil(GRAM_TILE);
let total = (s * s) as u32;
let limits = GpuLimits::from_client(client);
let partial_count = checked_cube_count("gram_partial", tiles, tiles, n_chunks as u32, &limits)?;
let reduce_count = checked_cube_count("gram_reduce", total.div_ceil(256), 1, 1, &limits)?;
unsafe {
gram_partial::launch_unchecked::<T, R>(
client,
partial_count,
CubeDim::new_2d(GRAM_TILE, GRAM_TILE),
y.clone().into_tensor_arg(),
gram_partials.clone().into_tensor_arg(),
n as u32,
s as u32,
rows_per_chunk,
);
gram_reduce::launch_unchecked::<T, R>(
client,
reduce_count,
CubeDim::new_1d(256),
gram_partials.clone().into_tensor_arg(),
g_scratch.clone().into_tensor_arg(),
s as u32,
n_chunks as u32,
);
}
Ok(())
}
const TSMM_ROWS: u32 = 8;
const TSMM_COLS: u32 = 32;
const TSMM_K_MAX: u32 = 512;
const TSMM_UNROLL: u32 = 4;
#[cube(launch_unchecked)]
pub fn tall_skinny_mm<F: Float>(
a: &Tensor<F>,
b: &Tensor<F>,
c: &mut Tensor<F>,
n: u32,
k_dim: u32,
s: u32,
) {
let tid = UNIT_POS_X;
let tr = tid / TSMM_COLS;
let tc = tid % TSMM_COLS;
let n_threads = TSMM_ROWS * TSMM_COLS;
let blk = CUBE_POS_Y * CUBE_COUNT_X + CUBE_POS_X;
let row0 = blk * TSMM_ROWS;
let col0 = CUBE_POS_Z * TSMM_COLS;
let mut sa = SharedMemory::<F>::new((TSMM_ROWS * TSMM_K_MAX) as usize);
let stage = TSMM_ROWS * k_dim;
let mut li = tid;
while li < stage {
let rr = li / k_dim;
let cc = li % k_dim;
let row = row0 + rr;
if row < n {
sa[li as usize] = a[(row * k_dim + cc) as usize];
} else {
sa[li as usize] = F::new(0.0);
}
li += n_threads;
}
sync_cube();
let row = row0 + tr;
let col = col0 + tc;
if row < n && col < s {
let abase = tr * k_dim;
let mut acc = F::new(0.0);
let mut i = 0u32;
while i + TSMM_UNROLL <= k_dim {
#[unroll]
for u in 0..TSMM_UNROLL {
acc += sa[(abase + i + u) as usize] * b[((i + u) * s + col) as usize];
}
i += TSMM_UNROLL;
}
while i < k_dim {
acc += sa[(abase + i) as usize] * b[(i * s + col) as usize];
i += 1u32;
}
c[(row * s + col) as usize] = acc;
}
}
fn cholesky_qr_pass<R, T, MP>(
client: &ComputeClient<R>,
input: &GpuTensor<R, T>,
output: &GpuTensor<R, T>,
g_scratch: &GpuTensor<R, T>,
gram_partials: &GpuTensor<R, T>,
n: usize,
s: usize,
) -> Result<(), BixverseErrors>
where
R: Runtime,
T: cubecl::prelude::Float + cubecl::CubeElement + BixverseFloat,
MP: MatmulPrecision,
{
gram::<R, T>(client, input, g_scratch, gram_partials, n, s)?;
let g_host = g_scratch.clone().read(client)?;
let r_inv_host = r_inverse_from_gram(&g_host, s)?;
let r_inv_gpu = GpuTensor::<R, T>::from_slice(&r_inv_host, vec![s, s], client)?;
if (s as u32) <= TSMM_K_MAX {
let limits = GpuLimits::from_client(client);
let (gx, gy) = grid_2d((n as u32).div_ceil(TSMM_ROWS), &limits)?;
let count = checked_cube_count(
"tall_skinny_mm",
gx,
gy,
(s as u32).div_ceil(TSMM_COLS),
&limits,
)?;
unsafe {
tall_skinny_mm::launch_unchecked::<T, R>(
client,
count,
CubeDim::new_1d(TSMM_ROWS * TSMM_COLS),
input.clone().into_tensor_arg(),
r_inv_gpu.clone().into_tensor_arg(),
output.clone().into_tensor_arg(),
n as u32,
s as u32,
s as u32,
);
}
} else {
dense_gemm::<R, MP>(
input.handle(),
[n, s],
false,
r_inv_gpu.handle(),
[s, s],
false,
output.handle(),
[n, s],
None,
client,
)?;
}
Ok(())
}
pub fn cholesky_qr2<R, T, MP>(
client: &ComputeClient<R>,
y: &GpuTensor<R, T>,
q: &GpuTensor<R, T>,
q1_scratch: &GpuTensor<R, T>,
g_scratch: &GpuTensor<R, T>,
n: usize,
s: usize,
) -> Result<(), BixverseErrors>
where
R: Runtime,
T: cubecl::prelude::Float + cubecl::CubeElement + BixverseFloat,
MP: MatmulPrecision,
{
let gram_partials = GpuTensor::<R, T>::empty(vec![gram_chunks(n), s, s], client)?;
cholesky_qr_pass::<R, T, MP>(client, y, q1_scratch, g_scratch, &gram_partials, n, s)?;
cholesky_qr_pass::<R, T, MP>(client, q1_scratch, q, g_scratch, &gram_partials, n, s)?;
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 matmul_row_major(a: &[f32], b: &[f32], m: usize, k: usize, n: usize) -> Vec<f32> {
let mut c = vec![0.0f32; m * n];
for i in 0..m {
for j in 0..n {
let mut acc = 0.0f32;
for l in 0..k {
acc += a[i * k + l] * b[l * n + j];
}
c[i * n + j] = acc;
}
}
c
}
fn sinusoidal_tall_skinny(n: usize, s: usize) -> Vec<f32> {
(0..n * s)
.map(|i| {
let row = i / s;
let col = i % s;
let x = (row as f32 + 0.5) / n as f32;
(2.0 * std::f32::consts::PI * (col + 1) as f32 * x).sin()
})
.collect()
}
fn gram_host(q: &[f32], n: usize, s: usize) -> Vec<f32> {
let mut out = vec![0.0f32; s * s];
for i in 0..s {
for j in 0..s {
let mut acc = 0.0f32;
for k in 0..n {
acc += q[k * s + i] * q[k * s + j];
}
out[i * s + j] = acc;
}
}
out
}
fn assert_close_to_identity(mat: &[f32], s: usize, tol: f32) {
for i in 0..s {
for j in 0..s {
let want = if i == j { 1.0 } else { 0.0 };
let got = mat[i * s + j];
assert!(
(got - want).abs() < tol,
"({}, {}): {} != {}",
i,
j,
got,
want
);
}
}
}
#[test]
fn test_r_inverse_from_gram_round_trip() {
let s = 4;
let g: Vec<f32> = vec![
4.0, 1.0, 0.0, 0.0, 1.0, 3.0, 1.0, 0.0, 0.0, 1.0, 2.5, 0.5, 0.0, 0.0, 0.5, 1.5,
];
let r_inv = r_inverse_from_gram(&g, s).unwrap();
for i in 0..s {
for j in 0..i {
assert!(
r_inv[i * s + j].abs() < 1e-6,
"r_inv not upper triangular at ({}, {})",
i,
j,
);
}
}
let mut r_inv_t = vec![0.0f32; s * s];
for i in 0..s {
for j in 0..s {
r_inv_t[i * s + j] = r_inv[j * s + i];
}
}
let g_inv = matmul_row_major(&r_inv, &r_inv_t, s, s, s);
let prod = matmul_row_major(&g, &g_inv, s, s, s);
assert_close_to_identity(&prod, s, 1e-4);
}
#[test]
fn test_cholesky_qr2_orthogonality() {
let Some(device) = try_device() else { return };
let client = WgpuRuntime::client(&device);
let (n, s) = (200, 8);
let y_host = sinusoidal_tall_skinny(n, s);
let y = GpuTensor::<WgpuRuntime, f32>::from_slice(&y_host, vec![n, s], &client).unwrap();
let q =
GpuTensor::<WgpuRuntime, f32>::from_slice(&vec![0.0f32; n * s], vec![n, s], &client)
.unwrap();
let q1_scratch = GpuTensor::<WgpuRuntime, f32>::empty(vec![n, s], &client).unwrap();
let g_scratch = GpuTensor::<WgpuRuntime, f32>::empty(vec![s, s], &client).unwrap();
cholesky_qr2::<WgpuRuntime, f32, f32>(&client, &y, &q, &q1_scratch, &g_scratch, n, s)
.unwrap();
let q_host = q.read(&client).unwrap();
let qtq = gram_host(&q_host, n, s);
assert_close_to_identity(&qtq, s, 1e-4);
}
#[test]
fn test_tall_skinny_grid_within_dispatch_limit() {
let (max_x, max_y, max_z) = WgpuRuntime::max_cube_count();
let gz = 130u32.div_ceil(TSMM_COLS);
assert!((900_000u32).div_ceil(TSMM_ROWS) > max_x);
for n in [524_281u32, 1_000_000, 5_000_000] {
let blocks = n.div_ceil(TSMM_ROWS);
let (gx, gy) = grid_2d_limited(blocks, max_x.min(max_y)).unwrap();
assert!(gx <= max_x && gy <= max_y && gz <= max_z, "n = {n}");
assert!(gx as u64 * gy as u64 >= blocks as u64, "n = {n} uncovered");
}
}
#[test]
#[cfg(feature = "large_scale_diagnostics")]
fn test_cholesky_qr2_above_dispatch_limit() {
let Some(device) = try_device() else { return };
let client = WgpuRuntime::client(&device);
let (n, s) = (600_000, 4);
let y_host = sinusoidal_tall_skinny(n, s);
let y = GpuTensor::<WgpuRuntime, f32>::from_slice(&y_host, vec![n, s], &client).unwrap();
let q =
GpuTensor::<WgpuRuntime, f32>::from_slice(&vec![0.0f32; n * s], vec![n, s], &client)
.unwrap();
let q1_scratch = GpuTensor::<WgpuRuntime, f32>::empty(vec![n, s], &client).unwrap();
let g_scratch = GpuTensor::<WgpuRuntime, f32>::empty(vec![s, s], &client).unwrap();
cholesky_qr2::<WgpuRuntime, f32, f32>(&client, &y, &q, &q1_scratch, &g_scratch, n, s)
.unwrap();
let q_host = q.read(&client).unwrap();
assert!(
q_host.iter().any(|v| v.abs() > 1e-6),
"Q is all zeros, the GPU did no work"
);
assert_close_to_identity(&gram_host(&q_host, n, s), s, 1e-4);
}
}