Skip to main content

diskann_quantization/multi_vector/distance/
factory.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Factory + concrete `MaxSimKernel<T>` impls for the multi-vector distance
7//! API. BYOTE entry point — see [`build_max_sim`].
8
9use 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, Standard};
29
30// ─────────────────────────────────────────────────────────────────────────
31//  Prepared<A, Q, NR> — concrete kernel for the arch-dispatched paths.
32// ─────────────────────────────────────────────────────────────────────────
33
34// ZST for selecting the packing for the documents.
35//
36// TODO: Some of these decisions should eventually ve moved lower into `matrix_kernels`.
37#[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        // SAFETY: The dimension check establishes that `a.k() == b.k() == k`.
93        // The length check establishes that `scores` occupies exactly the
94        // packed blocks in `a`.
95        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        // SAFETY: The dimension check establishes that `a.k() == b.k() == k`.
160        // The length check establishes that `scores` occupies exactly the
161        // packed blocks in `a`.
162        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        // SAFETY: The dimension check establishes that `a.k() == b.k() == k`.
224        // The length check establishes that `scores` occupies exactly the
225        // packed blocks in `a`.
226        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
245// ─────────────────────────────────────────────────────────────────────────
246//  ReferenceKernel<T> — double loop over the single-vector inner product.
247// ─────────────────────────────────────────────────────────────────────────
248
249/// Reference MaxSim implementation, selected by [`MaxSimElement::build`].
250///
251/// May assume `scores` and `doc` were validated by
252/// [`MaxSimKernel::compute_max_sim`].
253type ReferenceFn<T> =
254    fn(QueryMatRef<'_, Standard<T>>, MatRef<'_, Standard<T>>, &mut [<T as MaxSimElement>::Score]);
255
256/// Unoptimized kernel backing [`MaxSimIsa::Reference`].
257struct 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
303/// [`ReferenceKernel`] implementation for element types scored in `f32`.
304fn 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
314/// [`ReferenceKernel`] implementation for `i8`, which scores in exact `i32`.
315fn 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
323// ─────────────────────────────────────────────────────────────────────────
324//  BuildAndErase<E> — Target1 impls used by `dispatch1_no_features` (Auto).
325// ─────────────────────────────────────────────────────────────────────────
326
327struct BuildAndErase<E>(E);
328
329// ───── f32 Target1 impls ─────
330
331impl<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
386// ───── f16 Target1 impls ─────
387
388impl<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        // Neon dispatches to Scalar (no Neon-specific kernel).
454        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
468// ───── i8 Target1 impls ─────
469
470impl<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        // `PrepareB for V3` expects the query widened to `i16`.
489        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        // `PrepareB for V4` expects the query packed as `x + 128` in `u8`.
506        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
531// ─────────────────────────────────────────────────────────────────────────
532//  MaxSimElement — sealed trait gating accepted element types.
533// ─────────────────────────────────────────────────────────────────────────
534
535mod sealed {
536    pub trait Sealed {}
537}
538
539/// Scalar element types accepted by [`build_max_sim`].
540///
541/// Sealed: external crates cannot add impls. Quantized representations
542/// (PQ, SQ, packed sub-byte) are intentionally excluded — they need
543/// codebook/scale state that [`MatRef<'_, Standard<Self>>`] can't carry.
544pub trait MaxSimElement: sealed::Sealed + Sized + Copy + Send + Sync + 'static {
545    /// Score produced per query row: `f32` for floating-point elements, `i32`
546    /// for `i8` where the inner product is exact.
547    type Score: Copy + Default + PartialEq + std::fmt::Debug;
548
549    /// Score written for every query row when the document set is empty.
550    const NO_MATCH: Self::Score;
551
552    /// Build the concrete kernel for this element type and hand it to
553    /// `erase.erase(...)`.
554    ///
555    /// # Errors
556    ///
557    /// Returns [`BuildMaxSimError::NotSupported`] when the requested ISA cannot
558    /// run on this build (e.g. AVX-512 unavailable; aarch64 on x86_64), and
559    /// [`BuildMaxSimError::DimTooLarge`] when an `i8` query has more than
560    /// `131_071` dimensions, beyond which `i32` scores could overflow.
561    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
688/// Largest `i8` dimension for which every inner product fits in `i32`.
689const 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// ─────────────────────────────────────────────────────────────────────────
757//  Factory entry point.
758// ─────────────────────────────────────────────────────────────────────────
759
760/// Error returned by [`build_max_sim`].
761#[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}
769
770/// Build a multi-vector MaxSim kernel for any [`MaxSimElement`] type.
771///
772/// Thin wrapper over [`MaxSimElement::build`] so callers don't have to name
773/// the trait at the call site.
774///
775/// # Errors
776///
777/// Returns [`BuildMaxSimError::NotSupported`] when the requested ISA cannot run
778/// on this build, and [`BuildMaxSimError::DimTooLarge`] when an `i8` query has
779/// more than `131_071` dimensions.
780pub fn build_max_sim<T: MaxSimElement, E: Erase<T>>(
781    isa: MaxSimIsa,
782    query: MatRef<'_, Standard<T>>,
783    erase: E,
784) -> Result<E::Output, BuildMaxSimError> {
785    T::build(isa, query, erase)
786}
787
788#[cfg(test)]
789mod tests {
790    use super::*;
791    use crate::multi_vector::{BoxErase, Chamfer, MaxSim, QueryMatRef};
792    use diskann_vector::DistanceFunctionMut;
793
794    /// Local helper trait — picks a sane test value of `T` from an `f32`
795    /// so every element type shares the same data generator.
796    trait FromF32 {
797        fn from_f32(v: f32) -> Self;
798    }
799
800    impl FromF32 for f32 {
801        fn from_f32(v: f32) -> Self {
802            v
803        }
804    }
805
806    impl FromF32 for half::f16 {
807        fn from_f32(v: f32) -> Self {
808            diskann_wide::cast_f32_to_f16(v)
809        }
810    }
811
812    impl FromF32 for i8 {
813        fn from_f32(v: f32) -> Self {
814            v as i8
815        }
816    }
817
818    /// Projects a kernel score onto the `f32` distance the fallback path
819    /// produces, so every element type shares the same assertions.
820    trait ScoreAsF32: MaxSimElement {
821        fn score_as_f32(score: Self::Score) -> f32;
822    }
823
824    impl ScoreAsF32 for f32 {
825        fn score_as_f32(score: f32) -> f32 {
826            score
827        }
828    }
829
830    impl ScoreAsF32 for half::f16 {
831        fn score_as_f32(score: f32) -> f32 {
832            score
833        }
834    }
835
836    impl ScoreAsF32 for i8 {
837        fn score_as_f32(score: i32) -> f32 {
838            if score == Self::NO_MATCH {
839                f32::MAX
840            } else {
841                score as f32
842            }
843        }
844    }
845
846    fn scores_buffer<T: MaxSimElement>(len: usize) -> Vec<T::Score> {
847        vec![T::Score::default(); len]
848    }
849
850    fn make_mat<T: Copy>(data: &[T], nrows: usize, ncols: usize) -> MatRef<'_, Standard<T>> {
851        MatRef::new(Standard::new(nrows, ncols).unwrap(), data).unwrap()
852    }
853
854    fn make_test_data<T: FromF32>(len: usize, ceil: usize, shift: usize) -> Vec<T> {
855        (0..len)
856            .map(|v| T::from_f32(((v + shift) % ceil) as f32))
857            .collect()
858    }
859
860    /// Shapes for the `chamfer_matches_fallback` / `max_sim_matches_fallback`
861    /// agreement checks: `(num_queries, num_docs, dim)`.
862    ///
863    /// Targets the factory wiring (query setup, score writeback) above the
864    /// kernel layer; exhaustive panel/remainder coverage is pinned in
865    /// `kernels::tiled_reduce::tests`.
866    const TEST_CASES: &[(usize, usize, usize)] = &[
867        (0, 1, 1),   // Empty query
868        (1, 0, 1),   // Empty docs
869        (1, 1, 0),   // Empty dim
870        (1, 0, 0),   // Empty docs and dim
871        (1, 1, 4),   // Degenerate
872        (5, 3, 5),   // Prime k; nq > 1 and nd > 1 exercise per-row writeback
873        (17, 4, 64), // A-panel remainder crossing both Scalar and V3 panel widths
874        (16, 6, 32), // B-remainder ≠ 1 (V3 b_remainder = 2)
875    ];
876
877    fn check_chamfer_matches<T>(tol: f32, label: &str)
878    where
879        T: ScoreAsF32 + FromF32,
880        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
881    {
882        for &(nq, nd, dim) in TEST_CASES {
883            let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
884            let doc_data = make_test_data::<T>(nd * dim, dim, dim);
885
886            let query = make_mat(&query_data, nq, dim);
887            let doc = make_mat(&doc_data, nd, dim);
888
889            let expected = Chamfer::evaluate(QueryMatRef::from(query), doc);
890
891            let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
892            let mut scores = scores_buffer::<T>(nq);
893            kernel.compute_max_sim(doc, &mut scores).unwrap();
894            let actual: f32 = scores.iter().map(|&s| T::score_as_f32(s)).sum();
895
896            assert!(
897                (actual - expected).abs() < tol,
898                "{label}Chamfer mismatch for ({nq},{nd},{dim}): actual={actual}, expected={expected}",
899            );
900        }
901    }
902
903    fn check_max_sim_matches<T>(tol: f32, label: &str)
904    where
905        T: ScoreAsF32 + FromF32,
906        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
907    {
908        for &(nq, nd, dim) in TEST_CASES {
909            let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
910            let doc_data = make_test_data::<T>(nd * dim, dim, dim);
911
912            let query = make_mat(&query_data, nq, dim);
913            let doc = make_mat(&doc_data, nd, dim);
914
915            let mut expected_scores = vec![0.0f32; nq];
916            let _ = MaxSim::new(&mut expected_scores).evaluate(QueryMatRef::from(query), doc);
917
918            let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
919            let mut actual_scores = scores_buffer::<T>(nq);
920            kernel.compute_max_sim(doc, &mut actual_scores).unwrap();
921
922            for i in 0..nq {
923                let actual = T::score_as_f32(actual_scores[i]);
924                assert!(
925                    (actual - expected_scores[i]).abs() < tol,
926                    "{label}MaxSim[{i}] mismatch for ({nq},{nd},{dim}): actual={actual}, expected={}",
927                    expected_scores[i],
928                );
929            }
930        }
931    }
932
933    fn check_i8_isas(nq: usize, nd: usize, dim: usize, query_data: &[i8], doc_data: &[i8]) {
934        let query = make_mat(query_data, nq, dim);
935        let doc = make_mat(doc_data, nd, dim);
936
937        let mut expected = vec![0.0f32; nq];
938        let _ = MaxSim::new(&mut expected).evaluate(QueryMatRef::from(query), doc);
939
940        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
941            let kernel = build_max_sim::<i8, _>(isa, query, BoxErase).unwrap();
942            let mut scores = scores_buffer::<i8>(nq);
943            kernel.compute_max_sim(doc, &mut scores).unwrap();
944
945            for i in 0..nq {
946                let actual = <i8 as ScoreAsF32>::score_as_f32(scores[i]);
947                assert!(
948                    (actual - expected[i]).abs() < 1e-10,
949                    "i8 {isa} MaxSim[{i}] mismatch for ({nq},{nd},{dim}): \
950                     actual={actual}, expected={}",
951                    expected[i],
952                );
953            }
954        }
955    }
956
957    #[test]
958    fn i8_isas_match_oracle() {
959        for &(nq, nd, dim) in TEST_CASES {
960            let query_data = make_test_data::<i8>(nq * dim, dim, dim / 2);
961            let doc_data = make_test_data::<i8>(nd * dim, dim, dim);
962            check_i8_isas(nq, nd, dim, &query_data, &doc_data);
963        }
964
965        let full_range: &[(usize, usize, usize)] = if cfg!(miri) {
966            &[(5, 4, 64)]
967        } else {
968            &[(33, 13, 64), (70, 1000, 131)]
969        };
970        for &(nq, nd, dim) in full_range {
971            let query_data: Vec<i8> = (0..nq * dim).map(|v| (37 * v) as i8).collect();
972            let doc_data: Vec<i8> = (0..nd * dim).map(|v| (91 * v + 5) as i8).collect();
973            check_i8_isas(nq, nd, dim, &query_data, &doc_data);
974        }
975    }
976
977    #[test]
978    fn dimensions_f32() {
979        let data = vec![1.0f32; 5 * 8];
980        let query = make_mat(&data, 5, 8);
981        let kernel = build_max_sim::<f32, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
982        assert_eq!(kernel.nrows(), 5);
983    }
984
985    #[test]
986    fn dimensions_f16() {
987        let data = vec![diskann_wide::cast_f32_to_f16(1.0); 5 * 8];
988        let query = make_mat(data.as_slice(), 5, 8);
989        let kernel = build_max_sim::<half::f16, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
990        assert_eq!(kernel.nrows(), 5);
991    }
992
993    #[test]
994    fn dimensions_i8() {
995        let data = vec![1i8; 5 * 8];
996        let query = make_mat(&data, 5, 8);
997        let kernel = build_max_sim::<i8, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
998        assert_eq!(kernel.nrows(), 5);
999    }
1000
1001    #[test]
1002    fn i8_rejects_dim_too_large() {
1003        let data = vec![0i8; 131_072];
1004        let query = make_mat(&data, 1, 131_072);
1005
1006        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1007            let err = build_max_sim::<i8, _>(isa, query, BoxErase).err();
1008            assert!(
1009                matches!(err, Some(BuildMaxSimError::DimTooLarge(131_072, 131_071))),
1010                "{isa:?}: expected DimTooLarge(131_072, 131_071), got {err:?}",
1011            );
1012        }
1013    }
1014
1015    #[test]
1016    #[cfg(not(miri))]
1017    fn i8_max_dim_is_exact() {
1018        let data = vec![i8::MIN; 131_071];
1019        let query = make_mat(&data, 1, 131_071);
1020
1021        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1022            let kernel = build_max_sim::<i8, _>(isa, query, BoxErase).unwrap();
1023            let mut scores = scores_buffer::<i8>(1);
1024            kernel.compute_max_sim(query, &mut scores).unwrap();
1025            assert_eq!(scores[0], -131_071 * 16_384, "{isa:?}");
1026        }
1027    }
1028
1029    fn check_size_mismatch<T>(label: &str)
1030    where
1031        T: MaxSimElement + FromF32,
1032    {
1033        let query_data = make_test_data::<T>(3 * 4, 4, 0);
1034        let doc_data = make_test_data::<T>(2 * 4, 4, 1);
1035        let query = make_mat(&query_data, 3, 4);
1036        let doc = make_mat(&doc_data, 2, 4);
1037
1038        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1039            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1040
1041            let mut too_short = scores_buffer::<T>(2);
1042            match kernel.compute_max_sim(doc, &mut too_short) {
1043                Err(MaxSimError::InvalidBufferLength(2, 3)) => {}
1044                other => {
1045                    panic!("{label}({isa:?}) expected InvalidBufferLength(2, 3), got {other:?}",)
1046                }
1047            }
1048
1049            let mut too_long = scores_buffer::<T>(4);
1050            match kernel.compute_max_sim(doc, &mut too_long) {
1051                Err(MaxSimError::InvalidBufferLength(4, 3)) => {}
1052                other => {
1053                    panic!("{label}({isa:?}) expected InvalidBufferLength(4, 3), got {other:?}",)
1054                }
1055            }
1056        }
1057    }
1058
1059    fn check_zero_docs_fills_sentinel<T>(label: &str)
1060    where
1061        T: MaxSimElement + FromF32,
1062    {
1063        let query_data = make_test_data::<T>(3 * 4, 4, 0);
1064        let doc_data: Vec<T> = Vec::new();
1065        let query = make_mat(&query_data, 3, 4);
1066        let doc = make_mat(doc_data.as_slice(), 0, 4);
1067
1068        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1069            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1070            let mut scores = scores_buffer::<T>(3);
1071            kernel.compute_max_sim(doc, &mut scores).unwrap();
1072            for (i, &s) in scores.iter().enumerate() {
1073                assert_eq!(
1074                    s,
1075                    T::NO_MATCH,
1076                    "{label}({isa:?}) zero-doc slot {i} should be the NO_MATCH sentinel",
1077                );
1078            }
1079        }
1080    }
1081
1082    fn check_zero_query<T>(label: &str)
1083    where
1084        T: MaxSimElement + FromF32,
1085    {
1086        let query_data: Vec<T> = Vec::new();
1087        let doc_data = make_test_data::<T>(2 * 4, 4, 0);
1088        let query = make_mat(query_data.as_slice(), 0, 4);
1089        let doc = make_mat(&doc_data, 2, 4);
1090
1091        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
1092            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
1093            assert_eq!(
1094                kernel.nrows(),
1095                0,
1096                "{label}({isa:?}) empty query should yield nrows=0",
1097            );
1098            let mut scores = scores_buffer::<T>(0);
1099            kernel
1100                .compute_max_sim(doc, &mut scores)
1101                .unwrap_or_else(|e| panic!("{label}({isa:?}) expected Ok, got {e:?}"));
1102        }
1103    }
1104
1105    macro_rules! test_matches_fallback {
1106        ($mod_name:ident, $ty:ty, $tol:expr, $label:literal) => {
1107            mod $mod_name {
1108                use super::*;
1109
1110                #[test]
1111                fn chamfer_matches_fallback() {
1112                    check_chamfer_matches::<$ty>($tol, $label);
1113                }
1114
1115                #[test]
1116                fn max_sim_matches_fallback() {
1117                    check_max_sim_matches::<$ty>($tol, $label);
1118                }
1119
1120                #[test]
1121                fn errors_on_size_mismatch() {
1122                    check_size_mismatch::<$ty>($label);
1123                }
1124
1125                #[test]
1126                fn zero_docs_fills_sentinel() {
1127                    check_zero_docs_fills_sentinel::<$ty>($label);
1128                }
1129
1130                #[test]
1131                fn zero_query_returns_ok() {
1132                    check_zero_query::<$ty>($label);
1133                }
1134            }
1135        };
1136    }
1137
1138    test_matches_fallback!(f32, f32, 1e-10, "f32 ");
1139    test_matches_fallback!(f16, half::f16, 1e-10, "f16 ");
1140    test_matches_fallback!(i8, i8, 1e-10, "i8 ");
1141}