pub struct GpuSearchBackend {
#[cfg(feature = "gpu")]
accelerator: Option<rustyhdf5_gpu::GpuAccelerator>,
dim: usize,
threshold: usize,
num_vectors: usize,
}
impl GpuSearchBackend {
pub fn try_init(
vectors: &[Vec<f32>],
norms: &[f32],
dim: usize,
threshold: usize,
) -> Self {
#[cfg(feature = "gpu")]
{
if vectors.len() >= threshold {
match rustyhdf5_gpu::GpuAccelerator::new() {
Ok(mut accel) => {
let flat: Vec<f32> =
vectors.iter().flat_map(|v| v.iter().copied()).collect();
if accel.upload_vectors(&flat, dim).is_ok()
&& accel.upload_norms(norms).is_ok()
{
return Self {
accelerator: Some(accel),
dim,
threshold,
num_vectors: vectors.len(),
};
}
}
Err(e) => {
log_gpu_fallback(&e.to_string());
}
}
}
Self {
accelerator: None,
dim,
threshold,
num_vectors: vectors.len(),
}
}
#[cfg(not(feature = "gpu"))]
{
let _ = (vectors, norms);
Self {
dim,
threshold,
num_vectors: 0,
}
}
}
pub fn is_available(&self) -> bool {
#[cfg(feature = "gpu")]
{
self.accelerator.is_some()
}
#[cfg(not(feature = "gpu"))]
{
false
}
}
pub fn dim(&self) -> usize {
self.dim
}
pub fn threshold(&self) -> usize {
self.threshold
}
pub fn re_upload(&mut self, vectors: &[Vec<f32>], norms: &[f32]) {
self.num_vectors = vectors.len();
#[cfg(feature = "gpu")]
{
if let Some(ref mut accel) = self.accelerator {
if vectors.len() >= self.threshold {
let flat: Vec<f32> =
vectors.iter().flat_map(|v| v.iter().copied()).collect();
if accel.upload_vectors(&flat, self.dim).is_err()
|| accel.upload_norms(norms).is_err()
{
self.accelerator = None;
}
} else {
self.accelerator = None;
}
return;
}
if vectors.len() >= self.threshold {
if let Ok(mut accel) = rustyhdf5_gpu::GpuAccelerator::new() {
let flat: Vec<f32> =
vectors.iter().flat_map(|v| v.iter().copied()).collect();
if accel.upload_vectors(&flat, self.dim).is_ok()
&& accel.upload_norms(norms).is_ok()
{
self.accelerator = Some(accel);
}
}
}
}
#[cfg(not(feature = "gpu"))]
{
let _ = (vectors, norms);
}
}
pub fn search_cosine(
&self,
query: &[f32],
vectors: &[Vec<f32>],
norms: &[f32],
tombstones: &[u8],
k: usize,
) -> Vec<(usize, f32)> {
#[cfg(feature = "gpu")]
{
if let Some(ref accel) = self.accelerator {
match accel.cosine_search(query, k.min(self.num_vectors.max(1))) {
Ok(mut results) => {
results.retain(|(i, _)| {
*i < tombstones.len() && tombstones[*i] == 0
});
results.truncate(k);
return results;
}
Err(_) => {
}
}
}
}
cpu_fallback_cosine(query, vectors, norms, tombstones, k)
}
pub fn search_l2(
&self,
query: &[f32],
vectors: &[Vec<f32>],
tombstones: &[u8],
k: usize,
) -> Vec<(usize, f32)> {
#[cfg(feature = "gpu")]
{
if let Some(ref accel) = self.accelerator {
match accel.l2_search(query, k.min(self.num_vectors.max(1))) {
Ok(mut results) => {
results.retain(|(i, _)| {
*i < tombstones.len() && tombstones[*i] == 0
});
results.truncate(k);
return results;
}
Err(_) => {
}
}
}
}
cpu_fallback_l2(query, vectors, tombstones, k)
}
pub fn device_info(&self) -> String {
#[cfg(feature = "gpu")]
{
if let Some(ref accel) = self.accelerator {
return accel.device_info().to_string();
}
}
"none".to_string()
}
}
pub fn detect_gpu() -> bool {
#[cfg(feature = "gpu")]
{
rustyhdf5_gpu::GpuAccelerator::is_available()
}
#[cfg(not(feature = "gpu"))]
{
false
}
}
#[cfg(feature = "gpu")]
fn log_gpu_fallback(reason: &str) {
eprintln!("[edgehdf5-memory] GPU init failed, falling back to CPU: {reason}");
}
fn cpu_fallback_cosine(
query: &[f32],
vectors: &[Vec<f32>],
norms: &[f32],
tombstones: &[u8],
k: usize,
) -> Vec<(usize, f32)> {
let query_norm = rustyhdf5_accel::vector_norm(query);
if query_norm == 0.0 {
return Vec::new();
}
let mut results: Vec<(usize, f32)> = Vec::with_capacity(vectors.len());
for (i, vec) in vectors.iter().enumerate() {
if i < tombstones.len() && tombstones[i] != 0 {
continue;
}
let vec_norm = if i < norms.len() {
norms[i]
} else {
rustyhdf5_accel::vector_norm(vec)
};
let score = crate::cosine_similarity_prenorm(query, query_norm, vec, vec_norm);
results.push((i, score));
}
results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
results
}
fn cpu_fallback_l2(
query: &[f32],
vectors: &[Vec<f32>],
tombstones: &[u8],
k: usize,
) -> Vec<(usize, f32)> {
let mut results: Vec<(usize, f32)> = Vec::with_capacity(vectors.len());
for (i, vec) in vectors.iter().enumerate() {
if i < tombstones.len() && tombstones[i] != 0 {
continue;
}
let dist = rustyhdf5_accel::l2_distance(query, vec);
results.push((i, dist));
}
results.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal));
results.truncate(k);
results
}
#[cfg(test)]
mod tests {
use super::*;
fn make_vectors(n: usize, dim: usize, seed: u32) -> Vec<Vec<f32>> {
let mut s = seed;
let mut next = || -> f32 {
s = s.wrapping_mul(1103515245).wrapping_add(12345);
((s >> 16) as f32) / 65536.0 - 0.5
};
(0..n).map(|_| (0..dim).map(|_| next()).collect()).collect()
}
#[test]
fn gpu_detect_default_status() {
let detected = detect_gpu();
let _ = detected;
}
#[test]
fn gpu_backend_fallback_when_unavailable() {
let vectors = make_vectors(100, 32, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let backend = GpuSearchBackend::try_init(&vectors, &norms, 32, 50);
let tombstones = vec![0u8; 100];
let query = vectors[0].clone();
let results = backend.search_cosine(&query, &vectors, &norms, &tombstones, 10);
assert!(!results.is_empty());
assert!(results.len() <= 10);
assert_eq!(results[0].0, 0);
assert!((results[0].1 - 1.0).abs() < 1e-5);
}
#[test]
fn gpu_backend_below_threshold() {
let vectors = make_vectors(10, 32, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let backend = GpuSearchBackend::try_init(&vectors, &norms, 32, 100);
assert!(!backend.is_available());
assert_eq!(backend.dim(), 32);
assert_eq!(backend.threshold(), 100);
}
#[test]
fn gpu_cosine_cpu_fallback_matches() {
let vectors = make_vectors(200, 64, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let tombstones = vec![0u8; 200];
let query = vectors[5].clone();
let fallback = cpu_fallback_cosine(&query, &vectors, &norms, &tombstones, 10);
let backend = GpuSearchBackend::try_init(&vectors, &norms, 64, 50);
let backend_results =
backend.search_cosine(&query, &vectors, &norms, &tombstones, 10);
assert_eq!(fallback.len(), backend_results.len());
for (f, b) in fallback.iter().zip(&backend_results) {
assert_eq!(f.0, b.0);
assert!((f.1 - b.1).abs() < 1e-6);
}
}
#[test]
fn gpu_l2_search_returns_nearest() {
let vectors = vec![
vec![0.0, 0.0, 0.0],
vec![1.0, 0.0, 0.0],
vec![10.0, 10.0, 10.0],
];
let tombstones = vec![0u8; 3];
let query = vec![0.1, 0.0, 0.0];
let results = cpu_fallback_l2(&query, &vectors, &tombstones, 3);
assert_eq!(results[0].0, 0);
assert_eq!(results[1].0, 1);
assert_eq!(results[2].0, 2);
}
#[test]
fn gpu_search_respects_tombstones() {
let vectors = make_vectors(50, 16, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let mut tombstones = vec![0u8; 50];
tombstones[0] = 1;
tombstones[1] = 1;
let query = vectors[2].clone();
let results = cpu_fallback_cosine(&query, &vectors, &norms, &tombstones, 50);
assert!(results.iter().all(|r| r.0 != 0 && r.0 != 1));
}
#[test]
fn gpu_re_upload_updates_data() {
let vectors = make_vectors(10, 16, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let mut backend = GpuSearchBackend::try_init(&vectors, &norms, 16, 5);
let vectors2 = make_vectors(20, 16, 77);
let norms2: Vec<f32> = vectors2
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
backend.re_upload(&vectors2, &norms2);
let tombstones = vec![0u8; 20];
let query = vectors2[0].clone();
let results = backend.search_cosine(&query, &vectors2, &norms2, &tombstones, 5);
assert!(!results.is_empty());
}
#[test]
fn gpu_l2_search_respects_tombstones() {
let vectors = vec![vec![0.0, 0.0], vec![1.0, 0.0], vec![2.0, 0.0]];
let mut tombstones = vec![0u8; 3];
tombstones[0] = 1;
let query = vec![0.0, 0.0];
let results = cpu_fallback_l2(&query, &vectors, &tombstones, 3);
assert!(results.iter().all(|r| r.0 != 0));
assert_eq!(results[0].0, 1); }
#[test]
fn device_info_returns_string() {
let vectors = make_vectors(10, 16, 42);
let norms: Vec<f32> = vectors
.iter()
.map(|v| rustyhdf5_accel::vector_norm(v))
.collect();
let backend = GpuSearchBackend::try_init(&vectors, &norms, 16, 5);
let info = backend.device_info();
assert!(!info.is_empty());
}
}