faiss 0.6.2 → 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 (178) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -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/Clustering.h +8 -0
  7. data/vendor/faiss/faiss/IVFlib.cpp +14 -3
  8. data/vendor/faiss/faiss/Index.h +2 -2
  9. data/vendor/faiss/faiss/IndexAdditiveQuantizer.cpp +9 -10
  10. data/vendor/faiss/faiss/IndexAdditiveQuantizerFastScan.cpp +2 -3
  11. data/vendor/faiss/faiss/IndexBinaryFromFloat.cpp +24 -4
  12. data/vendor/faiss/faiss/IndexBinaryHNSW.cpp +16 -145
  13. data/vendor/faiss/faiss/IndexBinaryHNSW.h +0 -6
  14. data/vendor/faiss/faiss/IndexBinaryHash.cpp +5 -9
  15. data/vendor/faiss/faiss/IndexBinaryIVF.cpp +8 -18
  16. data/vendor/faiss/faiss/IndexBinaryIVF.h +8 -1
  17. data/vendor/faiss/faiss/IndexEDEN.cpp +273 -0
  18. data/vendor/faiss/faiss/IndexEDEN.h +57 -0
  19. data/vendor/faiss/faiss/IndexFastScan.cpp +15 -4
  20. data/vendor/faiss/faiss/IndexFlat.cpp +21 -54
  21. data/vendor/faiss/faiss/IndexFlat.h +2 -2
  22. data/vendor/faiss/faiss/IndexHNSW.cpp +311 -102
  23. data/vendor/faiss/faiss/IndexHNSW.h +31 -7
  24. data/vendor/faiss/faiss/IndexIDMap.cpp +26 -8
  25. data/vendor/faiss/faiss/IndexIDMap.h +2 -0
  26. data/vendor/faiss/faiss/IndexIVF.cpp +36 -10
  27. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizer.cpp +1 -1
  28. data/vendor/faiss/faiss/IndexIVFAdditiveQuantizerFastScan.cpp +3 -4
  29. data/vendor/faiss/faiss/IndexIVFEDEN.cpp +302 -0
  30. data/vendor/faiss/faiss/IndexIVFEDEN.h +70 -0
  31. data/vendor/faiss/faiss/IndexIVFFastScan.cpp +5 -6
  32. data/vendor/faiss/faiss/IndexIVFFlat.cpp +3 -4
  33. data/vendor/faiss/faiss/IndexIVFIndependentQuantizer.cpp +1 -1
  34. data/vendor/faiss/faiss/IndexIVFPQ.cpp +49 -23
  35. data/vendor/faiss/faiss/IndexIVFPQ.h +11 -0
  36. data/vendor/faiss/faiss/IndexIVFPQFastScan.cpp +0 -1
  37. data/vendor/faiss/faiss/IndexIVFRaBitQ.cpp +19 -49
  38. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.cpp +180 -76
  39. data/vendor/faiss/faiss/IndexIVFRaBitQFastScan.h +5 -4
  40. data/vendor/faiss/faiss/IndexIVFSpectralHash.cpp +8 -6
  41. data/vendor/faiss/faiss/IndexLSH.cpp +2 -3
  42. data/vendor/faiss/faiss/IndexLattice.cpp +5 -0
  43. data/vendor/faiss/faiss/IndexNNDescent.cpp +9 -2
  44. data/vendor/faiss/faiss/IndexNSG.cpp +7 -2
  45. data/vendor/faiss/faiss/IndexPQ.cpp +6 -8
  46. data/vendor/faiss/faiss/IndexPreTransform.cpp +15 -0
  47. data/vendor/faiss/faiss/IndexRaBitQ.cpp +2 -2
  48. data/vendor/faiss/faiss/IndexRaBitQFastScan.cpp +1 -2
  49. data/vendor/faiss/faiss/IndexRaBitQFastScan.h +5 -1
  50. data/vendor/faiss/faiss/IndexRefine.cpp +30 -1
  51. data/vendor/faiss/faiss/IndexReplicas.cpp +1 -2
  52. data/vendor/faiss/faiss/IndexShards.cpp +5 -5
  53. data/vendor/faiss/faiss/IndexShardsIVF.cpp +6 -5
  54. data/vendor/faiss/faiss/MetaIndexes.cpp +2 -4
  55. data/vendor/faiss/faiss/SuperKMeans.cpp +286 -247
  56. data/vendor/faiss/faiss/SuperKMeans.h +33 -2
  57. data/vendor/faiss/faiss/VectorTransform.cpp +71 -2
  58. data/vendor/faiss/faiss/VectorTransform.h +3 -0
  59. data/vendor/faiss/faiss/clone_index.cpp +8 -0
  60. data/vendor/faiss/faiss/factory_tools.cpp +47 -4
  61. data/vendor/faiss/faiss/gpu/GpuCloner.cpp +11 -11
  62. data/vendor/faiss/faiss/gpu/GpuClonerOptions.h +1 -5
  63. data/vendor/faiss/faiss/gpu/GpuDistance.h +2 -5
  64. data/vendor/faiss/faiss/gpu/GpuIndex.h +38 -16
  65. data/vendor/faiss/faiss/gpu/GpuIndexCagra.h +71 -1
  66. data/vendor/faiss/faiss/gpu/GpuIndexIVF.h +17 -0
  67. data/vendor/faiss/faiss/gpu/GpuIndexIVFScalarQuantizer.h +16 -0
  68. data/vendor/faiss/faiss/gpu/perf/PerfClustering.cpp +1 -1
  69. data/vendor/faiss/faiss/gpu/perf/PerfIVFPQAdd.cpp +2 -2
  70. data/vendor/faiss/faiss/gpu/test/TestGpuIndexIVFScalarQuantizer.cpp +180 -0
  71. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFFlat.h +1 -5
  72. data/vendor/faiss/faiss/gpu_metal/MetalIndexIVFPQ.h +88 -0
  73. data/vendor/faiss/faiss/gpu_metal/impl/MetalIVFPQ.h +134 -0
  74. data/vendor/faiss/faiss/impl/AdditiveQuantizer.cpp +1 -1
  75. data/vendor/faiss/faiss/impl/ClusteringInitialization.cpp +7 -4
  76. data/vendor/faiss/faiss/impl/DistanceComputer.h +34 -0
  77. data/vendor/faiss/faiss/impl/EDENQuantizer.h +119 -0
  78. data/vendor/faiss/faiss/impl/HNSW.cpp +528 -267
  79. data/vendor/faiss/faiss/impl/HNSW.h +46 -7
  80. data/vendor/faiss/faiss/impl/IDSelector.h +44 -0
  81. data/vendor/faiss/faiss/impl/LocalSearchQuantizer.cpp +2 -2
  82. data/vendor/faiss/faiss/impl/NNDescent.cpp +10 -3
  83. data/vendor/faiss/faiss/impl/NSG.cpp +3 -1
  84. data/vendor/faiss/faiss/impl/Panorama.h +20 -9
  85. data/vendor/faiss/faiss/impl/PolysemousTraining.cpp +152 -84
  86. data/vendor/faiss/faiss/impl/ProductQuantizer.cpp +38 -26
  87. data/vendor/faiss/faiss/impl/RaBitQUtils.cpp +45 -37
  88. data/vendor/faiss/faiss/impl/RaBitQUtils.h +35 -0
  89. data/vendor/faiss/faiss/impl/RaBitQuantizer.cpp +239 -72
  90. data/vendor/faiss/faiss/impl/RaBitQuantizer.h +66 -4
  91. data/vendor/faiss/faiss/impl/RaBitQuantizerMultiBit.cpp +4 -13
  92. data/vendor/faiss/faiss/impl/ResultHandler.h +34 -34
  93. data/vendor/faiss/faiss/impl/ScalarQuantizer.cpp +287 -84
  94. data/vendor/faiss/faiss/impl/ScalarQuantizer.h +26 -10
  95. data/vendor/faiss/faiss/impl/ThreadedIndex-inl.h +2 -2
  96. data/vendor/faiss/faiss/impl/VisitedTable.cpp +22 -2
  97. data/vendor/faiss/faiss/impl/VisitedTable.h +20 -0
  98. data/vendor/faiss/faiss/impl/binary_hamming/IndexBinaryIVF_impl.h +90 -14
  99. data/vendor/faiss/faiss/impl/binary_hamming/avx2.cpp +4 -4
  100. data/vendor/faiss/faiss/impl/expanded_scanners.h +5 -1
  101. data/vendor/faiss/faiss/impl/fast_scan/decompose_qbs.h +1 -0
  102. data/vendor/faiss/faiss/impl/fast_scan/dispatching.h +35 -2
  103. data/vendor/faiss/faiss/impl/hnsw/LockVector.cpp +1 -1
  104. data/vendor/faiss/faiss/impl/index_read.cpp +491 -50
  105. data/vendor/faiss/faiss/impl/index_write.cpp +86 -30
  106. data/vendor/faiss/faiss/impl/lattice_Zn.cpp +8 -9
  107. data/vendor/faiss/faiss/impl/platform_macros.h +3 -1
  108. data/vendor/faiss/faiss/impl/polysemous_training/avx512.cpp +284 -0
  109. data/vendor/faiss/faiss/impl/polysemous_training/dispatch.h +115 -0
  110. data/vendor/faiss/faiss/impl/pq_code_distance/IVFPQScanner_impl.h +73 -39
  111. data/vendor/faiss/faiss/impl/pq_code_distance/IVFPQ_QueryTables.cpp +0 -1
  112. data/vendor/faiss/faiss/impl/pq_code_distance/PQDistanceComputer_impl.h +26 -15
  113. data/vendor/faiss/faiss/impl/pq_code_distance/avx2.cpp +4 -4
  114. data/vendor/faiss/faiss/impl/pq_code_distance/pq_code_distance-generic.cpp +4 -4
  115. data/vendor/faiss/faiss/impl/result_handler/ResultHandler.cpp +195 -0
  116. data/vendor/faiss/faiss/impl/result_handler/avx2.cpp +133 -0
  117. data/vendor/faiss/faiss/impl/result_handler/avx512.cpp +281 -0
  118. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx2.cpp +72 -0
  119. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer-avx512.cpp +228 -0
  120. data/vendor/faiss/faiss/impl/scalar_quantizer/EDENQuantizer.cpp +887 -0
  121. data/vendor/faiss/faiss/impl/scalar_quantizer/distance_computers.h +2 -2
  122. data/vendor/faiss/faiss/impl/scalar_quantizer/quantizers.h +9 -8
  123. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx2.cpp +90 -24
  124. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-impl.h +30 -30
  125. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512-spr.cpp +4 -5
  126. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-avx512.cpp +101 -34
  127. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-dispatch.h +169 -13
  128. data/vendor/faiss/faiss/impl/scalar_quantizer/sq-neon.cpp +125 -26
  129. data/vendor/faiss/faiss/impl/simd_dispatch.h +70 -31
  130. data/vendor/faiss/faiss/index_factory.cpp +40 -7
  131. data/vendor/faiss/faiss/invlists/DirectMap.cpp +1 -1
  132. data/vendor/faiss/faiss/invlists/InvertedLists.cpp +9 -6
  133. data/vendor/faiss/faiss/invlists/OnDiskInvertedLists.cpp +29 -8
  134. data/vendor/faiss/faiss/python/python_callbacks.cpp +3 -1
  135. data/vendor/faiss/faiss/svs/IndexSVSFaissUtils.h +60 -0
  136. data/vendor/faiss/faiss/svs/IndexSVSFlat.cpp +26 -1
  137. data/vendor/faiss/faiss/svs/IndexSVSFlat.h +13 -0
  138. data/vendor/faiss/faiss/svs/IndexSVSIVF.cpp +1 -1
  139. data/vendor/faiss/faiss/svs/IndexSVSIVFLeanVec.cpp +1 -1
  140. data/vendor/faiss/faiss/svs/IndexSVSVamana.cpp +47 -5
  141. data/vendor/faiss/faiss/svs/IndexSVSVamana.h +23 -3
  142. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.cpp +4 -2
  143. data/vendor/faiss/faiss/svs/IndexSVSVamanaLVQ.h +2 -1
  144. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.cpp +10 -4
  145. data/vendor/faiss/faiss/svs/IndexSVSVamanaLeanVec.h +2 -1
  146. data/vendor/faiss/faiss/utils/approx_topk_hamming/approx_topk_hamming.h +1 -1
  147. data/vendor/faiss/faiss/utils/distances.cpp +30 -11
  148. data/vendor/faiss/faiss/utils/distances_dispatch.h +30 -24
  149. data/vendor/faiss/faiss/utils/distances_fused/distances_fused.cpp +1 -1
  150. data/vendor/faiss/faiss/utils/distances_simd.cpp +4 -3
  151. data/vendor/faiss/faiss/utils/extra_distances.cpp +4 -14
  152. data/vendor/faiss/faiss/utils/extra_distances.h +1 -2
  153. data/vendor/faiss/faiss/utils/hamming.cpp +16 -10
  154. data/vendor/faiss/faiss/utils/hamming.h +10 -1
  155. data/vendor/faiss/faiss/utils/hamming_distance/common.h +14 -3
  156. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx512_vpopcnt.cpp +24 -0
  157. data/vendor/faiss/faiss/utils/hamming_distance/hamming_computer-avx512.h +1 -1
  158. data/vendor/faiss/faiss/utils/hamming_distance/{hamming_computer-avx512_spr.h → hamming_computer-avx512_vpopcnt.h} +85 -24
  159. data/vendor/faiss/faiss/utils/hamming_distance/hamming_impl.h +141 -0
  160. data/vendor/faiss/faiss/utils/quantize_lut.cpp +29 -8
  161. data/vendor/faiss/faiss/utils/rabitq_simd.h +202 -0
  162. data/vendor/faiss/faiss/utils/simd_impl/distances_arm_sve.cpp +194 -30
  163. data/vendor/faiss/faiss/utils/simd_impl/distances_avx2.cpp +0 -1
  164. data/vendor/faiss/faiss/utils/simd_impl/distances_avx512.cpp +263 -15
  165. data/vendor/faiss/faiss/utils/simd_impl/distances_rvv.cpp +198 -18
  166. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx2.cpp +245 -0
  167. data/vendor/faiss/faiss/utils/simd_impl/rabitq_avx512.cpp +330 -40
  168. data/vendor/faiss/faiss/utils/simd_impl/{rabitq_avx512_spr.cpp → rabitq_avx512_vpopcnt.cpp} +112 -23
  169. data/vendor/faiss/faiss/utils/simd_impl/rabitq_neon.cpp +11 -0
  170. data/vendor/faiss/faiss/utils/simd_impl/rabitq_rvv.cpp +143 -6
  171. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_dispatch.h +2 -7
  172. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_kernels.h +6 -1
  173. data/vendor/faiss/faiss/utils/simd_impl/super_kmeans_kernels_sve.cpp +34 -0
  174. data/vendor/faiss/faiss/utils/simd_levels.cpp +196 -47
  175. data/vendor/faiss/faiss/utils/simd_levels.h +33 -8
  176. data/vendor/faiss/faiss/utils/utils.cpp +9 -27
  177. metadata +21 -5
  178. data/vendor/faiss/faiss/utils/hamming_distance/hamming_avx512_spr.cpp +0 -15
