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 product::tables::BasicTable,
23 random::{BoxedRngBuilder, RngBuilder},
24};
25
26pub struct LightPQTrainingParameters {
27 ncenters: usize,
29 lloyds_reps: usize,
31}
32
33impl LightPQTrainingParameters {
34 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 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 #[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 assert_eq!(data.ncols(), schema.dim());
103
104 let thunk = |i| -> Result<rowmajor::Owned<f32>, PQTrainingError> {
105 let range = schema.at(i);
106
107 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 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 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 let mut rng = rng_builder.build_boxed_rng(i);
153
154 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 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 exit_if_canceled()?;
178
179 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 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#[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 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 let offsets_distribution = Uniform::<f32>::new(-100.0, 100.0).unwrap();
315
316 let perturbation_distribution = rand_distr::StandardNormal;
319
320 let mut indices: Vec<usize> = (0..ndata).collect();
322
323 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 let offset = offsets_distribution.sample(rng);
333
334 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 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(¢ers, 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 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 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 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 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 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 assert!(
442 min_distance < 1.0,
443 "got a minimum distance of {}, pivot = {}. Row = {:?}",
444 min_distance,
445 i,
446 row,
447 );
448
449 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 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 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 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 #[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 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 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 *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 *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 *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}