diskann_quantization/scalar/train.rs
1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use super::quantizer::ScalarQuantizer;
7use crate::{
8 num::Positive,
9 utils::{compute_means_and_average_norm, compute_variances},
10};
11use diskann_utils::views::rowmajor;
12
13/// Parameters controlling the generation of the scalar quantization Quantizer.
14///
15/// When performing scalar quantization, the mean of each dimension will be calculated and
16/// the dataset will be shifted around this mean.
17///
18/// Next, the standard deviation of each dimension will be computed and the maximum `m` found.
19///
20/// The dynamic range of the final compressed encoding will then span
21/// `2 * standard_deviations * m` for each dimension symmetrically about the mean for each
22/// dimension. Values outside the spanned dynamic range will be clamped.
23pub struct ScalarQuantizationParameters {
24 standard_deviations: Positive<f64>,
25}
26
27impl ScalarQuantizationParameters {
28 /// Construct a new quantizer with the given parameters.
29 ///
30 /// # Arguments
31 ///
32 /// * `standard_deviations`: The number of maximal standard deviations to use for the
33 /// encoding's dynamic range. This number **must** be positive, and generally should
34 /// be greater than 1.0.
35 ///
36 /// A good starting value is generally 2.0.
37 pub fn new(standard_deviations: Positive<f64>) -> Self {
38 Self {
39 standard_deviations,
40 }
41 }
42
43 /// Return the current number of standard deviations being used to set the dynamic range.
44 pub fn standard_deviations(&self) -> Positive<f64> {
45 self.standard_deviations
46 }
47
48 /// Train a new [`ScalarQuantizer`] on the input training data.
49 ///
50 /// The training algoritm works as follows:
51 ///
52 /// 1. The medoid of the training data is computed.
53 ///
54 /// 2. The standard deviation for each dimension is then calculated across all rows
55 /// of the training set.
56 ///
57 /// 3. The maximum standard deviation `s` is computed and the dynamic range `dyn` of the
58 /// quantizer is computed as `dyn = 2.0 * self.standard_deviations() * s`.
59 ///
60 /// 4. The quantizer is then constructed with `scale = dyn / (2.pow(NBITS) - 1)`.
61 ///
62 /// # Complexity
63 ///
64 /// This method is linear in the number of rows and columns in `data`.
65 ///
66 /// # Allocates
67 ///
68 /// This method allocated memory on the order of `data.ncols()` (the dimensionality of
69 /// the data).
70 ///
71 /// # Parallelism
72 ///
73 /// This function is single threaded.
74 pub fn train<T>(&self, data: rowmajor::Ref<T>) -> ScalarQuantizer
75 where
76 T: Copy + Into<f64> + Into<f32>,
77 {
78 let (means, mean_norm) = compute_means_and_average_norm(data);
79 let variances = compute_variances(data, &means);
80
81 // Take the maximum variance - that will set our global scaling parameter.
82 let max_std = variances.iter().fold(0.0f64, |max, &x| max.max(x)).sqrt();
83 let p = max_std * self.standard_deviations.into_inner();
84
85 let scale = 2.0 * p;
86 let shift = means.into_iter().map(|i| (i - p) as f32).collect();
87
88 ScalarQuantizer::new(scale as f32, shift, Some(mean_norm as f32))
89 }
90}
91
92// 2.0 seems to be good starting point for scalar quantization.
93//
94// SAFETY: 2.0 is greater than 0.0.
95const DEFAULT_STDEV: Positive<f64> = unsafe { Positive::new_unchecked(2.0) };
96
97impl Default for ScalarQuantizationParameters {
98 fn default() -> Self {
99 Self::new(DEFAULT_STDEV)
100 }
101}
102
103///////////
104// Tests //
105///////////
106
107#[cfg(test)]
108mod tests {
109 use rand::{SeedableRng, rngs::StdRng};
110
111 use super::*;
112 use crate::test_util::create_test_problem;
113 use diskann_utils::views::rowmajor::Matrix;
114
115 fn test_train_impl(nrows: usize, ncols: usize, seed: u64) {
116 // Test Default
117 let default = ScalarQuantizationParameters::default();
118 assert_eq!(default.standard_deviations(), DEFAULT_STDEV);
119
120 let mut rng = StdRng::seed_from_u64(seed);
121 let problem = create_test_problem(nrows, ncols, &mut rng);
122
123 // Compute the maximum standard deviation from the expected variances.
124 let problem_std_max = problem
125 .variances
126 .iter()
127 .copied()
128 .reduce(|a, b| a.max(b))
129 .unwrap()
130 .sqrt();
131
132 // Provide a range of standard deviation requests to the training algoritm.
133 let standard_deviations: [f64; 3] = [1.0, 1.5, 2.0];
134 for std in standard_deviations {
135 let parameters = ScalarQuantizationParameters::new(Positive::new(std).unwrap());
136
137 let quantizer = parameters.train(problem.data.as_view());
138 assert_eq!(quantizer.dim(), ncols);
139
140 let expected_scale = std * 2.0 * problem_std_max;
141 let got_scale = quantizer.scale();
142
143 let relative_diff = (got_scale as f64 - expected_scale) / expected_scale;
144
145 assert!(
146 relative_diff < 1.0e-7,
147 "Relative difference in scaling of {}. Got {}, expected {} \
148 (nrows = {}, ncols = {})",
149 relative_diff,
150 got_scale,
151 expected_scale,
152 nrows,
153 ncols
154 );
155
156 assert_eq!(quantizer.mean_norm().unwrap(), problem.mean_norm as f32);
157
158 // The quantizer shift should be the dataset mean shifted by the appropriate
159 // amount for the unsigned quantization.
160 let shift = std * problem_std_max;
161 let quantizer_shift = quantizer.shift();
162 for (i, (&got, &expected)) in
163 std::iter::zip(quantizer_shift.iter(), problem.means.iter()).enumerate()
164 {
165 let expected = expected - shift;
166
167 assert_eq!(
168 got, expected as f32,
169 "Mismatch in shift amount at index {}, (nrows = {}, ncols = {})",
170 i, nrows, ncols,
171 );
172 }
173 }
174 }
175
176 #[test]
177 fn test_train() {
178 test_train_impl(10, 16, 0x0b1d3ccb952d3079);
179 test_train_impl(7, 8, 0xda9a5c0a672f43cd);
180 }
181}