mirror of
https://github.com/facebookresearch/faiss.git
synced 2026-10-12 02:13:36 +00:00
1e5780fbbfbe191f3ed3517f93ace8301239e9b2
29
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
8013fbd705 |
fbcode//faiss/tests (#3707)
Summary: Pull Request resolved: https://github.com/facebookresearch/faiss/pull/3707 Differential Revision: D60503059 fbshipit-source-id: af5405717e08d78ab11ee8ee431f2b8a00c2aa70 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |