Skip to main content

diskann_quantization/multi_vector/distance/
factory.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license.
3
4//! Factory + concrete `MaxSimKernel<T>` impls for the multi-vector distance
5//! API. BYOTE entry point — see [`build_max_sim`].
6
7use std::num::NonZeroUsize;
8
9use diskann_vector::distance::InnerProduct;
10use diskann_vector::{DistanceFunctionMut, PureDistanceFunction};
11use diskann_wide::Architecture;
12use diskann_wide::arch::Scalar;
13#[cfg(target_arch = "aarch64")]
14use diskann_wide::arch::aarch64::Neon;
15#[cfg(target_arch = "x86_64")]
16use diskann_wide::arch::x86_64::{V3, V4};
17
18use super::isa::{MaxSimIsa, NotSupported};
19use super::kernel::{Erase, MaxSimKernel};
20use super::max_sim::{MaxSim, MaxSimError};
21use crate::matrix_kernels as mk;
22use crate::multi_vector::distance::QueryMatRef;
23use crate::multi_vector::{BlockTransposed, Mat, MatRef, Standard};
24
25// ─────────────────────────────────────────────────────────────────────────
26//  Prepared<A, Q, NR> — concrete kernel for the arch-dispatched paths.
27// ─────────────────────────────────────────────────────────────────────────
28
29// ZST for selecting the packing for the documents.
30//
31// TODO: Some of these decisions should eventually ve moved lower into `matrix_kernels`.
32#[derive(Debug, Clone, Copy)]
33struct Pack<const NR: usize>;
34
35#[derive(Debug)]
36struct Prepared<A, Q, const NR: usize> {
37    arch: A,
38    prepared: Q,
39    _packing: Pack<NR>,
40}
41
42impl<A, const GROUP: usize, const NR: usize> MaxSimKernel<f32>
43    for Prepared<A, BlockTransposed<f32, GROUP>, NR>
44where
45    A: Architecture,
46    for<'a> mk::maxsim::packed_f32_x_unpacked_f32::Driver<'a, A, GROUP, NR>: mk::Drive,
47{
48    fn nrows(&self) -> usize {
49        self.prepared.nrows()
50    }
51
52    fn compute_max_sim(
53        &self,
54        doc: MatRef<'_, Standard<f32>>,
55        scores: &mut [f32],
56    ) -> Result<(), MaxSimError> {
57        if scores.len() != self.nrows() {
58            return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
59        }
60
61        if doc.vector_dim() != self.prepared.ncols() {
62            return Err(MaxSimError::UnequalDim(
63                doc.vector_dim(),
64                self.prepared.ncols(),
65            ));
66        }
67
68        let Some(k) = NonZeroUsize::new(self.prepared.ncols()).map(mk::DimK::new) else {
69            scores.fill(if doc.num_vectors() == 0 {
70                f32::MAX
71            } else {
72                0.0
73            });
74            return Ok(());
75        };
76
77        let Some(a) = mk::blocks::packed::View::from_block_transposed(self.prepared.as_view())
78        else {
79            return Ok(());
80        };
81
82        let Some(b) = mk::blocks::unpacked::View::from_matrix_view(doc.as_matrix_view()) else {
83            scores.fill(f32::MAX);
84            return Ok(());
85        };
86
87        // SAFETY: The dimension check establishes that `a.k() == b.k() == k`.
88        // The length check establishes that `scores` occupies exactly the
89        // packed blocks in `a`.
90        let mut driver = unsafe {
91            mk::maxsim::packed_f32_x_unpacked_f32::Driver::new(
92                self.arch,
93                a,
94                b,
95                scores,
96                k,
97                mk::Cache::detect(),
98            )
99        };
100
101        mk::Drive::drive(&mut driver);
102
103        scores.iter_mut().for_each(|s| *s = -*s);
104
105        Ok(())
106    }
107}
108
109impl<A, const GROUP: usize, const NR: usize> MaxSimKernel<half::f16>
110    for Prepared<A, BlockTransposed<f32, GROUP>, NR>
111where
112    A: Architecture,
113    for<'a> mk::maxsim::packed_f32_x_unpacked_f16::Driver<'a, A, GROUP, NR>: mk::Drive,
114{
115    fn nrows(&self) -> usize {
116        self.prepared.nrows()
117    }
118
119    fn compute_max_sim(
120        &self,
121        doc: MatRef<'_, Standard<half::f16>>,
122        scores: &mut [f32],
123    ) -> Result<(), MaxSimError> {
124        if scores.len() != self.nrows() {
125            return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
126        }
127
128        if doc.vector_dim() != self.prepared.ncols() {
129            return Err(MaxSimError::UnequalDim(
130                doc.vector_dim(),
131                self.prepared.ncols(),
132            ));
133        }
134
135        let Some(k) = NonZeroUsize::new(self.prepared.ncols()).map(mk::DimK::new) else {
136            scores.fill(if doc.num_vectors() == 0 {
137                f32::MAX
138            } else {
139                0.0
140            });
141            return Ok(());
142        };
143
144        let Some(a) = mk::blocks::packed::View::from_block_transposed(self.prepared.as_view())
145        else {
146            return Ok(());
147        };
148
149        let Some(b) = mk::blocks::unpacked::View::from_matrix_view(doc.as_matrix_view()) else {
150            scores.fill(f32::MAX);
151            return Ok(());
152        };
153
154        // SAFETY: The dimension check establishes that `a.k() == b.k() == k`.
155        // The length check establishes that `scores` occupies exactly the
156        // packed blocks in `a`.
157        let mut driver = unsafe {
158            mk::maxsim::packed_f32_x_unpacked_f16::Driver::new(
159                self.arch,
160                a,
161                b,
162                scores,
163                k,
164                mk::Cache::detect(),
165            )
166        };
167
168        mk::Drive::drive(&mut driver);
169
170        scores.iter_mut().for_each(|s| *s = -*s);
171
172        Ok(())
173    }
174}
175
176// ─────────────────────────────────────────────────────────────────────────
177//  ReferenceKernel<T> — non-SIMD fallback that wraps MaxSim::evaluate.
178// ─────────────────────────────────────────────────────────────────────────
179
180struct ReferenceKernel<T: Copy> {
181    query: Mat<Standard<T>>,
182}
183
184impl<T: Copy + std::fmt::Debug> std::fmt::Debug for ReferenceKernel<T> {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        f.debug_struct("ReferenceKernel")
187            .field("nrows", &self.query.num_vectors())
188            .finish()
189    }
190}
191
192impl<T: Copy> ReferenceKernel<T> {
193    fn new(query: MatRef<'_, Standard<T>>) -> Self {
194        Self {
195            query: query.to_owned(),
196        }
197    }
198}
199
200impl<T> MaxSimKernel<T> for ReferenceKernel<T>
201where
202    T: Copy + Send + Sync + std::fmt::Debug + 'static,
203    InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
204{
205    fn nrows(&self) -> usize {
206        self.query.num_vectors()
207    }
208
209    fn compute_max_sim(
210        &self,
211        doc: MatRef<'_, Standard<T>>,
212        scores: &mut [f32],
213    ) -> Result<(), MaxSimError> {
214        if scores.len() != self.nrows() {
215            return Err(MaxSimError::InvalidBufferLength(scores.len(), self.nrows()));
216        }
217        if doc.vector_dim() != self.query.vector_dim() {
218            return Err(MaxSimError::UnequalDim(
219                doc.vector_dim(),
220                self.query.vector_dim(),
221            ));
222        }
223        if doc.num_vectors() == 0 {
224            scores.fill(f32::MAX);
225            return Ok(());
226        }
227        let query: QueryMatRef<'_, Standard<T>> = self.query.as_view().into();
228        let mut max_sim = MaxSim::new(scores);
229        max_sim.evaluate(query, doc)
230    }
231}
232
233// ─────────────────────────────────────────────────────────────────────────
234//  BuildAndErase<E> — Target1 impls used by `dispatch1_no_features` (Auto).
235// ─────────────────────────────────────────────────────────────────────────
236
237struct BuildAndErase<E>(E);
238
239// ───── f32 Target1 impls ─────
240
241impl<E: Erase<f32>> diskann_wide::arch::Target1<Scalar, E::Output, MatRef<'_, Standard<f32>>>
242    for BuildAndErase<E>
243{
244    fn run(self, arch: Scalar, query: MatRef<'_, Standard<f32>>) -> E::Output {
245        let prepared = BlockTransposed::<f32, 8>::from_matrix_view(query.as_matrix_view());
246        self.0.erase(Prepared {
247            arch,
248            prepared,
249            _packing: Pack::<2>,
250        })
251    }
252}
253
254#[cfg(target_arch = "x86_64")]
255impl<E: Erase<f32>> diskann_wide::arch::Target1<V3, E::Output, MatRef<'_, Standard<f32>>>
256    for BuildAndErase<E>
257{
258    fn run(self, arch: V3, query: MatRef<'_, Standard<f32>>) -> E::Output {
259        let prepared = BlockTransposed::<f32, 16>::from_matrix_view(query.as_matrix_view());
260        self.0.erase(Prepared {
261            arch,
262            prepared,
263            _packing: Pack::<6>,
264        })
265    }
266}
267
268#[cfg(target_arch = "x86_64")]
269impl<E: Erase<f32>> diskann_wide::arch::Target1<V4, E::Output, MatRef<'_, Standard<f32>>>
270    for BuildAndErase<E>
271{
272    fn run(self, arch: V4, query: MatRef<'_, Standard<f32>>) -> E::Output {
273        let prepared = BlockTransposed::<f32, 32>::from_matrix_view(query.as_matrix_view());
274        self.0.erase(Prepared {
275            arch,
276            prepared,
277            _packing: Pack::<6>,
278        })
279    }
280}
281
282#[cfg(target_arch = "aarch64")]
283impl<E: Erase<f32>> diskann_wide::arch::Target1<Neon, E::Output, MatRef<'_, Standard<f32>>>
284    for BuildAndErase<E>
285{
286    fn run(self, arch: Neon, query: MatRef<'_, Standard<f32>>) -> E::Output {
287        let prepared = BlockTransposed::<f32, 8>::from_matrix_view(query.as_matrix_view());
288        self.0.erase(Prepared {
289            arch,
290            prepared,
291            _packing: Pack::<6>,
292        })
293    }
294}
295
296// ───── f16 Target1 impls ─────
297
298impl<E: Erase<half::f16>>
299    diskann_wide::arch::Target1<Scalar, E::Output, MatRef<'_, Standard<half::f16>>>
300    for BuildAndErase<E>
301{
302    fn run(self, arch: Scalar, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
303        let prepared = BlockTransposed::<f32, 8>::from_matrix_view(
304            query
305                .as_matrix_view()
306                .map(|v| diskann_wide::cast_f16_to_f32(*v))
307                .as_view(),
308        );
309        self.0.erase(Prepared {
310            arch,
311            prepared,
312            _packing: Pack::<2>,
313        })
314    }
315}
316
317#[cfg(target_arch = "x86_64")]
318impl<E: Erase<half::f16>>
319    diskann_wide::arch::Target1<V3, E::Output, MatRef<'_, Standard<half::f16>>>
320    for BuildAndErase<E>
321{
322    fn run(self, arch: V3, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
323        let prepared = BlockTransposed::<f32, 16>::from_matrix_view(
324            query
325                .as_matrix_view()
326                .map(|v| diskann_wide::cast_f16_to_f32(*v))
327                .as_view(),
328        );
329        self.0.erase(Prepared {
330            arch,
331            prepared,
332            _packing: Pack::<6>,
333        })
334    }
335}
336
337#[cfg(target_arch = "x86_64")]
338impl<E: Erase<half::f16>>
339    diskann_wide::arch::Target1<V4, E::Output, MatRef<'_, Standard<half::f16>>>
340    for BuildAndErase<E>
341{
342    fn run(self, arch: V4, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
343        let prepared = BlockTransposed::<f32, 32>::from_matrix_view(
344            query
345                .as_matrix_view()
346                .map(|v| diskann_wide::cast_f16_to_f32(*v))
347                .as_view(),
348        );
349        self.0.erase(Prepared {
350            arch,
351            prepared,
352            _packing: Pack::<6>,
353        })
354    }
355}
356
357#[cfg(target_arch = "aarch64")]
358impl<E: Erase<half::f16>>
359    diskann_wide::arch::Target1<Neon, E::Output, MatRef<'_, Standard<half::f16>>>
360    for BuildAndErase<E>
361{
362    fn run(self, arch: Neon, query: MatRef<'_, Standard<half::f16>>) -> E::Output {
363        // Neon dispatches to Scalar (no Neon-specific kernel).
364        let prepared = BlockTransposed::<f32, 8>::from_matrix_view(
365            query
366                .as_matrix_view()
367                .map(|v| diskann_wide::cast_f16_to_f32(*v))
368                .as_view(),
369        );
370        self.0.erase(Prepared {
371            arch,
372            prepared,
373            _packing: Pack::<6>,
374        })
375    }
376}
377
378// ─────────────────────────────────────────────────────────────────────────
379//  MaxSimElement — sealed trait gating accepted element types.
380// ─────────────────────────────────────────────────────────────────────────
381
382mod sealed {
383    pub trait Sealed {}
384}
385
386/// Scalar element types accepted by [`build_max_sim`].
387///
388/// Sealed: external crates cannot add impls. Quantized representations
389/// (PQ, SQ, packed sub-byte) are intentionally excluded — they need
390/// codebook/scale state that [`MatRef<'_, Standard<Self>>`] can't carry.
391pub trait MaxSimElement: sealed::Sealed + Sized + Copy + Send + Sync + 'static {
392    /// Build the concrete kernel for this element type and hand it to
393    /// `erase.erase(...)`.
394    ///
395    /// # Errors
396    ///
397    /// Returns [`NotSupported`] when the requested ISA cannot run on this
398    /// build (e.g. AVX-512 unavailable; aarch64 on x86_64).
399    fn build<E: Erase<Self>>(
400        isa: MaxSimIsa,
401        query: MatRef<'_, Standard<Self>>,
402        erase: E,
403    ) -> Result<E::Output, NotSupported>;
404}
405
406impl sealed::Sealed for f32 {}
407impl sealed::Sealed for half::f16 {}
408
409impl MaxSimElement for f32 {
410    fn build<E: Erase<f32>>(
411        isa: MaxSimIsa,
412        query: MatRef<'_, Standard<f32>>,
413        erase: E,
414    ) -> Result<E::Output, NotSupported> {
415        match isa {
416            MaxSimIsa::Auto => Ok(diskann_wide::arch::dispatch1_no_features(
417                BuildAndErase(erase),
418                query,
419            )),
420            MaxSimIsa::Scalar => Ok(Scalar::new().run1(BuildAndErase(erase), query)),
421            #[cfg(target_arch = "x86_64")]
422            MaxSimIsa::X86_64_V3 => {
423                let arch = V3::new_checked().ok_or(NotSupported {
424                    isa,
425                    reason: "AVX2/FMA unavailable on this CPU",
426                })?;
427                Ok(arch.run1(BuildAndErase(erase), query))
428            }
429            #[cfg(target_arch = "x86_64")]
430            MaxSimIsa::X86_64_V4 => {
431                let arch = V4::new_checked().ok_or(NotSupported {
432                    isa,
433                    reason: "AVX-512 unavailable on this CPU",
434                })?;
435                Ok(arch.run1(BuildAndErase(erase), query))
436            }
437            #[cfg(not(target_arch = "x86_64"))]
438            MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => Err(NotSupported {
439                isa,
440                reason: "x86_64 target only",
441            }),
442            #[cfg(target_arch = "aarch64")]
443            MaxSimIsa::Neon => {
444                let arch = Neon::new_checked().ok_or(NotSupported {
445                    isa,
446                    reason: "Neon unavailable on this CPU",
447                })?;
448                Ok(arch.run1(BuildAndErase(erase), query))
449            }
450            #[cfg(not(target_arch = "aarch64"))]
451            MaxSimIsa::Neon => Err(NotSupported {
452                isa,
453                reason: "aarch64 target only",
454            }),
455            MaxSimIsa::Reference => Ok(erase.erase(ReferenceKernel::<f32>::new(query))),
456        }
457    }
458}
459
460impl MaxSimElement for half::f16 {
461    fn build<E: Erase<half::f16>>(
462        isa: MaxSimIsa,
463        query: MatRef<'_, Standard<half::f16>>,
464        erase: E,
465    ) -> Result<E::Output, NotSupported> {
466        match isa {
467            MaxSimIsa::Auto => Ok(diskann_wide::arch::dispatch1_no_features(
468                BuildAndErase(erase),
469                query,
470            )),
471            MaxSimIsa::Scalar => Ok(Scalar::new().run1(BuildAndErase(erase), query)),
472            #[cfg(target_arch = "x86_64")]
473            MaxSimIsa::X86_64_V3 => {
474                let arch = V3::new_checked().ok_or(NotSupported {
475                    isa,
476                    reason: "AVX2/FMA unavailable on this CPU",
477                })?;
478                Ok(arch.run1(BuildAndErase(erase), query))
479            }
480            #[cfg(target_arch = "x86_64")]
481            MaxSimIsa::X86_64_V4 => {
482                let arch = V4::new_checked().ok_or(NotSupported {
483                    isa,
484                    reason: "AVX-512 unavailable on this CPU",
485                })?;
486                Ok(arch.run1(BuildAndErase(erase), query))
487            }
488            #[cfg(not(target_arch = "x86_64"))]
489            MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => Err(NotSupported {
490                isa,
491                reason: "x86_64 target only",
492            }),
493            #[cfg(target_arch = "aarch64")]
494            MaxSimIsa::Neon => {
495                let arch = Neon::new_checked().ok_or(NotSupported {
496                    isa,
497                    reason: "Neon unavailable on this CPU",
498                })?;
499                Ok(arch.run1(BuildAndErase(erase), query))
500            }
501            #[cfg(not(target_arch = "aarch64"))]
502            MaxSimIsa::Neon => Err(NotSupported {
503                isa,
504                reason: "aarch64 target only",
505            }),
506            MaxSimIsa::Reference => Ok(erase.erase(ReferenceKernel::<half::f16>::new(query))),
507        }
508    }
509}
510
511// ─────────────────────────────────────────────────────────────────────────
512//  Factory entry point.
513// ─────────────────────────────────────────────────────────────────────────
514
515/// Build a multi-vector MaxSim kernel for any [`MaxSimElement`] type.
516///
517/// Thin wrapper over [`MaxSimElement::build`] so callers don't have to name
518/// the trait at the call site.
519///
520/// # Errors
521///
522/// Returns [`NotSupported`] when the requested ISA cannot run on this build.
523pub fn build_max_sim<T: MaxSimElement, E: Erase<T>>(
524    isa: MaxSimIsa,
525    query: MatRef<'_, Standard<T>>,
526    erase: E,
527) -> Result<E::Output, NotSupported> {
528    T::build(isa, query, erase)
529}
530
531#[cfg(test)]
532mod tests {
533    use super::*;
534    use crate::multi_vector::{BoxErase, Chamfer, MaxSim, QueryMatRef};
535
536    /// Local helper trait — picks a sane test value of `T` from an `f32`
537    /// so both `f32` and `half::f16` parameterizations share the same data
538    /// generator.
539    trait FromF32 {
540        fn from_f32(v: f32) -> Self;
541    }
542
543    impl FromF32 for f32 {
544        fn from_f32(v: f32) -> Self {
545            v
546        }
547    }
548
549    impl FromF32 for half::f16 {
550        fn from_f32(v: f32) -> Self {
551            diskann_wide::cast_f32_to_f16(v)
552        }
553    }
554
555    fn make_mat<T: Copy>(data: &[T], nrows: usize, ncols: usize) -> MatRef<'_, Standard<T>> {
556        MatRef::new(Standard::new(nrows, ncols).unwrap(), data).unwrap()
557    }
558
559    fn make_test_data<T: FromF32>(len: usize, ceil: usize, shift: usize) -> Vec<T> {
560        (0..len)
561            .map(|v| T::from_f32(((v + shift) % ceil) as f32))
562            .collect()
563    }
564
565    /// Shapes for the `chamfer_matches_fallback` / `max_sim_matches_fallback`
566    /// agreement checks: `(num_queries, num_docs, dim)`.
567    ///
568    /// Targets the factory wiring (query setup, score writeback) above the
569    /// kernel layer; exhaustive panel/remainder coverage is pinned in
570    /// `kernels::tiled_reduce::tests`.
571    const TEST_CASES: &[(usize, usize, usize)] = &[
572        (0, 1, 1),   // Empty query
573        (1, 0, 1),   // Empty docs
574        (1, 1, 0),   // Empty dim
575        (1, 0, 0),   // Empty docs and dim
576        (1, 1, 4),   // Degenerate
577        (5, 3, 5),   // Prime k; nq > 1 and nd > 1 exercise per-row writeback
578        (17, 4, 64), // A-panel remainder crossing both Scalar and V3 panel widths
579        (16, 6, 32), // B-remainder ≠ 1 (V3 b_remainder = 2)
580    ];
581
582    fn check_chamfer_matches<T>(tol: f32, label: &str)
583    where
584        T: MaxSimElement + FromF32,
585        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
586    {
587        for &(nq, nd, dim) in TEST_CASES {
588            let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
589            let doc_data = make_test_data::<T>(nd * dim, dim, dim);
590
591            let query = make_mat(&query_data, nq, dim);
592            let doc = make_mat(&doc_data, nd, dim);
593
594            let expected = Chamfer::evaluate(QueryMatRef::from(query), doc);
595
596            let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
597            let mut scores = vec![0.0f32; nq];
598            kernel.compute_max_sim(doc, &mut scores).unwrap();
599            let actual: f32 = scores.iter().sum();
600
601            assert!(
602                (actual - expected).abs() < tol,
603                "{label}Chamfer mismatch for ({nq},{nd},{dim}): actual={actual}, expected={expected}",
604            );
605        }
606    }
607
608    fn check_max_sim_matches<T>(tol: f32, label: &str)
609    where
610        T: MaxSimElement + FromF32,
611        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
612    {
613        for &(nq, nd, dim) in TEST_CASES {
614            let query_data = make_test_data::<T>(nq * dim, dim, dim / 2);
615            let doc_data = make_test_data::<T>(nd * dim, dim, dim);
616
617            let query = make_mat(&query_data, nq, dim);
618            let doc = make_mat(&doc_data, nd, dim);
619
620            let mut expected_scores = vec![0.0f32; nq];
621            let _ = MaxSim::new(&mut expected_scores).evaluate(QueryMatRef::from(query), doc);
622
623            let kernel = build_max_sim::<T, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
624            let mut actual_scores = vec![0.0f32; nq];
625            kernel.compute_max_sim(doc, &mut actual_scores).unwrap();
626
627            for i in 0..nq {
628                assert!(
629                    (actual_scores[i] - expected_scores[i]).abs() < tol,
630                    "{label}MaxSim[{i}] mismatch for ({nq},{nd},{dim}): actual={}, expected={}",
631                    actual_scores[i],
632                    expected_scores[i],
633                );
634            }
635        }
636    }
637
638    #[test]
639    fn dimensions_f32() {
640        let data = vec![1.0f32; 5 * 8];
641        let query = make_mat(&data, 5, 8);
642        let kernel = build_max_sim::<f32, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
643        assert_eq!(kernel.nrows(), 5);
644    }
645
646    #[test]
647    fn dimensions_f16() {
648        let data = vec![diskann_wide::cast_f32_to_f16(1.0); 5 * 8];
649        let query = make_mat(data.as_slice(), 5, 8);
650        let kernel = build_max_sim::<half::f16, _>(MaxSimIsa::Auto, query, BoxErase).unwrap();
651        assert_eq!(kernel.nrows(), 5);
652    }
653
654    fn check_size_mismatch<T>(label: &str)
655    where
656        T: MaxSimElement + FromF32,
657        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
658    {
659        let query_data = make_test_data::<T>(3 * 4, 4, 0);
660        let doc_data = make_test_data::<T>(2 * 4, 4, 1);
661        let query = make_mat(&query_data, 3, 4);
662        let doc = make_mat(&doc_data, 2, 4);
663
664        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
665            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
666
667            let mut too_short = vec![0.0f32; 2];
668            match kernel.compute_max_sim(doc, &mut too_short) {
669                Err(MaxSimError::InvalidBufferLength(2, 3)) => {}
670                other => {
671                    panic!("{label}({isa:?}) expected InvalidBufferLength(2, 3), got {other:?}",)
672                }
673            }
674
675            let mut too_long = vec![0.0f32; 4];
676            match kernel.compute_max_sim(doc, &mut too_long) {
677                Err(MaxSimError::InvalidBufferLength(4, 3)) => {}
678                other => {
679                    panic!("{label}({isa:?}) expected InvalidBufferLength(4, 3), got {other:?}",)
680                }
681            }
682        }
683    }
684
685    fn check_zero_docs_fills_sentinel<T>(label: &str)
686    where
687        T: MaxSimElement + FromF32,
688        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
689    {
690        let query_data = make_test_data::<T>(3 * 4, 4, 0);
691        let doc_data: Vec<T> = Vec::new();
692        let query = make_mat(&query_data, 3, 4);
693        let doc = make_mat(doc_data.as_slice(), 0, 4);
694
695        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
696            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
697            let mut scores = vec![0.0f32; 3];
698            kernel.compute_max_sim(doc, &mut scores).unwrap();
699            for (i, &s) in scores.iter().enumerate() {
700                assert_eq!(
701                    s,
702                    f32::MAX,
703                    "{label}({isa:?}) zero-doc slot {i} should be f32::MAX sentinel",
704                );
705            }
706        }
707    }
708
709    fn check_zero_query<T>(label: &str)
710    where
711        T: MaxSimElement + FromF32,
712        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
713    {
714        let query_data: Vec<T> = Vec::new();
715        let doc_data = make_test_data::<T>(2 * 4, 4, 0);
716        let query = make_mat(query_data.as_slice(), 0, 4);
717        let doc = make_mat(&doc_data, 2, 4);
718
719        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
720            let kernel = build_max_sim::<T, _>(isa, query, BoxErase).unwrap();
721            assert_eq!(
722                kernel.nrows(),
723                0,
724                "{label}({isa:?}) empty query should yield nrows=0",
725            );
726            let mut scores: Vec<f32> = Vec::new();
727            kernel
728                .compute_max_sim(doc, &mut scores)
729                .unwrap_or_else(|e| panic!("{label}({isa:?}) expected Ok, got {e:?}"));
730        }
731    }
732
733    macro_rules! test_matches_fallback {
734        ($mod_name:ident, $ty:ty, $tol:expr, $label:literal) => {
735            mod $mod_name {
736                use super::*;
737
738                #[test]
739                fn chamfer_matches_fallback() {
740                    check_chamfer_matches::<$ty>($tol, $label);
741                }
742
743                #[test]
744                fn max_sim_matches_fallback() {
745                    check_max_sim_matches::<$ty>($tol, $label);
746                }
747
748                #[test]
749                fn errors_on_size_mismatch() {
750                    check_size_mismatch::<$ty>($label);
751                }
752
753                #[test]
754                fn zero_docs_fills_sentinel() {
755                    check_zero_docs_fills_sentinel::<$ty>($label);
756                }
757
758                #[test]
759                fn zero_query_returns_ok() {
760                    check_zero_query::<$ty>($label);
761                }
762            }
763        };
764    }
765
766    test_matches_fallback!(f32, f32, 1e-10, "f32 ");
767    test_matches_fallback!(f16, half::f16, 1e-10, "f16 ");
768}