faiss 0.6.2 → 0.6.3

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 (144) 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/AutoTune.cpp +3 -1
  5. data/vendor/faiss/faiss/Clustering.cpp +9 -1
  6. data/vendor/faiss/faiss/IVFlib.cpp +14 -3
  7. data/vendor/faiss/faiss/Index.h +2 -2
  8. data/vendor/faiss/faiss/IndexAdditiveQuantizer.cpp +9 -10
  9. data/vendor/faiss/faiss/IndexAdditiveQuantizerFastScan.cpp +2 -3
  10. data/vendor/faiss/faiss/IndexBinaryFromFloat.cpp +1 -2
  11. data/vendor/faiss/faiss/IndexBinaryHNSW.cpp +4 -5
  12. data/vendor/faiss/faiss/IndexBinaryHash.cpp +5 -9
  13. data/vendor/faiss/faiss/IndexBinaryIVF.cpp +2 -4
  14. data/vendor/faiss/faiss/IndexEDEN.cpp +273 -0
  15. data/vendor/faiss/faiss/IndexEDEN.h +57 -0
  16. data/vendor/faiss/faiss/IndexFastScan.cpp +15 -4
  17. data/vendor/faiss/faiss/IndexFlat.cpp +13 -50
  18. data/vendor/faiss/faiss/IndexHNSW.cpp +10 -11
  19. data/vendor/faiss/faiss/IndexIDMap.cpp +16 -3
  20. data/vendor/faiss/faiss/IndexIDMap.h +2 -0
  21. data/vendor/faiss/faiss/IndexIVF.cpp +17 -6
  22. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizer.cpp +1 -1
  23. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizerFastScan.cpp +3 -4
  24. data/vendor/faiss/faiss/IndexIVFEDEN.cpp +302 -0
  25. data/vendor/faiss/faiss/IndexIVFEDEN.h +70 -0
  26. data/vendor/faiss/faiss/IndexIVFFastScan.cpp +5 -6
  27. data/vendor/faiss/faiss/IndexIVFFlat.cpp +3 -4
  28. data/vendor/faiss/faiss/IndexIVFIndependentQuantizer.cpp +1 -1
  29. data/vendor/faiss/faiss/IndexIVFPQ.cpp +40 -22
  30. data/vendor/faiss/faiss/IndexIVFPQFastScan.cpp +0 -1
  31. data/vendor/faiss/faiss/IndexIVFRaBitQ.cpp +19 -49
  32. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.cpp +180 -76
  33. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.h +5 -4
  34. data/vendor/faiss/faiss/IndexIVFSpectralHash.cpp +8 -6
  35. data/vendor/faiss/faiss/IndexLSH.cpp +2 -3
  36. data/vendor/faiss/faiss/IndexLattice.cpp +5 -0
  37. data/vendor/faiss/faiss/IndexNNDescent.cpp +9 -2
  38. data/vendor/faiss/faiss/IndexNSG.cpp +7 -2
  39. data/vendor/faiss/faiss/IndexPQ.cpp +6 -8
  40. data/vendor/faiss/faiss/IndexPreTransform.cpp +15 -0
  41. data/vendor/faiss/faiss/IndexRaBitQ.cpp +2 -2
  42. data/vendor/faiss/faiss/IndexRaBitQFastScan.cpp +1 -2
  43. data/vendor/faiss/faiss/IndexRaBitQFastScan.h +5 -1
  44. data/vendor/faiss/faiss/IndexRefine.cpp +30 -1
  45. data/vendor/faiss/faiss/IndexReplicas.cpp +1 -2
  46. data/vendor/faiss/faiss/IndexShards.cpp +2 -2
  47. data/vendor/faiss/faiss/IndexShardsIVF.cpp +2 -2
  48. data/vendor/faiss/faiss/MetaIndexes.cpp +2 -4
  49. data/vendor/faiss/faiss/SuperKMeans.cpp +256 -240
  50. data/vendor/faiss/faiss/SuperKMeans.h +30 -0
  51. data/vendor/faiss/faiss/VectorTransform.cpp +33 -2
  52. data/vendor/faiss/faiss/clone_index.cpp +5 -0
  53. data/vendor/faiss/faiss/factory_tools.cpp +47 -4
  54. data/vendor/faiss/faiss/gpu/GpuCloner.cpp +11 -11
  55. data/vendor/faiss/faiss/gpu/GpuIndex.h +34 -11
  56. data/vendor/faiss/faiss/gpu/GpuIndexCagra.h +47 -0
  57. data/vendor/faiss/faiss/gpu/GpuIndexIVF.h +17 -0
  58. data/vendor/faiss/faiss/gpu/GpuIndexIVFScalarQuantizer.h +16 -0
  59. data/vendor/faiss/faiss/gpu/perf/PerfClustering.cpp +1 -1
  60. data/vendor/faiss/faiss/gpu/perf/PerfIVFPQAdd.cpp +2 -2
  61. data/vendor/faiss/faiss/gpu/test/TestGpuIndexIVFScalarQuantizer.cpp +180 -0
  62. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFFlat.h +1 -5
  63. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFPQ.h +88 -0
  64. data/vendor/faiss/faiss/gpu_metal/impl/MetalIVFPQ.h +134 -0
  65. data/vendor/faiss/faiss/impl/ClusteringInitialization.cpp +2 -2
  66. data/vendor/faiss/faiss/impl/DistanceComputer.h +34 -0
  67. data/vendor/faiss/faiss/impl/EDENQuantizer.h +119 -0
  68. data/vendor/faiss/faiss/impl/HNSW.cpp +109 -152
  69. data/vendor/faiss/faiss/impl/LocalSearchQuantizer.cpp +2 -2
  70. data/vendor/faiss/faiss/impl/NSG.cpp +3 -1
  71. data/vendor/faiss/faiss/impl/Panorama.h +9 -7
  72. data/vendor/faiss/faiss/impl/PolysemousTraining.cpp +152 -84
  73. data/vendor/faiss/faiss/impl/ProductQuantizer.cpp +34 -22
  74. data/vendor/faiss/faiss/impl/RaBitQUtils.cpp +44 -36
  75. data/vendor/faiss/faiss/impl/RaBitQUtils.h +35 -0
  76. data/vendor/faiss/faiss/impl/RaBitQuantizer.cpp +168 -67
  77. data/vendor/faiss/faiss/impl/RaBitQuantizer.h +19 -0
  78. data/vendor/faiss/faiss/impl/RaBitQuantizerMultiBit.cpp +2 -11
  79. data/vendor/faiss/faiss/impl/ResultHandler.h +25 -31
  80. data/vendor/faiss/faiss/impl/ScalarQuantizer.cpp +258 -57
  81. data/vendor/faiss/faiss/impl/ScalarQuantizer.h +20 -0
  82. data/vendor/faiss/faiss/impl/ThreadedIndex-inl.h +2 -2
  83. data/vendor/faiss/faiss/impl/VisitedTable.cpp +22 -2
  84. data/vendor/faiss/faiss/impl/VisitedTable.h +20 -0
  85. data/vendor/faiss/faiss/impl/binary_hamming/IndexBinaryIVF_impl.h +1 -1
  86. data/vendor/faiss/faiss/impl/binary_hamming/avx2.cpp +4 -4
  87. data/vendor/faiss/faiss/impl/fast_scan/dispatching.h +35 -2
  88. data/vendor/faiss/faiss/impl/hnsw/LockVector.cpp +1 -1
  89. data/vendor/faiss/faiss/impl/index_read.cpp +376 -36
  90. data/vendor/faiss/faiss/impl/index_write.cpp +55 -4
  91. data/vendor/faiss/faiss/impl/lattice_Zn.cpp +8 -9
  92. data/vendor/faiss/faiss/impl/platform_macros.h +3 -1
  93. data/vendor/faiss/faiss/impl/polysemous_training/avx512.cpp +284 -0
  94. data/vendor/faiss/faiss/impl/polysemous_training/dispatch.h +115 -0
  95. data/vendor/faiss/faiss/impl/pq_code_distance/IVFPQ_QueryTables.cpp +0 -1
  96. data/vendor/faiss/faiss/impl/pq_code_distance/PQDistanceComputer_impl.h +26 -15
  97. data/vendor/faiss/faiss/impl/pq_code_distance/avx2.cpp +4 -4
  98. data/vendor/faiss/faiss/impl/result_handler/ResultHandler.cpp +195 -0
  99. data/vendor/faiss/faiss/impl/result_handler/avx2.cpp +133 -0
  100. data/vendor/faiss/faiss/impl/result_handler/avx512.cpp +281 -0
  101. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx2.cpp +72 -0
  102. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx512.cpp +228 -0
  103. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer.cpp +882 -0
  104. data/vendor/faiss/faiss/impl/scalar_quantizer/quantizers.h +9 -8
  105. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx2.cpp +85 -23
  106. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-impl.h +30 -30
  107. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-spr.cpp +4 -5
  108. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512.cpp +101 -34
  109. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-dispatch.h +136 -0
  110. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-neon.cpp +16 -16
  111. data/vendor/faiss/faiss/impl/simd_dispatch.h +30 -9
  112. data/vendor/faiss/faiss/index_factory.cpp +32 -6
  113. data/vendor/faiss/faiss/invlists/DirectMap.cpp +1 -1
  114. data/vendor/faiss/faiss/invlists/InvertedLists.cpp +2 -2
  115. data/vendor/faiss/faiss/invlists/OnDiskInvertedLists.cpp +19 -4
  116. data/vendor/faiss/faiss/python/python_callbacks.cpp +3 -1
  117. data/vendor/faiss/faiss/svs/IndexSVSFaissUtils.h +60 -0
  118. data/vendor/faiss/faiss/svs/IndexSVSFlat.cpp +26 -1
  119. data/vendor/faiss/faiss/svs/IndexSVSFlat.h +13 -0
  120. data/vendor/faiss/faiss/svs/IndexSVSIVF.cpp +1 -1
  121. data/vendor/faiss/faiss/svs/IndexSVSIVFLeanVec.cpp +1 -1
  122. data/vendor/faiss/faiss/svs/IndexSVSVamana.cpp +31 -1
  123. data/vendor/faiss/faiss/svs/IndexSVSVamana.h +15 -2
  124. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.cpp +1 -2
  125. data/vendor/faiss/faiss/utils/approx_topk_hamming/approx_topk_hamming.h +1 -1
  126. data/vendor/faiss/faiss/utils/distances.cpp +14 -2
  127. data/vendor/faiss/faiss/utils/distances_simd.cpp +4 -3
  128. data/vendor/faiss/faiss/utils/extra_distances.cpp +4 -14
  129. data/vendor/faiss/faiss/utils/extra_distances.h +1 -2
  130. data/vendor/faiss/faiss/utils/hamming.cpp +1 -1
  131. data/vendor/faiss/faiss/utils/quantize_lut.cpp +29 -8
  132. data/vendor/faiss/faiss/utils/rabitq_simd.h +202 -0
  133. data/vendor/faiss/faiss/utils/simd_impl/distances_avx2.cpp +0 -1
  134. data/vendor/faiss/faiss/utils/simd_impl/distances_avx512.cpp +263 -15
  135. data/vendor/faiss/faiss/utils/simd_impl/distances_rvv.cpp +160 -18
  136. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx2.cpp +245 -0
  137. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512.cpp +273 -0
  138. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512_spr.cpp +92 -0
  139. data/vendor/faiss/faiss/utils/simd_impl/rabitq_neon.cpp +11 -0
  140. data/vendor/faiss/faiss/utils/simd_impl/rabitq_rvv.cpp +143 -6
  141. data/vendor/faiss/faiss/utils/simd_levels.cpp +44 -0
  142. data/vendor/faiss/faiss/utils/simd_levels.h +14 -0
  143. data/vendor/faiss/faiss/utils/utils.cpp +9 -27
  144. metadata +16 -1
