use cubecl::Device;
use faer::{Mat, MatRef};
use crate::gpu::sc_gpu::knn_gpu::{KnnParamsGpu, dispatch_knn_query_gpu};
use crate::prelude::*;
use crate::single_cell::sc_batch_correction::fast_mnn::fast_mnn_embedding;
use crate::single_cell::sc_processing::knn::to_true_distances;
#[derive(Clone, Debug)]
pub struct FastMnnParamsGpu {
pub ndist: f32,
pub cos_norm: bool,
pub knn_params: KnnParamsGpu,
}
impl FastMnnParamsGpu {
pub fn new() -> Self {
Self {
ndist: 3.0,
cos_norm: true,
knn_params: KnnParamsGpu::new(),
}
}
}
impl Default for FastMnnParamsGpu {
fn default() -> Self {
Self::new()
}
}
pub fn fast_mnn_gpu(
embd: MatRef<f32>,
batch_labels: &[usize],
params: &FastMnnParamsGpu,
seed: usize,
device: Device,
verbose: usize,
) -> Result<Mat<f32>, BixverseErrors>
where
Device: Clone,
{
let verbosity = parse_verbosity_level(verbose);
if verbosity.normal_verbosity() {
println!("fastMNN (GPU): correcting the embedding.")
}
let search = |query: MatRef<f32>, reference: MatRef<f32>, k: usize| -> ScKnnResults {
let (indices, dist) = dispatch_knn_query_gpu(
reference,
query,
k,
¶ms.knn_params,
device.clone(),
seed,
true,
verbosity.detailed_verbosity(),
)?;
let mut dist = dist.expect("distances were requested from the index");
to_true_distances(&mut dist, ¶ms.knn_params.ann_dist);
Ok((indices, dist))
};
fast_mnn_embedding(
&embd.to_owned(),
batch_labels,
params.cos_norm,
params.knn_params.k,
params.ndist,
&search,
verbose,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::single_cell::sc_batch_correction::batch_utils::batch_knn_search;
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 batched_data(n: usize, dim: usize, n_batches: usize) -> (Mat<f32>, Vec<usize>) {
let hash = |a: usize, b: usize| -> f32 {
let mut h = (a as u64)
.wrapping_mul(0x9E37_79B9_7F4A_7C15)
.wrapping_add((b as u64).wrapping_mul(0xC2B2_AE3D_27D4_EB4F));
h ^= h >> 29;
h = h.wrapping_mul(0xBF58_476D_1CE4_E5B9);
h ^= h >> 32;
((h >> 40) as f32) / 16_777_216.0 - 0.5
};
let labels: Vec<usize> = (0..n).map(|i| i % n_batches).collect();
let mat = Mat::from_fn(n, dim, |i, j| {
let centre = hash((i / n_batches) % 5 + 1000, j) * 20.0;
let offset = hash(labels[i] + 2000, j) * 4.0;
centre + offset + hash(i, j)
});
(mat, labels)
}
#[test]
fn test_fast_mnn_gpu_matches_cpu_exhaustive() {
let Some(device) = try_device() else {
eprintln!("No GPU device, skipping");
return;
};
let (embd, labels) = batched_data(900, 16, 3);
let params = FastMnnParamsGpu::new();
let gpu = fast_mnn_gpu(
embd.as_ref(),
&labels,
¶ms,
42,
device.clone().into(),
0,
)
.unwrap();
let mut cpu_knn = KnnParams::new();
cpu_knn.knn_method = "exhaustive".to_string();
let search =
|q: MatRef<f32>, r: MatRef<f32>, k: usize| batch_knn_search(q, r, k, &cpu_knn, 42, 0);
let cpu = fast_mnn_embedding(
&embd,
&labels,
params.cos_norm,
params.knn_params.k,
params.ndist,
&search,
0,
)
.unwrap();
assert_eq!(gpu.nrows(), cpu.nrows());
assert_eq!(gpu.ncols(), cpu.ncols());
let max_diff = (0..cpu.nrows())
.flat_map(|i| (0..cpu.ncols()).map(move |j| (i, j)))
.map(|(i, j)| (gpu[(i, j)] - cpu[(i, j)]).abs())
.fold(0.0_f32, f32::max);
assert!(max_diff < 1e-3, "max abs diff GPU vs CPU: {max_diff}");
}
#[test]
fn test_fast_mnn_gpu_single_batch_errors() {
let Some(device) = try_device() else {
eprintln!("No GPU device, skipping");
return;
};
let (embd, _) = batched_data(60, 8, 1);
let labels = vec![0; 60];
let res = fast_mnn_gpu(
embd.as_ref(),
&labels,
&FastMnnParamsGpu::new(),
42,
device.clone().into(),
0,
);
assert!(matches!(
res,
Err(BixverseErrors::NeedAtLeastTwoBatches { n_batches: 1 })
));
}
}