use ann_search_rs::gpu::tensor::GpuTensor;
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 crate::prelude::*;
use crate::utils::faer_parallelism;
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],
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, false, 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)
}
fn cholesky_qr_pass<R, T, MP>(
client: &ComputeClient<R>,
input: &GpuTensor<R, T>,
output: &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,
{
dense_gemm::<R, MP>(
input.handle(),
[s, n],
true, input.handle(),
[n, s],
g_scratch.handle(),
[s, s],
None,
client,
)?;
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);
dense_gemm::<R, MP>(
input.handle(),
[n, s],
false,
r_inv_gpu.handle(),
[s, s],
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>,
n: usize,
s: usize,
) -> Result<(), BixverseErrors>
where
R: Runtime,
T: cubecl::prelude::Float + cubecl::CubeElement + BixverseFloat,
MP: MatmulPrecision,
{
let g_scratch = GpuTensor::<R, T>::empty(vec![s, s], client);
let q1_scratch = GpuTensor::<R, T>::empty(vec![n, s], client);
cholesky_qr_pass::<R, T, MP>(client, y, &q1_scratch, &g_scratch, n, s)?;
cholesky_qr_pass::<R, T, MP>(client, &q1_scratch, q, &g_scratch, 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 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: 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();
let y = GpuTensor::<WgpuRuntime, f32>::from_slice(&y_host, vec![n, s], &client);
let q =
GpuTensor::<WgpuRuntime, f32>::from_slice(&vec![0.0f32; n * s], vec![n, s], &client);
cholesky_qr2::<WgpuRuntime, f32, f32>(&client, &y, &q, n, s).unwrap();
let q_host = q.read(&client).unwrap();
let mut qtq = 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_host[k * s + i] * q_host[k * s + j];
}
qtq[i * s + j] = acc;
}
}
assert_close_to_identity(&qtq, s, 1e-4);
}
}