Commit Graph
29 Commits
Author SHA1 Message Date
Michael Norris a424dcb809 faiss HNSW: add opt-in deterministic lock-free graph build (faiss::hnsw_deterministic_build) (#5486)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5486

TLDR: adds deterministic HNSW build inspired by ParlayANN. The deterministic build is reproducible AND faster than the lock-based build at every scale and thread count we measured. This is gated behind `faiss::hnsw_deterministic_build`. We plan to test internally, then make this the default flow and remove the existing flow.
--

https://arxiv.org/abs/2305.04359
--

HOW TO ENABLE
--
```
C++:     faiss::hnsw_deterministic_build = true;
Python:  faiss.cvar.hnsw_deterministic_build = True
```
Nothing else is needed, and nothing changes until you do it: the flag defaults
to false, so this diff is a no-op on its own.

An embedder driving this from its own configuration system should resolve the
value once at startup and assign the global, rather than per index: changing it
between two `add()` calls would leave one index built by each algorithm.

`python/__init__.pyi` now declares `faiss.cvar`. It was missing, so any Python
consumer assigning a faiss global failed Pyre with "No attribute `cvar` in
module `faiss`" -- fixed here rather than with a `pyre-ignore` in each consumer.

WHEN IT IS WORTH ENABLING
--
Only for builds that run multi-threaded. At one thread the existing lock-based
build is *already* deterministic, so the flag buys nothing beyond ~6% build
time. Verified: lock-based at OMP=1 is byte-identical across runs, while at
OMP=8 46% of neighbor slots differ. A consumer that pins
`omp_set_num_threads(1)` should not bother.

similarities to parlayANN:
- add vertices in doubling batches against frozen snapshot
- defer adding reciprocal edges immediately, add them later after parallel phase

differences from ParlayANN:
- re-uses Faiss HNSW pruning in `shrink_neighbor_list`

 ---

AI (with a bunch of edits) explanation in more detail:
--

What changed
- `IndexHNSW::add` and `IndexBinaryHNSW::add` pick the build at runtime. Both paths are compiled in and **the default is unchanged** (lock-based); nothing switches until the flag or the hook says so.
- Turning it on:
```
C++:     faiss::hnsw_deterministic_build = true;
Python:  faiss.cvar.hnsw_deterministic_build = True
```
- A single plain `bool` is the whole API: libfaiss takes no dependency on any configuration system, and an embedder that wants one resolves it itself and assigns the global:
```
faiss.cvar.hnsw_deterministic_build = <your config lookup>
```
- Resolve it once at startup, not per index. `add()` reads the flag each call, so changing it between two `add()` calls on one index would build part of the graph with each algorithm -- reproducible in neither.
- **This flag is transitional.** Once the deterministic build is validated in production, the follow-up makes it the only path and deletes the lock-based build and the flag.
- The deterministic path now supports the CAGRA level-0 import configuration: `init_level0=false` skips the level-0-only bucket (level 0 is supplied by the imported CAGRA graph), and `keep_max_size_level0` fills the base layer to 2*M. So `IndexHNSWCagra` (CPU) and `GpuIndexCagra::copyTo(IndexHNSWCagra*)` build through the deterministic path.
- `IndexBinaryHNSW::add` is gated by the same flag and shares the same deterministic implementation: `hnsw_add_vertices_deterministic` takes `make_distance_computer` / `set_query` callbacks, so the binary index just supplies its own Hamming distance computer. It keeps its own lock-based `hnsw_add_vertices` for the default path.

Background
--
HNSW construction in Faiss was non-deterministic under parallel builds: multiple runs of `IndexHNSW::add` with the same data and seeds could produce different graphs, a problem for persistence, crash recovery, and replication (the ParlayANN motivation, https://arxiv.org/abs/2305.04359). Sources of non-determinism were: (1) the reciprocal-link write race in `add_links_starting_from_impl`; (2) floating-point distance ties resolved in heap/visitation order; (3) the entry-point bootstrap `#pragma omp critical` race.

Algorithm (adapted from ParlayANN to Faiss's level-batched structure):
- Per level bucket (highest first, deterministic shuffle), points are inserted in prefix-doubling sub-batches (batch sizes 1, 2, 4, ... capped at 2% of the index).
- Phase A (`HNSW::compute_forward_links_deterministic`, parallel): each point greedily descends and computes its forward links against the immutable snapshot from the end of the previous sub-batch, writing only its own neighbor slots. Reciprocal-edge requests are collected, not applied, so this phase is race-free.
- Phase B (`HNSW::merge_reverse_links_deterministic`, parallel): reverse edges are grouped by destination with a fixed-size 256-bucket radix partition on the low bits of `dest` (a small constant bucket count, independent of `ntotal` and thread count, so grouping stays O(edges) in memory), each bucket sorted by `(level, dest)` and merged in parallel. Every affected node is merged exactly once in a total order (distance, ties by id) and re-pruned with the same RNG heuristic. Because every `dest` maps to exactly one bucket, distinct nodes touch disjoint slots (no locks) and the merge is order- and thread-count-independent. The Phase-B parallel-for uses `schedule(static)` — the libomp dynamic dispatcher segfaults in some build configs (the pre-existing lock-based build carried the same warning).

Guarantee: the resulting graph is reproducible across runs at a fixed thread count and, in practice, across thread counts (the merge is fully order-independent). Recall matches the previous default at every efSearch.

## Performance: build time (40M, d=128, M=32, efC=64, 166 threads)
10-round interleaved timing study (one deterministic + one lock-based build per round, so both see identical host conditions):
  deterministic per-round s: 285.58 275.03 280.84 272.62 273.73 272.61 272.05 272.90 269.87 272.29
  lock-based    per-round s: 306.23 352.97 322.76 294.23 339.32 303.04 291.46 341.96 359.31 282.92
  deterministic: min=269.87 mean=274.75 median=272.76 max=285.58 std=4.53
  lock-based:    min=282.92 mean=319.42 median=314.50 max=359.31 std=26.12
  det/lock: mean=0.860 (deterministic ~14% faster), median=0.867
The deterministic build is ~14% faster than the lock-based build at 40M and ~6x more stable run-to-run (std 4.53s vs 26.12s), since it does not depend on lock-contention timing. Peak RSS ~66GB vs ~56GB. Recall matches at every efSearch (byte-identical graph across builds).

## Performance: search time

Back on the deterministic HEAD, tree clean. Here's the matched A/B — same 40M synthetic data, same machine (AMD Genoa, 166 cores),
  search_repeat=100, deterministic (my HEAD) vs lock-based (parent commit). Since my diff doesn't touch search() at all, any difference is
  purely graph structure + measurement noise.

  Search QPS: deterministic vs lock-based (40M synthetic, repeat=100)

  HNSW16
```
  ┌──────────┬─────────────────┬─────────┬──────────┬───────┐
  │ efSearch │ recall det/lock │ QPS det │ QPS lock │   Δ   │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │       64 │ 0.828/0.820     │ 170,329 │  177,995 │ −4.3% │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │      128 │ 0.866/0.862     │ 112,727 │  110,727 │ +1.8% │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │      256 │ 0.886/0.888     │  59,815 │   56,784 │ +5.3% │
  └──────────┴─────────────────┴─────────┴──────────┴───────┘
```
  HNSW32
```
  ┌──────────┬─────────────────┬─────────┬──────────┬───────┐
  │ efSearch │ recall det/lock │ QPS det │ QPS lock │   Δ   │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │       64 │ 0.935/0.930     │ 110,186 │  108,411 │ +1.6% │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │      128 │ 0.958/0.953     │  68,019 │   66,308 │ +2.6% │
  ├──────────┼─────────────────┼─────────┼──────────┼───────┤
  │      256 │ 0.965/0.960     │  37,624 │   35,828 │ +5.0% │
  └──────────┴─────────────────┴─────────┴──────────┴───────┘
```
  HNSW32,SQ8
```
  ┌──────────┬─────────────────┬─────────┬──────────┬────────┐
  │ efSearch │ recall det/lock │ QPS det │ QPS lock │   Δ    │
  ├──────────┼─────────────────┼─────────┼──────────┼────────┤
  │       64 │ 0.926/0.934     │ 220,713 │  198,325 │ +11.3% │
  ├──────────┼─────────────────┼─────────┼──────────┼────────┤
  │      128 │ 0.948/0.953     │ 117,504 │  129,173 │  −9.0% │
  ├──────────┼─────────────────┼─────────┼──────────┼────────┤
  │      256 │ 0.961/0.963     │  58,582 │   65,551 │ −10.6% │
  └──────────┴─────────────────┴─────────┴──────────┴────────┘
```
(Low-ef points ef16/32 omitted from the verdict — even at 100 repeats their std is ~8–20%, too noisy; ef128/256 std is ~3–5%.)

Verdict: no search-QPS regression
- Pure HNSW (16, 32): QPS at parity — within ±5%, and actually slightly faster deterministic at the high-recall points (ef128/256), with
  equal-or-better recall.
- HNSW32,SQ8: more scatter (±10%, mixed direction) — but it tracks small correlated recall differences (det ef256 is 0.961 vs 0.963),
  i.e. the two different graphs sit at slightly different recall/QPS operating points, not a systematic slowdown. Search code is
  identical, so this is graph-structure + noise, not a code regression. If you want it pinned down, a recall-matched (interpolated)
  comparison would remove the operating-point confound.
- Bonus: the deterministic build was 2–3× faster in every case (e.g. HNSW32: 277 s vs 527 s; HNSW16: 164 s vs 429 s) — consistent with
  all prior results.

## Single-threaded (OMP_NUM_THREADS=1)
Customers frequently build with OMP=1 or OpenMP disabled, so this case matters. Measured at 1M / d=128 / M=32 / efC=64, single-threaded:
  build time: lock-based 188.36s vs deterministic 176.40s  (0.94x -> deterministic ~6% FASTER)
  peak RSS:   1.6 GB (both, identical)
  recall@10 ef 16/32/64/128: lock-based .8830/.9387/.9676/.9853 vs deterministic .8832/.9381/.9625/.9798
No single-threaded regression: the deterministic build is slightly faster (it avoids the per-node OpenMP lock ops), uses the same memory, and matches recall within noise. Note the lock-based build was already deterministic at a single thread, so single-threaded users lose nothing and gain a small speedup.

## Serialization compatibility
No on-disk format change, verified in `index_read.cpp` / `index_write.cpp`:
- `hnsw_deterministic_build` is a namespace-scope global, not a field on `HNSW` or `IndexHNSW`, so it is not part of any serialized struct. Like `retain_locks`, it is a pure runtime build flag.
- `write_HNSW` / `read_HNSW` and the `IndexHNSW` field layout are unchanged. The subtype fourcc tags, header, CAGRA block, graph CSR (entry_point / max_level / levels / offsets / neighbors / efC / efS), and storage are all as before.
- `keep_max_size_level0` is still serialized only for the CAGRA subtype (`IHc2`/`IHNc`); `init_level0` is build-only (not serialized).
- The deterministic build emits the same HNSW CSR structure (only neighbor content differs), so old indexes read unchanged and new indexes remain readable by older Faiss.
- Verified by the `io_and_retest` serialize -> deserialize -> re-search round-trips in `test_graph_based.py` / `test_hnsw.cpp` (all pass).
- Measured on a 1M index: written with the flag off, flag flipped on, read back
  -> `neighbors`, `offsets`, `entry_point`, `max_level` byte-identical and
  search ids+distances identical. The flag is read only inside `add()`.
- Mixed-mode append (lock-built graph extended by a deterministic `add()`) was
  measured against brute-force ground truth at 220k and is indistinguishable
  from either pure build -- recall@10 within +-0.02 of both at ef 32/64/128. No
  existing test covers this, since every test builds one way in one process.

## CAGRA API for HNSW build on multi-GPU (aka D106837134) — MAST verification
Verified end-to-end on MAST (8x H100 Grand Teton, Approach D, 100M vectors) with this change in the build — the multi-GPU CAGRA -> HNSW graph-build time is comparable to the D106837134 baseline (no regression):
  all_neighbors build: 367.4s   optimize: 231.7s   copyTo: 18.4s   serialize: 28.9s (66 GB)
  INDEX build -> serialize total: 661.7s (11.0 min)   [D106837134 baseline: 721s]
  recall@10 (tiled 100M): ef64 0.7746, ef128 0.8830, ef256 0.9429
- This confirms this CPU-side change builds, links, and runs in the GPU CAGRA binary at scale and does not regress the pipeline. Note the Approach-D run uses copyTo(base_level_only=True), which imports the CAGRA graph directly as HNSW level 0 and skips add(), so it does not itself route through the deterministic add().
- The deterministic CAGRA level-0 import this change adds (the copyTo path with base_level_only=False: init_level0=false skips the level-0 bucket; keep_max_size_level0 fills the base layer) is covered by passing unit tests: `Test_IndexHNSWCagra_BaseLevelOnly_RangeSearch` (C++), `test_hnsw_no_init_level0`, and `test_hnsw_cagra_IP` / `_base_level_only` (Python).

## Behavioral note: level-0 base layer under keep_max_size_level0 (reviewers, please note)
One deliberate difference from the lock-based build, in the CAGRA base-layer case only: the old build gated the "fill the level-0 list up to 2*M" behavior on the inserted point's OWN top level (`keep_max_size_level0 && pt_level == 0`), so a level>=1 node's level-0 list could be pruned below 2*M. The deterministic build gates on the LINK level (`keep_max_size_level0 && level == 0`), so EVERY node's level-0 list is filled to 2*M when `keep_max_size_level0` is set (not only the level-0-only points). This is a strict superset of the old coverage -- it fills exactly to the 2*M slot capacity (no overflow) and yields a fuller/denser base layer for CPU `IndexHNSWCagra`, which is what `GpuIndexCagra::copyFrom(IndexHNSWCagra*)` reads back. It is INERT for the default build (`keep_max_size_level0` defaults to false, so the gate is never true) and never affects a non-CAGRA graph. Called out explicitly so reviewers know the CPU `IndexHNSWCagra` base-layer graph is intentionally denser than the pre-diff build; worth a sanity check against GPU `copyFrom` expectations.

Reviewed By: pankajsingh88

Differential Revision: D112025877

fbshipit-source-id: dd111a5a3674c08200905bfea1d056f0fa5b3d42
2026-08-10 14:32:47 -07:00
Tia Lim 22220d2c4f Fix genuine flake8 lint in faiss Python (F401/F403/F841/B011) (#5351)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5351

Fixes 45 genuine (non-style) flake8 issues across 20 faiss Python files, on top of the black-80 base in this stack. These are real code issues, not formatting.

- `B011` (19): replaced `assert False` / `assert False, "msg"` with `raise AssertionError(...)`. `assert` statements are stripped under `python -O`, so an `assert False` guard silently vanishes in optimized builds; `raise AssertionError` always fires.
- `F401` (10) / `F403` (7): the intentional star-import re-exports in `python/loader.py` and `python/loader_fb.py` (which expose the SWIG symbols as `faiss.X`) are annotated with `# noqa: F401,F403`; genuinely unused imports in test/bench scripts are removed.
- `F841` (9): unused local variables removed, or replaced with `_` in tuple unpacking, preserving behavior.

No runtime behavior changes; formatting stays at black width 80.

Reviewed By: mnorris11

Differential Revision: D109739445

fbshipit-source-id: 7d140d86cab9b2df691003bd043fc880a31410cd
2026-06-29 18:38:01 -07:00
Tia Lim 0f0f728166 Format faiss Python files with black (line length 80) (#5349)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5349

# this is a no-op change

Ran `black -l 80` over all tracked Python files under `fbcode/faiss/`, using line length 80 to match faiss's documented coding style (`CONTRIBUTING.md`: "80 character line length (both for C++ and Python)"). 172 files were reformatted; 28 were already compliant.

This is a pure-formatting change. `black` runs its AST-equivalence safety check on every file, so no identifiers, string/numeric values, operators, or control flow were altered — only whitespace, indentation, line wrapping, blank lines, trailing commas, and string-quote normalization (single to double quotes).

`fbcode/faiss/` is intentionally excluded from Meta's standard Python autoformatter (`arc f` / `arc lint -a` report no formatter for these paths) because it mirrors to public GitHub, so the Python had drifted out of any consistent style. The C++ is already clang-format-clean (0 of 807 files change), so this effort is Python-only. This diff brings the Python to canonical `black` formatting.

Excluded `demos/index_pq_flat_separate_codes_from_codebook.py`: it contains a Jupyter `!rm` shell-magic line and is not valid Python, so `black` cannot parse it.

This is the base of a 3-diff stack: (1) this black reformat; (2) `.flake8` alignment (max-line-length 80 + black-compatible ignores E203/W503); (3) genuine lint fixes (F401/F403/F841/B011).

Reviewed By: mnorris11

Differential Revision: D109736099

fbshipit-source-id: 9a054635766ed11aa3760a788395babd3d198abf
2026-06-29 17:29:05 -07:00
Tia Lim 395eaf8c6f De-flake panorama / graph_based / hamming_utils tests (#5339)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5339

These three faiss CPU test modules were filed as flaky by the test-health tracker. Root causes and fixes (test-only):

- `test_flat_panorama.py::test_batch_boundaries`: the query matrix `xq` was drawn from an unseeded global NumPy RNG (the existing `np.random.seed(987)` only governs the database `xb`), so near-tied neighbors occasionally reordered between `IndexFlat` and `IndexFlatPanorama` and breached the tight comparison. Seed the query draw, and loosen the distance `atol` in `assert_search_results_equal` from `1e-7` to `1e-4` (the two distance formulas `(a-b)^2` vs `a*a+b*b-2ab` differ at the float32 ULP level).
- `test_graph_based.py`: `test_hnsw_flat_IP`, `test_nsg_pq`, and `test_nsg_sq` asserted recall/agreement floors only a few counts above the observed values, so OpenMP-parallel graph-build nondeterminism intermittently breached them. Loosened each floor with documented ~5-10% headroom; these still collapse by tens-to-hundreds of points on a real regression.
- `test_hamming_utils.py` (`BUCK` only): `for_all_simd_levels` mutates the process-global `faiss.SIMDConfig` level in `setUp`/`tearDown`; concurrent SIMD-level variant cases within one binary could stomp it. Gave `test_hamming_utils` a dedicated `python_unittest` target carrying `tpx_labels.serialize_test_cases` so its cases run one at a time. No source change to the test itself.

___

Differential Revision: D109488320

fbshipit-source-id: b9ec2ab6547fc4424072588558409de62f3dac01
2026-06-24 03:09:52 -07:00
Matthijs Douze e6b8f6de53 IndexHNSW: use HNSW::is_similarity for IP/similarity metrics + tests (#5226)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5226

Now that HNSW supports a native similarity mode (`hnsw.is_similarity`),
IndexHNSW no longer needs to wrap the storage's DistanceComputer in a
`NegativeDistanceComputer` to fake distance ordering for IP /
similarity metrics. The native distance computer is used directly,
HNSW search / build dispatch on `is_similarity` to pick the CMin
comparator, and the post-search `distances[i] = -distances[i]`
correction loops are gone.

## Code changes

- IndexHNSW constructors set
  `hnsw.is_similarity = is_similarity_metric(metric)`.
- `storage_distance_computer` returns the storage's raw DC (no
  NegativeDistanceComputer wrap).
- IndexHNSW::search / range_search / search1 dispatch the
  HeapBlockResultHandler / RangeSearchBlockResultHandler /
  SingleQueryBlockResultHandler on the metric, instantiating with
  HNSW::C_similarity (CMin) for similarity metrics and HNSW::C_distance
  (CMax) otherwise. The negation post-passes are removed.
- IndexHNSW::search_level_0 wraps its body in a templated lambda
  dispatched on the metric; the RH and the negation post-pass are
  gone.
- IndexHNSWCagra::search and IndexHNSWCagra::range_search dispatch
  their random-entry-point selection on C: they use `C::neutral()` as
  the initial "worst" sentinel and `C::cmp` for the better-than test,
  so the routine works correctly under both CMax and CMin. The range
  variant also drops the `-radius` flip and the negation post-pass.
- index_read.cpp: re-derive `hnsw.is_similarity` from the deserialized
  metric_type so loaded IP/similarity indices come back with the
  correct HNSW comparator. `HNSW::is_similarity` is intentionally not
  serialized; the read path is the only place it has to be re-derived.

The IndexHNSW2Level mixed-search code path keeps its hand-rolled
max-heap logic and is now only correct for L2 storage, which is the
historical configuration; if someone needs IP there in the future it
will need its own dispatch. IndexHNSWFlatPanorama still asserts
`!hnsw.is_similarity` (Panorama's progressive bound is L2-specific).

## Test coverage

Adds TestHNSWSimilarity in test_graph_based.py, which exercises every
IndexHNSW* variant (Flat, PQ, SQ, Cagra including base_level_only)
with METRIC_INNER_PRODUCT — paths that previously had essentially zero
test coverage. A single `_check_quantized_IP` helper drives each
variant through build -> train (if needed) -> add -> search ->
serialize -> deserialize -> search-again and asserts both top-1
agreement against IndexFlatIP and that hnsw.is_similarity survives
the round trip (important because the field is intentionally not
serialized and must be re-derived from metric_type by index_read).

| Test                                  | Covers                                                    |
|---------------------------------------|-----------------------------------------------------------|
| test_hnsw_flat_IP_is_similarity_set   | Constructor sets is_similarity for IP, clears it for L2.  |
| test_hnsw_flat_IP                     | IndexHNSWFlat IP build + search + save/load.              |
| test_hnsw_pq_IP                       | IndexHNSWPQ IP build + train + search + save/load.        |
| test_hnsw_sq_IP                       | IndexHNSWSQ (QT_8bit) IP build + train + search + save/load. |
| test_hnsw_cagra_IP                    | IndexHNSWCagra IP build + search + save/load.             |
| test_hnsw_cagra_IP_base_level_only    | Cagra base-only path (random entry-point + search_level_0). |

Agreement floors are calibrated against measured top-1 counts (Flat:
199, PQ: 26-30, SQ: 195, Cagra: 200 over nq=200) with ~5-10 points of
slack to absorb RNG noise — tight enough to catch ordering
regressions (which would collapse agreement to ~0) and meaningful
quantizer regressions.

Before this change the only HNSW + METRIC_INNER_PRODUCT coverage in
test_graph_based.py was the single-search `TestHNSW.test_hnsw_IP` and
`Issue3684.test_issue3684` (cosine via L2-normalized IP +
range_search). HNSWPQ / HNSWSQ / HNSWCagra / Cagra base-only and
save/load round trips were uncovered.

## Test Plan

-

Reviewed By: mnorris11

Differential Revision: D105968832

fbshipit-source-id: e8e1d4d56e6b023c3854941cedcd1f1f10a65c3c
2026-05-30 02:02:42 -07:00
Matthijs Douze c0575f2c13 avoid runtime checks in VisitedTable (#5234)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5234

There are two VisitedTable implementations. Avoid runtime checks between the two by making them separate classes and templatizing the visited tables on them.

Reviewed By: mnorris11

Differential Revision: D105961848

fbshipit-source-id: 8c82c4ba2cf13e221a4f6a4e534f49e58073355d
2026-05-30 02:02:42 -07:00
Alibek Zhakubayev 97d257433a Fix correctness bugs in NNDescent Nhood copy/move operations and gen_random bounds (#5072)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5072

Fix correctness bugs in `NNDescent` `Nhood` copy/move operations and `gen_random` bounds.

## Bug 1: Broken Nhood copy constructor and copy assignment operator

The copy constructor and copy assignment operator for `Nhood` were incomplete:
- Copy assignment used `std::back_inserter` to append to `nn_new` instead of replacing it, leading to duplicate entries on reassignment and heap-use-after-free on self-assignment.
- Neither operation copied `pool`, `nn_old`, `rnn_old`, or `rnn_new`, meaning copied `Nhood` objects had missing neighbor data.
- This caused data loss when `std::vector<Nhood>` reallocated during `push_back`.

Fixed both operations to properly copy all 6 data members. Added self-assignment guard (`if (this != &other)`) to the copy assignment operator. Changed the copy constructor to use a member initializer list in declaration order to avoid `-Wreorder` warnings.

**Proof:** `NhoodCopy.CopyConstructorPreservesAllFields` and `NhoodCopy.CopyAssignmentPreservesAllFields` fail without the fix — `pool.size()` is 0 (expected 3), `nn_old`, `rnn_new`, `rnn_old` are all empty. `NhoodCopy.CopyAssignmentSelfAssign` triggers heap-use-after-free without the self-assignment guard. `NhoodCopy.VectorReallocationPreservesData` shows data loss during `std::vector<Nhood>` reallocation.

## Bug 2: Division by zero in `gen_random`

When `size == N`, the expression `rng() % (N - size)` is a division by zero (undefined behavior). This occurs in `search()` when `search_L` or `topk` equals `ntotal`, because `L_2 = max(search_L, topk)` is passed to `gen_random(rng, init_ids.data(), L_2, ntotal)`.

Added a precondition assertion (`size > 0 && size <= N`) and a Fisher-Yates shuffle for the `size == N` special case.

**Proof:** `TestNNDescentGenRandom.test_search_L_equals_ntotal` crashes (process killed by SIGFPE) without the fix, passes with it.

## Performance: Added move constructor and move assignment operator

Since `std::mutex` is neither copyable nor movable, the compiler cannot generate implicit move operations for `Nhood`. With user-defined copy operations, implicit move generation is suppressed entirely. Without explicit move operations, `std::vector<Nhood>::push_back(Nhood&&)` falls back to copy — 5 unnecessary vector allocations per element.

Added `noexcept` move constructor and move assignment operator. `noexcept` is required for `std::vector` to prefer move over copy during reallocation. The move assignment operator is included for Rule of Five consistency.

**Proof:** All tests pass both with and without move operations, confirming these are a performance optimization, not a correctness fix.

## Cleanup: Removed misleading `omp_get_thread_num()`

The RNG seed in `nndescent()` was `random_seed * 6577 + omp_get_thread_num()`. This function is not inside any `#pragma omp parallel` region — the call chain is `IndexNNDescent::add()` -> `NNDescent::build()` -> `NNDescent::nndescent()`, all sequential. Per the OpenMP specification, `omp_get_thread_num()` returns 0 in sequential context. The `+ 0` is dead code.

**Proof:** No behavioral change. The seed was always `random_seed * 6577`.

Reviewed By: mnorris11

Differential Revision: D100155792

fbshipit-source-id: 042a7d0a53a7696915a96bf1e48a464507f044b3
2026-04-09 20:12:31 -07:00
Gergely Szilvasy f226be432c Roll out @for_all_simd_levels to key test files (#5061)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5061

Roll out the `for_all_simd_levels` decorator to 62 test classes across
11 test files. This ensures that every available SIMD level (NONE, AVX2,
AVX512, etc.) is exercised in CI, rather than only testing at the
auto-detected (highest) level.

Previously only `TestExtraDistances` in `test_extra_distances.py` used
the decorator. Now all test files covering DD-dispatched code paths are
parameterized: distances, PQ, SQ, fast scan, RaBitQ, partitioning,
HNSW, and binary indices.

Changes:
- Add `for_all_simd_levels` to 62 test classes across 11 test files
- BUCK: move decorated test targets to `supports_static_listing = False`
  (required because the decorator replaces class names with None,
  breaking TPX's static test enumeration)
- test_fast_scan.py, test_fast_scan_ivf.py: apply decorator manually
  after dynamic `setattr` method generation for TestAQFastScan and
  TestIVFAQFastScan (the `setattr` loops reference the class by name)
- test_rabitq_fastscan.py: extract `_create_fastscan_index` as
  module-level helper to fix cross-class method reference that broke
  when TestRaBitQFastScan was replaced by the decorator
- IndexFastScan.cpp: fix `search_implem_14` to auto-cap `qbs` based
  on `bbs`. The accumulate loop dispatch table only instantiates
  certain (nq, BB) pairs (e.g. BB=2 only has nq=1,2). Previously,
  using bbs=64 with the default qbs (batch of 4) would crash with
  "nq=3 bbs=64 not instantiated". Now the query batch size is
  automatically capped to the max nq supported for the given BB.
  Exposed by per-level testing of test_factory_with_batch_size.

Reviewed By: mdouze

Differential Revision: D99978401

fbshipit-source-id: 54858f3810bab91ac5ec8bf0ce0d55a77710727b
2026-04-08 10:56:41 -07:00
Michael Norris 7b2705c269 Debug manifold crash with NaN (#5025)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5025

In c++ when running in dev mode:
```
  Crash chain:
  1. index->add(1, vec) → IndexIVF::add_with_ids() (line 190)
  2. quantizer->assign() → Index::assign() → IndexHNSW::search() — searches the HNSW coarse quantizer with the NaN
  vector
  3. HNSW::search() computes d_nearest = qdis(entry_point) — this returns NaN because the input vector has NaN
  4. NaN is pushed into MinimaxHeap candidates. All comparisons with NaN return false, corrupting heap ordering
  5. search_from_candidates() pops a garbage node ID v0 from the corrupted heap
  6. neighbor_range(v0, ...) hits FAISS_CHECK_RANGE_DEBUG — v0 is out of bounds → crash
```

In python test or opt mode:
```
The test timed out — it hung instead of crashing. This is consistent with the analysis: in the Python bindings (which
  run with mode/opt, so no debug assertions), pop_min returns -1, neighbor_range(-1, ...) doesn't crash on the
  assertion (it's a debug-only check), and instead accesses offsets[-1] which is undefined behavior. The NaN corrupts
  the heap, the search loops forever because nvalid never decrements when pop_min returns -1.

  So in Python/opt mode: no stack trace, just a hang. The C++ debug build crashes with the assertion. Both are fixed by
  the same push() NaN→+inf change in HNSW.cpp.
```

Performance impact??
--
it is in the tightest loop in search_from_candidates.  Claude seems to think it is fine:
```
  In search_from_candidates (line 707), push() is called for every neighbor visited during greedy graph traversal — batched 4 at a time via
  add_to_heap. This is indeed the tightest inner loop in HNSW search.

  What std::isnan actually costs

  std::isnan(float) on x86_64 compiles to essentially v != v — a single ucomiss + conditional jump (jp). That's 1-2 instructions.

  Context: what else happens per push call

  Each push call is preceded by significantly more expensive work:
  1. Distance computation (distances_batch_4 at line 718): ~2×d FLOPs per vector (e.g., ~256 FLOPs for d=128)  NOTE: HNSW.cpp:718-726 (qdis.distances_batch_4(...))
  2. Existing comparisons in push: k == n, v >= dis[0], potential heap_pop/heap_push which are O(log efSearch) with memory accesses    NOTE: HNSW.cpp:1401  (k == n), 1402 (v >= dis[0]), 1408 (heap_pop), 1410 (heap_push)
  3. Visited table check (vt.set) with prefetching   NOTE:  HNSW.cpp:689 (vt.prefetch(v1)) and 714 (vt.set(v1))

  The isnan check is ~1 instruction compared to hundreds for the distance computation. Even against just the heap operations (several
  comparisons + pointer chasing), it's a rounding error.

  Branch prediction

  Since NaN distances essentially never occur in normal operation, this branch is always not-taken. The branch predictor will learn this
  within the first few iterations and predict it perfectly for the remainder of the search. A perfectly-predicted not-taken branch is ~0
  cycles on modern CPUs.
```

And mini benchmark:
```
  ┌────────┬────────────────────────┬────────────────────────┬───────────────────┐
  │        │ With NaN check (run 1) │ With NaN check (run 2) │ Without NaN check │
  ├────────┼────────────────────────┼────────────────────────┼───────────────────┤
  │ Mean   │ 401.146 ms             │ 400.515 ms             │ 401.191 ms        │
  ├────────┼────────────────────────┼────────────────────────┼───────────────────┤
  │ Median │ 396.746 ms             │ 398.172 ms             │ 398.520 ms        │
  ├────────┼────────────────────────┼────────────────────────┼───────────────────┤
  │ Min    │ 389.939 ms             │ 388.670 ms             │ 391.850 ms        │
  ├────────┼────────────────────────┼────────────────────────┼───────────────────┤
  │ Stddev │ 18.903 ms              │ 7.833 ms               │ 8.164 ms          │
  └────────┴────────────────────────┴────────────────────────┴───────────────────┘
```

I wanted to make sure it worked regardless of metric type:
```
  For inner product (and cosine, which is IP after normalization), HNSW wraps the distance computer in NegativeDistanceComputer, which
  negates the result. This means all metrics flow through the MinimaxHeap with the same convention: smaller = better.

  So the NaN → +inf replacement works correctly for all metrics:

  ┌────────┬──────────────────────────────┬──────┬───────┬─────────────────┐
  │ Metric │        qdis() returns        │ Best │ Worst │   +inf means    │
  ├────────┼──────────────────────────────┼──────┼───────┼─────────────────┤
  │ L2     │ ‖x-q‖²                       │ 0    │ +inf  │ worst (correct) │
  ├────────┼──────────────────────────────┼──────┼───────┼─────────────────┤
  │ IP     │ -<x,q> (negated)             │ -∞   │ +inf  │ worst (correct) │
  ├────────┼──────────────────────────────┼──────┼───────┼─────────────────┤
  │ Cosine │ -<x̂,q̂> (negated, normalized) │ -1   │ +inf  │ worst (correct) │
  └────────┴──────────────────────────────┴──────┴───────┴─────────────────┘

  In all cases, +inf sits at the top of the CMax heap and gets evicted first when the heap is full (v >= dis[0] returns early at line 1402).
   And pop_min() will never select it over any finite-distance candidate. The semantics are correct regardless of metric type.
```

Reviewed By: mdouze

Differential Revision: D99036639

fbshipit-source-id: e5d6392e800f243f66ce283c8cd35fe0e7558229
2026-04-07 10:29:54 -07:00
Andrejs Krasilnikovs 8ef2407692 Fix to graph deserialization (#4812)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4812

Previous commit introduced a change which bumps the `K` parameter in the graph, which in turn causes it to be serialized incorrectly.

This commit reverts the `K` value to the initial one and instead changes the deserialization routine to skip over the sentinel.

Reviewed By: junjieqi

Differential Revision: D93678078

fbshipit-source-id: d1f014ca384554ec33a160612702db63403a3798
2026-02-18 21:09:15 -08:00
Matthijs Douze 1ea99d8073 clean up serialization tests (#4700)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4700

A pass over the serialization / deserialization tests, so that they don't use temp files when not needed.

Reviewed By: pankajsingh88, junjieqi

Differential Revision: D87615610

fbshipit-source-id: 3e6366b938d41e083781745179ff19ecf5247ba2
2025-12-09 05:16:38 -08:00
Subhadeep Karan 3a43e35292 Fix typos in tests and contrib directories (#4672)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4672

Fixed typos in test files and contrib utilities:

tests/:
- Fixed 20 typos across test files (C++ and Python)
- Fixed function/class name typos in Python tests:
  * test_result_order_miltihash → test_result_order_multihash
  * TestOrthognalReconstruct → TestOrthogonalReconstruct
  * test_recons_orthogona_impossible → test_recons_orthogonal_impossible
- Fixed comment typos and error messages in C++ tests

contrib/:
- Fixed 9 typos in Python contrib utilities
- Fixed string literal typos in client_server.py and rpc.py
- Fixed documentation typos in evaluation.py and big_batch_search.py

All changes maintain backward compatibility - no API changes.

Reviewed By: mnorris11

Differential Revision: D86571375

fbshipit-source-id: bd0edf7ca2e004f78e0ccba7f4aac7db6621781a
2025-11-12 11:39:51 -08:00
Alibek Zhakubayev 1deba7b90f Add attribute validation to prevent silent failures in SWIG wrappers (#4583)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4583

This diff implements attribute validation in Faiss Python wrappers to prevent silent failures when setting non-existent attributes on SWIG-wrapped objects. Previously, attempting to set invalid attributes would fail silently, leading to bugs that were difficult to detect.

## Key Changes

**Core Implementation:**
- Added `replacement_setattr` method in `class_wrappers.py` that validates attribute existence before setting
- Raises `AttributeError` with descriptive messages for invalid attributes
- Preserves normal behavior for valid attributes and delegation patterns

**Bug Fixes Discovered and Resolved:**
The validation implementation successfully caught and helped fix **6 real bugs** in the codebase:

*  **test_fast_scan.py** :
   - Removed invalid `index.nprobe = 16` on `IndexResidualQuantizer` (RQ indices don't support nprobe)
   - Similar fix in subtest_from_idxaq method

*  **test_meta_index.py**:
   - Fixed `IndexShards.threaded` attribute - `threaded` is a construction-time parameter, not runtime settable
   - Solution: Create separate threaded/non-threaded instances instead of toggling at runtime

*  **test_refine.py**:
   - Fixed `IndexRefine.nprobe` access - nprobe belongs to the base index, not the refine wrapper
   - Solution: Use `index.base_index.nprobe = 4` instead of direct access

*  **test_graph_based.py**:
   - Fixed `IndexHNSWFlat.search_bounded_queue` access - belongs to hnsw sub-object
   - Solution: Use `index.hnsw.search_bounded_queue = False` instead of direct access

*  **test_index_accuracy.py**:
   - Fixed typo: `index.npobe = 16` → `index.nprobe = 16`
   - This typo caused search quality degradation (intersection count improved from ~460 to ~945 after fix)

*  **xri_algo/lyra/features2d/match_faiss.py**:
   - Added conditional check before setting nprobe: `if hasattr(self.index, 'nprobe'): self.index.nprobe = nprobe`
   - Prevents AttributeError on flat indices like IndexFlatL2 that don't support nprobe

## Impact

- **Improved Code Quality**: Converts silent failures into explicit errors with clear messages
- **Better Developer Experience**: Developers now get immediate feedback for invalid attribute usage
- **Prevents Future Bugs**: Makes it impossible to silently set non-existent attributes
- **Production Ready**: All tests pass, demonstrating the validation works without breaking existing functionality

The implementation has proven its value by catching multiple real bugs that were previously going unnoticed, including a performance-affecting typo that significantly degraded search accuracy.

Reviewed By: subhadeepkaran

Differential Revision: D82774442

fbshipit-source-id: 4cf7ff9dff2bc934abe6bbb04a6d1601f311890f
2025-09-19 15:12:21 -07:00
Tia Lim 5c61ed8c94 Fix E302 lint errors: Add required blank lines before class definitions (#4541)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4541

Per PEP 8 style guide (https://pep8.org/#blank-lines), top-level class
definitions should be surrounded by two blank lines. This commit addresses
\E302 lint violations across the faiss codebase by ensuring all class
definitions have exactly 2 blank lines before them.

Changes:
- Fixed \E302 errors in 28 Python files across tests, benchmarks, contrib,
  and GPU modules
- Applied consistent PEP 8 formatting for class definitions
- No functional changes - formatting only

Files affected include test files, benchmark scripts, contrib modules,
and GPU test utilities. All changes maintain existing functionality
while improving code style compliance.

Reviewed By: junjieqi

Differential Revision: D80682723

fbshipit-source-id: 6ad5458c3bf1e5c06f35dc8fcdbea2ac4ba83c5f
2025-08-21 15:40:14 -07:00
Matthijs Douze 5af8f99bc7 remove ABS_INNER_PRODUCT metric (#4408)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4408

The METRIC_ABS_INNER_PRODUCT metric is not useful, see rationale here: https://github.com/facebookresearch/faiss/issues/4407

Reviewed By: junjieqi

Differential Revision: D77150028

fbshipit-source-id: e1087e206efb7e41af3530c373ff8371c10f7f05
2025-07-08 19:15:12 -07:00
Subhadeep Karan d3f004ea74 Adding unit tests for supporting pre-built KNN while using IndexNSG (#4368)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4368

as title mentions

**For more context following is the text from first diff in this stack:**

__*several improvements are found and will be part of this diff stack:*__
*1. missing unit test for the affected function which allows supporting pre-computed KNN*

Added unit tests:
1. good case
2. bad case:
a. raises error when rebuild is attempt via this prebuilt KNN path
b. raises error when rebuild is attempted via default `add` path wherein KNN is built

Reviewed By: mnorris11

Differential Revision: D75515115

fbshipit-source-id: 23c95818ed1ce2b8ce519405f004b8beab4f91a8
2025-05-29 13:05:39 -07:00
Michael Norris 0868ffc5f4 Add more unit testing for IndexHNSW [2/n] (#4056)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4056

Part 2 of more HNSW unit tests

Added comments indicating some currently unused code.

Reviewed By: junjieqi

Differential Revision: D66782376

fbshipit-source-id: 5a7c210d516424e58054f36f0a74249859e48a8f
2024-12-05 15:14:14 -08:00
Michael Norris 661557e17f Add more unit testing for IndexHNSW [1/n] (#4054)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4054

Part 1 of more HNSW unit tests

Reviewed By: junjieqi

Differential Revision: D66690398

fbshipit-source-id: 29b35f1c0a919c168fd4cb770552f41eaed5b6b6
2024-12-05 13:43:28 -08:00
Shengqi Chen 98d335b172 Some chore fixes (#4010)
Summary:
- Fix a typo in comment
- Add missing header files to `FAISS_HEADERS` in CMake config

There should be some check against the inconsistency between `FAISS_HEADERS` and actual files, e.g. test compiling with installed headers and shared library (otherwise it always succeeds in the source dir).

This is not the first time that headers are missing (https://github.com/facebookresearch/faiss/issues/3218).

Pull Request resolved: https://github.com/facebookresearch/faiss/pull/4010

Reviewed By: junjieqi

Differential Revision: D65630647

Pulled By: kuarora

fbshipit-source-id: 2efcfc4bbd0b2d29efa817e1ff9371942c15d30a
2024-11-19 16:48:35 -08:00
Michael Norris eff0898a13 Enable linting: lint config changes plus arc lint command (#3966)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3966

This actually enables the linting.

Manual changes:
- tools/arcanist/lint/fbsource-licenselint-config.toml
- tools/arcanist/lint/fbsource-lint-engine.toml

Automated changes:
`arc lint --apply-patches --take LICENSELINT --paths-cmd 'hg files faiss'`

Reviewed By: asadoughi

Differential Revision: D64484165

fbshipit-source-id: 4f2f6e953c94ef6ebfea8a5ae035ccfbea65ed04
2024-10-22 09:46:48 -07:00
Matthijs Douze 52cf9af18a group SWIG tests into one file (#3807)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3807

This diff re-organizes the tests a bit:

* groups tests related to SWIG into a single file

* enable doxygen test conditionally

* removes a dep on platform.Version that does not exist on some envs

* move a few tests out of build_blocks to avoid it being a catch-all file for uncategorized tests

Reviewed By: asadoughi

Differential Revision: D61959750

fbshipit-source-id: fb6ada5b5a980ac07088254183d80b80ddfb1f45
2024-09-05 09:43:31 -07:00
generatedunixname2443911735787003 8013fbd705 fbcode//faiss/tests (#3707)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3707

Differential Revision: D60503059

fbshipit-source-id: af5405717e08d78ab11ee8ee431f2b8a00c2aa70
2024-07-31 08:08:46 -07:00
Matthijs Douze 7178bf8a1d Fix radius search with HSNW and IP (#3698)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3698

Addressed issue

https://github.com/facebookresearch/faiss/issues/3684

I forgot to negate the threshold of the radius search.
This diff adds a test and fixes the issue.

Reviewed By: mengdilin

Differential Revision: D60373054

fbshipit-source-id: 70f3daa8292177a4038846a94aff6221f88077e8
2024-07-29 09:52:52 -07:00
Matthijs Douze e758973fa0 Add ABS_INNER_PRODUCT metric (#3524)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3524

Searches with the metric abs(dot(query, database))
This makes it possible to search vectors that are closest to a hyperplane

* adds support for alternative metrics in faiss.knn in python

* checks that it works with HNSW

* simplifies the extra distances interface by removing the template on

Reviewed By: asadoughi

Differential Revision: D58695971

fbshipit-source-id: 2a0ff49c7f7ac2c005d85f141cc5de148081c9c4
2024-06-18 03:13:51 -07:00
Gergely Szilvasy 34feae48d4 typo in test_io_no_storage (#3515)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3515

Fix typo `test_io_no_storage`

Reviewed By: kuarora, asadoughi

Differential Revision: D58540190

fbshipit-source-id: b8b9cacd7ea6005c0edb94014de74188450318c1
2024-06-13 13:31:34 -07:00
Matthijs Douze bf73e38d10 add skip_storage flag to HNSW (#3487)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3487

Sometimes it is not useful to serialize the storage index along with a HNSW index. This diff adds a flag that supports skipping the storage of the index.

Searchign and adding to the index is not possible until a storage index is added back in.

Reviewed By: junjieqi

Differential Revision: D57911060

fbshipit-source-id: 5a4ceee4a8f53f6f746df59af3942b813a99c14f
2024-05-31 14:48:13 -07:00
Xiao Fu 5e452ed52a Cleaning up more unnecessary print (#3455)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3455

Code quality control by reducing the number of prints

Reviewed By: junjieqi

Differential Revision: D57502194

fbshipit-source-id: a6cd65ed4cc49590ce73d2978d41b640b5259c17
2024-05-17 16:59:36 -07:00
Matthijs Douze fa1f39ec9f Fix HNSW stats (#3309)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3309

Make sure that the HNSW search stats work, remove stats for deprecated functionality.
Remove code of the link and code paper that is not supported anymore.

Reviewed By: kuarora, junjieqi

Differential Revision: D55247802

fbshipit-source-id: 03f176be092bff6b2db359cc956905d8646ea702
2024-03-22 12:55:30 -07:00
Matthijs Douze 32f0e8cf92 Generalize ResultHanlder, support range search for HNSW and Fast Scan (#3190)
Summary:
Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3190

This diff adds more result handlers in order to expose them externally.
This enables range search for HSNW and Fast Scan, and nprobe parameter support for FastScan.

Reviewed By: pemazare

Differential Revision: D52547384

fbshipit-source-id: 271da5ffea6411df3d8e50641abade18bd7b774b
2024-01-11 11:46:30 -08:00