Skip to main content

diskann_quantization/multi_vector/distance/
fallback.rs

1// Copyright (c) Microsoft Corporation. All rights reserved.
2// Licensed under the MIT license.
3
4//! Fallback kernel implementation of multi-vector distance computation.
5
6use std::ops::Deref;
7
8use diskann_vector::distance::InnerProduct;
9use diskann_vector::{DistanceFunctionMut, PureDistanceFunction};
10
11use super::max_sim::{Chamfer, MaxSim};
12use super::projected_eigen::ProjectedEigen;
13use crate::multi_vector::{MatRef, MaxSimError, Repr, Standard};
14
15/////////////////
16// QueryMatRef //
17/////////////////
18
19/// A query matrix view for asymmetric distance functions.
20///
21/// This wrapper distinguishes query matrices from document matrices
22/// at compile time, preventing accidental argument swapping in asymmetric
23/// distance computations like [`MaxSim`] and [`Chamfer`].
24///
25/// # Example
26///
27/// ```
28/// use diskann_quantization::multi_vector::{MatRef, Standard};
29/// use diskann_quantization::multi_vector::distance::QueryMatRef;
30///
31/// let data = [1.0f32, 2.0, 3.0, 4.0];
32/// let view = MatRef::new(Standard::new(2, 2).unwrap(), &data).unwrap();
33/// let query: QueryMatRef<_> = view.into();
34/// ```
35#[derive(Debug, Clone, Copy)]
36pub struct QueryMatRef<'a, T: Repr>(pub MatRef<'a, T>);
37
38impl<'a, T: Repr> From<MatRef<'a, T>> for QueryMatRef<'a, T> {
39    fn from(view: MatRef<'a, T>) -> Self {
40        Self(view)
41    }
42}
43
44/// Deref so that we can transparently access the `MatRef` in distance functions.
45impl<'a, T: Repr> Deref for QueryMatRef<'a, T> {
46    type Target = MatRef<'a, T>;
47
48    fn deref(&self) -> &Self::Target {
49        &self.0
50    }
51}
52
53////////////////////
54// FallbackKernel //
55////////////////////
56
57/// Fallback double-loop kernel to compute max-sim distances over multi-vectors.
58///
59/// This kernel performs a simple double-loop over the rows of `query`
60/// and the `doc` and dispatches to [`InnerProduct`] to compute the similarity.
61pub struct FallbackKernel;
62
63impl FallbackKernel {
64    /// Core kernel for computing per-query-vector max similarities (min negated inner-product).
65    ///
66    /// For each `query` vector, computes the maximum similarity (negated inner product)
67    /// to any document vector, then calls `f(index, score)` with the result.
68    /// If there are no vectors in the `doc`, the score is `f32::MAX`.
69    ///
70    /// The callback can be used to aggregate or set scores as needed - as is the
71    /// case with [`MaxSim`] and [`Chamfer`].
72    ///
73    /// # Arguments
74    ///
75    /// * `query` - The query multi-vector (wrapped as [`QueryMatRef`])
76    /// * `doc` - The document multi-vector
77    /// * `f` - Callback invoked with `(query_index, similarity)` for each query vector
78    #[inline]
79    pub(crate) fn max_sim_kernel<F, T: Copy>(
80        query: QueryMatRef<'_, Standard<T>>,
81        doc: MatRef<'_, Standard<T>>,
82        mut f: F,
83    ) where
84        F: FnMut(usize, f32),
85        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
86    {
87        for (i, q_vec) in query.rows().enumerate() {
88            // `InnerProduct::evaluate` returns negated inner product
89            let mut min_dist = f32::MAX;
90
91            for d_vec in doc.rows() {
92                let dist = InnerProduct::evaluate(q_vec, d_vec);
93                min_dist = min_dist.min(dist);
94            }
95
96            f(i, min_dist);
97        }
98    }
99
100    /// Core kernel for computing per-query-vector projected-eigen scores.
101    ///
102    /// For each `query` vector, sums the negated squared inner product
103    /// against every document vector, then calls `f(index, score)` with the
104    /// result. If there are no vectors in the `doc`, the kernel returns
105    /// immediately.
106    ///
107    /// The callback can be used to aggregate scores as needed - as is the
108    /// case with [`ProjectedEigen`].
109    ///
110    /// # Arguments
111    ///
112    /// * `query` - The query multi-vector (wrapped as [`QueryMatRef`])
113    /// * `doc` - The document multi-vector
114    /// * `f` - Callback invoked with `(query_index, score)` for each query vector
115    #[inline]
116    pub(crate) fn projected_eigen_kernel<F, T: Copy>(
117        query: QueryMatRef<'_, Standard<T>>,
118        doc: MatRef<'_, Standard<T>>,
119        mut f: F,
120    ) where
121        F: FnMut(usize, f32),
122        InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
123    {
124        // Early exit if no doc vectors - callback should never be invoked
125        if doc.num_vectors() == 0 {
126            return;
127        }
128
129        for (i, q_vec) in query.rows().enumerate() {
130            let mut sum = 0.0f32;
131
132            for d_vec in doc.rows() {
133                // `InnerProduct::evaluate` returns the negated inner product;
134                // squaring discards the sign, so negate the squared value to
135                // obtain `-IP(q, d)²`.
136                let ip = InnerProduct::evaluate(q_vec, d_vec);
137                sum += -(ip * ip);
138            }
139
140            f(i, sum);
141        }
142    }
143}
144
145////////////
146// MaxSim //
147////////////
148
149impl<T: Copy>
150    DistanceFunctionMut<
151        QueryMatRef<'_, Standard<T>>,
152        MatRef<'_, Standard<T>>,
153        Result<(), MaxSimError>,
154    > for MaxSim<'_>
155where
156    InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
157{
158    #[inline(always)]
159    fn evaluate(
160        &mut self,
161        query: QueryMatRef<'_, Standard<T>>,
162        doc: MatRef<'_, Standard<T>>,
163    ) -> Result<(), MaxSimError> {
164        let size = self.size();
165        let n_queries = query.num_vectors();
166
167        if self.size() != query.num_vectors() {
168            return Err(MaxSimError::InvalidBufferLength(size, n_queries));
169        }
170
171        if query.vector_dim() != doc.vector_dim() {
172            return Err(MaxSimError::UnequalDim(
173                doc.vector_dim(),
174                query.vector_dim(),
175            ));
176        }
177
178        FallbackKernel::max_sim_kernel(query, doc, |i, score| {
179            // SAFETY: We asserted that self.size() == query.num_vectors(),
180            // and i < query.num_vectors() due to the kernel loop bound.
181            unsafe { *self.scores.get_unchecked_mut(i) = score };
182        });
183
184        Ok(())
185    }
186}
187
188/////////////
189// Chamfer //
190/////////////
191
192impl<T: Copy> PureDistanceFunction<QueryMatRef<'_, Standard<T>>, MatRef<'_, Standard<T>>, f32>
193    for Chamfer
194where
195    InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
196{
197    #[inline(always)]
198    fn evaluate(query: QueryMatRef<'_, Standard<T>>, doc: MatRef<'_, Standard<T>>) -> f32 {
199        let mut sum = 0.0f32;
200
201        FallbackKernel::max_sim_kernel(query, doc, |_i, score| {
202            sum += score;
203        });
204
205        sum
206    }
207}
208
209/////////////////////
210// ProjectedEigen //
211/////////////////////
212
213impl<T: Copy> PureDistanceFunction<QueryMatRef<'_, Standard<T>>, MatRef<'_, Standard<T>>, f32>
214    for ProjectedEigen
215where
216    InnerProduct: for<'a, 'b> PureDistanceFunction<&'a [T], &'b [T], f32>,
217{
218    #[inline(always)]
219    fn evaluate(query: QueryMatRef<'_, Standard<T>>, doc: MatRef<'_, Standard<T>>) -> f32 {
220        let mut sum = 0.0f32;
221
222        FallbackKernel::projected_eigen_kernel(query, doc, |_i, score| {
223            sum += score;
224        });
225
226        sum
227    }
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    /// Helper to create a QueryMatRef from raw data
235    fn make_query(data: &[f32], nrows: usize, ncols: usize) -> QueryMatRef<'_, Standard<f32>> {
236        MatRef::new(Standard::new(nrows, ncols).unwrap(), data)
237            .unwrap()
238            .into()
239    }
240
241    /// Helper to create a MatRef from raw data
242    fn make_doc(data: &[f32], nrows: usize, ncols: usize) -> MatRef<'_, Standard<f32>> {
243        MatRef::new(Standard::new(nrows, ncols).unwrap(), data).unwrap()
244    }
245
246    /// Naive implementation of max-sim for a single query vector against all doc vectors.
247    fn naive_max_sim_single(query_vec: &[f32], doc: &MatRef<'_, Standard<f32>>) -> f32 {
248        doc.rows()
249            .map(|d_vec| {
250                let ip: f32 = query_vec.iter().zip(d_vec.iter()).map(|(a, b)| a * b).sum();
251                -ip
252            })
253            .fold(f32::MAX, f32::min)
254    }
255
256    /// Naive implementation of projected-eigen for a single query vector
257    /// against all doc vectors: `\sum_{j} -IP(q, d_{j})^2`.
258    fn naive_projected_eigen_single(query_vec: &[f32], doc: &MatRef<'_, Standard<f32>>) -> f32 {
259        doc.rows()
260            .map(|d_vec| {
261                let ip: f32 = query_vec.iter().zip(d_vec.iter()).map(|(a, b)| a * b).sum();
262                -(ip * ip)
263            })
264            .sum()
265    }
266
267    /// Generate deterministic test data.
268    fn make_test_data(len: usize, ceil: usize, shift: usize) -> Vec<f32> {
269        (0..len).map(|v| ((v + shift) % ceil) as f32).collect()
270    }
271
272    mod query_mat_ref {
273        use super::*;
274
275        #[test]
276        fn from_mat_ref_and_deref() {
277            let data = [1.0f32, 2.0, 3.0, 4.0, 5.0, 6.0];
278            let view = MatRef::new(Standard::new(2, 3).unwrap(), &data).unwrap();
279            let query: QueryMatRef<_> = view.into();
280
281            // Deref access works
282            assert_eq!(query.num_vectors(), 2);
283            assert_eq!(query.vector_dim(), 3);
284            assert_eq!(query.get_row(0), Some(&[1.0f32, 2.0, 3.0][..]));
285        }
286
287        #[test]
288        fn is_copy() {
289            let data = [1.0f32, 2.0];
290            let query = make_query(&data, 1, 2);
291            let copy = query;
292            let _ = (query, copy); // Both usable
293        }
294    }
295
296    mod distance_functions {
297        use diskann_utils::Reborrow;
298
299        use super::*;
300
301        #[test]
302        fn max_sim_panics_on_size_mismatch() {
303            let query = make_query(&[1.0, 2.0, 3.0, 4.0], 2, 2);
304            let doc = make_doc(&[1.0, 1.0], 1, 2);
305
306            let mut scores = vec![0.0f32; 3]; // Wrong size
307            let r = MaxSim::new(&mut scores).evaluate(query, doc);
308            assert!(r.is_err());
309        }
310
311        /// Tests both MaxSim and Chamfer against naive implementations across
312        /// various matrix sizes including edge cases (single row/col).
313        #[test]
314        fn matches_naive_implementation() {
315            let test_cases = [
316                (1, 1, 4),   // Single query, single doc
317                (1, 5, 8),   // Single query, multiple docs
318                (5, 1, 8),   // Multiple queries, single doc
319                (3, 4, 16),  // General case
320                (7, 7, 32),  // Square case
321                (2, 3, 128), // Larger dimension
322            ];
323
324            for (nq, nd, dim) in test_cases.iter() {
325                let query_data = make_test_data(nq * dim, *dim, dim / 2);
326                let doc_data = make_test_data(nd * dim, *dim, *dim);
327
328                let query = make_query(&query_data, *nq, *dim);
329                let doc = make_doc(&doc_data, *nd, *dim);
330
331                // Test MaxSim
332                let mut scores = vec![0.0f32; *nq];
333                let r = MaxSim::new(&mut scores).evaluate(query, doc);
334                assert!(r.is_ok());
335
336                let expected_scores: Vec<f32> = query
337                    .rows()
338                    .map(|q_vec| naive_max_sim_single(q_vec, &doc))
339                    .collect();
340
341                for i in 0..*nq {
342                    assert!(
343                        (scores[i] - expected_scores[i]).abs() < 1e-10,
344                        "MaxSim mismatch at {} for ({},{},{})",
345                        i,
346                        nq,
347                        nd,
348                        dim
349                    );
350                }
351
352                // Check that FallbackKernel produces the same values as the naive reference.
353                FallbackKernel::max_sim_kernel(query, doc, |i, score| {
354                    assert!((expected_scores[i] - score).abs() <= 1e-10)
355                });
356
357                // Test Chamfer
358                let chamfer = Chamfer::evaluate(query, doc);
359                let expected_chamfer: f32 = expected_scores.iter().sum();
360
361                assert!(
362                    (chamfer - expected_chamfer).abs() < 1e-10,
363                    "Chamfer mismatch for ({},{},{})",
364                    nq,
365                    nd,
366                    dim
367                );
368
369                // Test ProjectedEigen
370                let projected = ProjectedEigen::evaluate(query, doc);
371                let expected_projected: f32 = query
372                    .rows()
373                    .map(|q_vec| naive_projected_eigen_single(q_vec, &doc))
374                    .sum();
375
376                assert!(
377                    (projected - expected_projected).abs()
378                        < 1e-6 * expected_projected.abs().max(1.0),
379                    "ProjectedEigen mismatch for ({},{},{})",
380                    nq,
381                    nd,
382                    dim
383                );
384            }
385        }
386
387        #[test]
388        fn chamfer_with_zero_queries_returns_zero() {
389            let query = make_query(&[], 0, 2);
390            let doc = make_doc(&[1.0, 0.0, 0.0, 1.0], 2, 2);
391
392            let result = Chamfer::evaluate(query, doc);
393
394            // No query vectors means sum is 0
395            assert_eq!(result, 0.0);
396
397            let result = Chamfer::evaluate(QueryMatRef::from(doc), query.deref().reborrow());
398
399            assert_eq!(result, f32::INFINITY);
400        }
401
402        #[test]
403        fn projected_eigen_with_zero_docs_returns_zero() {
404            let query = make_query(&[1.0, 0.0, 0.0, 1.0], 2, 2);
405            let doc = make_doc(&[], 0, 2);
406
407            // No document vectors means no pairs contribute, so the sum is 0.
408            let result = ProjectedEigen::evaluate(query, doc);
409            assert_eq!(result, 0.0);
410        }
411    }
412}