faiss 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/lib/faiss/version.rb +1 -1
  4. data/vendor/faiss/faiss/Clustering.h +8 -0
  5. data/vendor/faiss/faiss/Index.h +1 -1
  6. data/vendor/faiss/faiss/IndexBinaryFromFloat.cpp +23 -2
  7. data/vendor/faiss/faiss/IndexBinaryHNSW.cpp +12 -140
  8. data/vendor/faiss/faiss/IndexBinaryHNSW.h +0 -6
  9. data/vendor/faiss/faiss/IndexBinaryIVF.cpp +6 -14
  10. data/vendor/faiss/faiss/IndexBinaryIVF.h +8 -1
  11. data/vendor/faiss/faiss/IndexFlat.cpp +8 -4
  12. data/vendor/faiss/faiss/IndexFlat.h +2 -2
  13. data/vendor/faiss/faiss/IndexHNSW.cpp +301 -91
  14. data/vendor/faiss/faiss/IndexHNSW.h +31 -7
  15. data/vendor/faiss/faiss/IndexIDMap.cpp +11 -6
  16. data/vendor/faiss/faiss/IndexIVF.cpp +19 -4
  17. data/vendor/faiss/faiss/IndexIVFPQ.cpp +9 -1
  18. data/vendor/faiss/faiss/IndexIVFPQ.h +11 -0
  19. data/vendor/faiss/faiss/IndexShards.cpp +3 -3
  20. data/vendor/faiss/faiss/IndexShardsIVF.cpp +4 -3
  21. data/vendor/faiss/faiss/SuperKMeans.cpp +31 -8
  22. data/vendor/faiss/faiss/SuperKMeans.h +3 -2
  23. data/vendor/faiss/faiss/VectorTransform.cpp +38 -0
  24. data/vendor/faiss/faiss/VectorTransform.h +3 -0
  25. data/vendor/faiss/faiss/clone_index.cpp +3 -0
  26. data/vendor/faiss/faiss/gpu/GpuClonerOptions.h +1 -5
  27. data/vendor/faiss/faiss/gpu/GpuDistance.h +2 -5
  28. data/vendor/faiss/faiss/gpu/GpuIndex.h +4 -5
  29. data/vendor/faiss/faiss/gpu/GpuIndexCagra.h +65 -42
  30. data/vendor/faiss/faiss/impl/AdditiveQuantizer.cpp +1 -1
  31. data/vendor/faiss/faiss/impl/ClusteringInitialization.cpp +5 -2
  32. data/vendor/faiss/faiss/impl/HNSW.cpp +419 -115
  33. data/vendor/faiss/faiss/impl/HNSW.h +46 -7
  34. data/vendor/faiss/faiss/impl/IDSelector.h +44 -0
  35. data/vendor/faiss/faiss/impl/NNDescent.cpp +10 -3
  36. data/vendor/faiss/faiss/impl/Panorama.h +11 -2
  37. data/vendor/faiss/faiss/impl/ProductQuantizer.cpp +4 -4
  38. data/vendor/faiss/faiss/impl/RaBitQUtils.cpp +1 -1
  39. data/vendor/faiss/faiss/impl/RaBitQuantizer.cpp +74 -8
  40. data/vendor/faiss/faiss/impl/RaBitQuantizer.h +47 -4
  41. data/vendor/faiss/faiss/impl/RaBitQuantizerMultiBit.cpp +2 -2
  42. data/vendor/faiss/faiss/impl/ResultHandler.h +9 -3
  43. data/vendor/faiss/faiss/impl/ScalarQuantizer.cpp +32 -30
  44. data/vendor/faiss/faiss/impl/ScalarQuantizer.h +6 -10
  45. data/vendor/faiss/faiss/impl/binary_hamming/IndexBinaryIVF_impl.h +89 -13
  46. data/vendor/faiss/faiss/impl/expanded_scanners.h +5 -1
  47. data/vendor/faiss/faiss/impl/fast_scan/decompose_qbs.h +1 -0
  48. data/vendor/faiss/faiss/impl/index_read.cpp +130 -29
  49. data/vendor/faiss/faiss/impl/index_write.cpp +31 -26
  50. data/vendor/faiss/faiss/impl/pq_code_distance/IVFPQScanner_impl.h +73 -39
  51. data/vendor/faiss/faiss/impl/pq_code_distance/pq_code_distance-generic.cpp +4 -4
  52. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer.cpp +6 -1
  53. data/vendor/faiss/faiss/impl/scalar_quantizer/distance_computers.h +2 -2
  54. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx2.cpp +6 -2
  55. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-dispatch.h +37 -17
  56. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-neon.cpp +109 -10
  57. data/vendor/faiss/faiss/impl/simd_dispatch.h +40 -22
  58. data/vendor/faiss/faiss/index_factory.cpp +8 -1
  59. data/vendor/faiss/faiss/invlists/InvertedLists.cpp +7 -4
  60. data/vendor/faiss/faiss/invlists/OnDiskInvertedLists.cpp +10 -4
  61. data/vendor/faiss/faiss/svs/IndexSVSVamana.cpp +16 -4
  62. data/vendor/faiss/faiss/svs/IndexSVSVamana.h +8 -1
  63. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.cpp +4 -2
  64. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.h +2 -1
  65. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.cpp +9 -2
  66. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.h +2 -1
  67. data/vendor/faiss/faiss/utils/distances.cpp +20 -13
  68. data/vendor/faiss/faiss/utils/distances_dispatch.h +30 -24
  69. data/vendor/faiss/faiss/utils/distances_fused/distances_fused.cpp +1 -1
  70. data/vendor/faiss/faiss/utils/hamming.cpp +15 -9
  71. data/vendor/faiss/faiss/utils/hamming.h +10 -1
  72. data/vendor/faiss/faiss/utils/hamming_distance/common.h +14 -3
  73. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx512_vpopcnt.cpp +24 -0
  74. data/vendor/faiss/faiss/utils/hamming_distance/hamming_computer-avx512.h +1 -1
  75. data/vendor/faiss/faiss/utils/hamming_distance/{hamming_computer-avx512_spr.h → hamming_computer-avx512_vpopcnt.h} +85 -24
  76. data/vendor/faiss/faiss/utils/hamming_distance/hamming_impl.h +141 -0
  77. data/vendor/faiss/faiss/utils/simd_impl/distances_arm_sve.cpp +194 -30
  78. data/vendor/faiss/faiss/utils/simd_impl/distances_rvv.cpp +93 -55
  79. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512.cpp +57 -40
  80. data/vendor/faiss/faiss/utils/simd_impl/{rabitq_avx512_spr.cpp → rabitq_avx512_vpopcnt.cpp} +21 -24
  81. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_dispatch.h +2 -7
  82. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_kernels.h +6 -1
  83. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_kernels_sve.cpp +34 -0
  84. data/vendor/faiss/faiss/utils/simd_levels.cpp +172 -67
  85. data/vendor/faiss/faiss/utils/simd_levels.h +26 -15
  86. metadata +6 -5
  87. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx512_spr.cpp +0 -15