@@ -7,6 +7,7 @@
7
7
 
8
8
  #include <faiss/impl/HNSW.h>
9
9
 
10
+ #include <array>
10
11
  #include <cinttypes>
11
12
  #include <cstddef>
12
13
  #include <cstdlib>
@@ -16,8 +17,10 @@
16
17
 
17
18
  #include <faiss/impl/DistanceComputer.h>
18
19
  #include <faiss/impl/IDSelector.h>
20
+ #include <faiss/impl/RaBitQuantizer.h>
19
21
  #include <faiss/impl/ResultHandler.h>
20
22
  #include <faiss/impl/VisitedTable.h>
23
+ #include <faiss/impl/hnsw/LockVector.h>
21
24
  #include <faiss/impl/hnsw/MinimaxHeap.h>
22
25
 
23
26
  namespace faiss {
@@ -172,17 +175,21 @@ void HNSW::print_neighbor_stats(int level) const {
172
175
  }
173
176
 
174
177
  void HNSW::fill_with_random_links(size_t n) {
175
- int max_level_2 = prepare_level_tab(n);
178
+ if (n == 0) {
179
+ return;
180
+ }
181
+ max_level = prepare_level_tab(n);
182
+ entry_point = 0;
183
+
176
184
  RandomGenerator rng2(456);
177
185
 
178
- for (int level = max_level_2 - 1; level >= 0; --level) {
186
+ for (int level = max_level - 1; level >= 0; --level) {
179
187
  std::vector<int> elts;
180
188
  for (size_t i = 0; i < n; i++) {
181
189
  if (levels[i] > level) {
182
190
  elts.push_back(i);
183
191
  }
184
192
  }
185
- printf("linking %zd elements in level %d\n", elts.size(), level);
186
193
 
187
194
  if (elts.size() == 1) {
188
195
  continue;
@@ -217,17 +224,17 @@ int HNSW::prepare_level_tab(size_t n, bool preset_levels) {
217
224
  }
218
225
  }
219
226
 
220
- int max_level_2 = 0;
227
+ int local_max_level = 0;
221
228
  for (size_t i = 0; i < n; i++) {
222
229
  int pt_level = levels[i + n0] - 1;
223
- if (pt_level > max_level_2) {
224
- max_level_2 = pt_level;
230
+ if (pt_level > local_max_level) {
231
+ local_max_level = pt_level;
225
232
  }
226
233
  offsets.push_back(offsets.back() + cum_nb_neighbors(pt_level + 1));
227
234
  }
228
235
  neighbors.resize(offsets.back(), -1);
229
236
 
230
- return max_level_2;
237
+ return local_max_level;
231
238
  }
232
239
 
233
240
  /** Enumerate vertices from nearest to farthest from query, keep a
@@ -300,6 +307,11 @@ namespace {
300
307
 
301
308
  using storage_idx_t = HNSW::storage_idx_t;
302
309
 
310
+ inline size_t pruned_neighbor_size(size_t max_size, float prune_headroom) {
311
+ return static_cast<size_t>(
312
+ max_size - max_size * std::clamp(prune_headroom, 0.0f, 0.5f));
313
+ }
314
+
303
315
  // Map a (high-level) HNSW comparator C — which uses int64_t IDs — to the
304
316
  // (low-level) MinimaxHeap comparator HC, which uses int32_t IDs.
305
317
  template <class C>
@@ -779,83 +791,226 @@ HNSWStats hnsw_detail::greedy_update_nearest(
779
791
  hnsw, qdis, level, nearest, d_nearest);
780
792
  }
781
793
 
794
+ /**************************************************************
795
+ * Deterministic addition subroutines
796
+ **************************************************************/
797
+
782
798
  namespace {
783
799
 
784
800
  template <class C>
785
- void add_with_locks_impl(
801
+ void compute_forward_links_impl(
786
802
  HNSW& hnsw,
787
803
  DistanceComputer& ptdis,
788
804
  int pt_level,
789
- int pt_id,
790
- LockVector& locks,
805
+ storage_idx_t pt_id,
791
806
  VisitedTable& vt,
807
+ std::vector<std::pair<storage_idx_t, int>>& pt_reverse_edges,
792
808
  bool keep_max_size_level0) {
793
809
  storage_idx_t nearest = hnsw.entry_point;
794
- if (nearest == -1) { // avoid locking after the first point.
795
- #pragma omp critical
796
- if (hnsw.entry_point == -1) { // double-check under lock.
797
- hnsw.max_level = pt_level;
798
- hnsw.entry_point = pt_id;
799
- // leave nearest = -1 to trigger early exit after critical block.
800
- } else {
801
- // else: Another thread set the entry point.
802
- nearest = hnsw.entry_point;
803
- }
804
- }
805
-
806
- if (nearest < 0) {
807
- return;
808
- }
809
-
810
- locks.lock(pt_id);
811
-
812
- int level = hnsw.max_level; // level at which we start adding neighbors
810
+ FAISS_ASSERT(nearest >= 0);
813
811
  float d_nearest = ptdis(nearest);
814
812
 
815
- // greedy search on upper levels
813
+ int level = hnsw.max_level;
814
+ // greedy descent on the upper levels the point does not live on
816
815
  for (; level > pt_level; level--) {
817
816
  greedy_update_nearest_impl<C>(hnsw, ptdis, level, nearest, d_nearest);
818
817
  }
819
818
 
819
+ // levels the point lives on: search from the same entry point
820
820
  for (; level >= 0; level--) {
821
- add_links_starting_from_impl<C>(
821
+ std::priority_queue<HNSW::NodeDistCloserT<C>> link_targets;
822
+ search_neighbors_to_add_dispatch<C>(
822
823
  hnsw,
823
824
  ptdis,
824
- pt_id,
825
+ link_targets,
825
826
  nearest,
826
827
  d_nearest,
827
828
  level,
828
- locks,
829
829
  vt,
830
- keep_max_size_level0);
830
+ false);
831
+
832
+ int M = hnsw.nb_neighbors(level);
833
+ shrink_neighbor_list_inner<C>(
834
+ ptdis, link_targets, M, keep_max_size_level0 && (level == 0));
835
+
836
+ // pt_id is not reachable yet, so the snapshot stays immutable for the
837
+ // other points in this batch
838
+ size_t begin, end;
839
+ hnsw.neighbor_range(pt_id, level, &begin, &end);
840
+ size_t i = begin;
841
+ while (!link_targets.empty()) {
842
+ storage_idx_t other_id = link_targets.top().id;
843
+ link_targets.pop();
844
+ // pt_id is in its own candidate list when it is the entry point
845
+ if (other_id == pt_id) {
846
+ continue;
847
+ }
848
+ FAISS_ASSERT(i < end);
849
+ hnsw.neighbors[i++] = other_id;
850
+ pt_reverse_edges.emplace_back(other_id, level);
851
+ }
852
+ while (i < end) {
853
+ hnsw.neighbors[i++] = -1;
854
+ }
831
855
  }
856
+ }
832
857
 
833
- locks.unlock(pt_id);
858
+ template <class C>
859
+ void merge_reverse_links_impl(
860
+ HNSW& hnsw,
861
+ DistanceComputer& dis,
862
+ storage_idx_t node,
863
+ int level,
864
+ std::vector<storage_idx_t>& incoming,
865
+ bool keep_max_size_level0) {
866
+ size_t begin, end;
867
+ hnsw.neighbor_range(node, level, &begin, &end);
868
+ size_t max_size = end - begin;
834
869
 
835
- #pragma omp critical
836
- {
837
- if (pt_level > hnsw.max_level) {
838
- hnsw.max_level = pt_level;
839
- hnsw.entry_point = pt_id;
870
+ std::vector<storage_idx_t> cands;
871
+ cands.reserve(max_size + incoming.size());
872
+ for (size_t i = begin; i < end; i++) {
873
+ if (hnsw.neighbors[i] < 0) {
874
+ break;
840
875
  }
876
+ cands.push_back(hnsw.neighbors[i]);
877
+ }
878
+ size_t n_existing = cands.size();
879
+
880
+ std::sort(incoming.begin(), incoming.end());
881
+ incoming.erase(
882
+ std::unique(incoming.begin(), incoming.end()), incoming.end());
883
+ for (storage_idx_t v : incoming) {
884
+ if (v == node) {
885
+ continue;
886
+ }
887
+ bool present = false;
888
+ for (size_t i = 0; i < n_existing; i++) {
889
+ if (cands[i] == v) {
890
+ present = true;
891
+ break;
892
+ }
893
+ }
894
+ if (!present) {
895
+ cands.push_back(v);
896
+ }
897
+ }
898
+
899
+ if (cands.size() <= max_size) {
900
+ size_t i = begin;
901
+ for (storage_idx_t v : cands) {
902
+ hnsw.neighbors[i++] = v;
903
+ }
904
+ while (i < end) {
905
+ hnsw.neighbors[i++] = -1;
906
+ }
907
+ return;
908
+ }
909
+
910
+ // Ties broken by id, so the result is independent of collection order.
911
+ std::vector<std::pair<float, storage_idx_t>> arr;
912
+ arr.reserve(cands.size());
913
+ for (storage_idx_t v : cands) {
914
+ arr.emplace_back(dis.symmetric_dis(node, v), v);
915
+ }
916
+ std::sort(
917
+ arr.begin(),
918
+ arr.end(),
919
+ [](const std::pair<float, storage_idx_t>& a,
920
+ const std::pair<float, storage_idx_t>& b) {
921
+ if (a.first != b.first) {
922
+ // C::cmp(x, y) is true when y is "better" than x, so
923
+ // a before b iff a is better than b.
924
+ return C::cmp(b.first, a.first);
925
+ }
926
+ return a.second < b.second;
927
+ });
928
+
929
+ // Same headroom as add_link's reciprocal pruning, so degrees stay
930
+ // comparable between the two builds.
931
+ size_t pruned_size = pruned_neighbor_size(max_size, hnsw.prune_headroom);
932
+ if (pruned_size < 1) {
933
+ pruned_size = 1;
934
+ }
935
+
936
+ std::vector<std::pair<float, storage_idx_t>> kept;
937
+ std::vector<std::pair<float, storage_idx_t>> outsiders;
938
+ for (const auto& cand : arr) {
939
+ float dist_v1_q = cand.first;
940
+ bool good = true;
941
+ for (const auto& k : kept) {
942
+ float dist_v1_v2 = dis.symmetric_dis(k.second, cand.second);
943
+ if (C::cmp(dist_v1_q, dist_v1_v2)) {
944
+ good = false;
945
+ break;
946
+ }
947
+ }
948
+ if (good) {
949
+ kept.push_back(cand);
950
+ if (kept.size() >= pruned_size) {
951
+ break;
952
+ }
953
+ } else if (keep_max_size_level0) {
954
+ outsiders.push_back(cand);
955
+ }
956
+ }
957
+ for (size_t idx = 0; keep_max_size_level0 && kept.size() < max_size &&
958
+ idx < outsiders.size();
959
+ idx++) {
960
+ kept.push_back(outsiders[idx]);
961
+ }
962
+
963
+ size_t i = begin;
964
+ for (const auto& k : kept) {
965
+ hnsw.neighbors[i++] = k.second;
966
+ }
967
+ while (i < end) {
968
+ hnsw.neighbors[i++] = -1;
841
969
  }
842
970
  }
843
971
 
844
972
  } // namespace
845
973
 
846
- void HNSW::add_with_locks(
974
+ void HNSW::compute_forward_links_deterministic(
847
975
  DistanceComputer& ptdis,
848
976
  int pt_level,
849
- int pt_id,
850
- LockVector& locks,
977
+ storage_idx_t pt_id,
851
978
  VisitedTable& vt,
979
+ std::vector<std::pair<storage_idx_t, int>>& pt_reverse_edges,
852
980
  bool keep_max_size_level0) {
853
981
  if (is_similarity) {
854
- add_with_locks_impl<C_similarity>(
855
- *this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
982
+ compute_forward_links_impl<C_similarity>(
983
+ *this,
984
+ ptdis,
985
+ pt_level,
986
+ pt_id,
987
+ vt,
988
+ pt_reverse_edges,
989
+ keep_max_size_level0);
856
990
  } else {
857
- add_with_locks_impl<C_distance>(
858
- *this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
991
+ compute_forward_links_impl<C_distance>(
992
+ *this,
993
+ ptdis,
994
+ pt_level,
995
+ pt_id,
996
+ vt,
997
+ pt_reverse_edges,
998
+ keep_max_size_level0);
999
+ }
1000
+ }
1001
+
1002
+ void HNSW::merge_reverse_links_deterministic(
1003
+ DistanceComputer& dis,
1004
+ storage_idx_t node,
1005
+ int level,
1006
+ std::vector<storage_idx_t>& incoming,
1007
+ bool keep_max_size_level0) {
1008
+ if (is_similarity) {
1009
+ merge_reverse_links_impl<C_similarity>(
1010
+ *this, dis, node, level, incoming, keep_max_size_level0);
1011
+ } else {
1012
+ merge_reverse_links_impl<C_distance>(
1013
+ *this, dis, node, level, incoming, keep_max_size_level0);
859
1014
  }
860
1015
  }
861
1016
 
@@ -886,13 +1041,82 @@ inline void extract_search_params(
886
1041
  }
887
1042
  }
888
1043
 
889
- /** Templated body of `search_from_candidates` — instantiated once per
890
- * VisitedTable subclass × comparator.
1044
+ struct DefaultCandidateDistanceEvaluator {
1045
+ DistanceComputer& qdis;
1046
+
1047
+ template <typename GetThreshold, typename AddResult>
1048
+ size_t evaluate(
1049
+ const storage_idx_t* ids,
1050
+ int count,
1051
+ GetThreshold&& /* get_threshold */,
1052
+ AddResult&& add_result) {
1053
+ if (count == 4) {
1054
+ float distances[4];
1055
+ qdis.distances_batch_4(
1056
+ ids[0],
1057
+ ids[1],
1058
+ ids[2],
1059
+ ids[3],
1060
+ distances[0],
1061
+ distances[1],
1062
+ distances[2],
1063
+ distances[3]);
1064
+ for (int i = 0; i < 4; ++i) {
1065
+ add_result(ids[i], distances[i]);
1066
+ }
1067
+ } else {
1068
+ for (int i = 0; i < count; ++i) {
1069
+ add_result(ids[i], qdis(ids[i]));
1070
+ }
1071
+ }
1072
+ return count;
1073
+ }
1074
+ };
1075
+
1076
+ /** RaBitQ's staged evaluator plugs into the same graph traversal as the
1077
+ * default distance computer. Each candidate first receives a 1-bit estimate;
1078
+ * the full code is evaluated only when its error bound can still beat the
1079
+ * current result threshold.
891
1080
  */
892
- template <typename VTType, class C>
1081
+ struct RaBitQCandidateDistanceEvaluator {
1082
+ RaBitQDistanceComputer& rq;
1083
+ const bool is_similarity;
1084
+
1085
+ template <typename GetThreshold, typename AddResult>
1086
+ size_t evaluate(
1087
+ const storage_idx_t* ids,
1088
+ int count,
1089
+ GetThreshold&& get_threshold,
1090
+ AddResult&& add_result) {
1091
+ size_t ndis = 0;
1092
+ for (int i = 0; i < count; ++i) {
1093
+ const uint8_t* code =
1094
+ rq.codes + static_cast<size_t>(ids[i]) * rq.code_size;
1095
+ const float estimate = rq.distance_to_code_1bit(code);
1096
+ rq.stats.n_1bit++;
1097
+ ndis++;
1098
+
1099
+ float distance = estimate;
1100
+ if (rq.should_refine(
1101
+ code, estimate, get_threshold(), is_similarity)) {
1102
+ distance = rq.distance_to_code_full(code);
1103
+ rq.stats.n_refine++;
1104
+ ndis++;
1105
+ }
1106
+ add_result(ids[i], distance);
1107
+ }
1108
+ return ndis;
1109
+ }
1110
+ };
1111
+
1112
+ /** Templated body shared by the default and staged RaBitQ searches. The
1113
+ * evaluator changes only how a group of unvisited neighbors receives its
1114
+ * distance; traversal, result handling and stopping conditions stay identical.
1115
+ */
1116
+ template <typename VTType, class C, typename DistanceEvaluator>
893
1117
  int search_from_candidates_fixVT(
894
1118
  const HNSW& hnsw,
895
- DistanceComputer& qdis,
1119
+ DistanceEvaluator& evaluator,
896
1120
  ResultHandler& res,
897
1121
  MinimaxHeapT<HC_for<C>>& candidates,
898
1122
  VTType& vt,
@@ -901,7 +1125,7 @@ int search_from_candidates_fixVT(
901
1125
  int nres_in,
902
1126
  const SearchParameters* params) {
903
1127
  int nres = nres_in;
904
- int ndis = 0;
1128
+ size_t ndis = 0;
905
1129
 
906
1130
  bool do_dis_check;
907
1131
  int efSearch;
@@ -959,7 +1183,7 @@ int search_from_candidates_fixVT(
959
1183
  }
960
1184
 
961
1185
  int counter = 0;
962
- size_t saved_j[4];
1186
+ storage_idx_t saved_j[4];
963
1187
 
964
1188
  threshold = res.threshold;
965
1189
 
@@ -982,32 +1206,18 @@ int search_from_candidates_fixVT(
982
1206
  counter += vt.set(v1) ? 1 : 0;
983
1207
 
984
1208
  if (counter == 4) {
985
- float dis[4];
986
- qdis.distances_batch_4(
987
- saved_j[0],
988
- saved_j[1],
989
- saved_j[2],
990
- saved_j[3],
991
- dis[0],
992
- dis[1],
993
- dis[2],
994
- dis[3]);
995
-
996
- for (size_t id4 = 0; id4 < 4; id4++) {
997
- add_to_heap(saved_j[id4], dis[id4]);
998
- }
999
-
1000
- ndis += 4;
1001
-
1209
+ ndis += evaluator.evaluate(
1210
+ saved_j,
1211
+ counter,
1212
+ [&] { return threshold; },
1213
+ add_to_heap);
1002
1214
  counter = 0;
1003
1215
  }
1004
1216
  }
1005
1217
 
1006
- for (int icnt = 0; icnt < counter; icnt++) {
1007
- float dis = qdis(saved_j[icnt]);
1008
- add_to_heap(saved_j[icnt], dis);
1009
-
1010
- ndis += 1;
1218
+ if (counter > 0) {
1219
+ ndis += evaluator.evaluate(
1220
+ saved_j, counter, [&] { return threshold; }, add_to_heap);
1011
1221
  }
1012
1222
 
1013
1223
  nstep++;
@@ -1028,12 +1238,12 @@ int search_from_candidates_fixVT(
1028
1238
  return nres;
1029
1239
  }
1030
1240
 
1031
- /// Dispatches the VisitedTable concrete type for a given C, then calls
1032
- /// the templated `search_from_candidates_fixVT<VTType, C>`.
1033
- template <class C>
1034
- int search_from_candidates_dispatch(
1241
+ /// Dispatches the VisitedTable concrete type while keeping the distance
1242
+ /// evaluator statically selected outside the graph traversal loop.
1243
+ template <class C, typename DistanceEvaluator>
1244
+ int search_from_candidates_evaluator_dispatch(
1035
1245
  const HNSW& hnsw,
1036
- DistanceComputer& qdis,
1246
+ DistanceEvaluator& evaluator,
1037
1247
  ResultHandler& res,
1038
1248
  MinimaxHeapT<HC_for<C>>& candidates,
1039
1249
  VisitedTable& vt,
@@ -1044,7 +1254,7 @@ int search_from_candidates_dispatch(
1044
1254
  auto call = [&]<typename VTType>(VTType& vt_concrete) -> int {
1045
1255
  return search_from_candidates_fixVT<VTType, C>(
1046
1256
  hnsw,
1047
- qdis,
1257
+ evaluator,
1048
1258
  res,
1049
1259
  candidates,
1050
1260
  vt_concrete,
@@ -1060,6 +1270,59 @@ int search_from_candidates_dispatch(
1060
1270
  return call(vts);
1061
1271
  }
1062
1272
 
1273
+ template <class C>
1274
+ int search_from_candidates_dispatch(
1275
+ const HNSW& hnsw,
1276
+ DistanceComputer& qdis,
1277
+ ResultHandler& res,
1278
+ MinimaxHeapT<HC_for<C>>& candidates,
1279
+ VisitedTable& vt,
1280
+ HNSWStats& stats,
1281
+ int level,
1282
+ int nres_in,
1283
+ const SearchParameters* params) {
1284
+ DefaultCandidateDistanceEvaluator evaluator{qdis};
1285
+ return search_from_candidates_evaluator_dispatch<C>(
1286
+ hnsw,
1287
+ evaluator,
1288
+ res,
1289
+ candidates,
1290
+ vt,
1291
+ stats,
1292
+ level,
1293
+ nres_in,
1294
+ params);
1295
+ }
1296
+
1297
+ template <class C>
1298
+ int search_from_candidates_rabitq_dispatch(
1299
+ const HNSW& hnsw,
1300
+ DistanceComputer& qdis,
1301
+ ResultHandler& res,
1302
+ MinimaxHeapT<HC_for<C>>& candidates,
1303
+ VisitedTable& vt,
1304
+ HNSWStats& stats,
1305
+ int level,
1306
+ int nres_in,
1307
+ const SearchParameters* params) {
1308
+ auto* rq = dynamic_cast<RaBitQDistanceComputer*>(&qdis);
1309
+ FAISS_THROW_IF_NOT_MSG(
1310
+ rq, "staged RaBitQ search requires an IndexRaBitQ storage");
1311
+ FAISS_THROW_IF_NOT_MSG(
1312
+ rq->nb_bits >= 2, "staged RaBitQ search requires nb_bits >= 2");
1313
+ RaBitQCandidateDistanceEvaluator evaluator{*rq, hnsw.is_similarity};
1314
+ return search_from_candidates_evaluator_dispatch<C>(
1315
+ hnsw,
1316
+ evaluator,
1317
+ res,
1318
+ candidates,
1319
+ vt,
1320
+ stats,
1321
+ level,
1322
+ nres_in,
1323
+ params);
1324
+ }
1325
+
1063
1326
  } // namespace
1064
1327
 
1065
1328
  /** Do a BFS on the candidates list. Public dispatcher: only handles the
@@ -1092,8 +1355,8 @@ int hnsw_detail::search_from_candidates_panorama(
1092
1355
  const SearchParameters* params) {
1093
1356
  // Panorama's progressive-bound math is L2-specific: refuse to run in
1094
1357
  // similarity mode.
1095
- FAISS_THROW_IF_NOT_MSG(
1096
- !hnsw.is_similarity,
1358
+ FAISS_THROW_IF_MSG(
1359
+ hnsw.is_similarity,
1097
1360
  "search_from_candidates_panorama does not support is_similarity=true");
1098
1361
 
1099
1362
  using C = HNSW::C_distance;
@@ -1132,177 +1395,129 @@ int hnsw_detail::search_from_candidates_panorama(
1132
1395
  flat_codes_qdis,
1133
1396
  "DistanceComputer must be a FlatCodesDistanceComputer");
1134
1397
 
1135
- // Allocate space for the index array and exact distances.
1136
- size_t M = hnsw.nb_neighbors(0);
1137
- std::vector<idx_t> index_array(M);
1138
- std::vector<float> exact_distances(M);
1398
+ const auto& pano = panorama_index->pano;
1399
+ const size_t nb_per_parent = static_cast<size_t>(hnsw.nb_neighbors(level));
1400
+ const size_t num_panorama_levels = pano.n_levels;
1401
+ const size_t level_width_floats = pano.level_width_floats;
1139
1402
 
1140
- const float* query = flat_codes_qdis->q;
1141
- std::vector<float> query_cum_sums(panorama_index->pano.n_levels + 1);
1142
- panorama_index->pano.compute_query_cum_sums(query, query_cum_sums.data());
1143
- float query_norm_sq = query_cum_sums[0] * query_cum_sums[0];
1403
+ constexpr size_t kTargetBatch = 64;
1404
+ const size_t buf_cap = kTargetBatch + nb_per_parent;
1405
+ std::vector<uint32_t> index_array(buf_cap);
1406
+ std::vector<float> exact_distances(buf_cap);
1407
+ std::vector<float> dot_buffer(buf_cap);
1408
+ std::vector<float> query_cum_sums_buf(num_panorama_levels + 1);
1144
1409
 
1145
- int nstep = 0;
1410
+ const float* query = flat_codes_qdis->q;
1146
1411
  const size_t d = static_cast<size_t>(panorama_index->d);
1147
1412
 
1148
- PanoramaStats local_pano_stats;
1149
- local_pano_stats.reset();
1413
+ const float* cum_base = panorama_index->get_cum_sum(0);
1414
+ const size_t cum_stride = num_panorama_levels + 1;
1415
+ const auto* flat_storage =
1416
+ static_cast<const IndexFlat*>(panorama_index->storage);
1417
+ const float* xb_base =
1418
+ reinterpret_cast<const float*>(flat_storage->codes.data());
1419
+ const size_t feat_stride = d;
1150
1420
 
1151
- while (candidates.size() > 0) {
1152
- float d0 = 0;
1153
- int v0 = candidates.pop_min(&d0);
1421
+ pano.compute_query_cum_sums(query, query_cum_sums_buf.data());
1422
+ const float* query_cum_sums = query_cum_sums_buf.data();
1423
+ const float query_norm_sq = query_cum_sums[0] * query_cum_sums[0];
1154
1424
 
1155
- if (do_dis_check) {
1156
- // tricky stopping condition: there are more than ef
1157
- // distances that are processed already that are smaller
1158
- // than d0
1159
-
1160
- int n_dis_below = candidates.count_below(d0);
1161
- if (n_dis_below >= efSearch) {
1162
- break;
1163
- }
1164
- }
1425
+ int nstep = 0;
1165
1426
 
1166
- size_t begin, end;
1167
- hnsw.neighbor_range(v0, level, &begin, &end);
1427
+ PanoramaStats local_pano_stats;
1428
+ local_pano_stats.reset();
1168
1429
 
1169
- // Unlike the vanilla HNSW, we already remove (and compact) the visited
1170
- // nodes from the candidates list at this stage. We also remove nodes
1171
- // that are not selected.
1430
+ bool stop_flag = false;
1431
+ while (candidates.size() > 0 && !stop_flag) {
1172
1432
  size_t initial_size = 0;
1173
- for (size_t j = begin; j < end; j++) {
1174
- int v1 = hnsw.neighbors[j];
1175
- if (v1 < 0) {
1176
- break;
1433
+ size_t k_popped = 0;
1434
+ while (initial_size < kTargetBatch && candidates.size() > 0) {
1435
+ float d0 = 0;
1436
+ int v0 = candidates.pop_min(&d0);
1437
+ if (do_dis_check) {
1438
+ int n_dis_below = candidates.count_below(d0);
1439
+ if (n_dis_below >= efSearch) {
1440
+ if (k_popped == 0) {
1441
+ // Standard early-stop: nothing queued this
1442
+ // iteration, terminate the outer loop entirely
1443
+ // (matches the single-pop path's behavior).
1444
+ stop_flag = true;
1445
+ } else {
1446
+ // We already have parents queued; un-pop this
1447
+ // one so the next outer iteration sees it and
1448
+ // re-applies the stop check from a clean state.
1449
+ candidates.push(v0, d0);
1450
+ }
1451
+ break;
1452
+ }
1177
1453
  }
1454
+ k_popped++;
1178
1455
 
1179
- const float* cum_sums_v1 = panorama_index->get_cum_sum(v1);
1180
- index_array[initial_size] = v1;
1181
- exact_distances[initial_size] =
1182
- query_norm_sq + cum_sums_v1[0] * cum_sums_v1[0];
1456
+ size_t begin, end;
1457
+ hnsw.neighbor_range(v0, level, &begin, &end);
1458
+ for (size_t j = begin; j < end; j++) {
1459
+ int v1 = hnsw.neighbors[j];
1460
+ if (v1 < 0) {
1461
+ break;
1462
+ }
1183
1463
 
1184
- bool is_selected = !sel || sel->is_member(v1);
1185
- initial_size += is_selected && vt.set(v1) ? 1 : 0;
1464
+ bool is_new = vt.set(v1);
1465
+ bool is_selected = !sel || sel->is_member(v1);
1466
+ if (is_new && is_selected) {
1467
+ const float vsum =
1468
+ cum_base[static_cast<size_t>(v1) * cum_stride];
1469
+ index_array[initial_size] = v1;
1470
+ exact_distances[initial_size] = query_norm_sq + vsum * vsum;
1471
+ initial_size++;
1472
+ }
1473
+ }
1186
1474
  }
1187
1475
 
1188
1476
  local_pano_stats.total_dims += initial_size * d;
1477
+
1189
1478
  size_t batch_size = initial_size;
1190
1479
  size_t curr_panorama_level = 0;
1191
- const size_t num_panorama_levels = panorama_index->pano.n_levels;
1192
1480
  while (curr_panorama_level < num_panorama_levels && batch_size > 0) {
1193
- float query_cum_norm = query_cum_sums[curr_panorama_level + 1];
1481
+ const size_t cs_level_idx = curr_panorama_level + 1;
1482
+ const float query_cum_norm = query_cum_sums[cs_level_idx];
1483
+ const float two_qc = 2.0f * query_cum_norm;
1484
+
1485
+ const size_t start_dim = curr_panorama_level * level_width_floats;
1486
+ size_t end_dim = (curr_panorama_level + 1) * level_width_floats;
1487
+ end_dim = std::min(end_dim, d);
1488
+ const size_t dim_span = end_dim - start_dim;
1489
+
1490
+ const float* level_base = xb_base + start_dim;
1491
+ with_level_width(dim_span, [&]<size_t W>() {
1492
+ compute_level_dot_kernel<false, W>(
1493
+ query + start_dim,
1494
+ level_base,
1495
+ index_array.data(),
1496
+ batch_size,
1497
+ dim_span,
1498
+ dot_buffer.data(),
1499
+ feat_stride);
1500
+ });
1501
+ ndis += batch_size;
1194
1502
 
1195
- size_t start_dim = curr_panorama_level *
1196
- panorama_index->pano.level_width_floats;
1197
- size_t end_dim = (curr_panorama_level + 1) *
1198
- panorama_index->pano.level_width_floats;
1199
- end_dim = std::min(end_dim, static_cast<size_t>(panorama_index->d));
1200
-
1201
- size_t i = 0;
1202
1503
  size_t next_batch_size = 0;
1203
- for (; i + 3 < batch_size; i += 4) {
1204
- idx_t idx_0 = index_array[i];
1205
- idx_t idx_1 = index_array[i + 1];
1206
- idx_t idx_2 = index_array[i + 2];
1207
- idx_t idx_3 = index_array[i + 3];
1208
-
1209
- float dp[4];
1210
- flat_codes_qdis->partial_dot_product_batch_4(
1211
- idx_0,
1212
- idx_1,
1213
- idx_2,
1214
- idx_3,
1215
- dp[0],
1216
- dp[1],
1217
- dp[2],
1218
- dp[3],
1219
- start_dim,
1220
- end_dim - start_dim);
1221
- ndis += 4;
1222
1504
 
1223
- float new_exact_0 = exact_distances[i + 0] - 2 * dp[0];
1224
- float new_exact_1 = exact_distances[i + 1] - 2 * dp[1];
1225
- float new_exact_2 = exact_distances[i + 2] - 2 * dp[2];
1226
- float new_exact_3 = exact_distances[i + 3] - 2 * dp[3];
1227
-
1228
- float cum_sum_0 = panorama_index->get_cum_sum(
1229
- idx_0)[curr_panorama_level + 1];
1230
- float cum_sum_1 = panorama_index->get_cum_sum(
1231
- idx_1)[curr_panorama_level + 1];
1232
- float cum_sum_2 = panorama_index->get_cum_sum(
1233
- idx_2)[curr_panorama_level + 1];
1234
- float cum_sum_3 = panorama_index->get_cum_sum(
1235
- idx_3)[curr_panorama_level + 1];
1236
-
1237
- float cs_bound_0 = 2.0f * cum_sum_0 * query_cum_norm;
1238
- float cs_bound_1 = 2.0f * cum_sum_1 * query_cum_norm;
1239
- float cs_bound_2 = 2.0f * cum_sum_2 * query_cum_norm;
1240
- float cs_bound_3 = 2.0f * cum_sum_3 * query_cum_norm;
1241
-
1242
- float lower_bound_0 = new_exact_0 - cs_bound_0;
1243
- float lower_bound_1 = new_exact_1 - cs_bound_1;
1244
- float lower_bound_2 = new_exact_2 - cs_bound_2;
1245
- float lower_bound_3 = new_exact_3 - cs_bound_3;
1246
-
1247
- // The following code is not the most branch friendly (due to
1248
- // the maintenance of the candidate heap), but micro-benchmarks
1249
- // have shown that it is not worth it to write horrible code to
1250
- // squeeze out those cycles.
1251
- if (!C::cmp(lower_bound_0, threshold)) {
1252
- exact_distances[next_batch_size] = new_exact_0;
1253
- index_array[next_batch_size] = idx_0;
1254
- next_batch_size += 1;
1255
- } else {
1256
- candidates.push(idx_0, new_exact_0);
1257
- }
1258
- if (!C::cmp(lower_bound_1, threshold)) {
1259
- exact_distances[next_batch_size] = new_exact_1;
1260
- index_array[next_batch_size] = idx_1;
1261
- next_batch_size += 1;
1262
- } else {
1263
- candidates.push(idx_1, new_exact_1);
1264
- }
1265
- if (!C::cmp(lower_bound_2, threshold)) {
1266
- exact_distances[next_batch_size] = new_exact_2;
1267
- index_array[next_batch_size] = idx_2;
1268
- next_batch_size += 1;
1269
- } else {
1270
- candidates.push(idx_2, new_exact_2);
1271
- }
1272
- if (!C::cmp(lower_bound_3, threshold)) {
1273
- exact_distances[next_batch_size] = new_exact_3;
1274
- index_array[next_batch_size] = idx_3;
1275
- next_batch_size += 1;
1505
+ for (size_t i = 0; i < batch_size; i++) {
1506
+ float ne = exact_distances[i] - 2.0f * dot_buffer[i];
1507
+ float cum = cum_base
1508
+ [static_cast<size_t>(index_array[i]) * cum_stride +
1509
+ cs_level_idx];
1510
+ float lb = ne - two_qc * cum;
1511
+ if (lb <= threshold) {
1512
+ exact_distances[next_batch_size] = ne;
1513
+ index_array[next_batch_size] = index_array[i];
1514
+ next_batch_size++;
1276
1515
  } else {
1277
- candidates.push(idx_3, new_exact_3);
1516
+ candidates.push(index_array[i], ne);
1278
1517
  }
1279
1518
  }
1280
1519
 
1281
- // Process the remaining candidates.
1282
- for (; i < batch_size; i++) {
1283
- idx_t idx = index_array[i];
1284
-
1285
- float dp = flat_codes_qdis->partial_dot_product(
1286
- idx, start_dim, end_dim - start_dim);
1287
- ndis += 1;
1288
- float new_exact = exact_distances[i] - 2.0f * dp;
1289
-
1290
- float cum_sum = panorama_index->get_cum_sum(
1291
- idx)[curr_panorama_level + 1];
1292
- float cs_bound = 2.0f * cum_sum * query_cum_norm;
1293
- float lower_bound = new_exact - cs_bound;
1294
-
1295
- if (!C::cmp(lower_bound, threshold)) {
1296
- exact_distances[next_batch_size] = new_exact;
1297
- index_array[next_batch_size] = idx;
1298
- next_batch_size += 1;
1299
- } else {
1300
- candidates.push(idx, new_exact);
1301
- }
1302
- }
1303
-
1304
- local_pano_stats.total_dims_scanned +=
1305
- batch_size * (end_dim - start_dim);
1520
+ local_pano_stats.total_dims_scanned += batch_size * dim_span;
1306
1521
  batch_size = next_batch_size;
1307
1522
  curr_panorama_level++;
1308
1523
  }
@@ -1317,7 +1532,7 @@ int hnsw_detail::search_from_candidates_panorama(
1317
1532
  candidates.push(idx, exact_distances[i]);
1318
1533
  }
1319
1534
 
1320
- nstep++;
1535
+ nstep += static_cast<int>(k_popped);
1321
1536
  if (!do_dis_check && nstep > efSearch) {
1322
1537
  break;
1323
1538
  }
@@ -1534,37 +1749,49 @@ HNSWStats search_impl(
1534
1749
  }
1535
1750
 
1536
1751
  int ef = std::max(cur_efSearch, k);
1537
- if (bounded_queue) { // this is the most common branch, for now we only
1538
- // support Panorama search in this branch
1752
+ if (bounded_queue) {
1539
1753
  MinimaxHeapT<HC_for<C>> candidates(ef);
1540
1754
 
1541
1755
  candidates.push(nearest, d_nearest);
1542
1756
 
1543
- if (!hnsw.is_panorama) {
1544
- search_from_candidates_dispatch<C>(
1545
- hnsw, qdis, res, candidates, vt, stats, 0, 0, params);
1546
- } else {
1547
- // Panorama is L2-specific and is only valid for C_distance.
1548
- // The public dispatch ensures we never reach this code path
1549
- // with C != C_distance, but assert in debug builds.
1550
- if constexpr (std::is_same_v<C, HNSW::C_distance>) {
1551
- hnsw_detail::search_from_candidates_panorama(
1552
- hnsw,
1553
- index,
1554
- qdis,
1555
- res,
1556
- candidates,
1557
- vt,
1558
- stats,
1559
- 0,
1560
- 0,
1561
- params);
1562
- } else {
1563
- FAISS_THROW_MSG(
1564
- "Panorama search is not supported with is_similarity=true");
1565
- }
1757
+ switch (hnsw.search_method) {
1758
+ case HNSW::SM_DEFAULT:
1759
+ search_from_candidates_dispatch<C>(
1760
+ hnsw, qdis, res, candidates, vt, stats, 0, 0, params);
1761
+ break;
1762
+ case HNSW::SM_RABITQ:
1763
+ search_from_candidates_rabitq_dispatch<C>(
1764
+ hnsw, qdis, res, candidates, vt, stats, 0, 0, params);
1765
+ break;
1766
+ case HNSW::SM_PANORAMA:
1767
+ if constexpr (std::is_same_v<C, HNSW::C_distance>) {
1768
+ hnsw_detail::search_from_candidates_panorama(
1769
+ hnsw,
1770
+ index,
1771
+ qdis,
1772
+ res,
1773
+ candidates,
1774
+ vt,
1775
+ stats,
1776
+ 0,
1777
+ 0,
1778
+ params);
1779
+ } else {
1780
+ FAISS_THROW_MSG(
1781
+ "Panorama search does not support similarity metrics");
1782
+ }
1783
+ break;
1784
+ default:
1785
+ FAISS_THROW_MSG("invalid HNSW search method");
1566
1786
  }
1567
1787
  } else {
1788
+ FAISS_THROW_IF_NOT_MSG(
1789
+ hnsw.search_method != HNSW::SM_RABITQ,
1790
+ "staged RaBitQ search requires bounded_queue=true");
1791
+ FAISS_THROW_IF_NOT_MSG(
1792
+ hnsw.search_method == HNSW::SM_DEFAULT ||
1793
+ hnsw.search_method == HNSW::SM_PANORAMA,
1794
+ "invalid HNSW search method");
1568
1795
  auto call = [&]<typename VTType>(VTType& vt_concrete) {
1569
1796
  return search_from_candidate_unbounded_fixVT<VTType, C>(
1570
1797
  hnsw,
@@ -1622,6 +1849,50 @@ void search_level_0_impl(
1622
1849
 
1623
1850
  int k = extract_k_from_ResultHandler<C>(res);
1624
1851
 
1852
+ auto search_candidates = [&](MinimaxHeapT<HC_for<C>>& candidates,
1853
+ int nres) {
1854
+ switch (hnsw.search_method) {
1855
+ case HNSW::SM_DEFAULT:
1856
+ return search_from_candidates_dispatch<C>(
1857
+ hnsw,
1858
+ qdis,
1859
+ res,
1860
+ candidates,
1861
+ vt,
1862
+ search_stats,
1863
+ 0,
1864
+ nres,
1865
+ params);
1866
+ case HNSW::SM_RABITQ:
1867
+ return search_from_candidates_rabitq_dispatch<C>(
1868
+ hnsw,
1869
+ qdis,
1870
+ res,
1871
+ candidates,
1872
+ vt,
1873
+ search_stats,
1874
+ 0,
1875
+ nres,
1876
+ params);
1877
+ case HNSW::SM_PANORAMA:
1878
+ // Preserve the pre-enum behavior: search_level_0 did not use
1879
+ // Panorama's progressive traversal and fell back to the
1880
+ // ordinary HNSW candidate search.
1881
+ return search_from_candidates_dispatch<C>(
1882
+ hnsw,
1883
+ qdis,
1884
+ res,
1885
+ candidates,
1886
+ vt,
1887
+ search_stats,
1888
+ 0,
1889
+ nres,
1890
+ params);
1891
+ default:
1892
+ FAISS_THROW_MSG("invalid HNSW search method");
1893
+ }
1894
+ };
1895
+
1625
1896
  if (search_type == 1) {
1626
1897
  int nres = 0;
1627
1898
 
@@ -1641,16 +1912,7 @@ void search_level_0_impl(
1641
1912
 
1642
1913
  candidates.push(cj, nearest_d[j]);
1643
1914
 
1644
- nres = search_from_candidates_dispatch<C>(
1645
- hnsw,
1646
- qdis,
1647
- res,
1648
- candidates,
1649
- vt,
1650
- search_stats,
1651
- 0,
1652
- nres,
1653
- params);
1915
+ nres = search_candidates(candidates, nres);
1654
1916
  nres = std::min(nres, candidates_size);
1655
1917
  }
1656
1918
  } else if (search_type == 2) {
@@ -1667,8 +1929,7 @@ void search_level_0_impl(
1667
1929
  candidates.push(cj, nearest_d[j]);
1668
1930
  }
1669
1931
 
1670
- search_from_candidates_dispatch<C>(
1671
- hnsw, qdis, res, candidates, vt, search_stats, 0, 0, params);
1932
+ search_candidates(candidates, 0);
1672
1933
  }
1673
1934
  }
1674
1935