1use 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 random::{BoxedRngBuilder, RngBuilder},
23};
24
25pub struct LightPQTrainingParameters {
26 ncenters: usize,
28 lloyds_reps: usize,
30}
31
32impl LightPQTrainingParameters {
33 pub fn new(ncenters: usize, lloyds_reps: usize) -> Self {
35 Self {
36 ncenters,
37 lloyds_reps,
38 }
39 }
40}
41
42#[derive(Debug)]
43pub struct SimplePivots {
44 dim: usize,
45 ncenters: usize,
46 pivots: Vec<rowmajor::Owned<f32>>,
47}
48
49fn flatten<T: Copy + Default>(
50 pivots: &[rowmajor::Owned<T>],
51 ncenters: usize,
52 dim: usize,
53) -> rowmajor::Owned<T> {
54 let mut flattened = rowmajor::Owned::from_element(ncenters, dim, T::default());
55 let mut col_start = 0;
56 for matrix in pivots {
57 assert_eq!(matrix.nrows(), flattened.nrows());
58 for (row_index, row) in matrix.rows().enumerate() {
59 let dst = &mut flattened.row_mut(row_index)[col_start..col_start + row.len()];
60 dst.copy_from_slice(row);
61 }
62 col_start += matrix.ncols();
63 }
64 flattened
65}
66
67impl SimplePivots {
68 pub fn pivots(&self) -> &[rowmajor::Owned<f32>] {
70 &self.pivots
71 }
72
73 pub fn flatten(&self) -> Vec<f32> {
75 flatten(self.pivots(), self.ncenters, self.dim)
76 .into_inner()
77 .into()
78 }
79}
80
81pub trait TrainQuantizer {
82 type Quantizer;
83 type Error: std::error::Error;
84
85 fn train<R, C>(
86 &self,
87 data: views::rowmajor::Ref<f32>,
88 schema: crate::views::ChunkOffsetsView<'_>,
89 parallelism: Parallelism,
90 rng_builder: &R,
91 cancelation: &C,
92 ) -> Result<Self::Quantizer, Self::Error>
93 where
94 R: RngBuilder<usize> + Sync,
95 C: Cancelation + Sync;
96}
97
98impl TrainQuantizer for LightPQTrainingParameters {
99 type Quantizer = SimplePivots;
100 type Error = PQTrainingError;
101
102 fn train<R, C>(
117 &self,
118 data: views::rowmajor::Ref<f32>,
119 schema: crate::views::ChunkOffsetsView<'_>,
120 parallelism: Parallelism,
121 rng_builder: &R,
122 cancelation: &C,
123 ) -> Result<Self::Quantizer, Self::Error>
124 where
125 R: RngBuilder<usize> + Sync,
126 C: Cancelation + Sync,
127 {
128 #[inline(never)]
131 fn train(
132 trainer: &LightPQTrainingParameters,
133 data: views::rowmajor::Ref<f32>,
134 schema: crate::views::ChunkOffsetsView<'_>,
135 parallelism: Parallelism,
136 rng_builder: &(dyn BoxedRngBuilder<usize> + Sync),
137 cancelation: &(dyn Cancelation + Sync),
138 ) -> Result<SimplePivots, PQTrainingError> {
139 assert_eq!(data.ncols(), schema.dim());
141
142 let thunk = |i| -> Result<rowmajor::Owned<f32>, PQTrainingError> {
143 let range = schema.at(i);
144
145 let exit_if_canceled = || -> Result<(), PQTrainingError> {
147 if cancelation.should_cancel() {
148 Err(PQTrainingError {
149 chunk: i,
150 of: schema.len(),
151 dim: range.len(),
152 kind: PQTrainingErrorKind::Canceled,
153 })
154 } else {
155 Ok(())
156 }
157 };
158
159 exit_if_canceled()?;
162
163 let view = Strided::try_from_data(
164 &(data.as_slice()[range.start..]),
165 data.nrows(),
166 range.len(),
167 schema.dim(),
168 )
169 .map_err(|err| PQTrainingError {
170 chunk: i,
171 of: schema.len(),
172 dim: range.len(),
173 kind: PQTrainingErrorKind::InternalError(Box::new(err)),
174 })?;
175
176 let norms: Vec<f32> = view.rows().map(square_norm).collect();
178 let transpose = BlockTransposed::<f32, 16>::from_strided(view);
179 let mut centers =
180 rowmajor::Owned::try_from_element(trainer.ncenters, range.len(), 0.0).map_err(
181 |err| PQTrainingError {
182 chunk: i,
183 of: schema.len(),
184 dim: range.len(),
185 kind: PQTrainingErrorKind::InternalError(Box::new(err)),
186 },
187 )?;
188
189 let mut rng = rng_builder.build_boxed_rng(i);
191
192 kmeans::plusplus::kmeans_plusplus_into_inner(
194 centers.as_view_mut(),
195 view,
196 transpose.as_view(),
197 &norms,
198 &mut rng,
199 )
200 .or_else(|err| {
201 if !err.is_numerically_recoverable() {
203 Err(PQTrainingError {
204 chunk: i,
205 of: schema.len(),
206 dim: range.len(),
207 kind: PQTrainingErrorKind::Initialization(Box::new(err)),
208 })
209 } else {
210 Ok(())
211 }
212 })?;
213
214 exit_if_canceled()?;
216
217 kmeans::lloyds::lloyds_inner(
219 view,
220 &norms,
221 transpose.as_view(),
222 centers.as_view_mut(),
223 trainer.lloyds_reps,
224 );
225 Ok(centers)
226 };
227
228 let pivots: Result<Vec<_>, _> = match parallelism {
229 Parallelism::Sequential => (0..schema.len()).map(thunk).collect(),
230
231 #[cfg(feature = "rayon")]
232 Parallelism::Rayon => (0..schema.len()).into_par_iter().map(thunk).collect(),
233 };
234
235 let dim = data.ncols();
236 let ncenters = trainer.ncenters;
237 Ok(SimplePivots {
238 dim,
239 ncenters,
240 pivots: pivots?,
241 })
242 }
243
244 train(self, data, schema, parallelism, rng_builder, cancelation)
245 }
246}
247
248#[derive(Debug, Error)]
249#[error("pq training failed on chunk {chunk} of {of} (dim {dim})")]
250pub struct PQTrainingError {
251 chunk: usize,
252 of: usize,
253 dim: usize,
254 #[source]
255 kind: PQTrainingErrorKind,
256}
257
258impl PQTrainingError {
259 pub fn was_canceled(&self) -> bool {
261 matches!(self.kind, PQTrainingErrorKind::Canceled)
262 }
263}
264
265#[derive(Debug, Error)]
266#[non_exhaustive]
267enum PQTrainingErrorKind {
268 #[error("canceled by request")]
269 Canceled,
270 #[error("initial pivot selection error")]
271 Initialization(#[source] Box<dyn std::error::Error + Send + Sync>),
272 #[error("internal logic error")]
273 InternalError(#[source] Box<dyn std::error::Error + Send + Sync>),
274}
275
276#[cfg(not(miri))]
281#[cfg(test)]
282mod tests {
283 use std::sync::atomic::{AtomicUsize, Ordering};
284
285 use rand::{
286 Rng, SeedableRng,
287 distr::{Distribution, StandardUniform, Uniform},
288 rngs::StdRng,
289 seq::SliceRandom,
290 };
291
292 use diskann_utils::lazy_format;
293
294 use super::*;
295 use crate::{cancel::DontCancel, error::format, random::StdRngBuilder};
296
297 #[test]
300 fn test_flatten() {
301 let nrows = 5;
303 let sub_dims = [1, 2, 3, 4, 5];
305 let prefix_sum: Vec<usize> = sub_dims
307 .iter()
308 .scan(0, |state, i| {
309 let this = *state;
310 *state += *i;
311 Some(this)
312 })
313 .collect();
314
315 let dim: usize = sub_dims.iter().sum();
316
317 let matrices: Vec<rowmajor::Owned<usize>> =
319 std::iter::zip(sub_dims.iter(), prefix_sum.iter())
320 .map(|(&this_dim, &offset)| {
321 let mut m = rowmajor::Owned::from_element(nrows, this_dim, 0);
322 for r in 0..nrows {
323 for c in 0..this_dim {
324 *m.element_mut(r, c) = dim * r + offset + c;
325 }
326 }
327 m
328 })
329 .collect();
330
331 let flattened = flatten(&matrices, nrows, dim);
332 for (i, v) in flattened.as_slice().iter().enumerate() {
334 assert_eq!(*v, i, "failed at index {i}");
335 }
336 }
337
338 struct DatasetBuilder {
339 nclusters: usize,
340 cluster_size: usize,
341 step_between_clusters: f32,
342 }
343
344 struct ClusteredDataset {
345 data: rowmajor::Owned<f32>,
346 centers: rowmajor::Owned<f32>,
348 }
349
350 impl DatasetBuilder {
351 fn build<R>(
352 &self,
353 schema: crate::views::ChunkOffsetsView<'_>,
354 rng: &mut R,
355 ) -> ClusteredDataset
356 where
357 R: Rng,
358 {
359 let ndata = self.nclusters * self.cluster_size;
360
361 let offsets_distribution = Uniform::<f32>::new(-100.0, 100.0).unwrap();
364
365 let perturbation_distribution = rand_distr::StandardNormal;
368
369 let mut indices: Vec<usize> = (0..ndata).collect();
371
372 let (pieces, centers): (Vec<_>, Vec<_>) = (0..schema.len())
374 .map(|chunk| {
375 let dim = schema.at(chunk).len();
376
377 let mut initial = rowmajor::Owned::from_element(ndata, dim, 0.0);
378 let mut centers = rowmajor::Owned::from_element(self.nclusters, 1, 0.0);
379
380 let offset = offsets_distribution.sample(rng);
382
383 for cluster in 0..self.nclusters {
385 let this_offset = offset + (cluster as f32 * self.step_between_clusters);
386 *centers.element_mut(cluster, 0) = this_offset;
387
388 for element in 0..self.cluster_size {
389 let row = initial.row_mut(cluster * self.cluster_size + element);
390 for r in row.iter_mut() {
391 let perturbation: f32 = perturbation_distribution.sample(rng);
392 *r = this_offset + perturbation;
393 }
394 }
395 }
396
397 indices.shuffle(rng);
399 let mut piece = rowmajor::Owned::from_element(ndata, dim, 0.0);
400 for (dst, src) in indices.iter().enumerate() {
401 piece.row_mut(dst).copy_from_slice(initial.row(*src));
402 }
403 (piece, centers)
404 })
405 .unzip();
406
407 ClusteredDataset {
408 data: flatten(&pieces, ndata, schema.dim()),
409 centers: flatten(¢ers, self.nclusters, schema.len()),
410 }
411 }
412 }
413
414 fn broadcast_distance(x: &[f32], y: f32) -> f32 {
415 x.iter()
416 .map(|i| {
417 let d = *i - y;
418 d * d
419 })
420 .sum()
421 }
422
423 fn test_pq_training_happy_path(parallelism: Parallelism) {
425 let mut rng = StdRng::seed_from_u64(0x749cb951cf960384);
426 let builder = DatasetBuilder {
427 nclusters: 16,
428 cluster_size: 20,
429 step_between_clusters: 20.0,
432 };
433
434 let ncenters = builder.nclusters;
435
436 let offsets = [0, 2, 3, 8, 12, 16];
437 let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
438 let dataset = builder.build(schema, &mut rng);
439
440 let trainer = LightPQTrainingParameters::new(ncenters, 6);
441
442 let quantizer = trainer
443 .train(
444 dataset.data.as_view(),
445 schema,
446 parallelism,
447 &StdRngBuilder::new(StandardUniform {}.sample(&mut rng)),
448 &DontCancel,
449 )
450 .unwrap();
451
452 assert_eq!(quantizer.dim, schema.dim());
464 assert_eq!(quantizer.ncenters, ncenters);
465 assert_eq!(quantizer.pivots.len(), schema.len());
466 for (i, pivot) in quantizer.pivots.iter().enumerate() {
467 assert_eq!(
469 pivot.ncols(),
470 schema.at(i).len(),
471 "center {i} has the incorrect number of columns"
472 );
473 assert_eq!(pivot.nrows(), ncenters);
474
475 let mut seen: Vec<bool> = (0..dataset.centers.nrows()).map(|_| false).collect();
477 for row in pivot.rows() {
478 let mut min_distance = f32::MAX;
479 let mut min_index = 0;
480 for c in 0..dataset.centers.nrows() {
481 let distance = broadcast_distance(row, *dataset.centers.element(c, i));
482 if distance < min_distance {
483 min_distance = distance;
484 min_index = c;
485 }
486 }
487
488 assert!(
490 min_distance < 1.0,
491 "got a minimum distance of {}, pivot = {}. Row = {:?}",
492 min_distance,
493 i,
494 row,
495 );
496
497 let seen_before = &mut seen[min_index];
499 assert!(
500 !*seen_before,
501 "cluster {} has more than one assignment",
502 min_index
503 );
504 *seen_before = true;
505 }
506
507 assert!(seen.iter().all(|i| *i), "not all clusters were seen");
509 }
510
511 let flattened = quantizer.flatten();
513 assert_eq!(
514 &flattened,
515 flatten(&quantizer.pivots, quantizer.ncenters, quantizer.dim).as_slice()
516 );
517 }
518
519 #[test]
520 fn test_pq_training_happy_path_sequential() {
521 test_pq_training_happy_path(Parallelism::Sequential);
522 }
523
524 #[test]
525 #[cfg(feature = "rayon")]
526 fn test_pq_training_happy_path_parallel() {
527 test_pq_training_happy_path(Parallelism::Rayon);
528 }
529
530 struct CancelAfter {
532 counter: AtomicUsize,
533 after: usize,
534 }
535
536 impl CancelAfter {
537 fn new(after: usize) -> Self {
538 Self {
539 counter: AtomicUsize::new(0),
540 after,
541 }
542 }
543 }
544
545 impl Cancelation for CancelAfter {
546 fn should_cancel(&self) -> bool {
547 let v = self.counter.fetch_add(1, Ordering::Relaxed);
548 v >= self.after
549 }
550 }
551
552 #[test]
553 fn test_cancel() {
554 let mut rng = StdRng::seed_from_u64(0xb85352d38cc5353b);
555 let builder = DatasetBuilder {
556 nclusters: 16,
557 cluster_size: 20,
558 step_between_clusters: 20.0,
561 };
562
563 let offsets = [0, 2, 3, 8, 12, 16];
564 let schema = crate::views::ChunkOffsetsView::new(&offsets).unwrap();
565 let dataset = builder.build(schema, &mut rng);
566
567 let trainer = LightPQTrainingParameters::new(builder.nclusters, 6);
568
569 for after in 0..10 {
570 let parallelism = [
571 Parallelism::Sequential,
572 #[cfg(feature = "rayon")]
573 Parallelism::Rayon,
574 ];
575
576 for par in parallelism {
577 let result = trainer.train(
578 dataset.data.as_view(),
579 schema,
580 par,
581 &StdRngBuilder::new(StandardUniform {}.sample(&mut rng)),
582 &CancelAfter::new(after),
583 );
584 assert!(result.is_err(), "expected the operation to be canceled");
585 let err = result.unwrap_err();
586 assert!(
587 err.was_canceled(),
588 "expected the failure reason to be cancellation"
589 );
590 }
591 }
592 }
593
594 #[test]
597 fn tests_succeeded_with_too_many_pivots() {
598 let data = rowmajor::Owned::<f32>::from_element(10, 5, 1.0);
599 let offsets: Vec<usize> = vec![0, 1, 4, 5];
600
601 let trainer = LightPQTrainingParameters::new(2 * data.nrows(), 6);
602
603 let quantizer = trainer
604 .train(
605 data.as_view(),
606 crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
607 Parallelism::Sequential,
608 &StdRngBuilder::new(0),
609 &DontCancel,
610 )
611 .unwrap();
612
613 let flat = flatten(&quantizer.pivots, quantizer.ncenters, quantizer.dim);
619
620 assert!(
621 flat.row(0).iter().all(|i| *i == 1.0),
622 "expected pivot 0 to be the non-zero pivot"
623 );
624
625 for (i, row) in flat.rows().enumerate() {
626 if i == 0 {
628 continue;
629 }
630
631 assert!(
632 row.iter().all(|j| *j == 0.0),
633 "expected pivot {i} to be all zeros"
634 );
635 }
636 }
637
638 #[test]
639 fn test_infinity_and_nan_is_not_recoverable() {
640 let num_trials = 10;
641 let nrows = 10;
642 let ncols = 5;
643
644 let offsets: Vec<usize> = vec![0, 1, 4, 5];
645
646 let trainer = LightPQTrainingParameters::new(nrows, 6);
647
648 let row_distribution = Uniform::new(0, nrows).unwrap();
649 let col_distribution = Uniform::new(0, ncols).unwrap();
650 let mut rng = StdRng::seed_from_u64(0xe746cfebba2d7e35);
651
652 for trial in 0..num_trials {
653 let context = lazy_format!("trial {} of {}", trial + 1, num_trials);
654
655 let r = row_distribution.sample(&mut rng);
656 let c = col_distribution.sample(&mut rng);
657
658 let check_result = |r: Result<_, PQTrainingError>| {
659 assert!(
660 r.is_err(),
661 "expected error due to infinities/NaN -- {}",
662 context
663 );
664 let err = r.unwrap_err();
665 assert!(!err.was_canceled());
666 assert!(format(&err).contains("infinity"));
667 };
668
669 let mut data = rowmajor::Owned::<f32>::from_element(nrows, ncols, 1.0);
670
671 *data.element_mut(r, c) = f32::INFINITY;
673 let result = trainer.train(
674 data.as_view(),
675 crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
676 Parallelism::Sequential,
677 &StdRngBuilder::new(0),
678 &DontCancel,
679 );
680 check_result(result);
681
682 *data.element_mut(r, c) = f32::NEG_INFINITY;
684 let result = trainer.train(
685 data.as_view(),
686 crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
687 Parallelism::Sequential,
688 &StdRngBuilder::new(0),
689 &DontCancel,
690 );
691 check_result(result);
692
693 *data.element_mut(r, c) = f32::NAN;
695 let result = trainer.train(
696 data.as_view(),
697 crate::views::ChunkOffsetsView::new(&offsets).unwrap(),
698 Parallelism::Sequential,
699 &StdRngBuilder::new(0),
700 &DontCancel,
701 );
702 check_result(result);
703 }
704 }
705}