Summary: `NNDescent::update()` Step 3 (the reverse-link phase) has a data race on each node's candidate `pool`. While some threads read `other.pool.back().distance`, the thread that owns that node reorders the same pool via `std::make_heap` at the end of its own iteration, with no shared synchronization. Reported as https://github.com/facebookresearch/faiss/issues/5276. The originally proposed change (moving `LockGuard guard(other.lock)` before the `other.pool.back()` read) does not remove the race: the concurrent writer is the unlocked `std::make_heap`, so locking only the reader establishes no mutual exclusion. The race is also not with `Nhood::insert()`'s heap operations: `join()` and `update()` run in strictly separate phases (`join()` completes before `update()` begins), so `insert()` never runs concurrently with Step 3. We verified this: `Nhood::insert()` has exactly two call sites, both inside the `join()` lambda, and `update()` has exactly one call site, sequentially after `join()` returns. This diff removes the concurrency at its source. `std::make_heap` is moved out of the reverse-link loop into a separate `#pragma omp parallel for` that runs after the loop's implicit barrier. During the reverse-link loop no thread reorders or resizes a pool, so the `other.pool.back()` reads are safe without a lock (the lock continues to guard only the `rnn_*` updates, as in the original code). The only element write in that loop is to `flag`, a distinct memory location from the `distance` being read, so it is not a race either. There is no added lock traffic on the hot read path. **Behavior change: fewer reverse links, faster build, one extra iteration to converge.** This is not a behavior-preserving change, and the effect is larger than the race fix itself. Step 2 sorts each `pool` ascending, so `pool.back()` is the maximum (worst) distance. In the previous code `std::make_heap` had usually already run on the node being read, and in a max-heap built from sorted input the last element is a near-minimum leaf: | `L` (pool size) | rank of `back()` | |---|---| | 30 | 6 of 30 (21st percentile) | | 82 (default, `L = K + 50`) | 1 of 82 (second smallest) | The gate `nn.distance > other.pool.back().distance` therefore compared against roughly the pool minimum before, and against the true maximum now, so materially fewer reverse links are admitted. Reverse links feed `nn_new`/`nn_old` in Step 4 and drive the candidate pairs enumerated by the next `join()`, so each iteration refines the graph less and does less work. Measured on `TestNNDescentKNNG` (`K=10`, `S=10`, `R=32`, `L=30`, `iter=5`), graph recall per iteration: | iteration | before | after | |---|---|---| | 1 | 0.692 | 0.627 | | 2 | 0.973 | 0.958 | | 3 | 0.999 | 0.998 | | 4 | 1.000 | 1.000 | Convergence is approximately one iteration slower and the converged graph is unchanged: both reach 1.000 and both clear the test's `> 0.99` assertion. The build is correspondingly cheaper, at 1608 ms versus 2186 ms on the same workload, because the previous behavior computed many more candidate distances per iteration. The new gate matches the documented intent of the pre-existing comment ("push itself into `other.rnn_new` if it is not in the candidate pool of the other side"), which holds only when `back()` is the worst candidate. The previous threshold was an artifact of the race: which pools were already heapified depended on thread scheduling, so the extra exploration was nondeterministic and undefined behavior besides. There is nothing principled to preserve. Users at the default `iter = 10` are unaffected because both configurations saturate. Exposure is limited to reduced `iter` settings and to datasets needing more than 10 iterations. Credit: wonder-fly for reporting and diagnosing the race in #5276, and RohanMali2003 for PR #5413. The final fix takes a different approach (deferring `make_heap`) than the originally proposed lock reordering. Pull Request resolved: https://github.com/facebookresearch/faiss/pull/5413 Test Plan: - `buck test fbcode//mode/dev fbcode//faiss/tests:test_nndescent`: Pass 4. - `buck test fbcode//mode/dev fbcode//faiss/tests:test_graph_based -- --regex NNDescent`: Pass 6. - `arc f` / `arc lint` clean on `faiss/impl/NNDescent.cpp`. A/B verification of the behavior change: we reverted only the `make_heap` placement, rebuilt, and compared per-iteration graph recall from `TestNNDescentKNNG` verbose output. Numbers are in the Summary. No test in the repository guards this race, on either revision. We verified this by building and running both revisions: the graph-quality tests pass identically before and after the fix, because they measure converged recall and both configurations converge. An earlier draft of this diff added a C++ end-to-end test; we removed it because it measured `recall@10 = 1.0000` on both revisions, so it could not fail for the reason it claimed to guard, and it duplicated `TestNNDescentKNNG` at 20x to 100x the runtime. ThreadSanitizer note: fbcode `dev-tsan` serializes OpenMP to a single thread (verified: OMP threads in use = 1 even with `OMP_NUM_THREADS`/`OMP_THREAD_LIMIT` overrides), so this race cannot be reproduced through `update()` under local TSan. It is reproducible under an OSS/cmake TSan build with real OpenMP threads, which is how #5276 was found. The fix rests on the concurrency analysis in the Summary, on quality parity at convergence, and on the original OSS TSan report. Reviewed By: mnorris11 Differential Revision: D112612388 Pulled By: alibeklfc fbshipit-source-id: 32f3532603d5ff01ed0c89d444cb9a636c25f5ae
Faiss
Faiss is a library for efficient similarity search and clustering of dense vectors. It contains algorithms that search in sets of vectors of any size, up to ones that possibly do not fit in RAM. It also contains supporting code for evaluation and parameter tuning. Faiss is written in C++ with complete wrappers for Python/numpy. Some of the most useful algorithms are implemented on the GPU. It is developed primarily at Meta's Fundamental AI Research group.
News
See CHANGELOG.md for detailed information about latest features.
Introduction
Faiss contains several methods for similarity search. It assumes that the instances are represented as vectors and are identified by an integer, and that the vectors can be compared with L2 (Euclidean) distances or dot products. Vectors that are similar to a query vector are those that have the lowest L2 distance or the highest dot product with the query vector. It also supports cosine similarity, since this is a dot product on normalized vectors.
Some of the methods, like those based on binary vectors and compact quantization codes, solely use a compressed representation of the vectors and do not require to keep the original vectors. This generally comes at the cost of a less precise search but these methods can scale to billions of vectors in main memory on a single server. Other methods, like HNSW and NSG add an indexing structure on top of the raw vectors to make searching more efficient.
The GPU implementation can accept input from either CPU or GPU memory. On a server with GPUs, the GPU indexes can be used a drop-in replacement for the CPU indexes (e.g., replace IndexFlatL2 with GpuIndexFlatL2) and copies to/from GPU memory are handled automatically. Results will be faster however if both input and output remain resident on the GPU. Both single and multi-GPU usage is supported.
Installing
Faiss comes with precompiled libraries for Anaconda in Python, see faiss-cpu, faiss-gpu and faiss-gpu-cuvs. The library is mostly implemented in C++, the only dependency is a BLAS implementation. Optional GPU support is provided via CUDA or AMD ROCm, and the Python interface is also optional. The backend GPU implementations of NVIDIA cuVS can also be enabled optionally. It compiles with cmake. See INSTALL.md for details.
How Faiss works
Faiss is built around an index type that stores a set of vectors, and provides a function to search in them with L2 and/or dot product vector comparison. Some index types are simple baselines, such as exact search. Most of the available indexing structures correspond to various trade-offs with respect to
- search time
- search quality
- memory used per index vector
- training time
- adding time
- need for external data for unsupervised training
The optional GPU implementation provides what is likely (as of March 2017) the fastest exact and approximate (compressed-domain) nearest neighbor search implementation for high-dimensional vectors, fastest Lloyd's k-means, and fastest small k-selection algorithm known. The implementation is detailed here.
Full documentation of Faiss
The following are entry points for documentation:
- the full documentation can be found on the wiki page, including a tutorial, a FAQ and a troubleshooting section
- the doxygen documentation gives per-class information extracted from code comments
- to reproduce results from our research papers, Polysemous codes and Billion-scale similarity search with GPUs, refer to the benchmarks README. For Link and code: Fast indexing with graphs and compact regression codes, see the link_and_code README
Authors
The main authors of Faiss are:
- Hervé Jégou initiated the Faiss project and wrote its first implementation
- Matthijs Douze implemented most of the CPU Faiss
- Jeff Johnson implemented all of the GPU Faiss
- Lucas Hosseini implemented the binary indexes and the build system
- Chengqi Deng implemented NSG, NNdescent and much of the additive quantization code.
- Alexandr Guzhva many optimizations: SIMD, memory allocation and layout, fast decoding kernels for vector codecs, etc.
- Gergely Szilvasy build system, benchmarking framework.
Reference
References to cite when you use Faiss in a research paper:
@article{douze2024faiss,
title={The Faiss library},
author={Matthijs Douze and Alexandr Guzhva and Chengqi Deng and Jeff Johnson and Gergely Szilvasy and Pierre-Emmanuel Mazaré and Maria Lomeli and Lucas Hosseini and Hervé Jégou},
year={2024},
eprint={2401.08281},
archivePrefix={arXiv},
primaryClass={cs.LG}
}
For the GPU version of Faiss, please cite:
@article{johnson2019billion,
title={Billion-scale similarity search with {GPUs}},
author={Johnson, Jeff and Douze, Matthijs and J{\'e}gou, Herv{\'e}},
journal={IEEE Transactions on Big Data},
volume={7},
number={3},
pages={535--547},
year={2019},
publisher={IEEE}
}
Join the Faiss community
For public discussion of Faiss or for questions, visit https://github.com/facebookresearch/faiss/discussions.
We monitor the issues page of the repository. You can report bugs, ask questions, etc.
Legal
Faiss is MIT-licensed, refer to the LICENSE file in the top level directory.
Copyright © Meta Platforms, Inc.