1use std::time::Instant;
7
8use crate::vector_search;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum SearchStrategy {
22 Scalar,
24 SimdBruteForce,
26 Blas,
28 Accelerate,
30 RayonParallel,
32 Gpu,
34 IvfPq,
36}
37
38impl std::fmt::Display for SearchStrategy {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 match self {
41 SearchStrategy::Scalar => write!(f, "scalar"),
42 SearchStrategy::SimdBruteForce => write!(f, "simd"),
43 SearchStrategy::Blas => write!(f, "blas"),
44 SearchStrategy::Accelerate => write!(f, "accelerate"),
45 SearchStrategy::RayonParallel => write!(f, "rayon"),
46 SearchStrategy::Gpu => write!(f, "gpu"),
47 SearchStrategy::IvfPq => write!(f, "ivf-pq"),
48 }
49 }
50}
51
52#[derive(Debug, Clone)]
54pub struct SearchMetrics {
55 pub strategy: String,
57 pub search_time_us: u64,
59 pub candidates_scanned: usize,
61 pub rerank_time_us: Option<u64>,
63 pub backend: String,
65}
66
67#[derive(Debug, Clone, Copy)]
69pub struct HardwareCapabilities {
70 pub rayon_available: bool,
72 pub gpu_available: bool,
74 pub blas_available: bool,
76 pub accelerate_available: bool,
78}
79
80impl HardwareCapabilities {
81 pub fn detect() -> Self {
83 Self {
84 rayon_available: cfg!(feature = "parallel"),
85 gpu_available: {
86 #[cfg(feature = "gpu")]
87 {
88 rustyhdf5_gpu::GpuAccelerator::is_available()
89 }
90 #[cfg(not(feature = "gpu"))]
91 {
92 false
93 }
94 },
95 blas_available: cfg!(feature = "fast-math"),
96 accelerate_available: cfg!(any(feature = "accelerate", feature = "openblas")),
97 }
98 }
99}
100
101pub fn active_backend_name(gpu_active: bool) -> String {
103 if gpu_active {
104 return "gpu".to_string();
105 }
106 let backend = rustyhdf5_accel::detect_backend();
107 format!("{backend:?}").to_lowercase()
108}
109
110pub fn auto_select_strategy(num_vectors: usize, hw: &HardwareCapabilities) -> SearchStrategy {
119 if num_vectors > 500_000 {
120 return SearchStrategy::IvfPq;
121 }
122
123 if num_vectors > 50_000 {
124 if hw.accelerate_available {
125 return SearchStrategy::Accelerate;
126 }
127 if hw.blas_available {
128 return SearchStrategy::Blas;
129 }
130 if hw.gpu_available {
131 return SearchStrategy::Gpu;
132 }
133 return SearchStrategy::IvfPq;
134 }
135
136 if num_vectors > 10_000 {
137 if hw.accelerate_available {
138 return SearchStrategy::Accelerate;
139 }
140 if hw.blas_available {
141 return SearchStrategy::Blas;
142 }
143 if hw.rayon_available {
144 return SearchStrategy::RayonParallel;
145 }
146 if hw.gpu_available {
147 return SearchStrategy::Gpu;
148 }
149 return SearchStrategy::SimdBruteForce;
150 }
151
152 if num_vectors >= 1_000 {
153 if hw.accelerate_available {
154 return SearchStrategy::Accelerate;
155 }
156 if hw.blas_available {
157 return SearchStrategy::Blas;
158 }
159 return SearchStrategy::SimdBruteForce;
160 }
161
162 SearchStrategy::Scalar
163}
164
165#[allow(clippy::too_many_arguments)]
171pub fn search_with_metrics(
172 query: &[f32],
173 vectors: &[Vec<f32>],
174 norms: &[f32],
175 tombstones: &[u8],
176 k: usize,
177 strategy: SearchStrategy,
178 #[cfg(feature = "gpu")] gpu_backend: Option<&crate::gpu_search::GpuSearchBackend>,
179 #[cfg(not(feature = "gpu"))] _gpu_backend: Option<&()>,
180) -> (Vec<(usize, f32)>, SearchMetrics) {
181 let start = Instant::now();
182 let active_count = tombstones.iter().filter(|&&t| t == 0).count();
183
184 let gpu_active;
185 let results = match strategy {
186 SearchStrategy::Scalar => {
187 gpu_active = false;
188 scalar_search(query, vectors, tombstones, k)
189 }
190 SearchStrategy::SimdBruteForce => {
191 gpu_active = false;
192 let all = vector_search::cosine_similarity_batch_prenorm(
193 query, vectors, norms, tombstones,
194 );
195 vector_search::top_k(all, k)
196 }
197 SearchStrategy::Blas => {
198 gpu_active = false;
199 #[cfg(feature = "fast-math")]
200 {
201 crate::blas_search::blas_cosine_batch(query, vectors, norms, tombstones, k)
202 }
203 #[cfg(not(feature = "fast-math"))]
204 {
205 let all = vector_search::cosine_similarity_batch_prenorm(
206 query, vectors, norms, tombstones,
207 );
208 vector_search::top_k(all, k)
209 }
210 }
211 SearchStrategy::Accelerate => {
212 gpu_active = false;
213 #[cfg(any(feature = "accelerate", feature = "openblas"))]
214 {
215 crate::accelerate_search::accelerate_cosine_batch_vecs(
216 query, vectors, norms, tombstones, k,
217 )
218 }
219 #[cfg(not(any(feature = "accelerate", feature = "openblas")))]
220 {
221 let all = vector_search::cosine_similarity_batch_prenorm(
222 query, vectors, norms, tombstones,
223 );
224 vector_search::top_k(all, k)
225 }
226 }
227 SearchStrategy::RayonParallel => {
228 gpu_active = false;
229 #[cfg(feature = "parallel")]
230 {
231 vector_search::parallel_cosine_batch_prenorm(
232 query, vectors, norms, tombstones, k,
233 )
234 }
235 #[cfg(not(feature = "parallel"))]
236 {
237 let all = vector_search::cosine_similarity_batch_prenorm(
238 query, vectors, norms, tombstones,
239 );
240 vector_search::top_k(all, k)
241 }
242 }
243 SearchStrategy::Gpu => {
244 #[cfg(feature = "gpu")]
245 {
246 if let Some(backend) = gpu_backend {
247 gpu_active = backend.is_available();
248 backend.search_cosine(query, vectors, norms, tombstones, k)
249 } else {
250 gpu_active = false;
251 let all = vector_search::cosine_similarity_batch_prenorm(
252 query, vectors, norms, tombstones,
253 );
254 vector_search::top_k(all, k)
255 }
256 }
257 #[cfg(not(feature = "gpu"))]
258 {
259 gpu_active = false;
260 let all = vector_search::cosine_similarity_batch_prenorm(
261 query, vectors, norms, tombstones,
262 );
263 vector_search::top_k(all, k)
264 }
265 }
266 SearchStrategy::IvfPq => {
267 gpu_active = false;
268 let all = vector_search::cosine_similarity_batch_prenorm(
271 query, vectors, norms, tombstones,
272 );
273 vector_search::top_k(all, k)
274 }
275 };
276
277 let elapsed = start.elapsed();
278 let metrics = SearchMetrics {
279 strategy: strategy.to_string(),
280 search_time_us: elapsed.as_micros() as u64,
281 candidates_scanned: active_count,
282 rerank_time_us: None,
283 backend: active_backend_name(gpu_active),
284 };
285
286 (results, metrics)
287}
288
289fn scalar_search(
291 query: &[f32],
292 vectors: &[Vec<f32>],
293 tombstones: &[u8],
294 k: usize,
295) -> Vec<(usize, f32)> {
296 let query_norm = rustyhdf5_accel::vector_norm(query);
297 if query_norm == 0.0 {
298 return Vec::new();
299 }
300
301 let mut results: Vec<(usize, f32)> = Vec::with_capacity(vectors.len());
302
303 for (i, vec) in vectors.iter().enumerate() {
304 if i < tombstones.len() && tombstones[i] != 0 {
305 continue;
306 }
307 let vec_norm = rustyhdf5_accel::vector_norm(vec);
310 let score = crate::cosine_similarity_prenorm(query, query_norm, vec, vec_norm);
311 results.push((i, score));
312 }
313
314 results.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
315 results.truncate(k);
316 results
317}
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322
323 fn make_vectors(n: usize, dim: usize, seed: u32) -> Vec<Vec<f32>> {
324 let mut s = seed;
325 let mut next = || -> f32 {
326 s = s.wrapping_mul(1103515245).wrapping_add(12345);
327 ((s >> 16) as f32) / 65536.0 - 0.5
328 };
329 (0..n).map(|_| (0..dim).map(|_| next()).collect()).collect()
330 }
331
332 #[test]
335 fn strategy_scalar_under_1k() {
336 let hw = HardwareCapabilities {
337 rayon_available: false,
338 gpu_available: false,
339 blas_available: false,
340 accelerate_available: false,
341 };
342 assert_eq!(auto_select_strategy(0, &hw), SearchStrategy::Scalar);
343 assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
344 assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
345 }
346
347 #[test]
348 fn strategy_simd_1k_to_10k() {
349 let hw = HardwareCapabilities {
350 rayon_available: false,
351 gpu_available: false,
352 blas_available: false,
353 accelerate_available: false,
354 };
355 assert_eq!(
356 auto_select_strategy(1_000, &hw),
357 SearchStrategy::SimdBruteForce
358 );
359 assert_eq!(
360 auto_select_strategy(5_000, &hw),
361 SearchStrategy::SimdBruteForce
362 );
363 assert_eq!(
364 auto_select_strategy(10_000, &hw),
365 SearchStrategy::SimdBruteForce
366 );
367 }
368
369 #[test]
370 fn strategy_rayon_10k_to_50k_when_available() {
371 let hw = HardwareCapabilities {
372 rayon_available: true,
373 gpu_available: false,
374 blas_available: false,
375 accelerate_available: false,
376 };
377 assert_eq!(
378 auto_select_strategy(10_001, &hw),
379 SearchStrategy::RayonParallel
380 );
381 assert_eq!(
382 auto_select_strategy(30_000, &hw),
383 SearchStrategy::RayonParallel
384 );
385 assert_eq!(
386 auto_select_strategy(50_000, &hw),
387 SearchStrategy::RayonParallel
388 );
389 }
390
391 #[test]
392 fn strategy_gpu_10k_to_50k_when_no_rayon() {
393 let hw = HardwareCapabilities {
394 rayon_available: false,
395 gpu_available: true,
396 blas_available: false,
397 accelerate_available: false,
398 };
399 assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Gpu);
400 assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Gpu);
401 }
402
403 #[test]
404 fn strategy_gpu_50k_to_500k() {
405 let hw = HardwareCapabilities {
406 rayon_available: true,
407 gpu_available: true,
408 blas_available: false,
409 accelerate_available: false,
410 };
411 assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::Gpu);
412 assert_eq!(auto_select_strategy(200_000, &hw), SearchStrategy::Gpu);
413 assert_eq!(auto_select_strategy(500_000, &hw), SearchStrategy::Gpu);
414 }
415
416 #[test]
417 fn strategy_ivfpq_over_500k() {
418 let hw = HardwareCapabilities {
419 rayon_available: true,
420 gpu_available: true,
421 blas_available: true,
422 accelerate_available: true,
423 };
424 assert_eq!(auto_select_strategy(500_001, &hw), SearchStrategy::IvfPq);
425 assert_eq!(auto_select_strategy(1_000_000, &hw), SearchStrategy::IvfPq);
426 }
427
428 #[test]
429 fn strategy_ivfpq_fallback_50k_no_gpu() {
430 let hw = HardwareCapabilities {
431 rayon_available: false,
432 gpu_available: false,
433 blas_available: false,
434 accelerate_available: false,
435 };
436 assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::IvfPq);
437 }
438
439 #[test]
440 fn strategy_simd_fallback_10k_no_parallel_no_gpu() {
441 let hw = HardwareCapabilities {
442 rayon_available: false,
443 gpu_available: false,
444 blas_available: false,
445 accelerate_available: false,
446 };
447 assert_eq!(
448 auto_select_strategy(15_000, &hw),
449 SearchStrategy::SimdBruteForce
450 );
451 }
452
453 #[test]
456 fn strategy_display_names() {
457 assert_eq!(SearchStrategy::Scalar.to_string(), "scalar");
458 assert_eq!(SearchStrategy::SimdBruteForce.to_string(), "simd");
459 assert_eq!(SearchStrategy::Blas.to_string(), "blas");
460 assert_eq!(SearchStrategy::Accelerate.to_string(), "accelerate");
461 assert_eq!(SearchStrategy::RayonParallel.to_string(), "rayon");
462 assert_eq!(SearchStrategy::Gpu.to_string(), "gpu");
463 assert_eq!(SearchStrategy::IvfPq.to_string(), "ivf-pq");
464 }
465
466 #[test]
469 fn search_metrics_strategy_name() {
470 let metrics = SearchMetrics {
471 strategy: "simd".to_string(),
472 search_time_us: 100,
473 candidates_scanned: 1000,
474 rerank_time_us: None,
475 backend: "neon".to_string(),
476 };
477 assert_eq!(metrics.strategy, "simd");
478 assert_eq!(metrics.candidates_scanned, 1000);
479 assert_eq!(metrics.backend, "neon");
480 }
481
482 #[test]
485 fn search_with_metrics_scalar() {
486 let vectors = make_vectors(50, 16, 42);
487 let norms: Vec<f32> = vectors
488 .iter()
489 .map(|v| rustyhdf5_accel::vector_norm(v))
490 .collect();
491 let tombstones = vec![0u8; 50];
492 let query = vectors[0].clone();
493
494 let (results, metrics) = search_with_metrics(
495 &query,
496 &vectors,
497 &norms,
498 &tombstones,
499 5,
500 SearchStrategy::Scalar,
501 None,
502 );
503
504 assert_eq!(results.len(), 5);
505 assert_eq!(metrics.strategy, "scalar");
506 assert!(metrics.search_time_us > 0 || metrics.candidates_scanned > 0);
507 assert_eq!(metrics.candidates_scanned, 50);
508 assert!(metrics.rerank_time_us.is_none());
509 assert!(!metrics.backend.is_empty());
510 assert_eq!(results[0].0, 0);
512 }
513
514 #[test]
515 fn search_with_metrics_simd() {
516 let vectors = make_vectors(100, 32, 42);
517 let norms: Vec<f32> = vectors
518 .iter()
519 .map(|v| rustyhdf5_accel::vector_norm(v))
520 .collect();
521 let tombstones = vec![0u8; 100];
522 let query = vectors[0].clone();
523
524 let (results, metrics) = search_with_metrics(
525 &query,
526 &vectors,
527 &norms,
528 &tombstones,
529 10,
530 SearchStrategy::SimdBruteForce,
531 None,
532 );
533
534 assert_eq!(metrics.strategy, "simd");
535 assert!(!results.is_empty());
536 assert_eq!(results[0].0, 0);
537 }
538
539 #[test]
540 fn search_with_metrics_timing_nonzero() {
541 let vectors = make_vectors(1000, 64, 42);
542 let norms: Vec<f32> = vectors
543 .iter()
544 .map(|v| rustyhdf5_accel::vector_norm(v))
545 .collect();
546 let tombstones = vec![0u8; 1000];
547 let query = vectors[0].clone();
548
549 let (_, metrics) = search_with_metrics(
550 &query,
551 &vectors,
552 &norms,
553 &tombstones,
554 10,
555 SearchStrategy::SimdBruteForce,
556 None,
557 );
558
559 assert!(metrics.search_time_us < 1_000_000); assert_eq!(metrics.candidates_scanned, 1000);
562 }
563
564 #[test]
565 fn search_with_metrics_results_match_direct() {
566 let vectors = make_vectors(200, 32, 42);
567 let norms: Vec<f32> = vectors
568 .iter()
569 .map(|v| rustyhdf5_accel::vector_norm(v))
570 .collect();
571 let tombstones = vec![0u8; 200];
572 let query = vectors[3].clone();
573
574 let (results, _) = search_with_metrics(
575 &query,
576 &vectors,
577 &norms,
578 &tombstones,
579 10,
580 SearchStrategy::SimdBruteForce,
581 None,
582 );
583
584 let direct = vector_search::cosine_similarity_batch_prenorm(
585 &query, &vectors, &norms, &tombstones,
586 );
587 let direct_top = vector_search::top_k(direct, 10);
588
589 assert_eq!(results.len(), direct_top.len());
590 for (r, d) in results.iter().zip(&direct_top) {
591 assert_eq!(r.0, d.0);
592 assert!((r.1 - d.1).abs() < 1e-6);
593 }
594 }
595
596 #[test]
597 fn search_with_metrics_respects_tombstones() {
598 let vectors = make_vectors(100, 16, 42);
599 let norms: Vec<f32> = vectors
600 .iter()
601 .map(|v| rustyhdf5_accel::vector_norm(v))
602 .collect();
603 let mut tombstones = vec![0u8; 100];
604 tombstones[0] = 1;
605 tombstones[1] = 1;
606
607 let query = vectors[2].clone();
608 let (results, metrics) = search_with_metrics(
609 &query,
610 &vectors,
611 &norms,
612 &tombstones,
613 100,
614 SearchStrategy::Scalar,
615 None,
616 );
617
618 assert!(results.iter().all(|r| r.0 != 0 && r.0 != 1));
619 assert_eq!(metrics.candidates_scanned, 98);
620 }
621
622 #[test]
623 fn hardware_capabilities_detect() {
624 let hw = HardwareCapabilities::detect();
625 let _ = hw.rayon_available;
627 let _ = hw.gpu_available;
628 }
629
630 #[test]
631 fn active_backend_name_returns_valid() {
632 let name = active_backend_name(false);
633 assert!(!name.is_empty());
634 let valid = ["neon", "avx2", "avx512", "sse4", "wasmsimd128", "scalar"];
636 assert!(
637 valid.iter().any(|v| name.contains(v)),
638 "unexpected backend: {name}"
639 );
640 }
641
642 #[test]
643 fn search_metrics_has_backend_field() {
644 let vectors = make_vectors(50, 16, 42);
645 let norms: Vec<f32> = vectors
646 .iter()
647 .map(|v| rustyhdf5_accel::vector_norm(v))
648 .collect();
649 let tombstones = vec![0u8; 50];
650 let query = vectors[0].clone();
651
652 let (_, metrics) = search_with_metrics(
653 &query,
654 &vectors,
655 &norms,
656 &tombstones,
657 5,
658 SearchStrategy::Scalar,
659 None,
660 );
661
662 assert!(!metrics.backend.is_empty());
663 }
664
665 #[test]
668 fn strategy_blas_preferred_1k_to_100k() {
669 let hw = HardwareCapabilities {
670 rayon_available: true,
671 gpu_available: true,
672 blas_available: true,
673 accelerate_available: false,
674 };
675 assert_eq!(auto_select_strategy(1_000, &hw), SearchStrategy::Blas);
677 assert_eq!(auto_select_strategy(5_000, &hw), SearchStrategy::Blas);
678 assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Blas);
679 assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Blas);
680 assert_eq!(auto_select_strategy(100_000, &hw), SearchStrategy::Blas);
681 }
682
683 #[test]
684 fn strategy_blas_not_for_small() {
685 let hw = HardwareCapabilities {
686 rayon_available: false,
687 gpu_available: false,
688 blas_available: true,
689 accelerate_available: false,
690 };
691 assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
693 assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
694 }
695
696 #[test]
697 fn strategy_fallback_without_blas() {
698 let hw = HardwareCapabilities {
699 rayon_available: false,
700 gpu_available: false,
701 blas_available: false,
702 accelerate_available: false,
703 };
704 assert_eq!(
706 auto_select_strategy(5_000, &hw),
707 SearchStrategy::SimdBruteForce
708 );
709 assert_eq!(auto_select_strategy(50_001, &hw), SearchStrategy::IvfPq);
710 }
711
712 #[cfg(feature = "fast-math")]
713 #[test]
714 fn search_with_metrics_blas() {
715 let vectors = make_vectors(200, 32, 42);
716 let norms: Vec<f32> = vectors
717 .iter()
718 .map(|v| rustyhdf5_accel::vector_norm(v))
719 .collect();
720 let tombstones = vec![0u8; 200];
721 let query = vectors[0].clone();
722
723 let (results, metrics) = search_with_metrics(
724 &query,
725 &vectors,
726 &norms,
727 &tombstones,
728 10,
729 SearchStrategy::Blas,
730 None,
731 );
732
733 assert_eq!(metrics.strategy, "blas");
734 assert!(!results.is_empty());
735 assert_eq!(results[0].0, 0);
736 }
737
738 #[cfg(feature = "parallel")]
739 #[test]
740 fn search_with_metrics_rayon() {
741 let vectors = make_vectors(500, 32, 42);
742 let norms: Vec<f32> = vectors
743 .iter()
744 .map(|v| rustyhdf5_accel::vector_norm(v))
745 .collect();
746 let tombstones = vec![0u8; 500];
747 let query = vectors[0].clone();
748
749 let (results, metrics) = search_with_metrics(
750 &query,
751 &vectors,
752 &norms,
753 &tombstones,
754 10,
755 SearchStrategy::RayonParallel,
756 None,
757 );
758
759 assert_eq!(metrics.strategy, "rayon");
760 assert!(!results.is_empty());
761 assert_eq!(results[0].0, 0);
762 }
763
764 #[test]
767 fn strategy_accelerate_preferred_over_blas() {
768 let hw = HardwareCapabilities {
769 rayon_available: true,
770 gpu_available: true,
771 blas_available: true,
772 accelerate_available: true,
773 };
774 assert_eq!(auto_select_strategy(1_000, &hw), SearchStrategy::Accelerate);
776 assert_eq!(auto_select_strategy(5_000, &hw), SearchStrategy::Accelerate);
777 assert_eq!(auto_select_strategy(10_001, &hw), SearchStrategy::Accelerate);
778 assert_eq!(auto_select_strategy(50_000, &hw), SearchStrategy::Accelerate);
779 assert_eq!(auto_select_strategy(100_000, &hw), SearchStrategy::Accelerate);
780 }
781
782 #[test]
783 fn strategy_accelerate_not_for_small() {
784 let hw = HardwareCapabilities {
785 rayon_available: false,
786 gpu_available: false,
787 blas_available: false,
788 accelerate_available: true,
789 };
790 assert_eq!(auto_select_strategy(500, &hw), SearchStrategy::Scalar);
792 assert_eq!(auto_select_strategy(999, &hw), SearchStrategy::Scalar);
793 }
794
795 #[test]
796 fn strategy_accelerate_not_for_huge() {
797 let hw = HardwareCapabilities {
798 rayon_available: true,
799 gpu_available: true,
800 blas_available: true,
801 accelerate_available: true,
802 };
803 assert_eq!(auto_select_strategy(500_001, &hw), SearchStrategy::IvfPq);
805 }
806
807 #[cfg(any(feature = "accelerate", feature = "openblas"))]
808 #[test]
809 fn search_with_metrics_accelerate() {
810 let vectors = make_vectors(200, 32, 42);
811 let norms: Vec<f32> = vectors
812 .iter()
813 .map(|v| rustyhdf5_accel::vector_norm(v))
814 .collect();
815 let tombstones = vec![0u8; 200];
816 let query = vectors[0].clone();
817
818 let (results, metrics) = search_with_metrics(
819 &query,
820 &vectors,
821 &norms,
822 &tombstones,
823 10,
824 SearchStrategy::Accelerate,
825 None,
826 );
827
828 assert_eq!(metrics.strategy, "accelerate");
829 assert!(!results.is_empty());
830 assert_eq!(results[0].0, 0);
831 }
832}