Skip to main content

diskann_quantization/multi_vector/distance/
fallback.rs

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