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
@@ -7,8 +7,10 @@
7
7
 
8
8
  #pragma once
9
9
 
10
+ #include <functional>
10
11
  #include <optional>
11
12
  #include <queue>
13
+ #include <utility>
12
14
  #include <vector>
13
15
 
14
16
  #include <omp.h>
@@ -152,8 +154,14 @@ struct HNSW {
152
154
  /// use bounded queue during exploration
153
155
  bool search_bounded_queue = true;
154
156
 
155
- /// use Panorama progressive pruning in search
156
- bool is_panorama = false;
157
+ /// Specialized level-0 search implementations. This state is derived from
158
+ /// the owning IndexHNSW subtype after construction or deserialization.
159
+ enum Search_method_t : uint8_t {
160
+ SM_DEFAULT,
161
+ SM_PANORAMA,
162
+ SM_RABITQ,
163
+ };
164
+ Search_method_t search_method = SM_DEFAULT;
157
165
 
158
166
  /// distance comparison semantics: when true, distances are treated as
159
167
  /// similarity scores (larger is better). Default false matches the
@@ -204,14 +212,27 @@ struct HNSW {
204
212
  VisitedTable& vt,
205
213
  bool keep_max_size_level0 = false);
206
214
 
207
- /** add point pt_id on all levels <= pt_level and build the link
208
- * structure for them. */
209
- void add_with_locks(
215
+ /** Deterministic build, phase A: write pt_id's forward links against an
216
+ * immutable snapshot, touching only pt_id's own slots. Reciprocal edges
217
+ * are collected in `pt_reverse_edges` for phase B, not applied. Requires
218
+ * entry_point set. */
219
+ void compute_forward_links_deterministic(
210
220
  DistanceComputer& ptdis,
211
221
  int pt_level,
212
- int pt_id,
213
- LockVector& locks,
222
+ storage_idx_t pt_id,
214
223
  VisitedTable& vt,
224
+ std::vector<std::pair<storage_idx_t, int>>& pt_reverse_edges,
225
+ bool keep_max_size_level0 = false);
226
+
227
+ /** Deterministic build, phase B: merge `incoming` into `node` and
228
+ * re-prune in a total order (distance, ties by id) so the result is
229
+ * order-independent. Touches only `node`'s slots; `incoming` is sorted
230
+ * and deduplicated in place. */
231
+ void merge_reverse_links_deterministic(
232
+ DistanceComputer& dis,
233
+ storage_idx_t node,
234
+ int level,
235
+ std::vector<storage_idx_t>& incoming,
215
236
  bool keep_max_size_level0 = false);
216
237
 
217
238
  /// Search interface for 1 point, single thread
@@ -257,6 +278,24 @@ struct HNSW {
257
278
  void permute_entries(const idx_t* map);
258
279
  };
259
280
 
281
+ /** Deterministic, lock-free HNSW graph build. This is the only graph build
282
+ * that add() uses, and it is shared by IndexHNSW and
283
+ * IndexBinaryHNSW. The callbacks let both share the algorithm:
284
+ * `make_distance_computer()` returns a fresh DistanceComputer per thread
285
+ * (caller-owned) and `set_query(dc, pt_id)` points it at pt_id's vector. */
286
+ void hnsw_add_vertices_deterministic(
287
+ HNSW& hnsw,
288
+ size_t n0,
289
+ size_t n,
290
+ int d,
291
+ bool init_level0,
292
+ bool keep_max_size_level0,
293
+ bool preset_levels,
294
+ bool verbose,
295
+ const std::function<DistanceComputer*()>& make_distance_computer,
296
+ const std::function<void(DistanceComputer&, HNSW::storage_idx_t)>&
297
+ set_query);
298
+
260
299
  struct HNSWStats {
261
300
  size_t n1 = 0; /// number of vectors searched
262
301
  size_t n2 =
@@ -23,6 +23,50 @@ struct IDSelector {
23
23
  virtual ~IDSelector() {}
24
24
  };
25
25
 
26
+ /** Scan context handed to IDSelectorWithContext::is_member_with_context: the
27
+ * contiguous id block being scanned and the position of the id under test. */
28
+ struct IDScanContext {
29
+ /// the contiguous block of ids being scanned
30
+ const idx_t* ids;
31
+ /// number of entries in `ids`
32
+ size_t list_size;
33
+ /// index of the tested id within `ids` (i.e. ids[j] == id)
34
+ size_t j;
35
+ };
36
+
37
+ /** IDSelector that also receives the surrounding scan context on each
38
+ * membership test, letting an implementation exploit locality across a scan
39
+ * (for example, prefetching data for an entry it will be asked about soon).
40
+ * The policy is entirely up to the implementation. */
41
+ struct IDSelectorWithContext : IDSelector {
42
+ virtual bool is_member_with_context(idx_t id, const IDScanContext& ctx)
43
+ const = 0;
44
+ };
45
+
46
+ /** Routes each per-candidate membership test to is_member_with_context() when
47
+ * the selector implements IDSelectorWithContext, else to plain is_member().
48
+ * Construct one per inverted-list scan: the dynamic_cast is the only RTTI cost
49
+ * and the per-candidate cost is a single predicted branch (cf. the
50
+ * IDSelectorRange dynamic_cast in IndexIVF.cpp). The scan context is only
51
+ * meaningful when the scan exposes a real id array (i.e. !store_pairs); when
52
+ * store_pairs the context path is disabled and every test falls back to
53
+ * is_member. */
54
+ struct IDSelectorContextDispatch {
55
+ const IDSelector* sel;
56
+ const IDSelectorWithContext* ctx_sel;
57
+
58
+ IDSelectorContextDispatch(const IDSelector* sel, bool store_pairs)
59
+ : sel(sel),
60
+ ctx_sel((sel != nullptr && !store_pairs)
61
+ ? dynamic_cast<const IDSelectorWithContext*>(sel)
62
+ : nullptr) {}
63
+
64
+ bool is_member(idx_t id, const IDScanContext& ctx) const {
65
+ return ctx_sel ? ctx_sel->is_member_with_context(id, ctx)
66
+ : sel->is_member(id);
67
+ }
68
+ };
69
+
26
70
  /** ids between [imin, imax) */
27
71
  struct IDSelectorRange : IDSelector {
28
72
  idx_t imin, imax;
@@ -264,6 +264,8 @@ void NNDescent::update() {
264
264
  auto& nn_new = node.nn_new;
265
265
  auto& nn_old = node.nn_old;
266
266
 
267
+ // other.pool.back() below is read without other.lock: in this
268
+ // phase no thread reorders or resizes a pool. Lock guards rnn_*.
267
269
  for (int l = 0; l < node.M; ++l) {
268
270
  auto& nn = node.pool[l];
269
271
  auto& other = graph[nn.id]; // the other side of the edge
@@ -283,7 +285,6 @@ void NNDescent::update() {
283
285
  }
284
286
  }
285
287
  nn.flag = false;
286
-
287
288
  } else { // the node is old
288
289
  // push the neighbor into nn_old
289
290
  nn_old.push_back(nn.id);
@@ -300,11 +301,17 @@ void NNDescent::update() {
300
301
  }
301
302
  }
302
303
  }
303
- // make heap to join later (in join() function)
304
- std::make_heap(node.pool.begin(), node.pool.end());
305
304
  }
306
305
  }
307
306
 
307
+ // Restore the max-heap invariant for the next join()'s insert(). Must
308
+ // stay a separate region: re-heapifying during Step 3 would race with
309
+ // its unlocked other.pool.back() reads.
310
+ #pragma omp parallel for
311
+ for (int n = 0; n < ntotal; ++n) {
312
+ std::make_heap(graph[n].pool.begin(), graph[n].pool.end());
313
+ }
314
+
308
315
  // Step 4.
309
316
  // Combine the forward and the reverse links
310
317
  // R = 0 means no reverse links are used.
@@ -235,7 +235,8 @@ inline auto with_bool(bool value, Lambda&& fn) {
235
235
  * accelerating the refinement stage.
236
236
  */
237
237
  struct Panorama {
238
- static constexpr size_t kDefaultBatchSize = 128;
238
+ static constexpr size_t kDefaultBatchSize = 1024;
239
+ static constexpr size_t kLegacyBatchSize = 128;
239
240
 
240
241
  size_t d = 0;
241
242
  size_t code_size = 0;
@@ -322,12 +323,20 @@ struct Panorama {
322
323
  size_t batch_offset = batch_no * batch_size * code_size;
323
324
  const uint8_t* storage_base = codes_base + batch_offset;
324
325
 
326
+ // Honor IDSelectorWithContext: the scan-order position within the whole
327
+ // list is global_idx, so a lookahead crosses batch boundaries correctly
328
+ // (ids is the full list, length list_size).
329
+ IDSelectorContextDispatch sel_dispatch(
330
+ sel, /*store_pairs=*/ids == nullptr);
331
+
325
332
  // Initialize active set with ID-filtered vectors.
326
333
  size_t num_active = 0;
327
334
  for (size_t i = 0; i < curr_batch_size; i++) {
328
335
  size_t global_idx = batch_start + i;
329
336
  idx_t id = (ids == nullptr) ? global_idx : ids[global_idx];
330
- bool include = !use_sel || sel->is_member(id);
337
+ bool include = !use_sel ||
338
+ sel_dispatch.is_member(
339
+ id, IDScanContext{ids, list_size, global_idx});
331
340
 
332
341
  active_indices[num_active] = i;
333
342
  float cum_sum = batch_cum_sums[i];
@@ -280,7 +280,7 @@ void compute_1_code(const ProductQuantizer& pq, const float* x, uint8_t* code) {
280
280
  } // namespace
281
281
 
282
282
  void ProductQuantizer::compute_code(const float* x, uint8_t* code) const {
283
- with_simd_level([&]<SIMDLevel SL>() {
283
+ with_simd_level_with_sve([&]<SIMDLevel SL>() {
284
284
  switch (nbits) {
285
285
  case 8:
286
286
  compute_1_code<PQEncoder8, SL>(*this, x, code);
@@ -294,7 +294,7 @@ void ProductQuantizer::compute_code(const float* x, uint8_t* code) const {
294
294
  compute_1_code<PQEncoderGeneric, SL>(*this, x, code);
295
295
  break;
296
296
  }
297
- }); // with_simd_level
297
+ }); // with_simd_level_with_sve
298
298
  }
299
299
 
300
300
  template <class PQDecoder>
@@ -442,7 +442,7 @@ void ProductQuantizer::compute_codes(const float* x, uint8_t* codes, size_t n)
442
442
 
443
443
  void ProductQuantizer::compute_distance_table(const float* x, float* dis_table)
444
444
  const {
445
- with_simd_level([&]<SIMDLevel SL>() {
445
+ with_simd_level_with_sve([&]<SIMDLevel SL>() {
446
446
  if (transposed_centroids.empty()) {
447
447
  // use regular version
448
448
  for (size_t m = 0; m < M; m++) {
@@ -824,7 +824,7 @@ void ProductQuantizer::compute_sdc_table() {
824
824
  sdc_table.resize(M * ksub * ksub);
825
825
 
826
826
  if (dsub < 4) {
827
- with_simd_level([&]<SIMDLevel SL>() {
827
+ with_simd_level_with_sve([&]<SIMDLevel SL>() {
828
828
  #pragma omp parallel for
829
829
  for (int64_t mk = 0; mk < static_cast<int64_t>(M * ksub); mk++) {
830
830
  // allow omp to schedule in a more fine-grained way
@@ -329,7 +329,7 @@ float compute_full_multibit_distance(
329
329
  size_t d,
330
330
  size_t ex_bits,
331
331
  MetricType metric_type) {
332
- return with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_A0_SPR>(
332
+ return with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT>(
333
333
  [&]<SIMDLevel SL>() {
334
334
  return compute_full_multibit_distance<SL>(
335
335
  sign_bits,
@@ -17,6 +17,7 @@
17
17
  #include <faiss/utils/distances.h>
18
18
  #include <faiss/utils/rabitq_simd.h>
19
19
 
20
+ #include <algorithm>
20
21
  #include <cmath>
21
22
  #include <cstring>
22
23
  #include <memory>
@@ -24,6 +25,8 @@
24
25
 
25
26
  namespace faiss {
26
27
 
28
+ RaBitQStats rabitq_stats;
29
+
27
30
  // Import shared utilities from RaBitQUtils
28
31
  using rabitq_utils::ExtraBitsFactors;
29
32
  using rabitq_utils::QueryFactorsData;
@@ -224,6 +227,51 @@ void RaBitQuantizer::decode_core(
224
227
  }
225
228
  }
226
229
 
230
+ template <SIMDLevel SL>
231
+ float symmetric_dis_1bit(const RaBitQDistanceComputer& dc, idx_t i, idx_t j) {
232
+ FAISS_THROW_IF_NOT_MSG(
233
+ dc.metric_type == MetricType::METRIC_L2,
234
+ "RaBitQ symmetric distance supports only L2");
235
+ FAISS_ASSERT(i >= 0 && j >= 0);
236
+ FAISS_ASSERT(dc.codes != nullptr);
237
+
238
+ const size_t sign_bytes = (dc.d + 7) / 8;
239
+ const uint8_t* code_i = dc.codes + static_cast<size_t>(i) * dc.code_size;
240
+ const uint8_t* code_j = dc.codes + static_cast<size_t>(j) * dc.code_size;
241
+ const auto* factors_i =
242
+ reinterpret_cast<const SignBitFactors*>(code_i + sign_bytes);
243
+ const auto* factors_j =
244
+ reinterpret_cast<const SignBitFactors*>(code_j + sign_bytes);
245
+
246
+ const uint64_t xor_popcount =
247
+ rabitq::bitwise_xor_dot_product<SL>(code_i, code_j, sign_bytes, 1);
248
+ const float sign_dot =
249
+ static_cast<float>(dc.d) - 2.0f * static_cast<float>(xor_popcount);
250
+
251
+ // The L2-optimal reconstruction of residual r is alpha * sign(r), where
252
+ // alpha = ||r||_1 / d. The stored factors give
253
+ // alpha_i * alpha_j = ||r_i||^2 * ||r_j||^2 /
254
+ // (d * dp_multiplier_i * dp_multiplier_j).
255
+ float cross_term = 0.0f;
256
+ if (factors_i->dp_multiplier != 0.0f && factors_j->dp_multiplier != 0.0f) {
257
+ // Dividing each norm first avoids overflowing the product of two
258
+ // squared norms even when the final distance is representable.
259
+ const float scaled_norm_i =
260
+ factors_i->or_minus_c_l2sqr / factors_i->dp_multiplier;
261
+ const float scaled_norm_j =
262
+ factors_j->or_minus_c_l2sqr / factors_j->dp_multiplier;
263
+ cross_term = (scaled_norm_i * (sign_dot / static_cast<float>(dc.d))) *
264
+ scaled_norm_j;
265
+ }
266
+ const float distance = factors_i->or_minus_c_l2sqr +
267
+ factors_j->or_minus_c_l2sqr - 2.0f * cross_term;
268
+ return std::max(0.0f, distance);
269
+ }
270
+
271
+ float RaBitQDistanceComputer::symmetric_dis(idx_t i, idx_t j) {
272
+ return symmetric_dis_1bit<SIMDLevel::NONE>(*this, i, j);
273
+ }
274
+
227
275
  namespace {
228
276
 
229
277
  // Distance computers templatized on SIMDLevel to avoid per-call dynamic
@@ -240,6 +288,10 @@ struct RaBitQDistanceComputerNotQ final : RaBitQDistanceComputer {
240
288
 
241
289
  RaBitQDistanceComputerNotQ() = default;
242
290
 
291
+ float symmetric_dis(idx_t i, idx_t j) final {
292
+ return symmetric_dis_1bit<SL>(*this, i, j);
293
+ }
294
+
243
295
  // Compute distance using only 1-bit codes (fast)
244
296
  float distance_to_code_1bit_impl(
245
297
  const uint8_t* binary_data,
@@ -390,11 +442,17 @@ struct RaBitQDistanceComputerNotQ final : RaBitQDistanceComputer {
390
442
  const size_t ex_bits = nb_bits - 1;
391
443
  FAISS_ASSERT(ex_bits > 0);
392
444
 
445
+ // Honor IDSelectorWithContext on the multibit path too, so a RaBitQ
446
+ // index does not silently lose the context hook once nb_bits >= 2 (the
447
+ // 1-bit path already routes through run_scan_codes1).
448
+ const IDSelectorContextDispatch sel_dispatch(sel, store_pairs);
449
+
393
450
  size_t nup = 0;
394
451
  for (size_t j = 0; j < list_size; j++) {
395
452
  if (sel != nullptr) {
396
453
  idx_t id = store_pairs ? lo_build(list_no, j) : ids[j];
397
- if (!sel->is_member(id)) {
454
+ if (!sel_dispatch.is_member(
455
+ id, IDScanContext{ids, list_size, j})) {
398
456
  codes += code_size;
399
457
  continue;
400
458
  }
@@ -449,6 +507,10 @@ struct RaBitQDistanceComputerQ final : RaBitQDistanceComputer {
449
507
 
450
508
  RaBitQDistanceComputerQ() = default;
451
509
 
510
+ float symmetric_dis(idx_t i, idx_t j) final {
511
+ return symmetric_dis_1bit<SL>(*this, i, j);
512
+ }
513
+
452
514
  // Compute distance using only 1-bit codes (fast)
453
515
  float distance_to_code_1bit_impl(
454
516
  const uint8_t* binary_data,
@@ -601,11 +663,17 @@ struct RaBitQDistanceComputerQ final : RaBitQDistanceComputer {
601
663
  const size_t ex_bits = nb_bits - 1;
602
664
  FAISS_ASSERT(ex_bits > 0);
603
665
 
666
+ // Honor IDSelectorWithContext on the multibit path too, so a RaBitQ
667
+ // index does not silently lose the context hook once nb_bits >= 2 (the
668
+ // 1-bit path already routes through run_scan_codes1).
669
+ const IDSelectorContextDispatch sel_dispatch(sel, store_pairs);
670
+
604
671
  size_t nup = 0;
605
672
  for (size_t j = 0; j < list_size; j++) {
606
673
  if (sel != nullptr) {
607
674
  idx_t id = store_pairs ? lo_build(list_no, j) : ids[j];
608
- if (!sel->is_member(id)) {
675
+ if (!sel_dispatch.is_member(
676
+ id, IDScanContext{ids, list_size, j})) {
609
677
  codes += code_size;
610
678
  continue;
611
679
  }
@@ -653,12 +721,10 @@ FlatCodesDistanceComputer* RaBitQuantizer::get_distance_computer(
653
721
  // call the SIMD-specialized rabitq functions directly (no per-call
654
722
  // with_simd_level overhead).
655
723
  //
656
- // Use A0_SPR (which includes AVX512_SPR) so that on Sapphire Rapids
657
- // and later x86 microarchitectures the VPOPCNTDQ-based RaBitQ
658
- // specialization in rabitq_avx512_spr.cpp is selected. On AVX-512
659
- // CPUs without VPOPCNTDQ, dispatch falls through to the AVX512
660
- // specialization in rabitq_avx512.cpp.
661
- return with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_A0_SPR>(
724
+ // VPOPCNT rather than SPR: Ice Lake and Zen 4 have VPOPCNTDQ without the
725
+ // rest of the SPR feature set. Below it, dispatch falls through to
726
+ // rabitq_avx512.cpp.
727
+ return with_selected_simd_levels<AVAILABLE_SIMD_LEVELS_BASE_WITH_VPOPCNT>(
662
728
  [&]<SIMDLevel SL>() -> FlatCodesDistanceComputer* {
663
729
  if (qb == 0) {
664
730
  auto dc =
@@ -13,6 +13,8 @@
13
13
  #include <faiss/MetricType.h>
14
14
  #include <faiss/impl/DistanceComputer.h>
15
15
  #include <faiss/impl/Quantizer.h>
16
+ #include <faiss/impl/RaBitQUtils.h>
17
+ #include <faiss/impl/platform_macros.h>
16
18
 
17
19
  namespace faiss {
18
20
 
@@ -20,6 +22,31 @@ struct IDSelector;
20
22
  template <typename T, typename TI>
21
23
  struct ResultHandlerUnordered;
22
24
 
25
+ /** Statistics for staged RaBitQ distance evaluation.
26
+ *
27
+ * These counters belong to the quantizer rather than to a particular ANN
28
+ * graph. They are currently populated by HNSW's staged search path.
29
+ */
30
+ struct RaBitQStats {
31
+ uint64_t n_1bit = 0;
32
+ uint64_t n_refine = 0;
33
+
34
+ void reset() {
35
+ n_1bit = n_refine = 0;
36
+ }
37
+
38
+ void add(const RaBitQStats& other) {
39
+ n_1bit += other.n_1bit;
40
+ n_refine += other.n_refine;
41
+ }
42
+
43
+ double refine_ratio() const {
44
+ return n_1bit ? double(n_refine) / double(n_1bit) : 0.0;
45
+ }
46
+ };
47
+
48
+ FAISS_API extern RaBitQStats rabitq_stats;
49
+
23
50
  // the reference implementation of the https://arxiv.org/pdf/2405.12497
24
51
  // Jianyang Gao, Cheng Long, "RaBitQ: Quantizing High-Dimensional Vectors
25
52
  // with a Theoretical Error Bound for Approximate Nearest Neighbor Search".
@@ -122,10 +149,10 @@ struct RaBitQDistanceComputer : FlatCodesDistanceComputer {
122
149
  // Used with f_error to compute error bounds for two-stage filtering
123
150
  float g_error = 0.0f;
124
151
 
125
- float symmetric_dis(idx_t /*i*/, idx_t /*j*/) override {
126
- // Not used for RaBitQ
127
- FAISS_THROW_MSG("Not implemented");
128
- }
152
+ /// Per-distance-computer counters, aggregated by the owning index.
153
+ RaBitQStats stats;
154
+
155
+ float symmetric_dis(idx_t i, idx_t j) override;
129
156
 
130
157
  // Compute 1-bit distance estimate (fast)
131
158
  virtual float distance_to_code_1bit(const uint8_t* code) = 0;
@@ -133,6 +160,22 @@ struct RaBitQDistanceComputer : FlatCodesDistanceComputer {
133
160
  // Compute full multi-bit distance (accurate)
134
161
  virtual float distance_to_code_full(const uint8_t* code) = 0;
135
162
 
163
+ /// Apply the RaBitQ error bound without exposing the packed code layout to
164
+ /// ANN consumers. Precondition: nb_bits >= 2.
165
+ #ifndef SWIG
166
+ FAISS_ALWAYS_INLINE bool should_refine(
167
+ const uint8_t* code,
168
+ float estimate,
169
+ float threshold,
170
+ bool is_similarity) const {
171
+ const auto* factors =
172
+ reinterpret_cast<const rabitq_utils::SignBitFactorsWithError*>(
173
+ code + (d + 7) / 8);
174
+ return rabitq_utils::should_refine_candidate(
175
+ estimate, factors->f_error, g_error, threshold, is_similarity);
176
+ }
177
+ #endif
178
+
136
179
  virtual void set_centroid(const float* centroid_in) {
137
180
  centroid = centroid_in;
138
181
  }
@@ -302,9 +302,9 @@ void quantize_ex_bits(
302
302
  }
303
303
 
304
304
  // Step 6: Handle negative dimensions (flip bits)
305
- // For negative residuals, flip all bits: code' = ~code & max_code
305
+ // For negative or zero residuals, flip all bits: code' = ~code & max_code
306
306
  for (size_t i = 0; i < d; i++) {
307
- if (residual[i] < 0) {
307
+ if (residual[i] <= 0) {
308
308
  tmp_code[i] = (~tmp_code[i]) & max_code;
309
309
  }
310
310
  }
@@ -123,14 +123,20 @@ struct SingleQueryBlockResultHandler : BlockResultHandler<C, use_sel> {
123
123
  using ResultHandlerT<C>::threshold;
124
124
 
125
125
  explicit SingleResultHandler(SingleQueryBlockResultHandler& hr)
126
- : the_handler(hr.the_handler) {}
126
+ : the_handler(hr.the_handler) {
127
+ threshold = the_handler.threshold;
128
+ }
127
129
 
128
130
  /// begin results for query # i
129
- void begin(const size_t /* qid */) {}
131
+ void begin(const size_t /* qid */) {
132
+ threshold = the_handler.threshold;
133
+ }
130
134
 
131
135
  /// add one result for query i
132
136
  bool add_result(T dis, TI idx) final {
133
- return the_handler.add_result(dis, idx);
137
+ bool updated = the_handler.add_result(dis, idx);
138
+ threshold = the_handler.threshold;
139
+ return updated;
134
140
  }
135
141
 
136
142
  /// series of results for query i is done
@@ -7,6 +7,7 @@
7
7
 
8
8
  // -*- c++ -*-
9
9
 
10
+ #include <cmath>
10
11
  #include <cstring>
11
12
  #include <memory>
12
13
 
@@ -425,14 +426,29 @@ const LloydMaxTable kLloydMaxTables[] = {
425
426
  {kLloydMaxCentroids8, kLloydMaxBoundaries8}, // 8
426
427
  };
427
428
 
428
- void populate_lloyd_max_trained(size_t mse_bits, std::vector<float>& trained) {
429
+ // The tables are Lloyd-Max optimal for N(0, 1) input. Callers whose input has
430
+ // a different standard deviation pass it as `scale` to stretch the table.
431
+ void populate_lloyd_max_trained(
432
+ size_t mse_bits,
433
+ std::vector<float>& trained,
434
+ float scale = 1.0f) {
429
435
  FAISS_THROW_IF_NOT(mse_bits >= 1 && mse_bits <= 8);
430
436
  FAISS_THROW_IF_NOT(kLloydMaxTables[mse_bits].centroids);
431
437
  size_t k = size_t(1) << mse_bits;
432
438
  const auto& t = kLloydMaxTables[mse_bits];
433
439
  trained.resize(k + (k - 1));
434
- std::copy(t.centroids, t.centroids + k, trained.begin());
435
- std::copy(t.boundaries, t.boundaries + k - 1, trained.begin() + k);
440
+ for (size_t i = 0; i < k; i++) {
441
+ trained[i] = t.centroids[i] * scale;
442
+ }
443
+ for (size_t i = 0; i + 1 < k; i++) {
444
+ trained[k + i] = t.boundaries[i] * scale;
445
+ }
446
+ }
447
+
448
+ // Component scale of a unit-norm vector in R^d.
449
+ float unit_norm_component_scale(size_t d) {
450
+ FAISS_THROW_IF_NOT(d > 0);
451
+ return 1.0f / std::sqrt(static_cast<float>(d));
436
452
  }
437
453
 
438
454
  } // namespace
@@ -588,19 +604,24 @@ void ScalarQuantizer::train(size_t n, const float* x) {
588
604
  populate_lloyd_max_trained(bits, trained);
589
605
  break;
590
606
  case QT_1bit_tqmse:
591
- populate_lloyd_max_trained(1, trained);
607
+ populate_lloyd_max_trained(
608
+ 1, trained, unit_norm_component_scale(d));
592
609
  break;
593
610
  case QT_2bit_tqmse:
594
- populate_lloyd_max_trained(2, trained);
611
+ populate_lloyd_max_trained(
612
+ 2, trained, unit_norm_component_scale(d));
595
613
  break;
596
614
  case QT_3bit_tqmse:
597
- populate_lloyd_max_trained(3, trained);
615
+ populate_lloyd_max_trained(
616
+ 3, trained, unit_norm_component_scale(d));
598
617
  break;
599
618
  case QT_4bit_tqmse:
600
- populate_lloyd_max_trained(4, trained);
619
+ populate_lloyd_max_trained(
620
+ 4, trained, unit_norm_component_scale(d));
601
621
  break;
602
622
  case QT_8bit_tqmse:
603
- populate_lloyd_max_trained(8, trained);
623
+ populate_lloyd_max_trained(
624
+ 8, trained, unit_norm_component_scale(d));
604
625
  break;
605
626
  case QT_2bit_tq:
606
627
  case QT_3bit_tq:
@@ -614,7 +635,6 @@ void ScalarQuantizer::train(size_t n, const float* x) {
614
635
  trained.push_back(seed_f[0]);
615
636
  trained.push_back(seed_f[1]);
616
637
  trained.push_back(static_cast<float>(turboq_refine.qjl_type));
617
- turboq_refine.init_projection(d);
618
638
  break;
619
639
  }
620
640
  default:
@@ -622,29 +642,11 @@ void ScalarQuantizer::train(size_t n, const float* x) {
622
642
  }
623
643
  }
624
644
 
625
- void ScalarQuantizer::TurboQuantRefine::init_projection(size_t d) {
626
- if (use_fwht()) {
627
- padded_d = 1;
628
- while (padded_d < d) {
629
- padded_d <<= 1;
630
- }
631
- fwht_signs.resize(padded_d);
632
- RandomGenerator rng(seed);
633
- for (size_t i = 0; i < padded_d; i++) {
634
- fwht_signs[i] = (rng.rand_int(2) == 0) ? 1.0f : -1.0f;
635
- }
636
- } else {
637
- rr_matrix.resize(d * d);
638
- float_randn(rr_matrix.data(), d * d, seed);
639
- matrix_qr(static_cast<int>(d), static_cast<int>(d), rr_matrix.data());
640
- }
641
- }
642
-
643
645
  ScalarQuantizer::SQuantizer* ScalarQuantizer::select_quantizer() const {
644
646
  // A SIMD level's factory returns nullptr when the dimension is
645
647
  // incompatible (e.g. AVX-512 needs d % 16 == 0); the dispatcher then falls
646
648
  // back to the next-lower level (AVX-512 -> AVX2 -> scalar).
647
- return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_A0_SPR>(
649
+ return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_BASE_WITH_SPR>(
648
650
  [&]<SIMDLevel SL>() -> SQuantizer* {
649
651
  return scalar_quantizer::sq_select_quantizer<SL>(
650
652
  qtype, d, trained);
@@ -681,7 +683,7 @@ void ScalarQuantizer::decode(const uint8_t* codes, float* x, size_t n) const {
681
683
  ScalarQuantizer::SQDistanceComputer* ScalarQuantizer::get_distance_computer(
682
684
  MetricType metric) const {
683
685
  FAISS_THROW_IF_NOT(metric == METRIC_L2 || metric == METRIC_INNER_PRODUCT);
684
- return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_A0_SPR>(
686
+ return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_BASE_WITH_SPR>(
685
687
  [&]<SIMDLevel SL>() -> SQDistanceComputer* {
686
688
  return scalar_quantizer::sq_select_distance_computer<SL>(
687
689
  metric, qtype, d, trained);
@@ -694,7 +696,7 @@ InvertedListScanner* ScalarQuantizer::select_InvertedListScanner(
694
696
  bool store_pairs,
695
697
  const IDSelector* sel,
696
698
  bool by_residual) const {
697
- return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_A0_SPR>(
699
+ return with_simd_level_fallback<AVAILABLE_SIMD_LEVELS_BASE_WITH_SPR>(
698
700
  [&]<SIMDLevel SL>() -> InvertedListScanner* {
699
701
  return scalar_quantizer::sq_select_InvertedListScanner<SL>(
700
702
  qtype,
@@ -177,18 +177,9 @@ struct ScalarQuantizer : Quantizer {
177
177
  return s;
178
178
  }
179
179
 
180
+ /// The selected projection is built from `trained` by the quantizer.
180
181
  uint8_t qjl_type = 0;
181
182
  uint64_t seed = 42;
182
- size_t padded_d = 0;
183
- std::vector<float> fwht_signs;
184
- std::vector<float> rr_matrix;
185
- size_t nb_bits_lo = 0;
186
- size_t n_hi_dims = 0;
187
-
188
- void init_projection(size_t d);
189
- bool use_fwht() const {
190
- return qjl_type == 0;
191
- }
192
183
 
193
184
  struct DistanceComputer : SQDistanceComputer {
194
185
  virtual void configure(uint8_t qb, bool int_qjl) = 0;
@@ -199,6 +190,11 @@ struct ScalarQuantizer : Quantizer {
199
190
  };
200
191
  };
201
192
 
193
+ static_assert(
194
+ sizeof(TurboQuantRefine) <= 16,
195
+ "keep this a small config struct -- do not add projection buffers "
196
+ "here (T287092602)");
197
+
202
198
  TurboQuantRefine turboq_refine;
203
199
 
204
200
  SQDistanceComputer* get_distance_computer(