diskann_quantization/product/tables/
lookup.rs1use diskann_utils::views::rowmajor::{self, Matrix};
7use thiserror::Error;
8
9pub trait Lookup<T> {
14 type Accumulator;
18
19 type Output;
21
22 fn default(&self) -> Self::Accumulator;
24
25 fn accumulate(&self, v: &T, acc: Self::Accumulator) -> Self::Accumulator;
27
28 fn reduce(&self, a: Self::Accumulator, b: Self::Accumulator) -> Self::Accumulator;
30
31 fn finish(self, acc: Self::Accumulator) -> Self::Output;
33}
34
35pub fn lookup_single<P, T>(
60 policy: P,
61 data: rowmajor::Ref<'_, T>,
62 indices: &[u8],
63) -> Result<P::Output, LookupError>
64where
65 P: Lookup<T>,
66{
67 if indices.len() != data.nrows() {
69 return Err(LookupError::InvalidLength);
70 }
71
72 if let Ok(ncols) = u8::try_from(data.ncols())
78 && let Some(max) = indices.iter().max()
79 && *max >= ncols
80 {
81 return Err(LookupError::OutOfBounds);
82 }
83
84 const UNROLL: usize = 4;
85
86 let mut i = 0;
87 let mut a = if data.nrows() >= UNROLL {
88 let mut a0 = policy.default();
89 let mut a1 = policy.default();
90 let mut a2 = policy.default();
91 let mut a3 = policy.default();
92
93 while i + 4 <= data.nrows() {
94 let v0 = unsafe { data.element_unchecked(i, (*indices.get_unchecked(i)).into()) };
100 a0 = policy.accumulate(v0, a0);
101
102 let v1 =
104 unsafe { data.element_unchecked(i + 1, (*indices.get_unchecked(i + 1)).into()) };
105 a1 = policy.accumulate(v1, a1);
106
107 let v2 =
109 unsafe { data.element_unchecked(i + 2, (*indices.get_unchecked(i + 2)).into()) };
110 a2 = policy.accumulate(v2, a2);
111
112 let v3 =
114 unsafe { data.element_unchecked(i + 3, (*indices.get_unchecked(i + 3)).into()) };
115 a3 = policy.accumulate(v3, a3);
116
117 i += UNROLL;
118 }
119
120 policy.reduce(policy.reduce(a0, a1), policy.reduce(a2, a3))
121 } else {
122 policy.default()
123 };
124
125 if let Some(remainder) = data.nrows().checked_sub(i) {
126 let remainder = remainder.min(UNROLL - 1);
128 for j in 0..remainder {
129 let k = i + j;
130
131 let v = unsafe { data.element_unchecked(k, (*indices.get_unchecked(k)).into()) };
133 a = policy.accumulate(v, a);
134 }
135 }
136
137 Ok(policy.finish(a))
138}
139
140#[derive(Debug, Error, Clone, Copy)]
142#[non_exhaustive]
143pub enum LookupError {
144 #[error("number of lookup indices does not match the number of data rows")]
145 InvalidLength,
146 #[error("at least one of the lookup indices exceeds the number of data columns")]
147 OutOfBounds,
148}
149
150#[derive(Debug, Clone, Copy)]
152pub struct Sum;
153
154impl<T> Lookup<T> for Sum
155where
156 T: Default + std::ops::Add<Output = T> + Copy,
157{
158 type Accumulator = T;
159 type Output = T;
160
161 fn default(&self) -> T {
162 T::default()
163 }
164
165 fn accumulate(&self, v: &T, acc: T) -> T {
166 *v + acc
167 }
168
169 fn reduce(&self, a: T, b: T) -> T {
170 a + b
171 }
172
173 fn finish(self, acc: T) -> T {
174 acc
175 }
176}
177
178#[derive(Debug, Clone, Copy, Default)]
187#[repr(C)]
188pub struct DotAndNorm {
189 dot: f32,
190 square_norm: f32,
191}
192
193impl DotAndNorm {
194 pub const fn new(dot: f32, square_norm: f32) -> Self {
196 Self { dot, square_norm }
197 }
198
199 pub fn dot(&self) -> f32 {
201 self.dot
202 }
203
204 pub fn square_norm(&self) -> f32 {
206 self.square_norm
207 }
208
209 pub fn finish_cosine(&self, query_norm: f32) -> diskann_vector::SimilarityScore<f32> {
218 use diskann_vector::SimilarityScore;
219
220 if self.square_norm < f32::MIN_POSITIVE || query_norm < f32::MIN_POSITIVE {
221 SimilarityScore::new(1.0)
222 } else {
223 let v = self.dot / (self.square_norm.sqrt() * query_norm);
224 SimilarityScore::new(1.0 - (-1.0f32).max(1.0f32.min(v)))
225 }
226 }
227}
228
229impl std::ops::Add for DotAndNorm {
230 type Output = Self;
231 fn add(self, rhs: Self) -> Self {
232 Self {
233 dot: self.dot + rhs.dot,
234 square_norm: self.square_norm + rhs.square_norm,
235 }
236 }
237}
238
239#[cfg(test)]
244mod tests {
245 use super::*;
246
247 use diskann_utils::assert_contains;
248 use rand::{
249 SeedableRng,
250 distr::{Distribution, Uniform},
251 rngs::StdRng,
252 };
253
254 fn expected_sum(x: &[u8]) -> f32 {
255 let mut sum = 0.0;
256 for (i, v) in x.iter().enumerate() {
257 sum += (i as f32) + (*v as f32)
258 }
259 sum
260 }
261
262 #[test]
263 fn test_lookup_sum() {
264 let ntrials = if cfg!(miri) { 1 } else { 10 };
265
266 let mut rng = StdRng::seed_from_u64(0xd0cc501bde4c9ddd);
267 for nrows in 0..12 {
268 let mut codes = vec![0u8; nrows];
269 for ncols in [1, 2, 255, 256] {
270 let dist = if ncols == 0 {
271 Uniform::new(0, 1).unwrap()
272 } else {
273 Uniform::new(0, ncols).unwrap()
274 };
275
276 let table = rowmajor::Owned::from_fn(nrows, ncols, |rc| (rc.row + rc.col) as f32);
277
278 if ncols < 256 {
281 let nc = u8::try_from(ncols).unwrap();
282 codes.fill(0);
283 for r in 0..nrows {
284 codes[r] = nc;
285 let err = lookup_single(Sum, table.as_view(), &codes).unwrap_err();
286 assert_contains!(
287 err.to_string(),
288 "at least one of the lookup indices exceeds the number of data columns"
289 );
290 codes[r] = 0;
291 }
292 }
293
294 if nrows > 0 {
296 let too_short = vec![0; nrows - 1];
297 let err = lookup_single(Sum, table.as_view(), &too_short).unwrap_err();
298 assert_contains!(err.to_string(), "number of lookup indices does not match");
299 }
300
301 {
302 let too_long = vec![0; nrows + 1];
303 let err = lookup_single(Sum, table.as_view(), &too_long).unwrap_err();
304 assert_contains!(err.to_string(), "number of lookup indices does not match");
305 }
306
307 codes.iter_mut().for_each(|c| *c = 0);
309 assert_eq!(
310 lookup_single(Sum, table.as_view(), &codes).unwrap(),
311 expected_sum(&codes),
312 "all zeros - nrows = {nrows}, ncols = {ncols}",
313 );
314
315 codes
317 .iter_mut()
318 .for_each(|c| *c = (ncols - 1).try_into().unwrap());
319 assert_eq!(
320 lookup_single(Sum, table.as_view(), &codes).unwrap(),
321 expected_sum(&codes),
322 "all max - nrows = {nrows}, ncols = {ncols}",
323 );
324
325 for trial in 0..ntrials {
326 codes
327 .iter_mut()
328 .for_each(|c| *c = u8::try_from(dist.sample(&mut rng)).unwrap());
329
330 println!("codes = {:?}, ncols = {}", codes, ncols);
331
332 assert_eq!(
333 lookup_single(Sum, table.as_view(), &codes).unwrap(),
334 expected_sum(&codes),
335 "nrows = {nrows}, ncols = {ncols}, trial = {} of {}",
336 trial + 1,
337 ntrials,
338 );
339 }
340 }
341 }
342 }
343}