Skip to main content

diskann_quantization/scalar/
mod.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6//! Scalar quantization training, compression data, and distance comparisons.
7//!
8//! Scalar quantization works by converting floating point values to small n-bit integers
9//! using the formula
10//! ```math
11//! X' = round((X - B) / a).clamp(0, 2^n - 1)
12//! ```
13//! where `B` is a shift vector and `a` is a scaling parameter. The training algorithm is this:
14//!
15//! 1. Find the mean vector M of the training data set.
16//! 2. Compute the standard deviation of each dimension and find `stdmax`: the maximum
17//!    standard deviation.
18//! 3. The parameter `a` is then `(2 * S * stdmax) / (2^n - 1)` where `S` is configurable
19//!    in [`train::ScalarQuantizationParameters`].
20//! 4. `B` is finally computed as `M - S * stdmax`.
21//!
22//! Compression then via [`ScalarQuantizer`] simply consists of applying the above formula
23//! to each vector.
24//!
25//! # Training
26//!
27//! Training is provided by the [`train::ScalarQuantizationParameters`] struct.
28//! See the struct level documentation for more details.
29//!
30//! # Compensated Vectors
31//!
32//! Scalar compression involves data shifts by both the dataset mean and to shift the
33//! representable range into a domain representable by an unsigned integer.
34//!
35//! This does not preserve inner-products at all and can result in a scaled value for
36//! L2 computations.
37//!
38//! To deal with this, we can use [`CompensatedVector`]s, which consist of both the scalar
39//! quantization integer codes and the scaled inner product of the compressed value with the
40//! data set mean. This enables inner-products to be computed efficiently and correctly, at
41//! the cost of some extra storage.
42//!
43//! See the struct-level documentation for more information as well as the documentation
44//! for distance function objects:
45//!
46//! * [`CompensatedSquaredL2`]
47//! * [`CompensatedIP`]
48//!
49//! # Example
50//!
51//! In this example, we will do the following:
52//!
53//! 1. Create a sample data set with normally distributed data using random offsets.
54//! 2. Train a 4-bit scalar quantizer on this data.
55//! 3. Compress elements of the dataset using the quantizer.
56//! 4. Perform distance computations using the compressed vectors.
57//!
58//! ```
59//! use diskann_quantization::{
60//!     AsFunctor, CompressInto,
61//!     distances,
62//!     scalar::{self, train, CompensatedVector, CompensatedIP, CompensatedSquaredL2},
63//!     num::Positive,
64//! };
65//! use diskann_utils::{
66//!     Reborrow, ReborrowMut,
67//!     views::rowmajor::{self, Matrix, MatrixMut},
68//! };
69//! use rand::{rngs::StdRng, SeedableRng, distr::Distribution};
70//! use rand_distr::StandardNormal;
71//! use diskann_vector::{PureDistanceFunction, DistanceFunction, distance};
72//!
73//! let dim = 20;
74//! let nvectors = 100;
75//! let distribution = StandardNormal;
76//! let mut rng = StdRng::seed_from_u64(0xc674c06f4f8013f7);
77//! // Construct a set of offsets for each dimension.
78//! let offset: Vec<f32> = (0..dim).map(|_| distribution.sample(&mut rng)).collect();
79//! // The output dataset.
80//! let mut data = rowmajor::Owned::<f32>::from_element(nvectors, dim, 0.0);
81//! for row in data.rows_mut() {
82//!     std::iter::zip(row.iter_mut(), offset.iter()).for_each(|(r, i)| {
83//!         let v: f32 = distribution.sample(&mut rng);
84//!         *r = i + v;
85//!     });
86//! }
87//!
88//! // Create parameters for 4-bit compression.
89//! // Here, we use 2.0 standard deviations are our cutoff.
90//! let p = train::ScalarQuantizationParameters::new(Positive::new(2.0).unwrap());
91//!
92//! // Train a quantizer.
93//! let quantizer: scalar::ScalarQuantizer = p.train(data.as_view());
94//!
95//! // Compress vectors 0 and 1 into their scalar quantized representation.
96//! let mut v0 = CompensatedVector::<4>::new_boxed(quantizer.dim());
97//! let mut v1 = CompensatedVector::<4>::new_boxed(quantizer.dim());
98//! quantizer.compress_into(data.row(0), v0.reborrow_mut()).unwrap();
99//! quantizer.compress_into(data.row(1), v1.reborrow_mut()).unwrap();
100//!
101//! // Compute inner product distances.
102//! let f: CompensatedIP = quantizer.as_functor();
103//! let distance_quantized: distances::Result<f32> = f.evaluate_similarity(
104//!     v0.reborrow(),
105//!     v1.reborrow()
106//! );
107//! let distance_full: f32 = distance::InnerProduct::evaluate(data.row(0), data.row(1));
108//! let relative_error = (distance_quantized.unwrap() - distance_full).abs() / distance_full.abs();
109//! assert!(relative_error < 0.02);
110//!
111//! // Compute squared l2 distances.
112//! let f: CompensatedSquaredL2 = quantizer.as_functor();
113//! let distance_quantized: distances::Result<f32> = f.evaluate_similarity(
114//!     v0.reborrow(),
115//!     v1.reborrow()
116//! );
117//! let distance_full: f32 = distance::SquaredL2::evaluate(data.row(0), data.row(1));
118//! let relative_error = (distance_quantized.unwrap() - distance_full).abs() / distance_full.abs();
119//! assert!(relative_error < 0.03);
120//! ```
121
122pub mod train;
123
124mod quantizer;
125mod vectors;
126
127/////////////
128// Exports //
129/////////////
130
131/// Return the scaling parameter `a` that should be multiplied to compressed vector codes.
132pub const fn bit_scale<const NBITS: usize>() -> f32 {
133    (2_usize.pow(NBITS as u32) - 1) as f32
134}
135
136pub const fn inverse_bit_scale<const NBITS: usize>() -> f32 {
137    1.0 / bit_scale::<NBITS>()
138}
139
140// The central scalar quantization schema.
141// Error types.
142pub use quantizer::{InputContainsNaN, MeanNormMissing, ScalarQuantizer};
143// Distance Functions.
144pub use vectors::CompensatedIP;
145// Scalar-quantized specific vector types.
146pub use vectors::CompensatedVector;
147pub use vectors::{
148    CompensatedCosineNormalized, CompensatedSquaredL2, CompensatedVectorRef, Compensation,
149    MutCompensatedVectorRef,
150};