@@ -6,6 +6,7 @@
6
6
  */
7
7
 
8
8
  #include <faiss/IndexIVF.h>
9
+ #include <faiss/SuperKMeans.h>
9
10
 
10
11
  #include <omp.h>
11
12
  #include <atomic>
@@ -78,13 +79,27 @@ void Level1Quantizer::train_q1(
78
79
  printf("Training level-1 quantizer on %zd vectors in %zdD\n", n, d);
79
80
  }
80
81
 
81
- Clustering clus(static_cast<int>(d), static_cast<int>(nlist), cp);
82
+ FAISS_THROW_IF_MSG(
83
+ cp.use_super_kmeans && clustering_index,
84
+ "cp.use_super_kmeans is incompatible with a user-provided "
85
+ "clustering_index: SuperKMeans assigns with its own index");
86
+
82
87
  quantizer->reset();
83
- if (clustering_index) {
84
- clus.train(n, x, *clustering_index);
88
+ if (cp.use_super_kmeans) {
89
+ SuperKMeansParameters super_cp;
90
+ static_cast<ClusteringParameters&>(super_cp) = cp;
91
+ SuperKMeans clus(
92
+ static_cast<int>(d), static_cast<int>(nlist), super_cp);
93
+ clus.train(n, x);
85
94
  quantizer->add(nlist, clus.centroids.data());
86
95
  } else {
87
- clus.train(n, x, *quantizer);
96
+ Clustering clus(static_cast<int>(d), static_cast<int>(nlist), cp);
97
+ if (clustering_index) {
98
+ clus.train(n, x, *clustering_index);
99
+ quantizer->add(nlist, clus.centroids.data());
100
+ } else {
101
+ clus.train(n, x, *quantizer);
102
+ }
88
103
  }
89
104
  quantizer->is_trained = true;
90
105
  } else if (quantizer_trains_alone == 2) {
@@ -526,9 +526,17 @@ InvertedListScanner* IndexIVFPQ::get_InvertedListScanner(
526
526
  bool store_pairs,
527
527
  const IDSelector* sel,
528
528
  const IVFSearchParameters*) const {
529
+ return get_InvertedListScanner(
530
+ store_pairs, sel, IndexIVFPQ::ScannerMode::Precomputed);
531
+ }
532
+
533
+ InvertedListScanner* IndexIVFPQ::get_InvertedListScanner(
534
+ bool store_pairs,
535
+ const IDSelector* sel,
536
+ IndexIVFPQ::ScannerMode mode) const {
529
537
  return with_simd_level([&]<SIMDLevel SL>() -> InvertedListScanner* {
530
538
  return pq_code_distance::make_IVFPQInvertedListScanner<SL>(
531
- *this, store_pairs, sel);
539
+ *this, store_pairs, sel, mode);
532
540
  });
533
541
  }
