Skip to main content

diskann_quantization/product/
train.rs

1/*
2 * Copyright (c) Microsoft Corporation.
3 * Licensed under the MIT license.
4 */
5
6use diskann_utils::{
7    strided::Strided,
8    views::{
9        self,
10        rowmajor::{self, Matrix, MatrixMut},
11    },
12};
13#[cfg(feature = "rayon")]
14use rayon::iter::{IntoParallelIterator, ParallelIterator};
15use thiserror::Error;
16
17use crate::{
18    Parallelism,
19    algorithms::kmeans::{self, common::square_norm},
20    cancel::Cancelation,
21    multi_vector::BlockTransposed,
22    product::tables::BasicTable,
23    random::{BoxedRngBuilder, RngBuilder},
24};
25
26pub struct LightPQTrainingParameters {
27    /// The number of centers for each partition.
28    ncenters: usize,
29    /// The maximum number of iterations for Lloyd's algorithm.
30    lloyds_reps: usize,
31}
32
33impl LightPQTrainingParameters {
34    /// Construct a new light-weight PQ trainer.
35    pub fn new(ncenters: usize, lloyds_reps: usize) -> Self {
36        Self {
37            ncenters,
38            lloyds_reps,
39        }
40    }
41}
42
43pub trait TrainQuantizer {
44    type Quantizer;
45    type Error: std::error::Error;
46
47    fn train<R, C>(
48        &self,
49        data: views::rowmajor::Ref<f32>,
50        schema: crate::views::ChunkOffsetsView<'_>,
51        parallelism: Parallelism,
52        rng_builder: &R,
53        cancelation: &C,
54    ) -> Result<Self::Quantizer, Self::Error>
55    where
56        R: RngBuilder<usize> + Sync,
57        C: Cancelation + Sync;
58}
59
60impl TrainQuantizer for LightPQTrainingParameters {
61    type Quantizer = BasicTable;
62    type Error = PQTrainingError;
63
64    /// Perform product quantization training on the provided training set and return a
65    /// [`BasicTable`] containing the result of kmeans clustering on each partition.
66    ///
67    /// # Panics
68    ///
69    /// Panics if `data.nrows() != schema.dim()`.
70    ///
71    /// # Errors
72    ///
73    /// An error type is returned under the following circumstances:
74    ///
75    /// * A cancellation request is received. This case can be queried by calling
76    ///   `was_canceled` on the returned `PQTrainingError`.
77    /// * `NaN` or infinities are observed during the training process.
78    fn train<R, C>(
79        &self,
80        data: views::rowmajor::Ref<f32>,
81        schema: crate::views::ChunkOffsetsView<'_>,
82        parallelism: Parallelism,
83        rng_builder: &R,
84        cancelation: &C,
85    ) -> Result<Self::Quantizer, Self::Error>
86    where
87        R: RngBuilder<usize> + Sync,
88        C: Cancelation + Sync,
89    {
90        // Inner method where we `dyn` away the cancellation token to reduce compile-times.
91        // Unfortunately, we can't quite do the same with the RngBuilder.
92        #[inline(never)]
93        fn train(
94            trainer: &LightPQTrainingParameters,
95            data: views::rowmajor::Ref<f32>,
96            schema: crate::views::ChunkOffsetsView<'_>,
97            parallelism: Parallelism,
98            rng_builder: &(dyn BoxedRngBuilder<usize> + Sync),
99            cancelation: &(dyn Cancelation + Sync),
100        ) -> Result<BasicTable, PQTrainingError> {
101            // Make sure we're provided sane values for our schema.
102            assert_eq!(data.ncols(), schema.dim());
103
104            let thunk = |i| -> Result<rowmajor::Owned<f32>, PQTrainingError> {
105                let range = schema.at(i);
106
107                // Check for cancelation.
108                let exit_if_canceled = || -> Result<(), PQTrainingError> {
109                    if cancelation.should_cancel() {
110                        Err(PQTrainingError {
111                            chunk: i,
112                            of: schema.len(),
113                            dim: range.len(),
114                            kind: PQTrainingErrorKind::Canceled,
115                        })
116                    } else {
117                        Ok(())
118                    }
119                };
120
121                // This is an early check - if another task hit cancelation, this allows
122                // the remaining tasks to exit early.
123                exit_if_canceled()?;
124
125                let view = Strided::try_from_data(
126                    &(data.as_slice()[range.start..]),
127                    data.nrows(),
128                    range.len(),
129                    schema.dim(),
130                )
131                .map_err(|err| PQTrainingError {
132                    chunk: i,
133                    of: schema.len(),
134                    dim: range.len(),
135                    kind: PQTrainingErrorKind::InternalError(Box::new(err)),
136                })?;
137
138                // Allocate scratch data structures.
139                let norms: Vec<f32> = view.rows().map(square_norm).collect();
140                let transpose = BlockTransposed::<f32, 16>::from_strided(view);
141                let mut centers =
142                    rowmajor::Owned::try_from_element(trainer.ncenters, range.len(), 0.0).map_err(
143                        |err| PQTrainingError {
144                            chunk: i,
145                            of: schema.len(),
146                            dim: range.len(),
147                            kind: PQTrainingErrorKind::InternalError(Box::new(err)),
148                        },
149                    )?;
150
151                // Construct the random number generator seeded by the PQ chunk.
152                let mut rng = rng_builder.build_boxed_rng(i);
153
154                // Initialization
155                kmeans::plusplus::kmeans_plusplus_into_inner(
156                    centers.as_view_mut(),
157                    view,
158                    transpose.as_view(),
159                    &norms,
160                    &mut rng,
161                )
162                .or_else(|err| {
163                    // Suppress recoverable errors.
164                    if !err.is_numerically_recoverable() {
165                        Err(PQTrainingError {
166                            chunk: i,
167                            of: schema.len(),
168                            dim: range.len(),
169                            kind: PQTrainingErrorKind::Initialization(Box::new(err)),
170                        })
171                    } else {
172                        Ok(())
173                    }
174                })?;
175
176                // Did a cancelation request come while runing `kmeans++`?
177                exit_if_canceled()?;
178
179                // Kmeans
180                kmeans::lloyds::lloyds_inner(
181                    view,
182                    &norms,
183                    transpose.as_view(),
184                    centers.as_view_mut(),
185                    trainer.lloyds_reps,
186                );
187                Ok(centers)
188            };
189
190            let pivots: Result<Vec<rowmajor::Owned<f32>>, _> = match parallelism {
191                Parallelism::Sequential => (0..schema.len()).map(thunk).collect(),
192
193                #[cfg(feature = "rayon")]
194                Parallelism::Rayon => (0..schema.len()).into_par_iter().map(thunk).collect(),
195            };
196
197            let pivots = pivots?;
198            let mut packed = rowmajor::Owned::from_element(trainer.ncenters, schema.dim(), 0.0);
199
200            for (i, trained) in pivots.into_iter().enumerate() {
201                let offsets = schema.at(i);
202                for (dst, src) in std::iter::zip(packed.rows_mut(), trained.rows()) {
203                    dst[offsets.clone()].copy_from_slice(src);
204                }
205            }
206
207            let table =
208                BasicTable::new(packed, schema.to_owned()).map_err(|err| PQTrainingError {
209                    chunk: schema.len(),
210                    of: schema.len(),
211                    dim: data.ncols(),
212                    kind: PQTrainingErrorKind::InternalError(Box::new(err)),
213                })?;
214
215            Ok(table)
216        }
217
218        train(self, data, schema, parallelism, rng_builder, cancelation)
219    }
220}
221
222#[derive(Debug, Error)]
223#[error("pq training failed on chunk {chunk} of {of} (dim {dim})")]
224pub struct PQTrainingError {
225    chunk: usize,
226    of: usize,
227    dim: usize,
228    #[source]
229    kind: PQTrainingErrorKind,
230}
231
232impl PQTrainingError {
233    /// Return whether or not this error originated as a cancelation request.
234    pub fn was_canceled(&self) -> bool {
235        matches!(self.kind, PQTrainingErrorKind::Canceled)
236    }
237}
238
239#[derive(Debug, Error)]
240#[non_exhaustive]
241enum PQTrainingErrorKind {
242    #[error("canceled by request")]
243    Canceled,
244    #[error("initial pivot selection error")]
245    Initialization(#[source] Box<dyn std::error::Error + Send + Sync>),
246    #[error("internal logic error")]
247    InternalError(#[source] Box<dyn std::error::Error + Send + Sync>),
248}
249
250///////////
251// Tests //
252///////////
253
254#[cfg(not(miri))]
255#[cfg(test)]
256mod tests {
257    use std::sync::atomic::{AtomicUsize, Ordering};
258
259    use rand::{
260        Rng, SeedableRng,
261        distr::{Distribution, StandardUniform, Uniform},
262        rngs::StdRng,
263        seq::SliceRandom,
264    };
265
266    use diskann_utils::lazy_format;
267
268    use super::*;
269    use crate::{cancel::DontCancel, error::format, random::StdRngBuilder};
270
271    fn flatten<T: Copy + Default>(
272        pivots: &[rowmajor::Owned<T>],
273        ncenters: usize,
274        dim: usize,
275    ) -> rowmajor::Owned<T> {
276        let mut flattened = rowmajor::Owned::from_element(ncenters, dim, T::default());
277        let mut col_start = 0;
278        for matrix in pivots {
279            assert_eq!(matrix.nrows(), flattened.nrows());
280            for (row_index, row) in matrix.rows().enumerate() {
281                let dst = &mut flattened.row_mut(row_index)[col_start..col_start + row.len()];
282                dst.copy_from_slice(row);
283            }
284            col_start += matrix.ncols();
285        }
286        flattened
287    }
288
289    struct DatasetBuilder {
290        nclusters: usize,
291        cluster_size: usize,
292        step_between_clusters: f32,
293    }
294
295    struct ClusteredDataset {
296        data: rowmajor::Owned<f32>,
297        // The pre-configured center point for the manufactured clusters.
298        centers: rowmajor::Owned<f32>,
299    }
300
301    impl DatasetBuilder {
302        fn build<R>(
303            &self,
304            schema: crate::views::ChunkOffsetsView<'_>,
305            rng: &mut R,
306        ) -> ClusteredDataset
307        where
308            R: Rng,
309        {
310            let ndata = self.nclusters * self.cluster_size;
311
312            // Start the clustering points at a different location for each chunk.
313            // The starting offset is chosen from this distribution.
314            let offsets_distribution = Uniform::<f32>::new(-100.0, 100.0).unwrap();
315
316            // The perturbation for vectors within a cluster - all centered around some
317            // mean.
318            let perturbation_distribution = rand_distr::StandardNormal;
319
320            // Indices that we use to shuffle the order of elements in the dataset.
321            let mut indices: Vec<usize> = (0..ndata).collect();
322
323            // Construct the dataset in pieces.
324            let (pieces, centers): (Vec<_>, Vec<_>) = (0..schema.len())
325                .map(|chunk| {
326                    let dim = schema.at(chunk).len();
327
328                    let mut initial = rowmajor::Owned::from_element(ndata, dim, 0.0);
329                    let mut centers = rowmajor::Owned::from_element(self.nclusters, 1, 0.0);
330
331                    // The starting offset for clusters.
332                    let offset = offsets_distribution.sample(rng);
333
334                    // Create a dataset with `nclusters`, each cluster
335                    for cluster in 0..self.nclusters {
336                        let this_offset = offset + (cluster as f32 * self.step_between_clusters);
337                        *centers.element_mut(cluster, 0) = this_offset;
338
339                        for element in 0..self.cluster_size {
340                            let row = initial.row_mut(cluster * self.cluster_size + element);
341                            for r in row.iter_mut() {
342                                let perturbation: f32 = perturbation_distribution.sample(rng);
343                                *r = this_offset + perturbation;
344                            }
345                        }
346                    }
347
348                    // Shuffle the dataset.
349                    indices.shuffle(rng);
350                    let mut piece = rowmajor::Owned::from_element(ndata, dim, 0.0);
351                    for (dst, src) in indices.iter().enumerate() {
352                        piece.row_mut(dst).copy_from_slice(initial.row(*src));
353                    }
354                    (piece, centers)
355                })
356                .unzip();
357
358            ClusteredDataset {
359                data: flatten(&pieces, ndata, schema.dim()),
360                centers: flatten(&centers, self.nclusters, schema.len()),
361            }
362        }
363    }
364
365    fn broadcast_distance(x: &[f32], y: f32) -> f32 {
366        x.iter()
367            .map(|i| {
368                let d = *i - y;
369                d * d
370            })
371            .sum()
372    }
373
374    // Happy Path check - varying over parallelism.
375    fn test_pq_training_happy_path(parallelism: Parallelism) {
376        let mut rng = StdRng::seed_from_u64(0x749cb951cf960384);
377        let builder = DatasetBuilder {
378            nclusters: 16,
379            cluster_size: 20,
380            // NOTE: We need to keep the step between clusters fairly large to ensure that
381            // kmeans++ adequately initializes.
382            step_between_clusters: 20.0,
383        };
384
385        let ncenters = builder.nclusters;
386
387        let offsets = [0, 2, 3, 8, 12, 16];
388        let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
389        let dataset = builder.build(schema, &mut rng);
390
391        let trainer = LightPQTrainingParameters::new(ncenters, 6);
392
393        let quantizer = trainer
394            .train(
395                dataset.data.as_view(),
396                schema,
397                parallelism,
398                &StdRngBuilder::new(StandardUniform {}.sample(&mut rng)),
399                &DontCancel,
400            )
401            .unwrap();
402
403        // Now that we have trained the quantizer - we need to double check that the chosen
404        // centroids match what we expect.
405        //
406        // To do this - we loop through the centroids that training picked. We match the
407        // centroids with one of the known centers in our clustering.
408        //
409        // We perform two main checks:
410        //
411        // 1. We ensure that the quantizer's center actually aligns with a cluster (i.e.,
412        //    training did not invent values out of thin air).
413        // 2. Every clustering in the original dataset has a representative in the quantizer.
414        assert_eq!(quantizer.dim(), schema.dim());
415        assert_eq!(quantizer.ncenters(), ncenters);
416        assert_eq!(quantizer.nchunks(), schema.len());
417        for i in 0..quantizer.nchunks() {
418            let pivot = quantizer.pivots_for(i).unwrap();
419            // Make sure the pivot has the correct dimension.
420            assert_eq!(
421                pivot.ncols(),
422                schema.at(i).len(),
423                "center {i} has the incorrect number of columns"
424            );
425            assert_eq!(pivot.nrows(), ncenters);
426
427            // Start matching pivots to expected centers.
428            let mut seen: Vec<bool> = (0..dataset.centers.nrows()).map(|_| false).collect();
429            for row in pivot.rows() {
430                let mut min_distance = f32::MAX;
431                let mut min_index = 0;
432                for c in 0..dataset.centers.nrows() {
433                    let distance = broadcast_distance(row, *dataset.centers.element(c, i));
434                    if distance < min_distance {
435                        min_distance = distance;
436                        min_index = c;
437                    }
438                }
439
440                // Does the minimum distance suggest that we are inside a cluster.
441                assert!(
442                    min_distance < 1.0,
443                    "got a minimum distance of {}, pivot = {}. Row = {:?}",
444                    min_distance,
445                    i,
446                    row,
447                );
448
449                // Mark this index as seen.
450                let seen_before = &mut seen[min_index];
451                assert!(
452                    !*seen_before,
453                    "cluster {} has more than one assignment",
454                    min_index
455                );
456                *seen_before = true;
457            }
458
459            // Make sure that all clusters were seen.
460            assert!(seen.iter().all(|i| *i), "not all clusters were seen");
461        }
462    }
463
464    #[test]
465    fn test_pq_training_happy_path_sequential() {
466        test_pq_training_happy_path(Parallelism::Sequential);
467    }
468
469    #[test]
470    #[cfg(feature = "rayon")]
471    fn test_pq_training_happy_path_parallel() {
472        test_pq_training_happy_path(Parallelism::Rayon);
473    }
474
475    // A canceler that cancels after a set number of invocations.
476    struct CancelAfter {
477        counter: AtomicUsize,
478        after: usize,
479    }
480
481    impl CancelAfter {
482        fn new(after: usize) -> Self {
483            Self {
484                counter: AtomicUsize::new(0),
485                after,
486            }
487        }
488    }
489
490    impl Cancelation for CancelAfter {
491        fn should_cancel(&self) -> bool {
492            let v = self.counter.fetch_add(1, Ordering::Relaxed);
493            v >= self.after
494        }
495    }
496
497    #[test]
498    fn test_cancel() {
499        let mut rng = StdRng::seed_from_u64(0xb85352d38cc5353b);
500        let builder = DatasetBuilder {
501            nclusters: 16,
502            cluster_size: 20,
503            // NOTE: We need to keep the step between clusters fairly large to ensure that
504            // kmeans++ adequately initializes.
505            step_between_clusters: 20.0,
506        };
507
508        let offsets = [0, 2, 3, 8, 12, 16];
509        let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
510        let dataset = builder.build(schema, &mut rng);
511
512        let trainer = LightPQTrainingParameters::new(builder.nclusters, 6);
513
514        for after in 0..10 {
515            let parallelism = [
516                Parallelism::Sequential,
517                #[cfg(feature = "rayon")]
518                Parallelism::Rayon,
519            ];
520
521            for par in parallelism {
522                let result = trainer.train(
523                    dataset.data.as_view(),
524                    schema,
525                    par,
526                    &StdRngBuilder::new(StandardUniform {}.sample(&mut rng)),
527                    &CancelAfter::new(after),
528                );
529                assert!(result.is_err(), "expected the operation to be canceled");
530                let err = result.unwrap_err();
531                assert!(
532                    err.was_canceled(),
533                    "expected the failure reason to be cancellation"
534                );
535            }
536        }
537    }
538
539    // In this test - we ensure that clustering succeeds even if the number of requested
540    // pivots exceeds the number of dataset items.
541    #[test]
542    fn tests_succeeded_with_too_many_pivots() {
543        let data = rowmajor::Owned::<f32>::from_element(10, 5, 1.0);
544        let offsets: Vec<usize> = vec![0, 1, 4, 5];
545
546        let trainer = LightPQTrainingParameters::new(2 * data.nrows(), 6);
547
548        let quantizer = trainer
549            .train(
550                data.as_view(),
551                crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
552                Parallelism::Sequential,
553                &StdRngBuilder::new(0),
554                &DontCancel,
555            )
556            .unwrap();
557
558        // We are in the special position to actually know how this will behave.
559        // Since the input dataset lacks diversity, there should only have been a single
560        // pivot actually selected.
561        //
562        // All the rest should be zero.
563
564        assert!(
565            quantizer.view_pivots().row(0).iter().all(|i| *i == 1.0),
566            "expected pivot 0 to be the non-zero pivot"
567        );
568
569        for (i, row) in quantizer.view_pivots().rows().enumerate() {
570            // skip the first row.
571            if i == 0 {
572                continue;
573            }
574
575            assert!(
576                row.iter().all(|j| *j == 0.0),
577                "expected pivot {i} to be all zeros"
578            );
579        }
580    }
581
582    #[test]
583    fn test_infinity_and_nan_is_not_recoverable() {
584        let num_trials = 10;
585        let nrows = 10;
586        let ncols = 5;
587
588        let offsets: Vec<usize> = vec![0, 1, 4, 5];
589
590        let trainer = LightPQTrainingParameters::new(nrows, 6);
591
592        let row_distribution = Uniform::new(0, nrows).unwrap();
593        let col_distribution = Uniform::new(0, ncols).unwrap();
594        let mut rng = StdRng::seed_from_u64(0xe746cfebba2d7e35);
595
596        for trial in 0..num_trials {
597            let context = lazy_format!("trial {} of {}", trial + 1, num_trials);
598
599            let r = row_distribution.sample(&mut rng);
600            let c = col_distribution.sample(&mut rng);
601
602            let check_result = |r: Result<_, PQTrainingError>| {
603                assert!(
604                    r.is_err(),
605                    "expected error due to infinities/NaN -- {}",
606                    context
607                );
608                let err = r.unwrap_err();
609                assert!(!err.was_canceled());
610                assert!(format(&err).contains("infinity"));
611            };
612
613            let mut data = rowmajor::Owned::<f32>::from_element(nrows, ncols, 1.0);
614
615            // Positive Infinity
616            *data.element_mut(r, c) = f32::INFINITY;
617            let result = trainer.train(
618                data.as_view(),
619                crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
620                Parallelism::Sequential,
621                &StdRngBuilder::new(0),
622                &DontCancel,
623            );
624            check_result(result);
625
626            // Positive Infinity
627            *data.element_mut(r, c) = f32::NEG_INFINITY;
628            let result = trainer.train(
629                data.as_view(),
630                crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
631                Parallelism::Sequential,
632                &StdRngBuilder::new(0),
633                &DontCancel,
634            );
635            check_result(result);
636
637            // NaN
638            *data.element_mut(r, c) = f32::NAN;
639            let result = trainer.train(
640                data.as_view(),
641                crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
642                Parallelism::Sequential,
643                &StdRngBuilder::new(0),
644                &DontCancel,
645            );
646            check_result(result);
647        }
648    }
649}