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
@@ -39,17 +39,50 @@ namespace faiss {
39
39
 
40
40
  namespace {
41
41
 
42
+ // Whether the computer can measure a whole batch of codes in one call.
42
43
  template <class HammingComputer>
43
- struct IVFBinaryScannerL2 : BinaryInvertedListScanner {
44
+ constexpr bool has_hamming_batch =
45
+ requires(const uint8_t* tile, const uint8_t* codes, int32_t* dis) {
46
+ HammingComputer::batch_size;
47
+ HammingComputer::get_code_size();
48
+ HammingComputer::build_batch_query(tile, nullptr);
49
+ HammingComputer::hamming_batch(tile, codes, dis);
50
+ };
51
+
52
+ // The query repeated batch_size times, which is what hamming_batch() XORs
53
+ // against. Empty for a computer that does not batch, so those scanners carry
54
+ // neither the buffer nor its alignment.
55
+ template <class HammingComputer>
56
+ struct BatchQueryTile {};
57
+
58
+ template <class HammingComputer>
59
+ requires has_hamming_batch<HammingComputer>
60
+ struct BatchQueryTile<HammingComputer> {
61
+ alignas(64) uint8_t batch_query
62
+ [HammingComputer::batch_size * HammingComputer::get_code_size()];
63
+ };
64
+
65
+ template <class HammingComputer>
66
+ struct IVFBinaryScannerL2 : BinaryInvertedListScanner,
67
+ BatchQueryTile<HammingComputer> {
44
68
  HammingComputer hc;
45
69
  size_t code_size;
46
70
  bool store_pairs;
47
71
 
48
72
  IVFBinaryScannerL2(size_t code_size_, bool store_pairs_)
49
- : code_size(code_size_), store_pairs(store_pairs_) {}
73
+ : code_size(code_size_), store_pairs(store_pairs_) {
74
+ if constexpr (has_hamming_batch<HammingComputer>) {
75
+ // The batch kernel reads a fixed stride, so a caller that pairs
76
+ // this computer with another code size would read past the codes.
77
+ FAISS_THROW_IF_NOT(code_size == HammingComputer::get_code_size());
78
+ }
79
+ }
50
80
 
51
81
  void set_query(const uint8_t* query_vector) override {
52
82
  hc.set(query_vector, code_size);
83
+ if constexpr (has_hamming_batch<HammingComputer>) {
84
+ HammingComputer::build_batch_query(query_vector, this->batch_query);
85
+ }
53
86
  }
54
87
 
55
88
  idx_t list_no = 0;
@@ -61,6 +94,31 @@ struct IVFBinaryScannerL2 : BinaryInvertedListScanner {
61
94
  return hc.hamming(code);
62
95
  }
63
96
 
97
+ // Measures whole batches while at least batch_size codes remain, then
98
+ // leaves codes and j on the first code the caller must measure singly.
99
+ // bound is read per lane, so a caller that raises its heap top inside
100
+ // accept prunes the rest of the batch.
101
+ template <class Accept>
102
+ void scan_batch_prefix(
103
+ size_t n,
104
+ const uint8_t* __restrict& codes,
105
+ size_t& j,
106
+ const uint32_t& bound,
107
+ Accept&& accept) const {
108
+ if constexpr (has_hamming_batch<HammingComputer>) {
109
+ constexpr size_t B = HammingComputer::batch_size;
110
+ int32_t batch[B];
111
+ for (; j + B <= n; j += B, codes += B * code_size) {
112
+ HammingComputer::hamming_batch(this->batch_query, codes, batch);
113
+ for (size_t t = 0; t < B; t++) {
114
+ if (static_cast<uint32_t>(batch[t]) < bound) {
115
+ accept(batch[t], j + t);
116
+ }
117
+ }
118
+ }
119
+ }
120
+ }
121
+
64
122
  size_t scan_codes(
65
123
  size_t n,
66
124
  const uint8_t* __restrict codes,
@@ -70,15 +128,25 @@ struct IVFBinaryScannerL2 : BinaryInvertedListScanner {
70
128
  size_t k) const override {
71
129
  using C = CMax<int32_t, idx_t>;
72
130
 
131
+ uint32_t bound = static_cast<uint32_t>(simi[0]);
132
+
73
133
  size_t nup = 0;
74
- for (size_t j = 0; j < n; j++) {
134
+ size_t j = 0;
135
+
136
+ auto accept = [&](int32_t dis, size_t at) {
137
+ idx_t id = store_pairs ? lo_build(list_no, at) : ids[at];
138
+ heap_replace_top<C>(k, simi, idxi, dis, id);
139
+ bound = static_cast<uint32_t>(simi[0]);
140
+ nup++;
141
+ };
142
+
143
+ scan_batch_prefix(n, codes, j, bound, accept);
144
+
145
+ for (; j < n; j++, codes += code_size) {
75
146
  uint32_t dis = hc.hamming(codes);
76
- if (dis < static_cast<uint32_t>(simi[0])) {
77
- idx_t id = store_pairs ? lo_build(list_no, j) : ids[j];
78
- heap_replace_top<C>(k, simi, idxi, dis, id);
79
- nup++;
147
+ if (dis < bound) {
148
+ accept(static_cast<int32_t>(dis), j);
80
149
  }
81
- codes += code_size;
82
150
  }
83
151
  return nup;
84
152
  }
@@ -89,13 +157,21 @@ struct IVFBinaryScannerL2 : BinaryInvertedListScanner {
89
157
  const idx_t* __restrict ids,
90
158
  int radius,
91
159
  RangeQueryResult& result) const override {
92
- for (size_t j = 0; j < n; j++) {
160
+ const uint32_t bound = static_cast<uint32_t>(radius);
161
+ size_t j = 0;
162
+
163
+ auto accept = [&](int32_t dis, size_t at) {
164
+ int64_t id = store_pairs ? lo_build(list_no, at) : ids[at];
165
+ result.add(static_cast<uint32_t>(dis), id);
166
+ };
167
+
168
+ scan_batch_prefix(n, codes, j, bound, accept);
169
+
170
+ for (; j < n; j++, codes += code_size) {
93
171
  uint32_t dis = hc.hamming(codes);
94
- if (dis < static_cast<uint32_t>(radius)) {
95
- int64_t id = store_pairs ? lo_build(list_no, j) : ids[j];
96
- result.add(dis, id);
172
+ if (dis < bound) {
173
+ accept(static_cast<int32_t>(dis), j);
97
174
  }
98
- codes += code_size;
99
175
  }
100
176
  }
101
177
  };
@@ -35,12 +35,16 @@ size_t run_scan_codes1(
35
35
  size_t list_no = scanner.list_no;
36
36
  size_t code_size = scanner.code_size;
37
37
  const IDSelector* sel = scanner.sel;
38
+ // If the selector implements IDSelectorWithContext, hand it the scan
39
+ // context (ids, list_size, j) so it can exploit scan-order locality; the
40
+ // dispatch caches the once-per-list RTTI detection.
41
+ const IDSelectorContextDispatch sel_dispatch(sel, store_pairs);
38
42
  float threshold = handler.threshold;
39
43
  for (size_t j = 0; j < list_size; j++) {
40
44
  if (use_sel) {
41
45
  int64_t id = store_pairs ? lo_build(list_no, j) : ids[j];
42
46
  // skip code without computing distance
43
- if (!sel->is_member(id)) {
47
+ if (!sel_dispatch.is_member(id, IDScanContext{ids, list_size, j})) {
44
48
  codes += code_size;
45
49
  continue;
46
50
  }
@@ -40,6 +40,7 @@ void kernel_accumulate_block(
40
40
  #ifdef __AVX512F__
41
41
  if constexpr (
42
42
  KernelSL == SIMDLevel::AVX512 ||
43
+ KernelSL == SIMDLevel::AVX512_VPOPCNT ||
43
44
  KernelSL == SIMDLevel::AVX512_SPR) {
44
45
  pq4_kernel_qbs_512<NQ>(nsq, codes, LUT, res, scaler);
45
46
  } else {
@@ -545,7 +545,7 @@ std::unique_ptr<InvertedLists> read_InvertedLists_up(
545
545
  READ1(n_levels);
546
546
  FAISS_THROW_IF_NOT_FMT(
547
547
  n_levels > 0, "invalid ilpn n_levels %zd", n_levels);
548
- constexpr size_t bs = Panorama::kDefaultBatchSize;
548
+ constexpr size_t bs = Panorama::kLegacyBatchSize;
549
549
  auto ailp = std::make_unique<ArrayInvertedListsPanorama>(
550
550
  nlist, code_size, n_levels, bs);
551
551
  std::vector<size_t> sizes(nlist);
@@ -821,6 +821,10 @@ static void read_AdditiveQuantizer(AdditiveQuantizer& aq, IOReader* f) {
821
821
  }
822
822
 
823
823
  aq.set_derived_values();
824
+ FAISS_THROW_IF_NOT_FMT(
825
+ aq.code_size > 0,
826
+ "invalid AdditiveQuantizer: nbits sum to 0 bits, code_size %zd",
827
+ aq.code_size);
824
828
 
825
829
  // Sanity-check codebooks size without knowing the effective dimension.
826
830
  // codebooks stores effective_d * total_codebook_size floats, so its
@@ -1160,8 +1164,7 @@ void read_ScalarQuantizer(
1160
1164
  }
1161
1165
  }
1162
1166
 
1163
- // TurboQ full types: extract seed and qjl_type from trained,
1164
- // regenerate projection matrix.
1167
+ // TurboQ full types: extract seed and qjl_type from trained.
1165
1168
  if (ScalarQuantizer::TurboQuantRefine::is_turboq_full(ivsc->qtype) &&
1166
1169
  ivsc->trained.size() >= 3) {
1167
1170
  size_t n = ivsc->trained.size();
@@ -1170,7 +1173,6 @@ void read_ScalarQuantizer(
1170
1173
  ivsc->turboq_refine.seed =
1171
1174
  ScalarQuantizer::TurboQuantRefine::unpack_seed(
1172
1175
  ivsc->trained[n - 3], ivsc->trained[n - 2]);
1173
- ivsc->turboq_refine.init_projection(ivsc->d);
1174
1176
  }
1175
1177
  }
1176
1178
 
@@ -1688,6 +1690,10 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
1688
1690
  int d;
1689
1691
  size_t n_levels, batch_size;
1690
1692
  READ1(d);
1693
+ // This branch does not go through read_index_header, so the range
1694
+ // check that guards `d` for every other index type has to be
1695
+ // repeated here.
1696
+ FAISS_CHECK_RANGE(d, 0, (1 << 20) + 1);
1691
1697
  READ1(n_levels);
1692
1698
  FAISS_THROW_IF_NOT_FMT(n_levels > 0, "invalid n_levels %zd", n_levels);
1693
1699
  READ1(batch_size);
@@ -1702,24 +1708,36 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
1702
1708
  d, n_levels, batch_size);
1703
1709
  }
1704
1710
  READ1(idxp->ntotal);
1711
+ FAISS_THROW_IF_NOT_FMT(
1712
+ idxp->ntotal >= 0,
1713
+ "invalid ntotal %" PRId64 " read from IxFP index",
1714
+ (int64_t)idxp->ntotal);
1705
1715
  READ1_BOOL(idxp->is_trained);
1706
1716
  READVECTOR(idxp->codes);
1707
1717
  READVECTOR(idxp->cum_sums);
1708
- size_t num_slots = mul_no_overflow(
1709
- ((size_t)idxp->ntotal + idxp->batch_size - 1) /
1710
- idxp->batch_size,
1711
- idxp->batch_size,
1718
+ const size_t ntotal = (size_t)idxp->ntotal;
1719
+ const size_t num_batches =
1720
+ ntotal / batch_size + (ntotal % batch_size != 0);
1721
+ const size_t num_slots = mul_no_overflow(
1722
+ num_batches,
1723
+ batch_size,
1712
1724
  "IndexFlatPanorama num_batches*batch_size");
1713
- FAISS_THROW_IF_NOT(
1714
- idxp->codes.size() ==
1715
- mul_no_overflow(
1716
- num_slots, idxp->code_size, "IndexFlatPanorama codes"));
1717
- FAISS_THROW_IF_NOT(
1718
- idxp->cum_sums.size() ==
1719
- mul_no_overflow(
1720
- num_slots,
1721
- idxp->pano.n_levels + 1,
1722
- "IndexFlatPanorama cum_sums"));
1725
+ const size_t expected_codes_size = mul_no_overflow(
1726
+ num_slots, idxp->code_size, "IndexFlatPanorama codes");
1727
+ FAISS_THROW_IF_NOT_FMT(
1728
+ idxp->codes.size() == expected_codes_size,
1729
+ "IndexFlatPanorama codes size mismatch: got %zu, expected %zu",
1730
+ idxp->codes.size(),
1731
+ expected_codes_size);
1732
+ const size_t expected_cum_sums_size = mul_no_overflow(
1733
+ num_slots,
1734
+ idxp->pano.n_levels + 1,
1735
+ "IndexFlatPanorama cum_sums");
1736
+ FAISS_THROW_IF_NOT_FMT(
1737
+ idxp->cum_sums.size() == expected_cum_sums_size,
1738
+ "IndexFlatPanorama cum_sums size mismatch: got %zu, expected %zu",
1739
+ idxp->cum_sums.size(),
1740
+ expected_cum_sums_size);
1723
1741
  idxp->verbose = false;
1724
1742
  idx = std::move(idxp);
1725
1743
  } else if (
@@ -2136,7 +2154,7 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2136
2154
  read_ivf_header(ivfp.get(), f);
2137
2155
  ivfp->code_size = ivfp->d * sizeof(float);
2138
2156
  READ1(ivfp->n_levels);
2139
- ivfp->batch_size = Panorama::kDefaultBatchSize;
2157
+ ivfp->batch_size = Panorama::kLegacyBatchSize;
2140
2158
  read_InvertedLists(*ivfp, f, io_flags);
2141
2159
  idx = std::move(ivfp);
2142
2160
  } else if (h == fourcc("IwP2")) {
@@ -2163,6 +2181,12 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2163
2181
  read_ScalarQuantizer(&idxs->sq, f, *idxs);
2164
2182
  read_vector(idxs->codes, f);
2165
2183
  idxs->code_size = idxs->sq.code_size;
2184
+ FAISS_THROW_IF_NOT(
2185
+ idxs->codes.size() ==
2186
+ mul_no_overflow(
2187
+ (size_t)idxs->ntotal,
2188
+ idxs->code_size,
2189
+ "IndexScalarQuantizer codes"));
2166
2190
  idx = std::move(idxs);
2167
2191
  } else if (h == fourcc("IxLa")) {
2168
2192
  int d, nsq, scale_nbit, r2;
@@ -2374,8 +2398,18 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2374
2398
  // Validate transform chain dimension consistency:
2375
2399
  // chain[0].d_in must equal the outer index d, consecutive
2376
2400
  // transforms must have matching d_out/d_in, and the last
2377
- // transform's d_out must equal the sub-index d.
2378
- if (nt > 0) {
2401
+ // transform's d_out must equal the sub-index d. With an empty
2402
+ // chain the sub-index d must equal the outer d directly, because
2403
+ // reconstruct() then hands the caller's buffer -- sized from the
2404
+ // outer d -- straight to the sub-index.
2405
+ if (nt == 0 && ixpt->index) {
2406
+ FAISS_THROW_IF_NOT_FMT(
2407
+ ixpt->index->d == ixpt->d,
2408
+ "IndexPreTransform empty chain: sub-index d=%d "
2409
+ "!= index d=%d",
2410
+ ixpt->index->d,
2411
+ ixpt->d);
2412
+ } else if (nt > 0) {
2379
2413
  FAISS_THROW_IF_NOT_FMT(
2380
2414
  ixpt->chain[0]->d_in == ixpt->d,
2381
2415
  "IndexPreTransform chain[0] d_in=%d != index d=%d",
@@ -2497,7 +2531,7 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2497
2531
  } else if (
2498
2532
  h == fourcc("IHNf") || h == fourcc("IHNp") || h == fourcc("IHNs") ||
2499
2533
  h == fourcc("IHN2") || h == fourcc("IHNc") || h == fourcc("IHc2") ||
2500
- h == fourcc("IHfP") || h == fourcc("IH00")) {
2534
+ h == fourcc("IHfP") || h == fourcc("IHNr") || h == fourcc("IH00")) {
2501
2535
  std::unique_ptr<IndexHNSW> idxhnsw;
2502
2536
  if (h == fourcc("IH00")) {
2503
2537
  idxhnsw = std::make_unique<IndexHNSW>();
@@ -2515,6 +2549,8 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2515
2549
  idxhnsw = std::make_unique<IndexHNSWCagra>();
2516
2550
  } else if (h == fourcc("IHc2")) {
2517
2551
  idxhnsw = std::make_unique<IndexHNSWCagra>();
2552
+ } else if (h == fourcc("IHNr")) {
2553
+ idxhnsw = std::make_unique<IndexHNSWRaBitQ>();
2518
2554
  }
2519
2555
  read_index_header(*idxhnsw, f);
2520
2556
  if (h == fourcc("IHfP")) {
@@ -2525,10 +2561,21 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2525
2561
  "dynamic_cast to IndexHNSWFlatPanorama failed");
2526
2562
  size_t nlevels;
2527
2563
  READ1(nlevels);
2564
+ FAISS_THROW_IF_NOT_FMT(
2565
+ nlevels > 0, "invalid IHfP n_levels %zd", nlevels);
2528
2566
  const_cast<size_t&>(idx_panorama->num_panorama_levels) = nlevels;
2529
2567
  const_cast<Panorama&>(idx_panorama->pano) =
2530
2568
  Panorama(idx_panorama->d * sizeof(float), nlevels, 1);
2531
2569
  READVECTOR(idx_panorama->cum_sums);
2570
+ // Both the search path and get_cum_sum() index cum_sums at
2571
+ // stride pano.n_levels + 1, which may be lower than the
2572
+ // serialized nlevels if set_derived_values() truncated it.
2573
+ FAISS_THROW_IF_NOT(
2574
+ idx_panorama->cum_sums.size() ==
2575
+ mul_no_overflow(
2576
+ (size_t)idx_panorama->ntotal,
2577
+ idx_panorama->pano.n_levels + 1,
2578
+ "IndexHNSWFlatPanorama cum_sums"));
2532
2579
  } else if (h == fourcc("IHNc") || h == fourcc("IHc2")) {
2533
2580
  READ1_BOOL(idxhnsw->keep_max_size_level0);
2534
2581
  auto idx_hnsw_cagra = dynamic_cast<IndexHNSWCagra*>(idxhnsw.get());
@@ -2549,7 +2596,8 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2549
2596
  "HNSW levels size %zu != index ntotal %" PRId64,
2550
2597
  idxhnsw->hnsw.levels.size(),
2551
2598
  idxhnsw->ntotal);
2552
- idxhnsw->hnsw.is_panorama = (h == fourcc("IHfP"));
2599
+ idxhnsw->hnsw.search_method =
2600
+ h == fourcc("IHfP") ? HNSW::SM_PANORAMA : HNSW::SM_DEFAULT;
2553
2601
  // `HNSW::is_similarity` is intentionally not serialized, so we
2554
2602
  // re-derive it here from the persisted metric type. Without this,
2555
2603
  // a saved IP/similarity index would come back configured as a
@@ -2571,6 +2619,57 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2571
2619
  idxhnsw->storage->d,
2572
2620
  idxhnsw->d);
2573
2621
  }
2622
+ if (h == fourcc("IHNr")) {
2623
+ auto* idx_rabitq = dynamic_cast<IndexHNSWRaBitQ*>(idxhnsw.get());
2624
+ FAISS_THROW_IF_NOT_MSG(
2625
+ idx_rabitq, "IHNr must deserialize to an IndexHNSWRaBitQ");
2626
+ FAISS_THROW_IF_NOT_MSG(
2627
+ idxhnsw->metric_type == METRIC_L2,
2628
+ "IndexHNSWRaBitQ supports only the L2 metric");
2629
+ bool staged;
2630
+ READ1_BOOL(staged);
2631
+ idxhnsw->hnsw.search_method =
2632
+ staged ? HNSW::SM_RABITQ : HNSW::SM_DEFAULT;
2633
+ if (idxhnsw->storage) {
2634
+ auto* rq = dynamic_cast<IndexRaBitQ*>(idxhnsw->storage);
2635
+ FAISS_THROW_IF_NOT_MSG(
2636
+ rq, "IndexHNSWRaBitQ storage must be an IndexRaBitQ");
2637
+ FAISS_THROW_IF_NOT_MSG(
2638
+ rq->metric_type == idxhnsw->metric_type &&
2639
+ rq->rabitq.metric_type == idxhnsw->metric_type,
2640
+ "IndexHNSWRaBitQ storage metric mismatch");
2641
+ FAISS_THROW_IF_NOT_MSG(
2642
+ rq->is_trained == idxhnsw->is_trained,
2643
+ "IndexHNSWRaBitQ storage training state mismatch");
2644
+ FAISS_THROW_IF_NOT_FMT(
2645
+ rq->rabitq.nb_bits >= 1 && rq->rabitq.nb_bits <= 9,
2646
+ "invalid RaBitQ nb_bits=%zu",
2647
+ rq->rabitq.nb_bits);
2648
+ const size_t expected_code_size =
2649
+ rq->rabitq.compute_code_size(rq->d, rq->rabitq.nb_bits);
2650
+ validate_code_size_match(
2651
+ rq->rabitq.code_size,
2652
+ expected_code_size,
2653
+ "IndexHNSWRaBitQ quantizer");
2654
+ validate_code_size_match(
2655
+ rq->code_size,
2656
+ expected_code_size,
2657
+ "IndexHNSWRaBitQ storage");
2658
+ FAISS_THROW_IF_NOT(
2659
+ rq->codes.size() ==
2660
+ mul_no_overflow(
2661
+ static_cast<size_t>(rq->ntotal),
2662
+ rq->code_size,
2663
+ "IndexHNSWRaBitQ codes"));
2664
+ FAISS_THROW_IF_NOT_MSG(
2665
+ !rq->is_trained ||
2666
+ rq->center.size() == static_cast<size_t>(rq->d),
2667
+ "IndexHNSWRaBitQ center size mismatch");
2668
+ FAISS_THROW_IF_NOT_MSG(
2669
+ staged == (rq->rabitq.nb_bits >= 2),
2670
+ "IndexHNSWRaBitQ staged-search metadata mismatch");
2671
+ }
2672
+ }
2574
2673
  if (h == fourcc("IHN2")) {
2575
2674
  FAISS_THROW_IF_NOT_MSG(
2576
2675
  idxhnsw->storage,
@@ -2838,7 +2937,7 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2838
2937
  auto idxq = std::make_unique<IndexRaBitQ>();
2839
2938
  read_index_header(*idxq, f);
2840
2939
  read_RaBitQuantizer(idxq->rabitq, f, idxq->d, false);
2841
- READVECTOR(idxq->codes);
2940
+ read_vector(idxq->codes, f);
2842
2941
  READVECTOR(idxq->center);
2843
2942
  READ1(idxq->qb);
2844
2943
  // qb=0: Not quantized - direct distance computation on given float32s.
@@ -2857,7 +2956,7 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
2857
2956
  read_index_header(*idxq, f);
2858
2957
  read_RaBitQuantizer(
2859
2958
  idxq->rabitq, f, idxq->d, true); // Reads nb_bits from file
2860
- READVECTOR(idxq->codes);
2959
+ read_vector(idxq->codes, f);
2861
2960
  READVECTOR(idxq->center);
2862
2961
  READ1(idxq->qb);
2863
2962
  // qb=0: Not quantized - direct distance computation on given float32s.
@@ -3052,7 +3151,8 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
3052
3151
  bool initialized;
3053
3152
  READ1_BOOL(initialized);
3054
3153
  if (initialized) {
3055
- faiss::svs_io::ReaderStreambuf rbuf(f);
3154
+ faiss::svs_io::ReaderStreambuf rbuf(
3155
+ f, get_deserialization_vector_byte_limit());
3056
3156
  std::istream is(&rbuf);
3057
3157
  svs_ivf->deserialize_impl(is);
3058
3158
  }
@@ -3060,7 +3160,8 @@ std::unique_ptr<Index> read_index_up(IOReader* f, int io_flags) {
3060
3160
  bool trained;
3061
3161
  READ1_BOOL(trained);
3062
3162
  if (trained) {
3063
- faiss::svs_io::ReaderStreambuf rbuf(f);
3163
+ faiss::svs_io::ReaderStreambuf rbuf(
3164
+ f, get_deserialization_vector_byte_limit());
3064
3165
  std::istream is(&rbuf);
3065
3166
  auto* leanvec =
3066
3167
  dynamic_cast<IndexSVSIVFLeanVec*>(svs_ivf.get());
@@ -3390,7 +3491,7 @@ std::unique_ptr<IndexBinary> read_index_binary_up(IOReader* f, int io_flags) {
3390
3491
  auto idxhnsw = std::make_unique<IndexBinaryHNSW>();
3391
3492
  read_index_binary_header(*idxhnsw, f);
3392
3493
  read_HNSW(idxhnsw->hnsw, f);
3393
- idxhnsw->hnsw.is_panorama = false;
3494
+ idxhnsw->hnsw.search_method = HNSW::SM_DEFAULT;
3394
3495
  FAISS_THROW_IF_NOT_FMT(
3395
3496
  idxhnsw->hnsw.levels.size() == (size_t)idxhnsw->ntotal,
3396
3497
  "IndexBinaryHNSW HNSW levels size %zu != ntotal %" PRId64,
@@ -3414,7 +3515,7 @@ std::unique_ptr<IndexBinary> read_index_binary_up(IOReader* f, int io_flags) {
3414
3515
  READ1_BOOL(idxhnsw->base_level_only);
3415
3516
  READ1(idxhnsw->num_base_level_search_entrypoints);
3416
3517
  read_HNSW(idxhnsw->hnsw, f);
3417
- idxhnsw->hnsw.is_panorama = false;
3518
+ idxhnsw->hnsw.search_method = HNSW::SM_DEFAULT;
3418
3519
  FAISS_THROW_IF_NOT_FMT(
3419
3520
  idxhnsw->hnsw.levels.size() == (size_t)idxhnsw->ntotal,
3420
3521
  "IndexBinaryHNSWCagra HNSW levels size %zu != ntotal %" PRId64,
@@ -275,20 +275,12 @@ void write_InvertedLists(const InvertedLists* ils, IOWriter* f) {
275
275
  } else if (
276
276
  const auto& ailp =
277
277
  dynamic_cast<const ArrayInvertedListsPanorama*>(ils)) {
278
- if (ailp->pano.batch_size == Panorama::kDefaultBatchSize) {
279
- uint32_t h = fourcc("ilpn");
280
- WRITE1(h);
281
- WRITE1(ailp->nlist);
282
- WRITE1(ailp->code_size);
283
- WRITE1(ailp->n_levels);
284
- } else {
285
- uint32_t h = fourcc("ilp2");
286
- WRITE1(h);
287
- WRITE1(ailp->nlist);
288
- WRITE1(ailp->code_size);
289
- WRITE1(ailp->n_levels);
290
- WRITE1(ailp->pano.batch_size);
291
- }
278
+ uint32_t h = fourcc("ilp2");
279
+ WRITE1(h);
280
+ WRITE1(ailp->nlist);
281
+ WRITE1(ailp->code_size);
282
+ WRITE1(ailp->n_levels);
283
+ WRITE1(ailp->pano.batch_size);
292
284
  uint32_t list_type = fourcc("full");
293
285
  WRITE1(list_type);
294
286
  std::vector<size_t> sizes;
@@ -736,18 +728,11 @@ void write_index(const Index* idx, IOWriter* f, int io_flags) {
736
728
  } else if (
737
729
  const IndexIVFFlatPanorama* ivfp =
738
730
  dynamic_cast<const IndexIVFFlatPanorama*>(idx)) {
739
- if (ivfp->batch_size == Panorama::kDefaultBatchSize) {
740
- uint32_t h = fourcc("IwPn");
741
- WRITE1(h);
742
- write_ivf_header(ivfp, f);
743
- WRITE1(ivfp->n_levels);
744
- } else {
745
- uint32_t h = fourcc("IwP2");
746
- WRITE1(h);
747
- write_ivf_header(ivfp, f);
748
- WRITE1(ivfp->n_levels);
749
- WRITE1(ivfp->batch_size);
750
- }
731
+ uint32_t h = fourcc("IwP2");
732
+ WRITE1(h);
733
+ write_ivf_header(ivfp, f);
734
+ WRITE1(ivfp->n_levels);
735
+ WRITE1(ivfp->batch_size);
751
736
  write_InvertedLists(ivfp->invlists, f);
752
737
  } else if (
753
738
  const IndexIVFFlat* ivfl_2 =
@@ -888,12 +873,22 @@ void write_index(const Index* idx, IOWriter* f, int io_flags) {
888
873
  : dynamic_cast<const IndexHNSWSQ*>(idx) ? fourcc("IHNs")
889
874
  : dynamic_cast<const IndexHNSW2Level*>(idx) ? fourcc("IHN2")
890
875
  : dynamic_cast<const IndexHNSWCagra*>(idx) ? fourcc("IHc2")
876
+ : dynamic_cast<const IndexHNSWRaBitQ*>(idx) ? fourcc("IHNr")
891
877
  : typeid(*idx) == typeid(IndexHNSW) ? fourcc("IH00")
892
878
  : 0;
893
879
  FAISS_THROW_IF_NOT_FMT(
894
880
  h != 0,
895
881
  "don't know how to serialize this IndexHNSW subtype: %s",
896
882
  typeid(*idx).name());
883
+ const IndexRaBitQ* storage_rabitq = nullptr;
884
+ if (h == fourcc("IHNr")) {
885
+ storage_rabitq = dynamic_cast<const IndexRaBitQ*>(idxhnsw->storage);
886
+ FAISS_THROW_IF_NOT_MSG(
887
+ storage_rabitq ||
888
+ ((io_flags & IO_FLAG_SKIP_STORAGE) &&
889
+ idxhnsw->storage == nullptr),
890
+ "IndexHNSWRaBitQ requires IndexRaBitQ storage");
891
+ }
897
892
  WRITE1(h);
898
893
  write_index_header(idxhnsw, f);
899
894
  if (h == fourcc("IHfP")) {
@@ -916,6 +911,16 @@ void write_index(const Index* idx, IOWriter* f, int io_flags) {
916
911
  } else {
917
912
  write_index(idxhnsw->storage, f);
918
913
  }
914
+ if (h == fourcc("IHNr")) {
915
+ // The staged flag is graph-traversal state, so it has to live here:
916
+ // with IO_FLAG_SKIP_STORAGE there is no storage to derive it from.
917
+ // Storage-owned settings are not duplicated in this payload;
918
+ // IndexRaBitQ serializes whatever it owns.
919
+ const bool staged = storage_rabitq
920
+ ? storage_rabitq->rabitq.nb_bits >= 2
921
+ : idxhnsw->hnsw.search_method == HNSW::SM_RABITQ;
922
+ WRITE1(staged);
923
+ }
919
924
  } else if (const IndexNSG* idxnsg = dynamic_cast<const IndexNSG*>(idx)) {
920
925
  uint32_t h = dynamic_cast<const IndexNSGFlat*>(idx) ? fourcc("INSf")
921
926
  : dynamic_cast<const IndexNSGPQ*>(idx) ? fourcc("INSp")