534
542
 
@@ -32,6 +32,11 @@ FAISS_API extern size_t precomputed_table_max_bytes;
32
32
  * vector is encoded as a product quantizer code.
33
33
  */
34
34
  struct IndexIVFPQ : IndexIVF {
35
+ enum class ScannerMode {
36
+ Precomputed,
37
+ OnTheFly,
38
+ };
39
+
35
40
  ProductQuantizer pq; ///< produces the codes
36
41
 
37
42
  bool do_polysemous_training; ///< reorder PQ centroids after training?
@@ -144,6 +149,12 @@ struct IndexIVFPQ : IndexIVF {
144
149
  const IDSelector* sel,
145
150
  const IVFSearchParameters* params) const override;
146
151
 
152
+ /** Returns a scanner with a fixed PQ distance-computation strategy. */
153
+ InvertedListScanner* get_InvertedListScanner(
154
+ bool store_pairs,
155
+ const IDSelector* sel,
156
+ ScannerMode mode) const;
157
+
147
158
  /// build precomputed table
148
159
  void precompute_table();
149
160
 
@@ -243,8 +243,8 @@ void IndexShardsTemplate<IndexT>::search(
243
243
 
244
244
  this->runOnIndex(fn);
245
245
 
246
- if (this->metric_type == METRIC_L2) {
247
- merge_knn_results<idx_t, CMin<distance_t, int>>(
246
+ if (is_similarity_metric(this->metric_type)) {
247
+ merge_knn_results<idx_t, CMax<distance_t, int>>(
248
248
  n,
249
249
  k,
250
250
  nshard,
@@ -253,7 +253,7 @@ void IndexShardsTemplate<IndexT>::search(
253
253
  distances,
254
254
  labels);
255
255
  } else {
256
- merge_knn_results<idx_t, CMax<distance_t, int>>(
256
+ merge_knn_results<idx_t, CMin<distance_t, int>>(
257
257
  n,
258
258
  k,
259
259
  nshard,
@@ -13,6 +13,7 @@
13
13
  #include <cstdio>
14
14
  #include <functional>
15
15
 
16
+ #include <faiss/MetricType.h>
16
17
  #include <faiss/impl/FaissAssert.h>
17
18
  #include <faiss/utils/Heap.h>
18
19
  #include <faiss/utils/WorkerThread.h>
@@ -227,8 +228,8 @@ void IndexShardsIVF::search(
227
228
 
228
229
  this->runOnIndex(fn);
229
230
 
230
- if (this->metric_type == METRIC_L2) {
231
- merge_knn_results<idx_t, CMin<distance_t, int>>(
231
+ if (is_similarity_metric(metric_type)) {
232
+ merge_knn_results<idx_t, CMax<distance_t, int>>(
232
233
  n,
233
234
  k,
234
235
  nshard,
@@ -237,7 +238,7 @@ void IndexShardsIVF::search(
237
238
  distances,
238
239
  labels);
239
240
  } else {
240
- merge_knn_results<idx_t, CMax<distance_t, int>>(
241
+ merge_knn_results<idx_t, CMin<distance_t, int>>(
241
242
  n,
242
243
  k,
243
244
  nshard,
@@ -59,7 +59,7 @@ namespace {
59
59
  struct TrainState {
60
60
  /// Orthogonal rotation. Train in rotated space (X_tilde = X * R);
61
61
  /// un-rotate centroids before return.
62
- faiss::RandomRotationMatrix R;
62
+ std::unique_ptr<faiss::VectorTransform> R;
63
63
 
64
64
  std::vector<float> X_tilde; // (n, d) row-major
65
65
  int n = 0;
@@ -77,7 +77,17 @@ struct TrainState {
77
77
  int low_pruning_streak = 0;
78
78
  bool low_pruning_warning_printed = false;
79
79
 
80
- explicit TrainState(int d) : R(d, d) {}
80
+ explicit TrainState(int d, bool spherical)
81
+ : R([d, spherical]() -> std::unique_ptr<faiss::VectorTransform> {
82
+ // Spherical (inner-product) clustering only: a power-of-two
83
+ // dimension can use the fast Hadamard rotation instead of
84
+ // the generic random rotation. L2 training keeps the
85
+ // original RandomRotationMatrix path unchanged.
86
+ if (spherical && d > 0 && (d & (d - 1)) == 0) {
87
+ return std::make_unique<faiss::HadamardRotation>(d);
88
+ }
89
+ return std::make_unique<faiss::RandomRotationMatrix>(d, d);
90
+ }()) {}
81
91
  };
82
92
 
83
93
  /// PDX block layout for the trailing pruning sweep: block b covers original
@@ -295,10 +305,17 @@ std::unique_ptr<uint8_t[]> setup_train_state(
295
305
  "SuperKMeans: training set size exceeds INT_MAX after sampling");
296
306
  state.n = static_cast<int>(nx);
297
307
 
298
- state.R.init(cp.seed);
308
+ if (auto* R = dynamic_cast<HadamardRotation*>(state.R.get())) {
309
+ R->init(cp.seed);
310
+ } else {
311
+ auto* dense_rotation =
312
+ dynamic_cast<RandomRotationMatrix*>(state.R.get());
313
+ FAISS_ASSERT(dense_rotation != nullptr);
314
+ dense_rotation->init(cp.seed);
315
+ }
299
316
 
300
317
  state.X_tilde.resize(static_cast<size_t>(state.n) * d);
301
- state.R.apply_noalloc(state.n, x_sampled, state.X_tilde.data());
318
+ state.R->apply_noalloc(state.n, x_sampled, state.X_tilde.data());
302
319
 
303
320
  // Forgy init: pick k random rows from the rotated pool as initial
304
321
  // centroids. These remain in rotated space; un-rotation happens
@@ -313,6 +330,9 @@ std::unique_ptr<uint8_t[]> setup_train_state(
313
330
  state.X_tilde.data() + static_cast<size_t>(perm[j]) * d,
314
331
  sizeof(float) * d);
315
332
  }
333
+ if (cp.spherical) {
334
+ fvec_renorm_L2(d, k, state.Y_tilde.data());
335
+ }
316
336
  }
317
337
 
318
338
  state.d_prime =
@@ -339,7 +359,7 @@ std::unique_ptr<uint8_t[]> setup_train_state(
339
359
  /// reverse_transform applies R^T = R^-1.
340
360
  void untransform_centroids(
341
361
  std::vector<float>& centroids,
342
- const RandomRotationMatrix& R,
362
+ const VectorTransform& R,
343
363
  int d,
344
364
  int k,
345
365
  const float* Y_tilde) {
@@ -411,7 +431,7 @@ void SuperKMeans::train(idx_t n, const float* x) {
411
431
  static_cast<idx_t>(k) * cp.min_points_per_centroid);
412
432
  }
413
433
 
414
- TrainState state(d);
434
+ TrainState state(d, cp.spherical);
415
435
  std::vector<int64_t> labels64;
416
436
  SuperKMeansAssignScratch assign_scratch;
417
437
  std::vector<float> hassign;
@@ -446,6 +466,9 @@ void SuperKMeans::train(idx_t n, const float* x) {
446
466
 
447
467
  const int nsplit =
448
468
  update_centroids_and_split(d, k, state, labels64, hassign);
469
+ if (cp.spherical) {
470
+ fvec_renorm_L2(d, k, state.Y_tilde.data());
471
+ }
449
472
  const float pruning_rate = (iter == 0)
450
473
  ? 0.0f
451
474
  : adapt_d_prime(d, cp, state, total_pairs, pruned_at_gemm);
@@ -492,7 +515,7 @@ void SuperKMeans::train(idx_t n, const float* x) {
492
515
  (getmillisecs() - t_train_start) / 1000.0);
493
516
  }
494
517
 
495
- untransform_centroids(centroids, state.R, d, k, state.Y_tilde.data());
518
+ untransform_centroids(centroids, *state.R, d, k, state.Y_tilde.data());
496
519
  }
497
520
 
498
521
  void super_kmeans_assign_iteration(
@@ -598,7 +621,7 @@ void super_kmeans_assign_iteration(
598
621
  }
599
622
 
600
623
  // One SIMD dispatch per (xi, yj) tile.
601
- with_simd_level([&]<SIMDLevel SL>() {
624
+ with_simd_level_with_sve([&]<SIMDLevel SL>() {
602
625
  [[maybe_unused]] const int omp_chunk_local = cp.omp_chunk;
603
626
  int64_t tile_total = 0;
604
627
  int64_t tile_pruned = 0;
@@ -13,8 +13,9 @@
13
13
  // "A Super Fast K-means for Indexing Vector Embeddings."
14
14
  // arXiv preprint arXiv:2603.20009.
15
15
  //
16
- // Use when: L2 metric, k >= 1024, d >= 128, dense float embeddings.
17
- // Do not use for: IP/cosine (use Clustering with cp.spherical=true), small k,
16
+ // Use when: L2 metric, or spherical inner-product clustering with
17
+ // cp.spherical=true, k >= 1024, d >= 128, dense float embeddings.
18
+ // Do not use for: small k,
18
19
  // binary data (use IndexBinaryIVF), or near-unit-sphere embeddings with
19
20
  // k < 4096 (chi-squared assumption breaks down).
20
21
  //
@@ -513,6 +513,44 @@ void HadamardRotation::apply_noalloc(idx_t n, const float* x, float* xt) const {
513
513
  }
514
514
  }
515
515
 
516
+ void HadamardRotation::reverse_transform(idx_t n, const float* xt, float* x)
517
+ const {
518
+ FAISS_THROW_IF_NOT_MSG(is_trained, "Transformation not trained yet");
519
+ FAISS_THROW_IF_NOT_MSG(
520
+ d_in == d_out,
521
+ "HadamardRotation inverse requires equal input/output dimensions");
522
+
523
+ const size_t p = d_out;
524
+ // Reverse of apply_noalloc: three unnormalized FWHT rounds scale norms
525
+ // by (sqrt(p))^3 = p*sqrt(p); the forward pass cancels this with
526
+ // total_scale = 1/(p*sqrt(p)), so the inverse applies the same factor.
527
+ const float inverse_scale = 1.0f / (p * std::sqrt(static_cast<float>(p)));
528
+
529
+ #pragma omp parallel for schedule(dynamic)
530
+ for (idx_t i = 0; i < n; i++) {
531
+ const float* xi = xt + i * p;
532
+ float* xo = x + i * p;
533
+
534
+ // The inverse reverses the three sign-flip/Hadamard factors.
535
+ std::memcpy(xo, xi, p * sizeof(float));
536
+ fwht_inplace(xo, p);
537
+
538
+ for (size_t j = 0; j < p; j++) {
539
+ xo[j] *= signs3[j];
540
+ }
541
+ fwht_inplace(xo, p);
542
+
543
+ for (size_t j = 0; j < p; j++) {
544
+ xo[j] *= signs2[j];
545
+ }
546
+ fwht_inplace(xo, p);
547
+
548
+ for (size_t j = 0; j < p; j++) {
549
+ xo[j] *= signs1[j] * inverse_scale;
550
+ }
551
+ }
552
+ }
553
+
516
554
  void HadamardRotation::check_identical(const VectorTransform& other) const {
517
555
  auto* hr = dynamic_cast<const HadamardRotation*>(&other);
518
556
  FAISS_THROW_IF_NOT_MSG(hr, "failed to cast to HadamardRotation");
@@ -144,6 +144,9 @@ struct HadamardRotation : VectorTransform {
144
144
 
145
145
  void apply_noalloc(idx_t n, const float* x, float* xt) const override;
146
146
 
147
+ /// Apply the inverse transform when d_in == d_out.
148
+ void reverse_transform(idx_t n, const float* xt, float* x) const override;
149
+
147
150
  void check_identical(const VectorTransform& other) const override;
148
151
 
149
152
  HadamardRotation() {}
@@ -38,6 +38,7 @@
38
38
  #include <faiss/IndexPQ.h>
39
39
  #include <faiss/IndexPQFastScan.h>
40
40
  #include <faiss/IndexPreTransform.h>
41
+ #include <faiss/IndexRaBitQ.h>
41
42
  #include <faiss/IndexRefine.h>
42
43
  #include <faiss/IndexRowwiseMinMax.h>
43
44
  #include <faiss/IndexScalarQuantizer.h>
@@ -139,6 +140,7 @@ IndexIDMap* clone_IndexIDMap(const IndexIDMap* im) {
139
140
  IndexHNSW* clone_IndexHNSW(const IndexHNSW* ihnsw) {
140
141
  TRYCLONE(IndexHNSW2Level, ihnsw)
141
142
  TRYCLONE(IndexHNSWFlatPanorama, ihnsw)
143
+ TRYCLONE(IndexHNSWRaBitQ, ihnsw)
142
144
  TRYCLONE(IndexHNSWFlat, ihnsw)
143
145
  TRYCLONE(IndexHNSWPQ, ihnsw)
144
146
  TRYCLONE(IndexHNSWSQ, ihnsw)
@@ -294,6 +296,7 @@ Index* Cloner::clone_Index(const Index* index) {
294
296
  TRYCLONE(IndexEDEN, index)
295
297
 
296
298
  TRYCLONE(IndexScalarQuantizer, index)
299
+ TRYCLONE(IndexRaBitQ, index)
297
300
  TRYCLONE(MultiIndexQuantizer, index)
298
301
 
299
302
  if (const IndexIVF* ivf = dynamic_cast<const IndexIVF*>(index)) {
@@ -39,12 +39,8 @@ struct GpuClonerOptions {
39
39
  /// Set verbose options on the index
40
40
  bool verbose = false;
41
41
 
42
- /// use the cuVS implementation
43
- #if defined USE_NVIDIA_CUVS
44
- bool use_cuvs = true;
45
- #else
42
+ /// Use the cuVS implementation. Opt-in: see GpuIndexConfig::use_cuvs.
46
43
  bool use_cuvs = false;
47
- #endif
48
44
 
49
45
  /// This flag controls the CPU fallback logic for coarse quantizer
50
46
  /// component of the index. When set to false (default), the cloner will
@@ -107,12 +107,9 @@ struct GpuDistanceParams {
107
107
  /// execution
108
108
  int device = -1;
109
109
 
110
- /// Should the index dispatch down to cuVS?
111
- #if defined USE_NVIDIA_CUVS
112
- bool use_cuvs = true;
113
- #else
110
+ /// Should the search dispatch down to cuVS? Opt-in: see
111
+ /// GpuIndexConfig::use_cuvs.
114
112
  bool use_cuvs = false;
115
- #endif
116
113
  };
117
114
 
118
115
  /// A function that determines whether cuVS should be used based on various
@@ -38,12 +38,11 @@ struct GpuIndexConfig {
38
38
  /// more memory than is available on the GPU.
39
39
  MemorySpace memorySpace = MemorySpace::Device;
40
40
 
41
- /// Should the index dispatch down to cuVS?
42
- #if defined USE_NVIDIA_CUVS
43
- bool use_cuvs = true;
44
- #else
41
+ /// Should the index dispatch down to cuVS? Opt-in: dispatching to cuVS
42
+ /// selects a different implementation with its own numerical behaviour, so
43
+ /// it must be an explicit choice by the caller rather than a consequence of
44
+ /// how the binary happened to be built.
45
45
  bool use_cuvs = false;
46
- #endif
47
46
  };
48
47
 
49
48
  /// A centralized function that determines whether cuVS should
@@ -47,7 +47,9 @@ enum class graph_build_algo {
47
47
  /// Use NN-Descent to build all-neighbors knn graph
48
48
  NN_DESCENT,
49
49
  /// Use iterative search to build knn graph
50
- ITERATIVE_SEARCH
50
+ ITERATIVE_SEARCH,
51
+ /// Exact knn graph via tiled brute force
52
+ BRUTE_FORCE
51
53
  };
52
54
 
53
55
  /// A type for specifying how PQ codebooks are created.
@@ -173,6 +175,34 @@ struct IVFPQSearchCagraConfig {
173
175
  uint32_t max_internal_batch_size = 4096;
174
176
  };
175
177
 
178
+ /// Knobs for the multi-GPU build path, selected by listing more than one
179
+ /// device in GpuIndexCagraConfig::devices. Degrees, build_algo and metric
180
+ /// still come from GpuIndexCagraConfig.
181
+ struct AllNeighborsCagraConfig {
182
+ /// Number of overlapping clusters the dataset is partitioned into.
183
+ /// 0 selects max(2 * number of devices, 4).
184
+ size_t n_clusters = 0;
185
+
186
+ /// Clusters each vector is assigned to. Must be >= 2: with 1 there are no
187
+ /// cross-cluster edges and recall collapses. 0 selects 2.
188
+ size_t overlap_factor = 2;
189
+
190
+ /// Bounds IVF-PQ search memory during the knn build. cuVS's default can
191
+ /// exhaust device memory around 100M vectors; 8192 avoids that and does
192
+ /// not change recall. 0 keeps cuVS's default.
193
+ uint32_t ivf_pq_search_batch_size = 0;
194
+
195
+ /// IVF-PQ refinement multiplier. The refine pass runs on the CPU, so cost
196
+ /// scales with this. Raising it above 1.0 measured both slower and less
197
+ /// accurate, so it is separate from the shared refine_rate.
198
+ float refinement_rate = 1.0f;
199
+
200
+ /// Size the IVF-PQ index for one cluster rather than the whole dataset.
201
+ /// cuVS reuses these params for every cluster without rescaling, so
202
+ /// sizing from the full dataset badly over-partitions each one.
203
+ bool ivf_pq_size_from_cluster = true;
204
+ };
205
+
176
206
  struct GpuIndexCagraConfig : public GpuIndexConfig {
177
207
  /// Degree of input graph for pruning.
178
208
  size_t intermediate_graph_degree = 128;
@@ -190,6 +220,27 @@ struct GpuIndexCagraConfig : public GpuIndexConfig {
190
220
 
191
221
  /// Whether to use MST optimization to guarantee graph connectivity.
192
222
  bool guarantee_connectivity = false;
223
+
224
+ /// Devices to build on. More than one selects the multi-GPU build in
225
+ /// train(); see AllNeighborsCagraConfig and train() for its restrictions.
226
+ /// Empty or one device uses GpuIndexConfig::device as usual.
227
+ std::vector<int> devices;
228
+
229
+ AllNeighborsCagraConfig all_neighbors_params;
230
+
231
+ /// Build the HNSW upper levels on the GPU during copyTo() instead of by
232
+ /// CPU insertion. Roughly 10x faster, but measured worse recall than
233
+ /// base_level_only, which is cheaper still. Off by default.
234
+ bool gpu_hnsw_upper_levels = false;
235
+
236
+ /// intermediate_graph_degree for the per-level subgraph builds.
237
+ /// 0 = twice the upper-level degree.
238
+ size_t gpu_hnsw_intermediate_degree = 0;
239
+
240
+ /// MST connectivity pass on the per-level subgraphs. On by default:
241
+ /// upper levels are walked greedily with no backtracking, so a
242
+ /// disconnected component is a trap the descent cannot escape.
243
+ bool gpu_hnsw_guarantee_connectivity = true;
193
244
  };
194
245
 
195
246
  enum class search_algo {
@@ -284,46 +335,6 @@ struct GpuIndexCagra : public GpuIndex {
284
335
  /// in the index instance
285
336
  void copyTo(faiss::IndexHNSWCagra* index) const;
286
337
 
287
- /// Train CAGRA using multiple GPUs by sharding the dataset.
288
- /// Uses cuVS native SNMG (single-node multi-GPU) CAGRA build.
289
- /// Each device builds one shard in parallel via OpenMP.
290
- /// Float32 only. After training, call copyTo() to produce a CPU
291
- /// IndexHNSWCagra with full HNSW upper levels.
292
- /// The training data pointer must remain valid until copyTo() completes.
293
- /// stitch_mode: 0=CPU HNSW (Approach C), 1=GPU brute-force (Approach B)
294
- void trainMultiGpu(
295
- idx_t n,
296
- const float* x,
297
- std::vector<GpuResourcesProvider*>& providers,
298
- std::vector<int>& devices,
299
- idx_t stitch_per_shard = 0,
300
- int stitch_k = 2,
301
- int stitch_mode = 0);
302
-
303
- /// Build a unified CAGRA graph using cuVS all_neighbors
304
- /// (multi-GPU kNN graph construction with overlapping clusters) followed
305
- /// by cagra::optimize (graph pruning). Produces a single unified graph
306
- /// without stitching. The training data pointer must remain valid until
307
- /// copyTo() completes.
308
- /// build_algo: 0=NN-descent (default), 1=brute-force, 2=IVF-PQ
309
- /// refinement_rate: IVF-PQ refinement multiplier (only used when
310
- /// build_algo==2). Higher values trade build time for recall; the cuVS
311
- /// default is 2.0.
312
- /// ivfpq_search_batch: cap the IVF-PQ search `max_internal_batch_size` used
313
- /// during the all_neighbors kNN build (build_algo==2). 0 = cuVS default
314
- /// (128*1024), which can OOM at 100M; a smaller value (e.g. 8192) bounds
315
- /// the GPU search workspace with no effect on results (recall-neutral).
316
- void trainAllNeighbors(
317
- idx_t n,
318
- const float* x,
319
- std::vector<int>& devices,
320
- int n_clusters = 0,
321
- int overlap_factor = 0,
322
- bool multi_gpu_optimize = false,
323
- int build_algo = 0,
324
- float refinement_rate = 2.0f,
325
- int ivfpq_search_batch = 0);
326
-
327
338
  void reset() override;
328
339
 
329
340
  std::vector<idx_t> get_knngraph() const;
@@ -357,6 +368,18 @@ struct GpuIndexCagra : public GpuIndex {
357
368
  idx_t* labels,
358
369
  const SearchParameters* search_params) const override;
359
370
 
371
+ /// Multi-GPU build path taken by train() when cagraConfig_.devices lists
372
+ /// more than one device: cuVS `all_neighbors` knn graph construction over
373
+ /// overlapping clusters, followed by graph pruning. Leaves the result in
374
+ /// merged_knngraph_ for copyTo(); index_ stays empty.
375
+ void trainAllNeighbors_(idx_t n, const float* x);
376
+
377
+ /// Populate HNSW levels >= 1 of `index` by building a CAGRA graph over
378
+ /// each level's node subset on the GPU. Requires the level table to be
379
+ /// prepared and the storage populated. Returns the max level.
380
+ void buildHnswUpperLevelsGpu_(faiss::IndexHNSWCagra* index, int max_lvl)
381
+ const;
382
+
360
383
  void copyToMultiGpu_(faiss::IndexHNSWCagra* index) const;
361
384
 
362
385
  /// Our configuration options
@@ -372,7 +395,7 @@ struct GpuIndexCagra : public GpuIndex {
372
395
  std::shared_ptr<CuvsCagra<int8_t>>>
373
396
  index_;
374
397
 
375
- /// Multi-GPU state: populated by trainMultiGpu(), used by copyTo()
398
+ /// Multi-GPU state: populated by trainAllNeighbors_(), used by copyTo()
376
399
  std::vector<idx_t> merged_knngraph_;
377
400
  idx_t merged_knngraph_degree_ = 0;
378
401
  const float* multi_gpu_dataset_ = nullptr;
@@ -376,7 +376,7 @@ void AdditiveQuantizer::compute_centroid_norms(float* norms) const {
376
376
  size_t ntotal = (size_t)1 << tot_bits;
377
377
  int64_t ntotal_signed = ntotal;
378
378
  // TODO: make tree of partial sums
379
- with_simd_level([&]<SIMDLevel SL>() {
379
+ with_simd_level_with_sve([&]<SIMDLevel SL>() {
380
380
  #pragma omp parallel
381
381
  {
382
382
  std::vector<float> tmp(d);
@@ -235,8 +235,11 @@ void ClusteringInitialization::init_kmeans_plus_plus(
235
235
  float* new_centroid = centroids + c * d;
236
236
  std::memcpy(new_centroid, x + next_idx * d, d * sizeof(float));
237
237
 
238
- // Update min distances incrementally
239
- for (size_t i = 0; i < n; i++) {
238
+ // Update min distances incrementally. The writes are independent,
239
+ // so the loop parallelizes without changing the output.
240
+ const int64_t ni = static_cast<int64_t>(n);
241
+ #pragma omp parallel for
242
+ for (int64_t i = 0; i < ni; i++) {
240
243
  double dist = fvec_L2sqr<SL>(x + i * d, new_centroid, d);
241
244
  min_distances[i] = std::min(min_distances[i], dist);
242
245
  }