1use diskann_vector::{MathematicalValue, PureDistanceFunction};
7use thiserror::Error;
8
9use super::{
10 bit_scale, inverse_bit_scale,
11 vectors::{
12 CompensatedCosineNormalized, CompensatedIP, CompensatedSquaredL2, Compensation,
13 MutCompensatedVectorRef,
14 },
15};
16use crate::{
17 AsFunctor, CompressInto,
18 bits::{MutBitSlice, PermutationStrategy, Representation, Unsigned},
19};
20
21#[derive(Clone, Debug)]
86pub struct ScalarQuantizer {
87 scale: f32,
89
90 shift: Vec<f32>,
101
102 shift_square_norm: f32,
105
106 mean_norm: Option<f32>,
112}
113
114impl ScalarQuantizer {
115 pub fn new(scale: f32, shift: Vec<f32>, mean_norm: Option<f32>) -> Self {
117 let shift_square_norm: MathematicalValue<f32> =
118 diskann_vector::distance::InnerProduct::evaluate(&*shift, &*shift);
119
120 Self {
121 scale,
122 shift,
123 shift_square_norm: shift_square_norm.into_inner(),
124 mean_norm,
125 }
126 }
127
128 pub fn dim(&self) -> usize {
130 self.shift.len()
131 }
132
133 pub fn scale(&self) -> f32 {
135 self.scale
136 }
137
138 pub fn shift_square_norm(&self) -> f32 {
140 self.shift_square_norm
141 }
142
143 pub fn shift(&self) -> &[f32] {
150 &self.shift
151 }
152
153 pub fn mean_norm(&self) -> Option<f32> {
155 self.mean_norm
156 }
157
158 pub fn rescale(&self, x: &mut [f32]) -> Result<(), MeanNormMissing> {
166 match self.mean_norm {
167 Some(mean_norm) => {
168 rescale(x, mean_norm);
169 Ok(())
170 }
171 None => Err(MeanNormMissing),
172 }
173 }
174
175 fn compress<const NBITS: usize, T, F, Perm>(
191 &self,
192 from: &[T],
193 mut into: MutBitSlice<'_, NBITS, Unsigned, Perm>,
194 mut callback: F,
195 ) -> Result<(), InputContainsNaN>
196 where
197 T: Copy + Into<f32>,
198 F: FnMut(f32, usize),
199 Unsigned: Representation<NBITS>,
200 Perm: PermutationStrategy<NBITS>,
201 {
202 let len = self.shift.len();
203 assert_eq!(from.len(), len);
204 assert_eq!(into.len(), len);
205
206 let domain = Unsigned::domain_const::<NBITS>();
207 let min = *domain.start() as f32;
208 let max = *domain.end() as f32;
209 let inverse_scale = bit_scale::<NBITS>() / (self.scale);
210 let mut nan_check = false;
211
212 std::iter::zip(from.iter(), self.shift.iter())
213 .enumerate()
214 .for_each(|(i, (&f, &s))| {
215 let f: f32 = f.into();
218 nan_check |= f.is_nan();
219
220 let code: f32 = ((f - s) * inverse_scale).clamp(min, max).round();
221
222 callback(code, i);
224
225 unsafe { into.set_unchecked(i, code as u8) };
231 });
232
233 if nan_check {
234 Err(InputContainsNaN)
235 } else {
236 Ok(())
237 }
238 }
239
240 pub fn compare(&self, other: &Self) -> Result<(), SQComparisonError> {
244 if self.scale != other.scale {
245 return Err(SQComparisonError::Scale(self.scale, other.scale));
246 }
247
248 if self.shift.len() != other.shift.len() {
249 return Err(SQComparisonError::ShiftLength(
250 self.shift.len(),
251 other.shift.len(),
252 ));
253 }
254
255 for (i, (a, b)) in self.shift.iter().zip(other.shift.iter()).enumerate() {
256 if a != b {
257 return Err(SQComparisonError::ShiftElement {
258 index: i,
259 a: *a,
260 b: *b,
261 });
262 }
263 }
264
265 if self.shift_square_norm != other.shift_square_norm {
266 return Err(SQComparisonError::ShiftSquareNorm(
267 self.shift_square_norm,
268 other.shift_square_norm,
269 ));
270 }
271
272 match (&self.mean_norm, &other.mean_norm) {
273 (Some(a), Some(b)) => {
274 if a != b {
275 return Err(SQComparisonError::MeanNorm(*a, *b));
276 }
277 }
278 (None, None) => {
279 }
281 _ => {
282 return Err(SQComparisonError::MeanNormPresence);
283 }
284 }
285
286 Ok(())
287 }
288}
289
290#[derive(Debug, Error, Clone, Copy)]
291#[error("mean norm is missing from the quantizer")]
292#[non_exhaustive]
293pub struct MeanNormMissing;
294
295#[derive(Debug, Error, Clone, Copy)]
296#[error("input contains NaN")]
297#[non_exhaustive]
298pub struct InputContainsNaN;
299
300fn rescale(x: &mut [f32], to_norm: f32) {
301 let norm_square: MathematicalValue<f32> =
302 diskann_vector::distance::InnerProduct::evaluate(&*x, &*x);
303 let norm = norm_square.into_inner().sqrt();
304 if norm == 0.0 {
305 return;
306 }
307
308 let scale = to_norm / norm;
309 x.iter_mut().for_each(|i| (*i) *= scale);
310}
311
312impl AsFunctor<CompensatedSquaredL2> for ScalarQuantizer {
317 fn as_functor(&self) -> CompensatedSquaredL2 {
318 let scale = self.scale();
319 CompensatedSquaredL2::new(scale * scale)
320 }
321}
322
323impl AsFunctor<CompensatedIP> for ScalarQuantizer {
324 fn as_functor(&self) -> CompensatedIP {
325 let scale = self.scale();
326 CompensatedIP::new(scale * scale, self.shift_square_norm())
327 }
328}
329
330impl AsFunctor<CompensatedCosineNormalized> for ScalarQuantizer {
331 fn as_functor(&self) -> CompensatedCosineNormalized {
332 let scale = self.scale();
333 CompensatedCosineNormalized::new(scale * scale)
334 }
335}
336
337impl<const NBITS: usize, T, Perm> CompressInto<&[T], MutBitSlice<'_, NBITS, Unsigned, Perm>>
342 for ScalarQuantizer
343where
344 T: Copy + Into<f32>,
345 Unsigned: Representation<NBITS>,
346 Perm: PermutationStrategy<NBITS>,
347{
348 type Error = InputContainsNaN;
349
350 type Output = ();
351
352 fn compress_into(
370 &self,
371 from: &[T],
372 into: MutBitSlice<'_, NBITS, Unsigned, Perm>,
373 ) -> Result<(), Self::Error> {
374 ScalarQuantizer::compress(self, from, into, |_, _| {})
377 }
378}
379
380impl<const NBITS: usize, T, Perm> CompressInto<&[T], MutCompensatedVectorRef<'_, NBITS, Perm>>
381 for ScalarQuantizer
382where
383 T: Copy + Into<f32>,
384 Unsigned: Representation<NBITS>,
385 Perm: PermutationStrategy<NBITS>,
386{
387 type Error = InputContainsNaN;
388
389 type Output = ();
390
391 fn compress_into(
408 &self,
409 from: &[T],
410 mut into: MutCompensatedVectorRef<'_, NBITS, Perm>,
411 ) -> Result<(), Self::Error> {
412 let mut dot: f32 = 0.0;
417 let result = ScalarQuantizer::compress(
418 self,
419 from,
420 into.vector_mut(),
421 |code: f32, index: usize| {
423 dot = code.mul_add(self.shift[index], dot);
424 },
425 );
426 into.set_meta(Compensation(
427 self.scale * inverse_bit_scale::<NBITS>() * dot,
428 ));
429 result
430 }
431}
432
433#[derive(Debug, Error, PartialEq)]
434pub enum SQComparisonError {
435 #[error("Scale mismatch: {0} vs {1}")]
436 Scale(f32, f32),
437
438 #[error("Shift vector length mismatch: {0} vs {1}")]
439 ShiftLength(usize, usize),
440
441 #[error("Shift element at index {index} mismatch: {a} vs {b}")]
442 ShiftElement { index: usize, a: f32, b: f32 },
443
444 #[error("Shift square norm mismatch: {0} vs {1}")]
445 ShiftSquareNorm(f32, f32),
446
447 #[error("Mean norm mismatch: {0} vs {1}")]
448 MeanNorm(f32, f32),
449
450 #[error("Mean norm is missing in one quantizer but present in the other")]
451 MeanNormPresence,
452}
453
454#[cfg(test)]
459mod tests {
460 use std::collections::HashSet;
461
462 use diskann_utils::{
463 ReborrowMut, views,
464 views::rowmajor::{Matrix, MatrixMut},
465 };
466
467 use rand::{
468 SeedableRng,
469 distr::{Distribution, Uniform},
470 rngs::StdRng,
471 seq::SliceRandom,
472 };
473 use rand_distr::Normal;
474
475 use super::*;
476 use crate::{
477 bits::BoxedBitSlice,
478 scalar::{CompensatedVector, inverse_bit_scale},
479 };
480
481 #[test]
483 fn test_rescale() {
484 let dim = 32;
485 let to_norm = 25.0;
486
487 let mut rng = StdRng::seed_from_u64(0x64e956ca2eb726ee);
488 let distribution = Normal::<f32>::new(0.0, 16.0).unwrap();
489
490 let mut v: Vec<f32> = distribution.sample_iter(&mut rng).take(dim).collect();
491 let norm = v.iter().map(|&i| i * i).sum::<f32>().sqrt();
492
493 rescale(&mut v, to_norm);
494 let norm_next = v.iter().map(|&i| i * i).sum::<f32>().sqrt();
495 let relative_error = (norm_next - to_norm).abs() / to_norm;
496
497 assert!(
498 relative_error <= 1.0e-7,
499 "vector was not renormalized, expected {}, got {}, started with {}. Relative error: {}",
500 to_norm,
501 norm_next,
502 norm,
503 relative_error,
504 );
505
506 let mut v: Vec<f32> = vec![0.0; dim];
508 rescale(&mut v, 10.0);
509 assert!(v.iter().all(|&i| i == 0.0));
510
511 let mut quantizer = ScalarQuantizer::new(0.0, vec![0.0; dim], Some(to_norm));
513
514 let mut v: Vec<f32> = distribution.sample_iter(&mut rng).take(dim).collect();
515 let norm = v.iter().map(|&i| i * i).sum::<f32>().sqrt();
516
517 quantizer.rescale(&mut v).unwrap();
518 let norm_next = v.iter().map(|&i| i * i).sum::<f32>().sqrt();
519 let relative_error = (norm_next - to_norm).abs() / to_norm;
520
521 assert!(
522 relative_error <= 1.0e-7,
523 "vector was not renormalized, expected {}, got {}, started with {}. Relative error: {}",
524 to_norm,
525 norm_next,
526 norm,
527 relative_error,
528 );
529
530 let mut v: Vec<f32> = vec![0.0; dim];
532 quantizer.rescale(&mut v).unwrap();
533 assert!(v.iter().all(|&i| i == 0.0));
534
535 quantizer.mean_norm = None;
537 let r = quantizer.rescale(&mut v);
538 assert!(matches!(r, Err(MeanNormMissing)));
539 }
540
541 fn test_nbit_quantizer<const NBITS: usize>(dim: usize, rng: &mut StdRng)
557 where
558 Unsigned: Representation<NBITS>,
559 ScalarQuantizer: for<'a, 'b> CompressInto<&'a [f32], MutBitSlice<'b, NBITS, Unsigned>>
560 + for<'a, 'b> CompressInto<&'a [f32], MutCompensatedVectorRef<'b, NBITS>>,
561 {
562 let distribution = Uniform::new_inclusive::<i64, i64>(-10, 10).unwrap();
563 let shift: Vec<f32> = (0..dim).map(|_| distribution.sample(rng) as f32).collect();
564 let scale: f32 = 2.0;
565 let mean_norm: f32 = 1.0;
566
567 let quantizer =
568 ScalarQuantizer::new(scale * bit_scale::<NBITS>(), shift.clone(), Some(mean_norm));
569
570 assert_eq!(quantizer.dim(), dim);
571 assert_eq!(quantizer.scale(), scale * bit_scale::<NBITS>());
572 assert_eq!(quantizer.shift(), shift);
573 assert_eq!(quantizer.mean_norm().unwrap(), mean_norm);
574
575 let expected_shift_norm: f32 = shift.iter().map(|&i| i * i).sum();
576 assert_eq!(quantizer.shift_square_norm(), expected_shift_norm);
577
578 {
580 let l2: CompensatedSquaredL2 = quantizer.as_functor();
581 assert_eq!(l2.scale_squared, quantizer.scale() * quantizer.scale());
582
583 let ip: CompensatedIP = quantizer.as_functor();
584 assert_eq!(ip.scale_squared, quantizer.scale() * quantizer.scale());
585 assert_eq!(ip.shift_square_norm, quantizer.shift_square_norm());
586 }
587
588 let sample_points: f32 = 1.25 * (2_usize.pow(NBITS as u32) as f32) + 10.0;
592
593 let min_encodable: f32 = 0.0;
594 let max_encodable: f32 = (*Unsigned::domain_const::<NBITS>().end() as f32) * scale;
595
596 let dim_offsets: views::rowmajor::Owned<f32> = {
600 let range_min = -min_encodable - 3.0 * scale;
601 let range_max = max_encodable + 3.0 * scale;
602 let mut base: Vec<f32> = Vec::new();
603
604 let step_size = (range_max - range_min) / sample_points;
605 let mut i: f32 = range_min;
606 while i < range_max {
607 base.push(i);
608 i += step_size;
609 }
610 base.push(i);
612
613 let mut output = views::rowmajor::Owned::from_element(base.len(), dim, 0.0);
614 (0..dim).for_each(|j| {
615 base.shuffle(rng);
616 for (i, b) in base.iter().enumerate() {
617 *output.element_mut(i, j) = *b;
618 }
619 });
620 output
621 };
622 let ntests = dim_offsets.nrows();
623 assert!(ntests as f32 >= sample_points);
624
625 let mut seen_below_min = false;
627 let mut seen_above_max = false;
628 let mut seen: Vec<HashSet<i64>> = (0..dim).map(|_| HashSet::new()).collect();
629
630 let mut query: Vec<f32> = vec![0.0; dim];
632 for test_number in 0..ntests {
633 let offsets = dim_offsets.row(test_number);
634 query
635 .iter_mut()
636 .zip(std::iter::zip(shift.iter(), offsets.iter()))
637 .for_each(|(q, (c, o))| {
638 *q = *c + *o;
639 });
640
641 let mut bitslice = BoxedBitSlice::<NBITS, _>::new_boxed(dim);
643 let mut compensated = CompensatedVector::<NBITS>::new_boxed(dim);
644
645 quantizer
646 .compress_into(&*query, bitslice.reborrow_mut())
647 .unwrap();
648 quantizer
649 .compress_into(&*query, compensated.reborrow_mut())
650 .unwrap();
651
652 let domain = Unsigned::domain_const::<NBITS>();
654
655 let mut computed_compensation: f32 = 0.0;
657 for d in 0..dim {
658 let code = bitslice.get(d).unwrap();
659 computed_compensation = (code as f32).mul_add(shift[d], computed_compensation);
660
661 seen[d].insert(code);
663
664 let offset = offsets[d];
665 if offset <= min_encodable {
666 assert_eq!(
667 code,
668 *domain.start(),
669 "expected values below threshold to be set to zero \
670 test_number = {}, dim = {} of {}, offset = {}, scale = {}",
671 test_number,
672 d,
673 dim,
674 offset,
675 scale,
676 );
677 seen_below_min = true;
678 } else if offset >= max_encodable {
679 assert_eq!(
680 code,
681 *domain.end(),
682 "expected values below threshold to be set to max value \
683 test_number = {}, dim = {} of {}, offset = {}, scale = {}",
684 test_number,
685 d,
686 dim,
687 offset,
688 scale,
689 );
690 seen_above_max = true;
691 } else {
692 let reconstructed =
695 quantizer.scale() * (code as f32) * inverse_bit_scale::<NBITS>();
696 let error = (offset - reconstructed).abs();
697 assert!(
698 error <= scale / 2.0,
699 "failed reconstruction check: \
700 test_number = {}, dim = {} of {}, offset = {}, scale = {} \
701 code = {}, reconstructed = {}, error = {}",
702 test_number,
703 d,
704 dim,
705 offset,
706 scale,
707 code,
708 reconstructed,
709 error,
710 );
711 }
712
713 assert_eq!(
716 compensated.vector().get(d).unwrap(),
717 code,
718 "compensated disagrees with bitslice"
719 );
720 }
721 assert_eq!(scale * computed_compensation, compensated.meta().0);
722 }
723
724 assert!(seen_below_min);
726 assert!(seen_above_max);
727 let num_codes = 2usize.pow(NBITS as u32);
728 for (i, s) in seen.iter().enumerate() {
729 assert_eq!(
730 s.len(),
731 num_codes,
732 "dimension {} did not have full coverage",
733 i
734 );
735 }
736
737 {
739 let mut query: Vec<f32> = shift.clone();
740 let mut bitslice = BoxedBitSlice::<NBITS, _>::new_boxed(query.len());
741 let mut compensated = CompensatedVector::<NBITS>::new_boxed(query.len());
742 for i in 0..query.len() {
743 let last = query[i];
744 query[i] = f32::NAN;
745
746 let err = quantizer
747 .compress_into(&*query, bitslice.reborrow_mut())
748 .unwrap_err();
749 assert_eq!(err.to_string(), "input contains NaN");
750
751 let err = quantizer
752 .compress_into(&*query, compensated.reborrow_mut())
753 .unwrap_err();
754 assert_eq!(err.to_string(), "input contains NaN");
755
756 query[i] = last;
757 }
758 }
759 }
760
761 cfg_if::cfg_if! {
762 if #[cfg(miri)] {
763 const TEST_DIM: usize = 2;
764 } else {
765 const TEST_DIM: usize = 10;
766 }
767 }
768
769 macro_rules! test_quantizer {
770 ($name:ident, $nbits:literal, $seed:literal) => {
771 #[test]
772 fn $name() {
773 let mut rng = StdRng::seed_from_u64($seed);
774 test_nbit_quantizer::<$nbits>(TEST_DIM, &mut rng);
775 }
776 };
777 }
778
779 test_quantizer!(test_8bit_quantizer, 8, 0xb7b4c124102b9fb9);
780 test_quantizer!(test_7bit_quantizer, 7, 0x86d19a821fe934d1);
781 test_quantizer!(test_6bit_quantizer, 6, 0x0de9610f0b9be4f7);
782 test_quantizer!(test_5bit_quantizer, 5, 0x605ed3e7ed775047);
783 test_quantizer!(test_4bit_quantizer, 4, 0x9b66ace7090fa728);
784 test_quantizer!(test_3bit_quantizer, 3, 0x0ce424ddc61ebdb0);
785 test_quantizer!(test_2bit_quantizer, 2, 0x2ba8e5ef6415d4f0);
786 test_quantizer!(test_1bit_quantizer, 1, 0xdcd8c10c4a407956);
787
788 fn base_quantizer() -> ScalarQuantizer {
789 ScalarQuantizer {
790 scale: 2.0,
791 shift: vec![1.0, -1.0, 0.5],
792 shift_square_norm: 1.0_f32 * 1.0 + (-1.0_f32) * (-1.0) + 0.5_f32 * 0.5,
793 mean_norm: Some(4.13),
794 }
795 }
796
797 #[test]
798 fn test_compare_identical_returns_ok() {
799 let q1 = base_quantizer();
800 let q2 = base_quantizer();
801 assert!(q1.compare(&q2).is_ok());
802 }
803
804 #[test]
805 fn test_compare_scale_mismatch() {
806 let q1 = base_quantizer();
807 let mut q2 = base_quantizer();
808 q2.scale = 4.0;
809 let err = q1.compare(&q2).unwrap_err();
810 assert_eq!(err, SQComparisonError::Scale(2.0, 4.0));
811 }
812
813 #[test]
814 fn test_compare_shift_length_mismatch() {
815 let q1 = base_quantizer();
816 let mut q2 = base_quantizer();
817 q2.shift.push(0.0);
818 let err = q1.compare(&q2).unwrap_err();
819 assert_eq!(
820 err,
821 SQComparisonError::ShiftLength(q1.shift.len(), q2.shift.len())
822 );
823 }
824
825 #[test]
826 fn test_compare_shift_element_mismatch() {
827 let q1 = base_quantizer();
828 let mut q2 = base_quantizer();
829 q2.shift[2] = 0.0;
830 let err = q1.compare(&q2).unwrap_err();
831 match err {
832 SQComparisonError::ShiftElement { index, a, b } => {
833 assert_eq!(index, 2);
834 assert_eq!(a, 0.5);
835 assert_eq!(b, 0.0);
836 }
837 _ => panic!("Expected ShiftElementMismatch variant"),
838 }
839 }
840
841 #[test]
842 fn test_compare_shift_square_norm_mismatch() {
843 let q1 = base_quantizer();
844 let mut q2 = base_quantizer();
845 q2.shift_square_norm = 9.0;
846 let err = q1.compare(&q2).unwrap_err();
847 assert_eq!(err, SQComparisonError::ShiftSquareNorm(2.25, 9.0));
848 }
849
850 #[test]
851 fn test_compare_mean_norm_value_mismatch() {
852 let q1 = base_quantizer();
853 let mut q2 = base_quantizer();
854 q2.mean_norm = Some(1.0);
855 let err = q1.compare(&q2).unwrap_err();
856 assert_eq!(err, SQComparisonError::MeanNorm(4.13, 1.0));
857 }
858
859 #[test]
860 fn test_compare_mean_norm_presence_mismatch_left_none() {
861 let mut q1 = base_quantizer();
862 let q2 = base_quantizer();
863 q1.mean_norm = None;
864 let err = q1.compare(&q2).unwrap_err();
865 assert_eq!(err, SQComparisonError::MeanNormPresence);
866 }
867
868 #[test]
869 fn test_compare_mean_norm_presence_mismatch_right_none() {
870 let q1 = base_quantizer();
871 let mut q2 = base_quantizer();
872 q2.mean_norm = None;
873 let err = q1.compare(&q2).unwrap_err();
874 assert_eq!(err, SQComparisonError::MeanNormPresence);
875 }
876}