@@ -65,23 +65,14 @@ struct TrainState {
65
65
  int n = 0;
66
66
  std::vector<float> Y_tilde; // (k, d) row-major
67
67
 
68
- std::vector<int> assignments; // size n
69
- std::vector<float> best_dists; // size n; tau per vector
70
-
71
- /// ||X_tilde[i, 0:d_prime]||^2; recomputed when d_prime changes.
72
- std::vector<float> x_norms_partial;
68
+ std::vector<int32_t> assignments; // size n
69
+ std::vector<float> best_dists; // size n; tau per vector
73
70
 
74
71
  int d_prime = 0;
75
72
 
76
73
  /// ADSampling threshold table; size d+1.
77
74
  std::vector<float> ad_coeff;
78
75
 
79
- /// PDX block layout for the trailing pruning sweep: block b covers
80
- /// original dims [true_block_end[b] - block_dim[b], true_block_end[b]).
81
- /// Recomputed when d_prime changes.
82
- std::vector<int> block_dim;
83
- std::vector<int> true_block_end;
84
-
85
76
  /// Counter for the verbose-mode "low pruning" warning.
86
77
  int low_pruning_streak = 0;
87
78
  bool low_pruning_warning_printed = false;
@@ -89,38 +80,34 @@ struct TrainState {
89
80
  explicit TrainState(int d) : R(d, d) {}
90
81
  };
91
82
 
92
- /// Rebuild state.block_dim and state.true_block_end from the current
93
- /// state.d_prime and pdx_block_size. Call after any change to d_prime.
94
- void rebuild_pdx_block_layout(int d, int pdx_block_size, TrainState& state) {
95
- const int dp = state.d_prime;
96
- const int d_trail = d - dp;
83
+ /// PDX block layout for the trailing pruning sweep: block b covers original
84
+ /// dims [true_block_end[b] - block_dim[b], true_block_end[b]).
85
+ struct LocalBlockLayout {
86
+ std::vector<int> block_dim;
87
+ std::vector<int> true_block_end;
88
+ };
89
+
90
+ LocalBlockLayout build_pdx_block_layout(
91
+ int d,
92
+ int d_prime,
93
+ int pdx_block_size) {
94
+ LocalBlockLayout layout;
95
+ const int d_trail = d - d_prime;
97
96
  const int n_full_blocks = d_trail / pdx_block_size;
98
97
  const int tail = d_trail % pdx_block_size;
99
98
  const int n_blocks = n_full_blocks + (tail > 0 ? 1 : 0);
100
- state.block_dim.assign(n_blocks, pdx_block_size);
101
- state.true_block_end.resize(n_blocks);
102
- if (n_blocks > 0) {
103
- assert(!state.block_dim.empty());
104
- assert(!state.true_block_end.empty());
105
- for (int b = 0; b < n_full_blocks; ++b) {
106
- state.true_block_end[b] = dp + (b + 1) * pdx_block_size;
107
- }
108
- if (tail > 0) {
109
- state.block_dim[n_full_blocks] = tail;
110
- state.true_block_end[n_full_blocks] = d;
111
- }
99
+ layout.block_dim.assign(n_blocks, pdx_block_size);
100
+ layout.true_block_end.resize(n_blocks);
101
+ for (int b = 0; b < n_full_blocks; ++b) {
102
+ layout.true_block_end[b] = d_prime + (b + 1) * pdx_block_size;
103
+ }
104
+ if (tail > 0) {
105
+ layout.block_dim[n_full_blocks] = tail;
106
+ layout.true_block_end[n_full_blocks] = d;
112
107
  }
108
+ return layout;
113
109
  }
114
110
 
115
- struct IterScratch {
116
- std::vector<float> partial_ip; // (bx_max, by_max) for the GEMM tile
117
- std::vector<float> Y_pdx; // PDX-laid-out trailing block
118
- std::vector<float> Y_trail; // row-major (k, d_trail) input to pdxify
119
- std::vector<float> y_norms_partial; // ||Y_tilde[j, 0:dp]||^2
120
- std::vector<int64_t> labels64; // size n; widened state.assignments
121
- int prev_d_trail = -1;
122
- };
123
-
124
111
  /// Iter 0: full GEMM via knn_L2sqr (vanilla Lloyd's). Fills
125
112
  /// state.assignments and state.best_dists. Returns objective.
126
113
  double run_iter0_full_gemm(int d, int k, TrainState& state) {
@@ -148,183 +135,56 @@ double run_iter0_full_gemm(int d, int k, TrainState& state) {
148
135
  return objective;
149
136
  }
150
137
 
151
- /// Iter 1+: partial GEMM over [0, d_prime) + ADSampling progressive
152
- /// pruning over the PDX-laid-out trailing block. Updates
153
- /// state.assignments and state.best_dists. Writes total_pairs and
154
- /// pruned_at_gemm. Returns objective.
138
+ /// Iter 1+: refresh tau, then run one shared super_kmeans_assign_iteration
139
+ /// pass (partial GEMM + ADSampling progressive pruning). Updates
140
+ /// state.assignments and state.best_dists, writes total_pairs and
141
+ /// pruned_at_gemm, and returns the objective.
155
142
  double run_iter_pruned(
156
143
  int d,
157
144
  int k,
158
145
  const SuperKMeansParameters& cp,
159
146
  TrainState& state,
160
- IterScratch& scratch,
147
+ SuperKMeansAssignScratch& assign_scratch,
161
148
  int64_t& total_pairs,
162
149
  int64_t& pruned_at_gemm) {
163
- const int dp = state.d_prime;
164
- assert(dp >= 1);
165
- assert(!state.ad_coeff.empty());
166
- assert(!scratch.partial_ip.empty());
167
- assert(!scratch.y_norms_partial.empty());
168
- const int d_trail = d - dp;
169
150
  const int n_train = state.n;
151
+ assert(state.d_prime >= 1);
152
+ assert(!state.ad_coeff.empty());
170
153
  assert(static_cast<int>(state.best_dists.size()) >= n_train);
171
- assert(static_cast<int>(state.x_norms_partial.size()) >= n_train);
172
154
  assert(static_cast<int>(state.assignments.size()) >= n_train);
173
155
 
174
- if (d_trail != scratch.prev_d_trail) {
175
- scratch.Y_pdx.resize(static_cast<size_t>(k) * d_trail);
176
- scratch.Y_trail.resize(static_cast<size_t>(k) * d_trail);
177
- scratch.prev_d_trail = d_trail;
178
- }
179
- for (int j = 0; j < k; ++j) {
180
- std::memcpy(
181
- scratch.Y_trail.data() + static_cast<size_t>(j) * d_trail,
182
- state.Y_tilde.data() + static_cast<size_t>(j) * d + dp,
183
- d_trail * sizeof(float));
184
- }
185
- detail::pdxify(
186
- scratch.Y_trail.data(),
187
- k,
188
- d_trail,
189
- cp.pdx_block_size,
190
- scratch.Y_pdx.data());
191
-
192
- detail::compute_partial_norms(
193
- state.Y_tilde.data(), k, d, dp, scratch.y_norms_partial.data());
194
-
195
- const int n_blocks = static_cast<int>(state.block_dim.size());
196
-
197
- for (int xi = 0; xi < n_train; xi += cp.x_batch) {
198
- const int bx = std::min(cp.x_batch, n_train - xi);
199
-
200
- // Refresh tau: recompute full-d L2 distance to the previously
201
- // assigned centroid. This is intentionally over all d dims (not
202
- // just d_prime) because tau must be an exact distance for the
203
- // chi-squared pruning bound to be valid. Cost is O(bx * d) per
204
- // x-batch, amortized across the y-batch tiles that follow.
156
+ // Refresh tau: exact full-d L2 distance to the currently assigned centroid.
157
+ // super_kmeans_assign_iteration requires an exact tau (the chi-squared
158
+ // bound assumes it), and centroids moved since the previous assignment.
205
159
  #pragma omp parallel for
206
- for (int i = 0; i < bx; ++i) {
207
- const int j_prev = state.assignments[xi + i];
208
- const float* xrow =
209
- state.X_tilde.data() + static_cast<size_t>(xi + i) * d;
210
- const float* yrow =
211
- state.Y_tilde.data() + static_cast<size_t>(j_prev) * d;
212
- float tau = 0.0f;
213
- for (int m = 0; m < d; ++m) {
214
- const float diff = xrow[m] - yrow[m];
215
- tau += diff * diff;
216
- }
217
- state.best_dists[xi + i] = tau;
218
- }
219
-
220
- for (int yj = 0; yj < k; yj += cp.y_batch) {
221
- const int by = std::min(cp.y_batch, k - yj);
222
-
223
- // GEMM phase: column-major sgemm computes
224
- // partial_ip[i*by + j] = <X[xi+i, 0:dp], Y[yj+j, 0:dp]>.
225
- {
226
- FINTEGER M = by;
227
- FINTEGER N_ = bx;
228
- FINTEGER K_ = dp;
229
- float alpha = 1.0f;
230
- float beta = 0.0f;
231
- FINTEGER lda_y = d;
232
- FINTEGER lda_x = d;
233
- FINTEGER ldc = by;
234
- sgemm_("Transpose",
235
- "Not transpose",
236
- &M,
237
- &N_,
238
- &K_,
239
- &alpha,
240
- state.Y_tilde.data() + static_cast<size_t>(yj) * d,
241
- &lda_y,
242
- state.X_tilde.data() + static_cast<size_t>(xi) * d,
243
- &lda_x,
244
- &beta,
245
- scratch.partial_ip.data(),
246
- &ldc);
247
- }
248
-
249
- // One SIMD dispatch per (xi, yj) tile — block_l2<SL> below is
250
- // a direct call (no per-call switch on SIMDConfig::level).
251
- with_simd_level([&]<SIMDLevel SL>() {
252
- [[maybe_unused]] const int omp_chunk_local = cp.omp_chunk;
253
- int64_t total_pairs_local = 0;
254
- int64_t pruned_at_gemm_local = 0;
255
- #pragma omp parallel for schedule(dynamic, omp_chunk_local) \
256
- reduction(+ : total_pairs_local) reduction(+ : pruned_at_gemm_local)
257
- for (int i = 0; i < bx; ++i) {
258
- // tau is the best full-d distance found so far for this
259
- // point; tightened as closer centroids are found.
260
- float tau = state.best_dists[xi + i];
261
- int best_j = state.assignments[xi + i];
262
- const float xnp_i = state.x_norms_partial[xi + i];
263
- const float* xrow = state.X_tilde.data() +
264
- static_cast<size_t>(xi + i) * d;
265
-
266
- for (int j = 0; j < by; ++j) {
267
- ++total_pairs_local;
268
-
269
- // L2-from-IP; clamp to handle catastrophic
270
- // cancellation when the true distance is ~0.
271
- float pd = xnp_i + scratch.y_norms_partial[yj + j] -
272
- 2.0f *
273
- scratch.partial_ip
274
- [static_cast<size_t>(i) * by +
275
- j];
276
- if (pd < 0.0f) {
277
- pd = 0.0f;
278
- }
279
-
280
- if (pd > state.ad_coeff[dp] * tau) {
281
- ++pruned_at_gemm_local;
282
- continue;
283
- }
284
-
285
- // double accumulator mitigates float drift over many
286
- // block additions.
287
- double dist = pd;
288
- bool keep = true;
289
-
290
- // Progressive pruning across PDX blocks. Per block:
291
- // stride = k * block_dim[b] floats, column-major
292
- // across centroids.
293
- size_t pdx_offset = 0;
294
- for (int b = 0; b < n_blocks; ++b) {
295
- const int n_in_block = state.block_dim.at(b);
296
- const int true_end = state.true_block_end.at(b);
297
- const float* xblk = xrow + (true_end - n_in_block);
298
- const float* yblk = scratch.Y_pdx.data() +
299
- pdx_offset +
300
- static_cast<size_t>(yj + j) * n_in_block;
301
- dist += faiss::detail::block_l2<SL>(
302
- xblk, yblk, n_in_block);
303
- pdx_offset += static_cast<size_t>(k) * n_in_block;
304
-
305
- if (dist >
306
- static_cast<double>(state.ad_coeff[true_end]) *
307
- tau) {
308
- keep = false;
309
- break;
310
- }
311
- }
312
-
313
- if (keep && dist < tau) {
314
- tau = static_cast<float>(dist);
315
- best_j = yj + j;
316
- }
317
- }
318
-
319
- state.best_dists[xi + i] = tau;
320
- state.assignments[xi + i] = best_j;
321
- }
322
- total_pairs += total_pairs_local;
323
- pruned_at_gemm += pruned_at_gemm_local;
324
- });
160
+ for (int i = 0; i < n_train; ++i) {
161
+ const int j_prev = state.assignments[i];
162
+ const float* xrow = state.X_tilde.data() + static_cast<size_t>(i) * d;
163
+ const float* yrow =
164
+ state.Y_tilde.data() + static_cast<size_t>(j_prev) * d;
165
+ float tau = 0.0f;
166
+ for (int m = 0; m < d; ++m) {
167
+ const float diff = xrow[m] - yrow[m];
168
+ tau += diff * diff;
325
169
  }
170
+ state.best_dists[i] = tau;
326
171
  }
327
172
 
173
+ super_kmeans_assign_iteration(
174
+ state.X_tilde.data(),
175
+ n_train,
176
+ d,
177
+ state.Y_tilde.data(),
178
+ k,
179
+ state.best_dists.data(),
180
+ state.assignments.data(),
181
+ state.d_prime,
182
+ state.ad_coeff.data(),
183
+ cp,
184
+ &total_pairs,
185
+ &pruned_at_gemm,
186
+ &assign_scratch);
187
+
328
188
  double objective = 0.0;
329
189
  for (int i = 0; i < n_train; ++i) {
330
190
  objective += state.best_dists[i];
@@ -337,13 +197,13 @@ int update_centroids_and_split(
337
197
  int d,
338
198
  int k,
339
199
  TrainState& state,
340
- IterScratch& scratch,
200
+ std::vector<int64_t>& labels64,
341
201
  std::vector<float>& hassign) {
342
202
  std::fill(hassign.begin(), hassign.end(), 0.0f);
343
- assert(!scratch.labels64.empty());
203
+ assert(!labels64.empty());
344
204
  assert(!state.assignments.empty());
345
205
  for (int i = 0; i < state.n; ++i) {
346
- scratch.labels64[i] = static_cast<int64_t>(state.assignments[i]);
206
+ labels64[i] = static_cast<int64_t>(state.assignments[i]);
347
207
  }
348
208
  detail::compute_centroids(
349
209
  d,
@@ -352,7 +212,7 @@ int update_centroids_and_split(
352
212
  /*k_frozen=*/0,
353
213
  reinterpret_cast<const uint8_t*>(state.X_tilde.data()),
354
214
  /*codec=*/nullptr,
355
- scratch.labels64.data(),
215
+ labels64.data(),
356
216
  /*weights=*/nullptr,
357
217
  hassign.data(),
358
218
  state.Y_tilde.data());
@@ -368,9 +228,8 @@ int update_centroids_and_split(
368
228
  state.Y_tilde.data());
369
229
  }
370
230
 
371
- /// Stay-in-band controller: nudge state.d_prime based on observed
372
- /// pruning rate. Recomputes x_norms_partial if d_prime changed. Returns
373
- /// the observed pruning rate (0 when there were no pairs).
231
+ /// Stay-in-band controller: nudge state.d_prime based on observed pruning
232
+ /// rate. Returns the observed pruning rate (0 when there were no pairs).
374
233
  float adapt_d_prime(
375
234
  int d,
376
235
  const SuperKMeansParameters& cp,
@@ -392,25 +251,16 @@ float adapt_d_prime(
392
251
  }
393
252
  new_dp = std::max(cp.d_prime_min, new_dp);
394
253
  new_dp = std::min(d / 2, new_dp);
395
- if (new_dp != state.d_prime) {
396
- state.d_prime = new_dp;
397
- detail::compute_partial_norms(
398
- state.X_tilde.data(),
399
- state.n,
400
- d,
401
- state.d_prime,
402
- state.x_norms_partial.data());
403
- rebuild_pdx_block_layout(d, cp.pdx_block_size, state);
404
- }
254
+ state.d_prime = new_dp;
405
255
  return pruning_rate;
406
256
  }
407
257
 
408
258
  /// Pre-loop setup: subsample, rotate, Forgy init, build ADSampling table,
409
- /// allocate scratch. Returned `sampled_x_owner` keeps the subsampled buffer
410
- /// alive when subsampling occurred (otherwise empty).
259
+ /// size the label scratch. Returned `sampled_x_owner` keeps the subsampled
260
+ /// buffer alive when subsampling occurred (otherwise empty).
411
261
  std::unique_ptr<uint8_t[]> setup_train_state(
412
262
  TrainState& state,
413
- IterScratch& scratch,
263
+ std::vector<int64_t>& labels64,
414
264
  std::vector<float>& hassign,
415
265
  const SuperKMeansParameters& cp,
416
266
  int d,
@@ -468,17 +318,6 @@ std::unique_ptr<uint8_t[]> setup_train_state(
468
318
  state.d_prime =
469
319
  std::max(cp.d_prime_min, static_cast<int>(d * cp.d_prime_fraction));
470
320
  state.d_prime = std::min(state.d_prime, d / 2);
471
- rebuild_pdx_block_layout(d, cp.pdx_block_size, state);
472
-
473
- // Iter 1+ uses L2-from-IP only over [0, d_prime), so full ||X[i]||^2 is
474
- // never read; iter 0 routes through knn_L2sqr which carries its own.
475
- state.x_norms_partial.resize(state.n);
476
- detail::compute_partial_norms(
477
- state.X_tilde.data(),
478
- state.n,
479
- d,
480
- state.d_prime,
481
- state.x_norms_partial.data());
482
321
 
483
322
  const double epsilon = static_cast<double>(cp.ad_epsilon_factor) / d;
484
323
  state.ad_coeff = detail::precompute_ad_thresholds(d, epsilon);
@@ -491,11 +330,7 @@ std::unique_ptr<uint8_t[]> setup_train_state(
491
330
 
492
331
  hassign.assign(k, 0.0f);
493
332
 
494
- const int by_max = std::min(cp.y_batch, k);
495
- const int bx_max = std::min(cp.x_batch, state.n);
496
- scratch.partial_ip.resize(static_cast<size_t>(bx_max) * by_max);
497
- scratch.y_norms_partial.resize(k);
498
- scratch.labels64.resize(state.n);
333
+ labels64.resize(state.n);
499
334
 
500
335
  return sampled_x_owner;
501
336
  }
@@ -557,7 +392,7 @@ SuperKMeans::SuperKMeans(int d, int k, const SuperKMeansParameters& cp_in)
557
392
 
558
393
  void SuperKMeans::train(idx_t n, const float* x) {
559
394
  FAISS_THROW_IF_NOT_MSG(n > 0, "SuperKMeans: n must be positive");
560
- FAISS_THROW_IF_NOT_MSG(x != nullptr, "SuperKMeans: x must not be null");
395
+ FAISS_THROW_IF_MSG(x == nullptr, "SuperKMeans: x must not be null");
561
396
  FAISS_THROW_IF_NOT_MSG(
562
397
  n >= static_cast<idx_t>(k), "SuperKMeans: n must be >= k");
563
398
  if (cp.check_input_data_for_NaNs) {
@@ -577,10 +412,11 @@ void SuperKMeans::train(idx_t n, const float* x) {
577
412
  }
578
413
 
579
414
  TrainState state(d);
580
- IterScratch scratch;
415
+ std::vector<int64_t> labels64;
416
+ SuperKMeansAssignScratch assign_scratch;
581
417
  std::vector<float> hassign;
582
418
  [[maybe_unused]] auto sampled_x_owner =
583
- setup_train_state(state, scratch, hassign, cp, d, k, n, x);
419
+ setup_train_state(state, labels64, hassign, cp, d, k, n, x);
584
420
 
585
421
  iteration_stats.clear();
586
422
  iteration_stats.reserve(cp.niter);
@@ -599,11 +435,17 @@ void SuperKMeans::train(idx_t n, const float* x) {
599
435
  objective = run_iter0_full_gemm(d, k, state);
600
436
  } else {
601
437
  objective = run_iter_pruned(
602
- d, k, cp, state, scratch, total_pairs, pruned_at_gemm);
438
+ d,
439
+ k,
440
+ cp,
441
+ state,
442
+ assign_scratch,
443
+ total_pairs,
444
+ pruned_at_gemm);
603
445
  }
604
446
 
605
447
  const int nsplit =
606
- update_centroids_and_split(d, k, state, scratch, hassign);
448
+ update_centroids_and_split(d, k, state, labels64, hassign);
607
449
  const float pruning_rate = (iter == 0)
608
450
  ? 0.0f
609
451
  : adapt_d_prime(d, cp, state, total_pairs, pruned_at_gemm);
@@ -653,4 +495,178 @@ void SuperKMeans::train(idx_t n, const float* x) {
653
495
  untransform_centroids(centroids, state.R, d, k, state.Y_tilde.data());
654
496
  }
655
497
 
498
+ void super_kmeans_assign_iteration(
499
+ const float* X_tilde,
500
+ int n,
501
+ int d,
502
+ const float* Y_tilde,
503
+ int k,
504
+ float* tau,
505
+ int32_t* assignments,
506
+ int d_prime,
507
+ const float* ad_coeff,
508
+ const SuperKMeansParameters& cp,
509
+ int64_t* total_pairs_out,
510
+ int64_t* pruned_at_gemm_out,
511
+ SuperKMeansAssignScratch* scratch_in) {
512
+ FAISS_THROW_IF_NOT_MSG(
513
+ d_prime >= 1,
514
+ "super_kmeans_assign_iteration: d_prime must be >= 1");
515
+ FAISS_THROW_IF_NOT_MSG(
516
+ d_prime < d, "super_kmeans_assign_iteration: d_prime must be < d");
517
+ FAISS_THROW_IF_MSG(
518
+ ad_coeff == nullptr,
519
+ "super_kmeans_assign_iteration: ad_coeff must not be null");
520
+ FAISS_THROW_IF_MSG(
521
+ tau == nullptr,
522
+ "super_kmeans_assign_iteration: tau must not be null");
523
+ FAISS_THROW_IF_MSG(
524
+ assignments == nullptr,
525
+ "super_kmeans_assign_iteration: assignments must not be null");
526
+
527
+ const int d_trail = d - d_prime;
528
+ const auto layout = build_pdx_block_layout(d, d_prime, cp.pdx_block_size);
529
+ const int n_blocks = static_cast<int>(layout.block_dim.size());
530
+
531
+ // Reuse the caller's scratch when provided (grow-only buffers, so a caller
532
+ // looping over iterations allocates nothing in steady state); otherwise use
533
+ // a local instance for one-shot callers.
534
+ SuperKMeansAssignScratch local_scratch;
535
+ SuperKMeansAssignScratch& s = scratch_in ? *scratch_in : local_scratch;
536
+
537
+ // PDX-lay out the centroid trailing block once per call.
538
+ auto& Y_trail = s.Y_trail;
539
+ auto& Y_pdx = s.Y_pdx;
540
+ Y_trail.resize(static_cast<size_t>(k) * d_trail);
541
+ Y_pdx.resize(static_cast<size_t>(k) * d_trail);
542
+ for (int j = 0; j < k; ++j) {
543
+ std::memcpy(
544
+ Y_trail.data() + static_cast<size_t>(j) * d_trail,
545
+ Y_tilde + static_cast<size_t>(j) * d + d_prime,
546
+ d_trail * sizeof(float));
547
+ }
548
+ detail::pdxify(Y_trail.data(), k, d_trail, cp.pdx_block_size, Y_pdx.data());
549
+
550
+ auto& x_norms_partial = s.x_norms_partial;
551
+ auto& y_norms_partial = s.y_norms_partial;
552
+ x_norms_partial.resize(n);
553
+ detail::compute_partial_norms(
554
+ X_tilde, n, d, d_prime, x_norms_partial.data());
555
+ y_norms_partial.resize(k);
556
+ detail::compute_partial_norms(
557
+ Y_tilde, k, d, d_prime, y_norms_partial.data());
558
+
559
+ int64_t total_pairs = 0;
560
+ int64_t pruned_at_gemm = 0;
561
+
562
+ // Size the GEMM tile buffer to the actual max tile, not cp.x_batch *
563
+ // cp.y_batch, which over-allocates when n < x_batch or k < y_batch.
564
+ const int bx_max = std::min(cp.x_batch, n);
565
+ const int by_max = std::min(cp.y_batch, k);
566
+ auto& partial_ip = s.partial_ip;
567
+ partial_ip.resize(static_cast<size_t>(bx_max) * by_max);
568
+
569
+ for (int xi = 0; xi < n; xi += cp.x_batch) {
570
+ const int bx = std::min(cp.x_batch, n - xi);
571
+ for (int yj = 0; yj < k; yj += cp.y_batch) {
572
+ const int by = std::min(cp.y_batch, k - yj);
573
+
574
+ // Partial GEMM over [0, d_prime): partial_ip[i*by + j] =
575
+ // <X[xi+i, 0:d_prime], Y[yj+j, 0:d_prime]>.
576
+ {
577
+ FINTEGER M = by;
578
+ FINTEGER N_ = bx;
579
+ FINTEGER K_ = d_prime;
580
+ float alpha = 1.0f;
581
+ float beta = 0.0f;
582
+ FINTEGER lda_y = d;
583
+ FINTEGER lda_x = d;
584
+ FINTEGER ldc = by;
585
+ sgemm_("Transpose",
586
+ "Not transpose",
587
+ &M,
588
+ &N_,
589
+ &K_,
590
+ &alpha,
591
+ Y_tilde + static_cast<size_t>(yj) * d,
592
+ &lda_y,
593
+ X_tilde + static_cast<size_t>(xi) * d,
594
+ &lda_x,
595
+ &beta,
596
+ partial_ip.data(),
597
+ &ldc);
598
+ }
599
+
600
+ // One SIMD dispatch per (xi, yj) tile.
601
+ with_simd_level([&]<SIMDLevel SL>() {
602
+ [[maybe_unused]] const int omp_chunk_local = cp.omp_chunk;
603
+ int64_t tile_total = 0;
604
+ int64_t tile_pruned = 0;
605
+ #pragma omp parallel for schedule(dynamic, omp_chunk_local) \
606
+ reduction(+ : tile_total) reduction(+ : tile_pruned)
607
+ for (int i = 0; i < bx; ++i) {
608
+ const float xnp_i = x_norms_partial[xi + i];
609
+ float tau_i = tau[xi + i];
610
+ int32_t best_j = assignments[xi + i];
611
+ const float* xrow =
612
+ X_tilde + static_cast<size_t>(xi + i) * d;
613
+ for (int j = 0; j < by; ++j) {
614
+ ++tile_total;
615
+ // L2-from-IP; clamp against catastrophic cancellation
616
+ // when the true distance is ~0.
617
+ float pd = xnp_i + y_norms_partial[yj + j] -
618
+ 2.0f *
619
+ partial_ip
620
+ [static_cast<size_t>(i) * by +
621
+ j];
622
+ if (pd < 0.0f) {
623
+ pd = 0.0f;
624
+ }
625
+ // ADSampling chi-squared bound at the d_prime boundary.
626
+ if (pd > ad_coeff[d_prime] * tau_i) {
627
+ ++tile_pruned;
628
+ continue;
629
+ }
630
+ // Progressive pruning across PDX blocks; double
631
+ // accumulator mitigates float drift over many block
632
+ // additions.
633
+ double dist = pd;
634
+ bool keep = true;
635
+ size_t pdx_offset = 0;
636
+ for (int b = 0; b < n_blocks; ++b) {
637
+ const int n_in_block = layout.block_dim[b];
638
+ const int true_end = layout.true_block_end[b];
639
+ const float* xblk = xrow + (true_end - n_in_block);
640
+ const float* yblk = Y_pdx.data() + pdx_offset +
641
+ static_cast<size_t>(yj + j) * n_in_block;
642
+ dist += faiss::detail::block_l2<SL>(
643
+ xblk, yblk, n_in_block);
644
+ pdx_offset += static_cast<size_t>(k) * n_in_block;
645
+ if (dist > static_cast<double>(ad_coeff[true_end]) *
646
+ tau_i) {
647
+ keep = false;
648
+ break;
649
+ }
650
+ }
651
+ if (keep && dist < tau_i) {
652
+ tau_i = static_cast<float>(dist);
653
+ best_j = static_cast<int32_t>(yj + j);
654
+ }
655
+ }
656
+ tau[xi + i] = tau_i;
657
+ assignments[xi + i] = best_j;
658
+ }
659
+ total_pairs += tile_total;
660
+ pruned_at_gemm += tile_pruned;
661
+ });
662
+ }
663
+ }
664
+ if (total_pairs_out) {
665
+ *total_pairs_out = total_pairs;
666
+ }
667
+ if (pruned_at_gemm_out) {
668
+ *pruned_at_gemm_out = pruned_at_gemm;
669
+ }
670
+ }
671
+
656
672
  } // namespace faiss
@@ -23,6 +23,7 @@
23
23
 
24
24
  #pragma once
25
25
 
26
+ #include <cstdint>
26
27
  #include <vector>
27
28
 
28
29
  #include <faiss/Clustering.h>
@@ -94,4 +95,33 @@ struct SuperKMeans {
94
95
  void train(idx_t n, const float* x);
95
96
  };
96
97
 
98
+ /// Reusable scratch for super_kmeans_assign_iteration; pass one instance across
99
+ /// a loop of calls to avoid reallocating (buffers grow only as needed).
100
+ struct SuperKMeansAssignScratch {
101
+ std::vector<float> Y_trail; // (k, d_trail) row-major, input to pdxify
102
+ std::vector<float> Y_pdx; // PDX-laid-out trailing block
103
+ std::vector<float> x_norms_partial; // ||X[i, 0:d_prime]||^2
104
+ std::vector<float> y_norms_partial; // ||Y[j, 0:d_prime]||^2
105
+ std::vector<float> partial_ip; // partial-GEMM tile buffer
106
+ };
107
+
108
+ /// One SuperKMeans iter-1+ assignment pass: partial GEMM over the front
109
+ /// `d_prime` dims + ADSampling progressive pruning over the PDX-laid-out
110
+ /// trailing block. Updates `tau` and `assignments` in place.
111
+ void super_kmeans_assign_iteration(
112
+ const float* X_tilde, // (n, d) points, row-major, rotated space
113
+ int n,
114
+ int d,
115
+ const float* Y_tilde, // (k, d) centroids, row-major, rotated space
116
+ int k,
117
+ float* tau, // in/out, len n; entry = exact full-d L2 to assignment
118
+ int32_t* assignments, // in/out, len n
119
+ int d_prime, // GEMM/pruning split, 1 <= d_prime < d
120
+ const float* ad_coeff, // ADSampling threshold table, len d+1
121
+ const SuperKMeansParameters& cp,
122
+ int64_t* total_pairs = nullptr, // optional out-counter
123
+ int64_t* pruned_at_gemm = nullptr, // optional out-counter
124
+ SuperKMeansAssignScratch* scratch =
125
+ nullptr); // optional, reuse across calls
126
+
97
127
  } // namespace faiss