1use std::num::NonZeroUsize;
10
11use diskann_utils::views::rowmajor::Matrix;
12use diskann_vector::PureDistanceFunction;
13use diskann_vector::distance::InnerProduct;
14use diskann_wide::Architecture;
15use diskann_wide::arch::Scalar;
16#[cfg(target_arch = "aarch64")]
17use diskann_wide::arch::aarch64::Neon;
18#[cfg(target_arch = "x86_64")]
19use diskann_wide::arch::x86_64::{V3, V4};
20use thiserror::Error;
21
22use super::fallback::FallbackKernel;
23use super::isa::{MaxSimIsa, NotSupported};
24use super::kernel::{Erase, MaxSimKernel};
25use super::max_sim::MaxSimError;
26use crate::matrix_kernels as mk;
27use crate::multi_vector::distance::QueryMatRef;
28use crate::multi_vector::{BlockTransposed, Mat, MatRef, Overflow, Standard};
29
30#[derive(Debug, Clone, Copy)]
38struct Pack<const NR: usize>;
39
40#[derive(Debug)]
41struct Prepared<A, Q, const NR: usize> {
42 arch: A,
43 prepared: Q,
44 _packing: Pack<NR>,
45}
46
47impl<A, const GROUP: usize, const NR: usize> MaxSimKernel<f32>
48 for Prepared<A, BlockTransposed<f32, GROUP>, NR>
49where
50 A: Architecture,
51 for<'a> mk::maxsim::packed_f32_x_unpacked_f32::Driver<'a, A, GROUP, NR>: mk::Drive,
52{
53 fn nrows(&self) -> usize {
54 self.prepared.nrows()
55 }
56
57 fn compute_max_sim(
58 &self,
59 doc: MatRef<'_, Standard<f32>>,
60 scores: &mut [f32],
61 ) -> Result<(), MaxSimError> {
62 if scores.len() != self.nrows() {
63 return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
64 }
65
66 if doc.vector_dim() != self.prepared.ncols() {
67 return Err(MaxSimError::UnequalDim(
68 doc.vector_dim(),
69 self.prepared.ncols(),
70 ));
71 }
72
73 let Some(k) = NonZeroUsize::new(self.prepared.ncols()).map(mk::DimK::new) else {
74 scores.fill(if doc.num_vectors() == 0 {
75 f32::MAX
76 } else {
77 0.0
78 });
79 return Ok(());
80 };
81
82 let Some(a) = mk::blocks::packed::View::from_block_transposed(self.prepared.as_view())
83 else {
84 return Ok(());
85 };
86
87 let Some(b) = mk::blocks::unpacked::View::from_matrix_view(doc.as_matrix_view()) else {
88 scores.fill(f32::MAX);
89 return Ok(());
90 };
91
92 let mut driver = unsafe {
96 mk::maxsim::packed_f32_x_unpacked_f32::Driver::new(
97 self.arch,
98 a,
99 b,
100 scores,
101 k,
102 mk::Cache::detect(),
103 )
104 };
105
106 mk::Drive::drive(&mut driver);
107
108 scores.iter_mut().for_each(|s| *s = -*s);
109
110 Ok(())
111 }
112}
113
114impl<A, const GROUP: usize, const NR: usize> MaxSimKernel<half::f16>
115 for Prepared<A, BlockTransposed<f32, GROUP>, NR>
116where
117 A: Architecture,
118 for<'a> mk::maxsim::packed_f32_x_unpacked_f16::Driver<'a, A, GROUP, NR>: mk::Drive,
119{
120 fn nrows(&self) -> usize {
121 self.prepared.nrows()
122 }
123
124 fn compute_max_sim(
125 &self,
126 doc: MatRef<'_, Standard<half::f16>>,
127 scores: &mut [f32],
128 ) -> Result<(), MaxSimError> {
129 if scores.len() != self.nrows() {
130 return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
131 }
132
133 if doc.vector_dim() != self.prepared.ncols() {
134 return Err(MaxSimError::UnequalDim(
135 doc.vector_dim(),
136 self.prepared.ncols(),
137 ));
138 }
139
140 let Some(k) = NonZeroUsize::new(self.prepared.ncols()).map(mk::DimK::new) else {
141 scores.fill(if doc.num_vectors() == 0 {
142 f32::MAX
143 } else {
144 0.0
145 });
146 return Ok(());
147 };
148
149 let Some(a) = mk::blocks::packed::View::from_block_transposed(self.prepared.as_view())
150 else {
151 return Ok(());
152 };
153
154 let Some(b) = mk::blocks::unpacked::View::from_matrix_view(doc.as_matrix_view()) else {
155 scores.fill(f32::MAX);
156 return Ok(());
157 };
158
159 let mut driver = unsafe {
163 mk::maxsim::packed_f32_x_unpacked_f16::Driver::new(
164 self.arch,
165 a,
166 b,
167 scores,
168 k,
169 mk::Cache::detect(),
170 )
171 };
172
173 mk::Drive::drive(&mut driver);
174
175 scores.iter_mut().for_each(|s| *s = -*s);
176
177 Ok(())
178 }
179}
180
181impl<A, Q, const GROUP: usize, const NR: usize, const PACK: usize> MaxSimKernel<i8>
182 for Prepared<A, BlockTransposed<Q, GROUP, PACK>, NR>
183where
184 Q: Copy + Send + Sync + std::fmt::Debug,
185 A: mk::maxsim::packed_i8_x_unpacked_i8::PrepareB<AElem = Q> + Architecture,
186 for<'a> mk::maxsim::packed_i8_x_unpacked_i8::Driver<'a, A, GROUP, NR, PACK>: mk::Drive,
187{
188 fn nrows(&self) -> usize {
189 self.prepared.nrows()
190 }
191
192 fn compute_max_sim(
193 &self,
194 doc: MatRef<'_, Standard<i8>>,
195 scores: &mut [i32],
196 ) -> Result<(), MaxSimError> {
197 if scores.len() != self.nrows() {
198 return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
199 }
200
201 if doc.vector_dim() != self.prepared.ncols() {
202 return Err(MaxSimError::UnequalDim(
203 doc.vector_dim(),
204 self.prepared.ncols(),
205 ));
206 }
207
208 let Some(k) = NonZeroUsize::new(self.prepared.ncols()).map(mk::DimK::new) else {
209 scores.fill(if doc.num_vectors() == 0 { i32::MAX } else { 0 });
210 return Ok(());
211 };
212
213 let Some(a) = mk::blocks::packed::View::from_block_transposed(self.prepared.as_view())
214 else {
215 return Ok(());
216 };
217
218 let Some(b) = mk::blocks::unpacked::View::from_matrix_view(doc.as_matrix_view()) else {
219 scores.fill(i32::MAX);
220 return Ok(());
221 };
222
223 let mut driver = unsafe {
227 mk::maxsim::packed_i8_x_unpacked_i8::Driver::new(
228 self.arch,
229 a,
230 b,
231 scores,
232 k,
233 mk::Cache::detect(),
234 )
235 };
236
237 mk::Drive::drive(&mut driver);
238
239 scores.iter_mut().for_each(|s| *s = -*s);
240
241 Ok(())
242 }
243}
244
245type ReferenceFn<T> =
254 fn(QueryMatRef<'_, Standard<T>>, MatRef<'_, Standard<T>>, &mut [<T as MaxSimElement>::Score]);
255
256struct ReferenceKernel<T: MaxSimElement> {
258 query: Mat<Standard<T>>,
259 run: ReferenceFn<T>,
260}
261
262impl<T: MaxSimElement> std::fmt::Debug for ReferenceKernel<T> {
263 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
264 f.debug_struct("ReferenceKernel")
265 .field("nrows", &self.query.num_vectors())
266 .finish()
267 }
268}
269
270impl<T: MaxSimElement> ReferenceKernel<T> {
271 fn new(query: MatRef<'_, Standard<T>>, run: ReferenceFn<T>) -> Self {
272 Self {
273 query: query.to_owned(),
274 run,
275 }
276 }
277}
278
279impl<T: MaxSimElement> MaxSimKernel<T> for ReferenceKernel<T> {
280 fn nrows(&self) -> usize {
281 self.query.num_vectors()
282 }
283
284 fn compute_max_sim(
285 &self,
286 doc: MatRef<'_, Standard<T>>,
287 scores: &mut [T::Score],
288 ) -> Result<(), MaxSimError> {
289 if scores.len() != self.nrows() {
290 return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
291 }
292 if doc.vector_dim() != self.query.vector_dim() {
293 return Err(MaxSimError::UnequalDim(
294 doc.vector_dim(),
295 self.query.vector_dim(),
296 ));
297 }
298 (self.run)(self.query.as_view().into(), doc, scores);
299 Ok(())
300 }
301}
302
303fn reference_scores<T: Copy>(
305 query: QueryMatRef<'_, Standard<T>>,
306 doc: MatRef<'_, Standard<T>>,
307 scores: &mut [f32],
308) where
309 InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
310{
311 FallbackKernel::max_sim_kernel(query, doc, |i, score| scores[i] = score);
312}
313
314fn reference_scores_i8(
316 query: QueryMatRef<'_, Standard<i8>>,
317 doc: MatRef<'_, Standard<i8>>,
318 scores: &mut [i32],
319) {
320 FallbackKernel::max_sim_kernel_i8(query, doc, |i, score| scores[i] = score);
321}
322
323struct BuildAndErase<E>(E);
328
329impl<E: Erase<f32>> diskann_wide::arch::Target1<Scalar, E::Output, MatRef<'_, Standard<f32>>>
332 for BuildAndErase<E>
333{
334 fn run(self, arch: Scalar, query: MatRef<'_, Standard<f32>>) -> E::Output {
335 let prepared = BlockTransposed::<f32, 8>::from_matrix_view(query.as_matrix_view());
336 self.0.erase(Prepared {
337 arch,
338 prepared,
339 _packing: Pack::<2>,
340 })
341 }
342}
343
344#[cfg(target_arch = "x86_64")]
345impl<E: Erase<f32>> diskann_wide::arch::Target1<V3, E::Output, MatRef<'_, Standard<f32>>>
346 for BuildAndErase<E>
347{
348 fn run(self, arch: V3, query: MatRef<'_, Standard<f32>>) -> E::Output {
349 let prepared = BlockTransposed::<f32, 16>::from_matrix_view(query.as_matrix_view());
350 self.0.erase(Prepared {
351 arch,
352 prepared,
353 _packing: Pack::<6>,
354 })
355 }
356}
357
358#[cfg(target_arch = "x86_64")]
359impl<E: Erase<f32>> diskann_wide::arch::Target1<V4, E::Output, MatRef<'_, Standard<f32>>>
360 for BuildAndErase<E>
361{
362 fn run(self, arch: V4, query: MatRef<'_, Standard<f32>>) -> E::Output {
363 let prepared = BlockTransposed::<f32, 32>::from_matrix_view(query.as_matrix_view());
364 self.0.erase(Prepared {
365 arch,
366 prepared,
367 _packing: Pack::<6>,
368 })
369 }
370}
371
372#[cfg(target_arch = "aarch64")]
373impl<E: Erase<f32>> diskann_wide::arch::Target1<Neon, E::Output, MatRef<'_, Standard<f32>>>
374 for BuildAndErase<E>
375{
376 fn run(self, arch: Neon, query: MatRef<'_, Standard<f32>>) -> E::Output {
377 let prepared = BlockTransposed::<f32, 8>::from_matrix_view(query.as_matrix_view());
378 self.0.erase(Prepared {
379 arch,
380 prepared,
381 _packing: Pack::<6>,
382 })
383 }
384}
385
386impl<E: Erase<half::f16>>
389 diskann_wide::arch::Target1<Scalar, E::Output, MatRef<'_, Standard<half::f16>>>
390 for BuildAndErase<E>
391{
392 fn run(self, arch: Scalar, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
393 let prepared = BlockTransposed::<f32, 8>::from_matrix_view(
394 query
395 .as_matrix_view()
396 .map(|v| diskann_wide::cast_f16_to_f32(*v))
397 .as_view(),
398 );
399 self.0.erase(Prepared {
400 arch,
401 prepared,
402 _packing: Pack::<2>,
403 })
404 }
405}
406
407#[cfg(target_arch = "x86_64")]
408impl<E: Erase<half::f16>>
409 diskann_wide::arch::Target1<V3, E::Output, MatRef<'_, Standard<half::f16>>>
410 for BuildAndErase<E>
411{
412 fn run(self, arch: V3, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
413 let prepared = BlockTransposed::<f32, 16>::from_matrix_view(
414 query
415 .as_matrix_view()
416 .map(|v| diskann_wide::cast_f16_to_f32(*v))
417 .as_view(),
418 );
419 self.0.erase(Prepared {
420 arch,
421 prepared,
422 _packing: Pack::<6>,
423 })
424 }
425}
426
427#[cfg(target_arch = "x86_64")]
428impl<E: Erase<half::f16>>
429 diskann_wide::arch::Target1<V4, E::Output, MatRef<'_, Standard<half::f16>>>
430 for BuildAndErase<E>
431{
432 fn run(self, arch: V4, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
433 let prepared = BlockTransposed::<f32, 32>::from_matrix_view(
434 query
435 .as_matrix_view()
436 .map(|v| diskann_wide::cast_f16_to_f32(*v))
437 .as_view(),
438 );
439 self.0.erase(Prepared {
440 arch,
441 prepared,
442 _packing: Pack::<6>,
443 })
444 }
445}
446
447#[cfg(target_arch = "aarch64")]
448impl<E: Erase<half::f16>>
449 diskann_wide::arch::Target1<Neon, E::Output, MatRef<'_, Standard<half::f16>>>
450 for BuildAndErase<E>
451{
452 fn run(self, arch: Neon, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
453 let prepared = BlockTransposed::<f32, 8>::from_matrix_view(
455 query
456 .as_matrix_view()
457 .map(|v| diskann_wide::cast_f16_to_f32(*v))
458 .as_view(),
459 );
460 self.0.erase(Prepared {
461 arch,
462 prepared,
463 _packing: Pack::<6>,
464 })
465 }
466}
467
468impl<E: Erase<i8>> diskann_wide::arch::Target1<Scalar, E::Output, MatRef<'_, Standard<i8>>>
471 for BuildAndErase<E>
472{
473 fn run(self, arch: Scalar, query: MatRef<'_, Standard<i8>>) -> E::Output {
474 let prepared = BlockTransposed::<i8, 8, 2>::from_matrix_view(query.as_matrix_view());
475 self.0.erase(Prepared {
476 arch,
477 prepared,
478 _packing: Pack::<2>,
479 })
480 }
481}
482
483#[cfg(target_arch = "x86_64")]
484impl<E: Erase<i8>> diskann_wide::arch::Target1<V3, E::Output, MatRef<'_, Standard<i8>>>
485 for BuildAndErase<E>
486{
487 fn run(self, arch: V3, query: MatRef<'_, Standard<i8>>) -> E::Output {
488 let prepared = BlockTransposed::<i16, 16, 2>::from_matrix_view(
490 query.as_matrix_view().map(|v| i16::from(*v)).as_view(),
491 );
492 self.0.erase(Prepared {
493 arch,
494 prepared,
495 _packing: Pack::<6>,
496 })
497 }
498}
499
500#[cfg(target_arch = "x86_64")]
501impl<E: Erase<i8>> diskann_wide::arch::Target1<V4, E::Output, MatRef<'_, Standard<i8>>>
502 for BuildAndErase<E>
503{
504 fn run(self, arch: V4, query: MatRef<'_, Standard<i8>>) -> E::Output {
505 let prepared = BlockTransposed::<u8, 32, 4>::from_matrix_view(
507 query.as_matrix_view().map(|v| (*v as u8) ^ 0x80).as_view(),
508 );
509 self.0.erase(Prepared {
510 arch,
511 prepared,
512 _packing: Pack::<6>,
513 })
514 }
515}
516
517#[cfg(target_arch = "aarch64")]
518impl<E: Erase<i8>> diskann_wide::arch::Target1<Neon, E::Output, MatRef<'_, Standard<i8>>>
519 for BuildAndErase<E>
520{
521 fn run(self, arch: Neon, query: MatRef<'_, Standard<i8>>) -> E::Output {
522 let prepared = BlockTransposed::<i8, 8, 4>::from_matrix_view(query.as_matrix_view());
523 self.0.erase(Prepared {
524 arch,
525 prepared,
526 _packing: Pack::<6>,
527 })
528 }
529}
530
531mod sealed {
536 pub trait Sealed {}
537}
538
539pub trait MaxSimElement: sealed::Sealed + Sized + Copy + Send + Sync + 'static {
545 type Score: Copy + Default + PartialEq + std::fmt::Debug;
548
549 const NO_MATCH: Self::Score;
551
552 fn build<E: Erase<Self>>(
562 isa: MaxSimIsa,
563 query: MatRef<'_, Standard<Self>>,
564 erase: E,
565 ) -> Result<E::Output, BuildMaxSimError>;
566}
567
568impl sealed::Sealed for f32 {}
569impl sealed::Sealed for half::f16 {}
570impl sealed::Sealed for i8 {}
571
572impl MaxSimElement for f32 {
573 type Score = f32;
574 const NO_MATCH: f32 = f32::MAX;
575
576 fn build<E: Erase<f32>>(
577 isa: MaxSimIsa,
578 query: MatRef<'_, Standard<f32>>,
579 erase: E,
580 ) -> Result<E::Output, BuildMaxSimError> {
581 match isa {
582 MaxSimIsa::Auto => Ok(diskann_wide::arch::dispatch1_no_features(
583 BuildAndErase(erase),
584 query,
585 )),
586 MaxSimIsa::Scalar => Ok(Scalar::new().run1(BuildAndErase(erase), query)),
587 #[cfg(target_arch = "x86_64")]
588 MaxSimIsa::X86_64_V3 => {
589 let arch = V3::new_checked().ok_or(NotSupported {
590 isa,
591 reason: "AVX2/FMA unavailable on this CPU",
592 })?;
593 Ok(arch.run1(BuildAndErase(erase), query))
594 }
595 #[cfg(target_arch = "x86_64")]
596 MaxSimIsa::X86_64_V4 => {
597 let arch = V4::new_checked().ok_or(NotSupported {
598 isa,
599 reason: "AVX-512 unavailable on this CPU",
600 })?;
601 Ok(arch.run1(BuildAndErase(erase), query))
602 }
603 #[cfg(not(target_arch = "x86_64"))]
604 MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => Err(NotSupported {
605 isa,
606 reason: "x86_64 target only",
607 }
608 .into()),
609 #[cfg(target_arch = "aarch64")]
610 MaxSimIsa::Neon => {
611 let arch = Neon::new_checked().ok_or(NotSupported {
612 isa,
613 reason: "Neon unavailable on this CPU",
614 })?;
615 Ok(arch.run1(BuildAndErase(erase), query))
616 }
617 #[cfg(not(target_arch = "aarch64"))]
618 MaxSimIsa::Neon => Err(NotSupported {
619 isa,
620 reason: "aarch64 target only",
621 }
622 .into()),
623 MaxSimIsa::Reference => {
624 Ok(erase.erase(ReferenceKernel::new(query, reference_scores::<f32>)))
625 }
626 }
627 }
628}
629
630impl MaxSimElement for half::f16 {
631 type Score = f32;
632 const NO_MATCH: f32 = f32::MAX;
633
634 fn build<E: Erase<half::f16>>(
635 isa: MaxSimIsa,
636 query: MatRef<'_, Standard<half::f16>>,
637 erase: E,
638 ) -> Result<E::Output, BuildMaxSimError> {
639 match isa {
640 MaxSimIsa::Auto => Ok(diskann_wide::arch::dispatch1_no_features(
641 BuildAndErase(erase),
642 query,
643 )),
644 MaxSimIsa::Scalar => Ok(Scalar::new().run1(BuildAndErase(erase), query)),
645 #[cfg(target_arch = "x86_64")]
646 MaxSimIsa::X86_64_V3 => {
647 let arch = V3::new_checked().ok_or(NotSupported {
648 isa,
649 reason: "AVX2/FMA unavailable on this CPU",
650 })?;
651 Ok(arch.run1(BuildAndErase(erase), query))
652 }
653 #[cfg(target_arch = "x86_64")]
654 MaxSimIsa::X86_64_V4 => {
655 let arch = V4::new_checked().ok_or(NotSupported {
656 isa,
657 reason: "AVX-512 unavailable on this CPU",
658 })?;
659 Ok(arch.run1(BuildAndErase(erase), query))
660 }
661 #[cfg(not(target_arch = "x86_64"))]
662 MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => Err(NotSupported {
663 isa,
664 reason: "x86_64 target only",
665 }
666 .into()),
667 #[cfg(target_arch = "aarch64")]
668 MaxSimIsa::Neon => {
669 let arch = Neon::new_checked().ok_or(NotSupported {
670 isa,
671 reason: "Neon unavailable on this CPU",
672 })?;
673 Ok(arch.run1(BuildAndErase(erase), query))
674 }
675 #[cfg(not(target_arch = "aarch64"))]
676 MaxSimIsa::Neon => Err(NotSupported {
677 isa,
678 reason: "aarch64 target only",
679 }
680 .into()),
681 MaxSimIsa::Reference => {
682 Ok(erase.erase(ReferenceKernel::new(query, reference_scores::<half::f16>)))
683 }
684 }
685 }
686}
687
688const MAX_I8_DIM: usize = (i32::MAX / (128 * 128)) as usize;
690
691impl MaxSimElement for i8 {
692 type Score = i32;
693 const NO_MATCH: i32 = i32::MAX;
694
695 fn build<E: Erase<i8>>(
696 isa: MaxSimIsa,
697 query: MatRef<'_, Standard<i8>>,
698 erase: E,
699 ) -> Result<E::Output, BuildMaxSimError> {
700 if query.vector_dim() > MAX_I8_DIM {
701 return Err(BuildMaxSimError::DimTooLarge(
702 query.vector_dim(),
703 MAX_I8_DIM,
704 ));
705 }
706
707 match isa {
708 MaxSimIsa::Auto => Ok(diskann_wide::arch::dispatch1_no_features(
709 BuildAndErase(erase),
710 query,
711 )),
712 MaxSimIsa::Scalar => Ok(Scalar::new().run1(BuildAndErase(erase), query)),
713 #[cfg(target_arch = "x86_64")]
714 MaxSimIsa::X86_64_V3 => {
715 let arch = V3::new_checked().ok_or(NotSupported {
716 isa,
717 reason: "AVX2/FMA unavailable on this CPU",
718 })?;
719 Ok(arch.run1(BuildAndErase(erase), query))
720 }
721 #[cfg(target_arch = "x86_64")]
722 MaxSimIsa::X86_64_V4 => {
723 let arch = V4::new_checked().ok_or(NotSupported {
724 isa,
725 reason: "AVX-512 unavailable on this CPU",
726 })?;
727 Ok(arch.run1(BuildAndErase(erase), query))
728 }
729 #[cfg(not(target_arch = "x86_64"))]
730 MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => Err(NotSupported {
731 isa,
732 reason: "x86_64 target only",
733 }
734 .into()),
735 #[cfg(target_arch = "aarch64")]
736 MaxSimIsa::Neon => {
737 let arch = Neon::new_checked().ok_or(NotSupported {
738 isa,
739 reason: "Neon unavailable on this CPU",
740 })?;
741 Ok(arch.run1(BuildAndErase(erase), query))
742 }
743 #[cfg(not(target_arch = "aarch64"))]
744 MaxSimIsa::Neon => Err(NotSupported {
745 isa,
746 reason: "aarch64 target only",
747 }
748 .into()),
749 MaxSimIsa::Reference => {
750 Ok(erase.erase(ReferenceKernel::new(query, reference_scores_i8)))
751 }
752 }
753 }
754}
755
756#[derive(Debug, Clone, Copy, Error)]
762#[non_exhaustive]
763pub enum BuildMaxSimError {
764 #[error(transparent)]
765 NotSupported(#[from] NotSupported),
766 #[error("query-vector dim {0} exceeds the maximum of {1}")]
767 DimTooLarge(usize, usize),
768 #[error(transparent)]
769 Overflow(#[from] Overflow),
770}
771
772pub fn build_max_sim<T: MaxSimElement, E: Erase<T>>(
783 isa: MaxSimIsa,
784 query: MatRef<'_, Standard<T>>,
785 erase: E,
786) -> Result<E::Output, BuildMaxSimError> {
787 T::build(isa, query, erase)
788}
789
790#[cfg(test)]
791mod tests {
792 use super::*;
793 use crate::multi_vector::{BoxErase, Chamfer, MaxSim, QueryMatRef};
794 use diskann_vector::DistanceFunctionMut;
795
796 trait FromF32 {
799 fn from_f32(v: f32) -> Self;
800 }
801
802 impl FromF32 for f32 {
803 fn from_f32(v: f32) -> Self {
804 v
805 }
806 }
807
808 impl FromF32 for half::f16 {
809 fn from_f32(v: f32) -> Self {
810 diskann_wide::cast_f32_to_f16(v)
811 }
812 }
813
814 impl FromF32 for i8 {
815 fn from_f32(v: f32) -> Self {
816 v as i8
817 }
818 }
819
820 trait ScoreAsF32: MaxSimElement {
823 fn score_as_f32(score: Self::Score) -> f32;
824 }
825
826 impl ScoreAsF32 for f32 {
827 fn score_as_f32(score: f32) -> f32 {
828 score
829 }
830 }
831
832 impl ScoreAsF32 for half::f16 {
833 fn score_as_f32(score: f32) -> f32 {
834 score
835 }
836 }
837
838 impl ScoreAsF32 for i8 {
839 fn score_as_f32(score: i32) -> f32 {
840 if score == Self::NO_MATCH {
841 f32::MAX
842 } else {
843 score as f32
844 }
845 }
846 }
847
848 fn scores_buffer<T: MaxSimElement>(len: usize) -> Vec<T::Score> {
849 vec![T::Score::default(); len]
850 }
851
852 fn make_mat<T: Copy>(data: &[T], nrows: usize, ncols: usize) -> MatRef<'_, Standard<T>> {
853 MatRef::new(Standard::new(nrows, ncols).unwrap(), data).unwrap()
854 }
855
856 fn make_test_data<T: FromF32>(len: usize, ceil: usize, shift: usize) -> Vec<T> {
857 (0..len)
858 .map(|v| T::from_f32(((v + shift) % ceil) as f32))
859 .collect()
860 }
861
862 const TEST_CASES: &[(usize, usize, usize)] = &[
869 (0, 1, 1), (1, 0, 1), (1, 1, 0), (1, 0, 0), (1, 1, 4), (5, 3, 5), (17, 4, 64), (16, 6, 32), ];
878
879 fn check_chamfer_matches<T>(tol: f32, label: &str)
880 where
881 T: ScoreAsF32 + FromF32,
882 InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
883 {
884 for &(nq, nd, dim) in TEST_CASES {
885 let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
886 let doc_data = make_test_data::<T>(nd * dim, dim, dim);
887
888 let query = make_mat(&query_data, nq, dim);
889 let doc = make_mat(&doc_data, nd, dim);
890
891 let expected = Chamfer::evaluate(QueryMatRef::from(query), doc);
892
893 let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
894 let mut scores = scores_buffer::<T>(nq);
895 kernel.compute_max_sim(doc, &mut scores).unwrap();
896 let actual: f32 = scores.iter().map(|&s| T::score_as_f32(s)).sum();
897
898 assert!(
899 (actual - expected).abs() < tol,
900 "{label}Chamfer mismatch for ({nq},{nd},{dim}): actual={actual}, expected={expected}",
901 );
902 }
903 }
904
905 fn check_max_sim_matches<T>(tol: f32, label: &str)
906 where
907 T: ScoreAsF32 + FromF32,
908 InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
909 {
910 for &(nq, nd, dim) in TEST_CASES {
911 let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
912 let doc_data = make_test_data::<T>(nd * dim, dim, dim);
913
914 let query = make_mat(&query_data, nq, dim);
915 let doc = make_mat(&doc_data, nd, dim);
916
917 let mut expected_scores = vec![0.0f32; nq];
918 let _ = MaxSim::new(&mut expected_scores).evaluate(QueryMatRef::from(query), doc);
919
920 let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
921 let mut actual_scores = scores_buffer::<T>(nq);
922 kernel.compute_max_sim(doc, &mut actual_scores).unwrap();
923
924 for i in 0..nq {
925 let actual = T::score_as_f32(actual_scores[i]);
926 assert!(
927 (actual - expected_scores[i]).abs() < tol,
928 "{label}MaxSim[{i}] mismatch for ({nq},{nd},{dim}): actual={actual}, expected={}",
929 expected_scores[i],
930 );
931 }
932 }
933 }
934
935 fn check_i8_isas(nq: usize, nd: usize, dim: usize, query_data: &[i8], doc_data: &[i8]) {
936 let query = make_mat(query_data, nq, dim);
937 let doc = make_mat(doc_data, nd, dim);
938
939 let mut expected = vec![0.0f32; nq];
940 let _ = MaxSim::new(&mut expected).evaluate(QueryMatRef::from(query), doc);
941
942 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
943 let kernel = build_max_sim::<i8, _>(isa, query, BoxErase).unwrap();
944 let mut scores = scores_buffer::<i8>(nq);
945 kernel.compute_max_sim(doc, &mut scores).unwrap();
946
947 for i in 0..nq {
948 let actual = <i8 as ScoreAsF32>::score_as_f32(scores[i]);
949 assert!(
950 (actual - expected[i]).abs() < 1e-10,
951 "i8 {isa} MaxSim[{i}] mismatch for ({nq},{nd},{dim}): \
952 actual={actual}, expected={}",
953 expected[i],
954 );
955 }
956 }
957 }
958
959 #[test]
960 fn i8_isas_match_oracle() {
961 for &(nq, nd, dim) in TEST_CASES {
962 let query_data = make_test_data::<i8>(nq * dim, dim, dim / 2);
963 let doc_data = make_test_data::<i8>(nd * dim, dim, dim);
964 check_i8_isas(nq, nd, dim, &query_data, &doc_data);
965 }
966
967 let full_range: &[(usize, usize, usize)] = if cfg!(miri) {
968 &[(5, 4, 64)]
969 } else {
970 &[(33, 13, 64), (70, 1000, 131)]
971 };
972 for &(nq, nd, dim) in full_range {
973 let query_data: Vec<i8> = (0..nq * dim).map(|v| (37 * v) as i8).collect();
974 let doc_data: Vec<i8> = (0..nd * dim).map(|v| (91 * v + 5) as i8).collect();
975 check_i8_isas(nq, nd, dim, &query_data, &doc_data);
976 }
977 }
978
979 #[test]
980 fn dimensions_f32() {
981 let data = vec![1.0f32; 5 * 8];
982 let query = make_mat(&data, 5, 8);
983 let kernel = build_max_sim::<f32, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
984 assert_eq!(kernel.nrows(), 5);
985 }
986
987 #[test]
988 fn dimensions_f16() {
989 let data = vec![diskann_wide::cast_f32_to_f16(1.0); 5 * 8];
990 let query = make_mat(data.as_slice(), 5, 8);
991 let kernel = build_max_sim::<half::f16, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
992 assert_eq!(kernel.nrows(), 5);
993 }
994
995 #[test]
996 fn dimensions_i8() {
997 let data = vec![1i8; 5 * 8];
998 let query = make_mat(&data, 5, 8);
999 let kernel = build_max_sim::<i8, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
1000 assert_eq!(kernel.nrows(), 5);
1001 }
1002
1003 #[test]
1004 fn i8_rejects_dim_too_large() {
1005 let data = vec![0i8; 131_072];
1006 let query = make_mat(&data, 1, 131_072);
1007
1008 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1009 let err = build_max_sim::<i8, _>(isa, query, BoxErase).err();
1010 assert!(
1011 matches!(err, Some(BuildMaxSimError::DimTooLarge(131_072, 131_071))),
1012 "{isa:?}: expected DimTooLarge(131_072, 131_071), got {err:?}",
1013 );
1014 }
1015 }
1016
1017 #[test]
1018 #[cfg(not(miri))]
1019 fn i8_max_dim_is_exact() {
1020 let data = vec![i8::MIN; 131_071];
1021 let query = make_mat(&data, 1, 131_071);
1022
1023 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1024 let kernel = build_max_sim::<i8, _>(isa, query, BoxErase).unwrap();
1025 let mut scores = scores_buffer::<i8>(1);
1026 kernel.compute_max_sim(query, &mut scores).unwrap();
1027 assert_eq!(scores[0], -131_071 * 16_384, "{isa:?}");
1028 }
1029 }
1030
1031 fn check_size_mismatch<T>(label: &str)
1032 where
1033 T: MaxSimElement + FromF32,
1034 {
1035 let query_data = make_test_data::<T>(3 * 4, 4, 0);
1036 let doc_data = make_test_data::<T>(2 * 4, 4, 1);
1037 let query = make_mat(&query_data, 3, 4);
1038 let doc = make_mat(&doc_data, 2, 4);
1039
1040 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1041 let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1042
1043 let mut too_short = scores_buffer::<T>(2);
1044 match kernel.compute_max_sim(doc, &mut too_short) {
1045 Err(MaxSimError::InvalidBufferLength(2, 3)) => {}
1046 other => {
1047 panic!("{label}({isa:?}) expected InvalidBufferLength(2, 3), got {other:?}",)
1048 }
1049 }
1050
1051 let mut too_long = scores_buffer::<T>(4);
1052 match kernel.compute_max_sim(doc, &mut too_long) {
1053 Err(MaxSimError::InvalidBufferLength(4, 3)) => {}
1054 other => {
1055 panic!("{label}({isa:?}) expected InvalidBufferLength(4, 3), got {other:?}",)
1056 }
1057 }
1058 }
1059 }
1060
1061 fn check_zero_docs_fills_sentinel<T>(label: &str)
1062 where
1063 T: MaxSimElement + FromF32,
1064 {
1065 let query_data = make_test_data::<T>(3 * 4, 4, 0);
1066 let doc_data: Vec<T> = Vec::new();
1067 let query = make_mat(&query_data, 3, 4);
1068 let doc = make_mat(doc_data.as_slice(), 0, 4);
1069
1070 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1071 let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1072 let mut scores = scores_buffer::<T>(3);
1073 kernel.compute_max_sim(doc, &mut scores).unwrap();
1074 for (i, &s) in scores.iter().enumerate() {
1075 assert_eq!(
1076 s,
1077 T::NO_MATCH,
1078 "{label}({isa:?}) zero-doc slot {i} should be the NO_MATCH sentinel",
1079 );
1080 }
1081 }
1082 }
1083
1084 fn check_zero_query<T>(label: &str)
1085 where
1086 T: MaxSimElement + FromF32,
1087 {
1088 let query_data: Vec<T> = Vec::new();
1089 let doc_data = make_test_data::<T>(2 * 4, 4, 0);
1090 let query = make_mat(query_data.as_slice(), 0, 4);
1091 let doc = make_mat(&doc_data, 2, 4);
1092
1093 for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1094 let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1095 assert_eq!(
1096 kernel.nrows(),
1097 0,
1098 "{label}({isa:?}) empty query should yield nrows=0",
1099 );
1100 let mut scores = scores_buffer::<T>(0);
1101 kernel
1102 .compute_max_sim(doc, &mut scores)
1103 .unwrap_or_else(|e| panic!("{label}({isa:?}) expected Ok, got {e:?}"));
1104 }
1105 }
1106
1107 macro_rules! test_matches_fallback {
1108 ($mod_name:ident, $ty:ty, $tol:expr, $label:literal) => {
1109 mod $mod_name {
1110 use super::*;
1111
1112 #[test]
1113 fn chamfer_matches_fallback() {
1114 check_chamfer_matches::<$ty>($tol, $label);
1115 }
1116
1117 #[test]
1118 fn max_sim_matches_fallback() {
1119 check_max_sim_matches::<$ty>($tol, $label);
1120 }
1121
1122 #[test]
1123 fn errors_on_size_mismatch() {
1124 check_size_mismatch::<$ty>($label);
1125 }
1126
1127 #[test]
1128 fn zero_docs_fills_sentinel() {
1129 check_zero_docs_fills_sentinel::<$ty>($label);
1130 }
1131
1132 #[test]
1133 fn zero_query_returns_ok() {
1134 check_zero_query::<$ty>($label);
1135 }
1136 }
1137 };
1138 }
1139
1140 test_matches_fallback!(f32, f32, 1e-10, "f32 ");
1141 test_matches_fallback!(f16, half::f16, 1e-10, "f16 ");
1142 test_matches_fallback!(i8, i8, 1e-10, "i8 ");
1143}