Skip to main content

diskann_quantization/minmax/multi/
factory.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use diskann_wide::arch::Scalar;
7#[cfg(target_arch = "aarch64")]
8use diskann_wide::arch::aarch64::Neon;
9#[cfg(target_arch = "x86_64")]
10use diskann_wide::arch::x86_64::{V3, V4};
11
12use super::{
13    kernel::{MinMaxMaxSim, MinMaxMaxSimKernel},
14    prepared,
15    reference::ReferenceKernel,
16};
17use crate::{
18    bits::{Representation, Unsigned},
19    matrix_kernels::maxsim::packed_minmax_x_unpacked_minmax::max_k,
20    minmax::MinMaxMeta,
21    multi_vector::{BuildMaxSimError, MatRef, MaxSimIsa, NotSupported, QueryMatRef},
22};
23
24/// Build a kernel that owns a copy of the MinMax query.
25///
26/// `Auto` selects a backend at construction. Explicit ISA requests never silently
27/// fall back. `Reference` uses an independent scalar implementation.
28///
29/// Kernels return negative maximum inner products, matching the MaxSim distance convention.
30///
31/// Supports equal query/document bit widths from 1 through 8, plus 8-bit queries
32/// with 1-, 2-, or 4-bit documents. Other combinations fail to compile.
33///
34/// # Errors
35///
36/// Returns [`BuildMaxSimError`] for an unavailable ISA, a query dimension exceeding
37/// the bit-width pair's accumulator limit, or an overflowing storage size.
38/// Dimension and canonical-size checks apply to every backend, including empty queries;
39/// packed-size checks apply only to prepared kernels.
40/// Allocation exhaustion follows the allocator's normal behavior.
41///
42/// # Example
43///
44/// ```
45/// use diskann_quantization::{
46///     minmax::{MinMaxMeta, build_minmax_max_sim},
47///     multi_vector::{Defaulted, Mat, MaxSimIsa, QueryMatRef},
48/// };
49/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
50/// let query = Mat::new(MinMaxMeta::<8>::new(2, 16), Defaulted)?;
51/// let document = Mat::new(MinMaxMeta::<4>::new(3, 16), Defaulted)?;
52/// let query = QueryMatRef::from(query.as_view());
53/// let kernel = build_minmax_max_sim::<8, 4>(MaxSimIsa::Auto, query)?;
54/// let mut scores = vec![0.0; kernel.nrows()];
55/// kernel.compute_max_sim(document.as_view(), &mut scores)?;
56/// assert_eq!(scores, vec![0.0; 2]);
57/// # Ok(())
58/// # }
59/// # example().unwrap();
60/// ```
61pub fn build_minmax_max_sim<const A_BITS: usize, const B_BITS: usize>(
62    isa: MaxSimIsa,
63    query: QueryMatRef<'_, MinMaxMeta<A_BITS>>,
64) -> Result<MinMaxMaxSim<B_BITS>, BuildMaxSimError>
65where
66    Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
67{
68    <Unsigned as MinMaxKernelFactory<A_BITS, B_BITS>>::build(isa, query)
69}
70
71/// Precompiled query/document bit-width combinations.
72#[doc(hidden)]
73pub trait MinMaxKernelFactory<const A_BITS: usize, const B_BITS: usize>:
74    Representation<A_BITS> + Representation<B_BITS>
75{
76    fn build(
77        isa: MaxSimIsa,
78        query: QueryMatRef<'_, MinMaxMeta<A_BITS>>,
79    ) -> Result<MinMaxMaxSim<B_BITS>, BuildMaxSimError>
80    where
81        Unsigned: Representation<A_BITS> + Representation<B_BITS>;
82}
83
84// Concrete, non-generic implementations keep backend compilation in this crate.
85macro_rules! factory {
86    ($a:literal, $b:literal, $scalar:ident, $v3:ident, $v4:ident) => {
87        impl MinMaxKernelFactory<$a, $b> for Unsigned {
88            fn build(
89                isa: MaxSimIsa,
90                query: QueryMatRef<'_, MinMaxMeta<$a>>,
91            ) -> Result<MinMaxMaxSim<$b>, BuildMaxSimError> {
92                let query = query.0;
93                validate_query::<$a, $b>(query)?;
94                let inner: Box<dyn MinMaxMaxSimKernel<$b>> = match RuntimeArch::resolve(isa)? {
95                    RuntimeArch::Scalar(a) => Box::new(prepared::$scalar::<$b>::new(a, query)?),
96                    #[cfg(target_arch = "x86_64")]
97                    RuntimeArch::V3(a) => Box::new(prepared::$v3::<$b>::new(a, query)?),
98                    #[cfg(target_arch = "x86_64")]
99                    RuntimeArch::V4(a) => Box::new(prepared::$v4::<$b>::new(a, query)?),
100                    #[cfg(target_arch = "aarch64")]
101                    RuntimeArch::Neon(a) => Box::new(prepared::NeonBytes::<$b>::new(a, query)?),
102                    RuntimeArch::Reference => Box::new(ReferenceKernel::<$a, $b>::new(query)?),
103                };
104                Ok(MinMaxMaxSim { inner })
105            }
106        }
107    };
108}
109factory!(1, 1, ScalarBytes, V3Bytes, V4Bytes);
110factory!(2, 2, ScalarBytes, V3Bytes, V4Bytes);
111factory!(3, 3, ScalarBytes, V3Bytes, V4Bytes);
112factory!(4, 4, ScalarBytes, V3Bytes, V4Bytes);
113factory!(5, 5, ScalarBytes, V3Bytes, V4Bytes);
114factory!(6, 6, ScalarBytes, V3Bytes, V4Bytes);
115factory!(7, 7, ScalarBytes, V3Words, V4Bytes);
116factory!(8, 8, ScalarWords, V3Words, V4Words);
117factory!(8, 1, ScalarBytes, V3Bytes, V4Bytes);
118factory!(8, 2, ScalarBytes, V3Bytes, V4Bytes);
119factory!(8, 4, ScalarBytes, V3Bytes, V4Bytes);
120
121fn validate_query<const A_BITS: usize, const B_BITS: usize>(
122    query: MatRef<'_, MinMaxMeta<A_BITS>>,
123) -> Result<(), BuildMaxSimError>
124where
125    Unsigned: Representation<A_BITS> + Representation<B_BITS>,
126{
127    let dim = query.repr().intrinsic_dim();
128    let limit = max_k(A_BITS, B_BITS);
129    if dim > limit {
130        return Err(BuildMaxSimError::DimTooLarge(dim, limit));
131    }
132    query.as_canonical_view()?;
133    Ok(())
134}
135
136#[derive(Debug, Clone, Copy)]
137enum RuntimeArch {
138    Scalar(Scalar),
139    #[cfg(target_arch = "x86_64")]
140    V3(V3),
141    #[cfg(target_arch = "x86_64")]
142    V4(V4),
143    #[cfg(target_arch = "aarch64")]
144    Neon(Neon),
145    Reference,
146}
147
148impl RuntimeArch {
149    fn resolve(isa: MaxSimIsa) -> Result<Self, NotSupported> {
150        let unavailable = || NotSupported {
151            isa,
152            reason: "requested ISA unavailable on this host",
153        };
154        Ok(match isa {
155            MaxSimIsa::Auto => {
156                #[cfg(target_arch = "x86_64")]
157                {
158                    if let Some(arch) = V4::new_checked() {
159                        return Ok(Self::V4(arch));
160                    }
161                    if let Some(arch) = V3::new_checked() {
162                        return Ok(Self::V3(arch));
163                    }
164                }
165                #[cfg(target_arch = "aarch64")]
166                if let Some(arch) = Neon::new_checked() {
167                    return Ok(Self::Neon(arch));
168                }
169                Self::Scalar(Scalar::new())
170            }
171            MaxSimIsa::Scalar => Self::Scalar(Scalar::new()),
172            MaxSimIsa::Reference => Self::Reference,
173            #[cfg(target_arch = "x86_64")]
174            MaxSimIsa::X86_64_V3 => Self::V3(V3::new_checked().ok_or_else(unavailable)?),
175            #[cfg(target_arch = "x86_64")]
176            MaxSimIsa::X86_64_V4 => Self::V4(V4::new_checked().ok_or_else(unavailable)?),
177            #[cfg(not(target_arch = "x86_64"))]
178            MaxSimIsa::X86_64_V3 | MaxSimIsa::X86_64_V4 => return Err(unavailable()),
179            #[cfg(target_arch = "aarch64")]
180            MaxSimIsa::Neon => Self::Neon(Neon::new_checked().ok_or_else(unavailable)?),
181            #[cfg(not(target_arch = "aarch64"))]
182            MaxSimIsa::Neon => return Err(unavailable()),
183        })
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use crate::{
191        minmax::MinMaxCompensation,
192        multi_vector::{Defaulted, Mat, MaxSimError},
193    };
194
195    fn matrix<const BITS: usize>(rows: usize, dim: usize) -> Mat<MinMaxMeta<BITS>>
196    where
197        Unsigned: Representation<BITS>,
198    {
199        let mut mat = Mat::new(MinMaxMeta::new(rows, dim), Defaulted).unwrap();
200        for (i, mut row) in mat.rows_mut().enumerate() {
201            let mut sum = 0;
202            for d in 0..dim {
203                let code = ((i * 17 + d * 29 + 7) % (1 << BITS)) as i64;
204                row.vector_mut().set(d, code).unwrap();
205                sum += code;
206            }
207            let a = (i + 1) as f32 / 8.0;
208            row.set_meta(MinMaxCompensation {
209                dim: dim as u32,
210                a,
211                b: i as f32 / 4.0 - 1.0,
212                n: a * sum as f32,
213                norm_squared: 0.0,
214            });
215        }
216        mat
217    }
218
219    fn constant_matrix<const BITS: usize>(
220        rows: usize,
221        dim: usize,
222        code: i64,
223    ) -> Mat<MinMaxMeta<BITS>>
224    where
225        Unsigned: Representation<BITS>,
226    {
227        let mut mat = Mat::new(MinMaxMeta::new(rows, dim), Defaulted).unwrap();
228        for mut row in mat.rows_mut() {
229            for d in 0..dim {
230                row.vector_mut().set(d, code).unwrap();
231            }
232            row.set_meta(MinMaxCompensation {
233                dim: dim as u32,
234                a: 1.0,
235                n: dim as f32 * code as f32,
236                ..Default::default()
237            });
238        }
239        mat
240    }
241
242    fn available_isas() -> impl Iterator<Item = MaxSimIsa> {
243        [
244            MaxSimIsa::Scalar,
245            MaxSimIsa::Auto,
246            MaxSimIsa::X86_64_V3,
247            MaxSimIsa::X86_64_V4,
248            MaxSimIsa::Neon,
249            MaxSimIsa::Reference,
250        ]
251        .into_iter()
252        .filter(|isa| isa.is_available())
253    }
254
255    fn assert_matches_reference<const A_BITS: usize, const B_BITS: usize>()
256    where
257        Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
258    {
259        // Exercise partial query panels, document panels, and K packs on every backend.
260        let (rows, docs, dim) = (17, 9, 65);
261        let query = matrix::<A_BITS>(rows, dim);
262        let document = matrix::<B_BITS>(docs, dim);
263        let reference =
264            build_minmax_max_sim::<A_BITS, B_BITS>(MaxSimIsa::Reference, query.as_view().into())
265                .unwrap();
266        let mut expected = vec![0.0; rows];
267        reference
268            .compute_max_sim(document.as_view(), &mut expected)
269            .unwrap();
270        for isa in available_isas().filter(|&isa| isa != MaxSimIsa::Reference) {
271            let kernel =
272                build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into()).unwrap();
273            assert_eq!(kernel.nrows(), rows);
274            let mut guarded = vec![123.0; rows + 2];
275            kernel
276                .compute_max_sim(document.as_view(), &mut guarded[1..rows + 1])
277                .unwrap();
278            for (actual, expected) in guarded[1..rows + 1].iter().zip(&expected) {
279                assert!(
280                    actual == expected
281                        || (actual.is_finite()
282                            && expected.is_finite()
283                            && (actual - expected).abs() <= 1e-5 * expected.abs().max(1.0)),
284                    "{A_BITS}x{B_BITS}, {isa}: {actual} != {expected}"
285                );
286            }
287            assert_eq!((guarded[0], guarded[rows + 1]), (123.0, 123.0));
288        }
289    }
290
291    #[test]
292    fn matches_reference_across_supported_bit_widths() {
293        assert_matches_reference::<1, 1>();
294        assert_matches_reference::<2, 2>();
295        assert_matches_reference::<3, 3>();
296        assert_matches_reference::<4, 4>();
297        assert_matches_reference::<5, 5>();
298        assert_matches_reference::<6, 6>();
299        assert_matches_reference::<7, 7>();
300        assert_matches_reference::<8, 8>();
301        assert_matches_reference::<8, 1>();
302        assert_matches_reference::<8, 2>();
303        assert_matches_reference::<8, 4>();
304    }
305
306    #[test]
307    fn handles_empty_inputs() {
308        fn check<const B_BITS: usize>()
309        where
310            Unsigned: MinMaxKernelFactory<8, B_BITS>,
311        {
312            for (rows, docs, dim, expected) in [
313                (0, 5, 7, 0.0),
314                (3, 0, 7, f32::MAX),
315                (3, 5, 0, 0.0),
316                (3, 0, 0, f32::MAX),
317            ] {
318                let query = matrix::<8>(rows, dim);
319                let document = matrix::<B_BITS>(docs, dim);
320                for isa in available_isas() {
321                    let kernel =
322                        build_minmax_max_sim::<8, B_BITS>(isa, query.as_view().into()).unwrap();
323                    let mut scores = vec![f32::NAN; rows];
324                    kernel
325                        .compute_max_sim(document.as_view(), &mut scores)
326                        .unwrap();
327                    assert_eq!(
328                        scores,
329                        vec![expected; rows],
330                        "{isa}, B_BITS={B_BITS}, {rows}x{docs}x{dim}"
331                    );
332                }
333            }
334        }
335        check::<4>();
336        check::<8>();
337    }
338
339    #[test]
340    fn preserves_scores_on_error() {
341        let query = matrix::<8>(2, 5);
342        for isa in available_isas() {
343            let kernel = build_minmax_max_sim::<8, 8>(isa, query.as_view().into()).unwrap();
344            let doc = matrix::<8>(1, 6);
345            let mut scores = [123.0; 2];
346            assert!(matches!(
347                kernel.compute_max_sim(doc.as_view(), &mut scores),
348                Err(MaxSimError::UnequalDim(6, 5))
349            ));
350            assert_eq!(scores, [123.0; 2]);
351            let doc = matrix::<8>(1, 5);
352            let mut scores = [123.0];
353            assert!(matches!(
354                kernel.compute_max_sim(doc.as_view(), &mut scores),
355                Err(MaxSimError::InvalidBufferLength(1, 2))
356            ));
357            assert_eq!(scores, [123.0]);
358        }
359    }
360
361    #[test]
362    fn rejects_unavailable_isas() {
363        let query = matrix::<8>(2, 5);
364        for isa in [MaxSimIsa::X86_64_V3, MaxSimIsa::X86_64_V4, MaxSimIsa::Neon] {
365            if !isa.is_available() {
366                assert!(matches!(
367                    build_minmax_max_sim::<8, 8>(isa, query.as_view().into()),
368                    Err(BuildMaxSimError::NotSupported(error)) if error.isa == isa
369                ));
370            }
371        }
372    }
373
374    #[test]
375    fn rejects_excessive_dimensions() {
376        let limit = max_k(8, 8);
377        let dim = limit + 1;
378        let query = MatRef::new(MinMaxMeta::<8>::new(0, dim), &[] as &[u8]).unwrap();
379        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
380            let result = build_minmax_max_sim::<8, 8>(isa, query.into());
381            assert!(
382                matches!(result, Err(BuildMaxSimError::DimTooLarge(found, max))
383                if found == dim && max == limit),
384                "{isa}"
385            );
386        }
387    }
388
389    #[test]
390    fn owns_query_data() {
391        let document = constant_matrix::<4>(2, 3, 2);
392        for isa in available_isas() {
393            let kernel = {
394                let query = constant_matrix::<8>(2, 3, 1);
395                build_minmax_max_sim::<8, 4>(isa, query.as_view().into()).unwrap()
396            };
397            let mut scores = [f32::NAN; 2];
398            kernel
399                .compute_max_sim(document.as_view(), &mut scores)
400                .unwrap();
401            assert_eq!(scores, [-6.0; 2], "{isa}");
402        }
403    }
404
405    #[test]
406    fn selects_backend_and_layout() {
407        fn assert_layout<const A_BITS: usize, const B_BITS: usize, K: 'static>(isa: MaxSimIsa)
408        where
409            Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
410        {
411            let query = matrix::<A_BITS>(1, 3);
412            assert_eq!(
413                build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into())
414                    .unwrap()
415                    .inner
416                    .inner_type_id(),
417                std::any::TypeId::of::<K>(),
418                "{isa}, A_BITS={A_BITS}, B_BITS={B_BITS}",
419            );
420        }
421
422        assert_layout::<7, 7, prepared::ScalarBytes<7>>(MaxSimIsa::Scalar);
423        assert_layout::<8, 8, prepared::ScalarWords<8>>(MaxSimIsa::Scalar);
424        #[cfg(target_arch = "x86_64")]
425        {
426            if V3::new_checked().is_some() {
427                assert_layout::<6, 6, prepared::V3Bytes<6>>(MaxSimIsa::X86_64_V3);
428                assert_layout::<7, 7, prepared::V3Words<7>>(MaxSimIsa::X86_64_V3);
429            }
430            if V4::new_checked().is_some() {
431                assert_layout::<7, 7, prepared::V4Bytes<7>>(MaxSimIsa::X86_64_V4);
432                assert_layout::<8, 8, prepared::V4Words<8>>(MaxSimIsa::X86_64_V4);
433            }
434        }
435        #[cfg(target_arch = "aarch64")]
436        if Neon::new_checked().is_some() {
437            assert_layout::<8, 8, prepared::NeonBytes<8>>(MaxSimIsa::Neon);
438        }
439    }
440
441    #[test]
442    fn prepared_type_is_independent_of_query_bit_width() {
443        fn check<const B_BITS: usize>()
444        where
445            Unsigned: MinMaxKernelFactory<B_BITS, B_BITS> + MinMaxKernelFactory<8, B_BITS>,
446        {
447            let narrow = matrix::<B_BITS>(1, 3);
448            let wide = matrix::<8>(1, 3);
449            for isa in available_isas().filter(|&isa| isa != MaxSimIsa::Reference) {
450                let narrow =
451                    build_minmax_max_sim::<B_BITS, B_BITS>(isa, narrow.as_view().into()).unwrap();
452                let wide = build_minmax_max_sim::<8, B_BITS>(isa, wide.as_view().into()).unwrap();
453                assert_eq!(
454                    narrow.inner.inner_type_id(),
455                    wide.inner.inner_type_id(),
456                    "{isa}, B_BITS={B_BITS}",
457                );
458            }
459        }
460        check::<1>();
461        check::<2>();
462        check::<4>();
463    }
464
465    #[test]
466    fn handles_nonfinite_scores() {
467        fn check<const A_BITS: usize, const B_BITS: usize>()
468        where
469            Unsigned: MinMaxKernelFactory<A_BITS, B_BITS>,
470        {
471            let max_code = (1i64 << B_BITS) - 1;
472            let max_query = (1i64 << A_BITS) - 1;
473            let query = constant_matrix::<A_BITS>(1, 5, max_query);
474            let mut document = constant_matrix::<B_BITS>(2, 5, max_code);
475            let finite_score = -(5 * max_query * max_code) as f32;
476            for isa in available_isas() {
477                let kernel =
478                    build_minmax_max_sim::<A_BITS, B_BITS>(isa, query.as_view().into()).unwrap();
479                let mut scores = [0.0];
480                for (scale, expected) in [
481                    (1.0, finite_score),
482                    (f32::NEG_INFINITY, f32::MAX),
483                    (f32::INFINITY, f32::NEG_INFINITY),
484                    (f32::NAN, f32::MAX),
485                    (1.0, finite_score),
486                ] {
487                    for mut row in document.rows_mut() {
488                        let mut meta = row.meta();
489                        meta.a = scale;
490                        row.set_meta(meta);
491                    }
492                    kernel
493                        .compute_max_sim(document.as_view(), &mut scores)
494                        .unwrap();
495                    assert_eq!(scores, [expected], "{isa}, B_BITS={B_BITS}");
496                }
497                let mut row = document.get_row_mut(0).unwrap();
498                let mut meta = row.meta();
499                meta.a = f32::NAN;
500                row.set_meta(meta);
501                kernel
502                    .compute_max_sim(document.as_view(), &mut scores)
503                    .unwrap();
504                assert_eq!(
505                    scores,
506                    [finite_score],
507                    "{isa}, B_BITS={B_BITS}, mixed NaN/finite"
508                );
509            }
510        }
511        check::<6, 6>();
512        check::<7, 7>();
513        check::<8, 8>();
514        check::<8, 4>();
515    }
516
517    #[cfg(not(debug_assertions))]
518    #[test]
519    fn rejects_query_size_overflow() {
520        // With release overflow behavior, rows * 20 wraps to zero for K=0.
521        // Reject this view before either packing or cloning can access its claimed rows.
522        let rows = 1usize << (usize::BITS - 2);
523        let query = MatRef::new(MinMaxMeta::<8>::new(rows, 0), &[] as &[u8]).unwrap();
524        for isa in [MaxSimIsa::Auto, MaxSimIsa::Reference] {
525            assert!(matches!(
526                build_minmax_max_sim::<8, 4>(isa, query.into()),
527                Err(BuildMaxSimError::Overflow(_))
528            ));
529        }
530    }
531}