faiss HNSW: delete the flag and the lock-based graph build (#5611)

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

D112025877 added the `faiss::hnsw_deterministic_build` flag. The flag chose
between the lock-based graph build and the deterministic graph build. This
diff deletes the flag. The deterministic build is now the only build.

Reviewed By: trang-nm-nguyen

Differential Revision: D118342471

fbshipit-source-id: d3c5e5238aca2bcda463eb27959ec22827222c15
This commit is contained in:
Michael Norris
2026-09-14 13:13:11 -07:00
committed by meta-codesync[bot]
parent 4fdbc9dc18
commit a74867cf56
11 changed files with 32 additions and 509 deletions
-41
View File
@@ -191,44 +191,3 @@ if "nsg" in todo:
print("search_L", search_L, end=" ")
index.nsg.search_L = search_L
evaluate(index)
if "hnsw_locks" in todo:
ntotal, _ = xb.shape
batch_size = ntotal // 100
print(
f"Testing HNSW Flat: add with {batch_size=}, "
"with and without retaining locks"
)
# Unbatched
t0 = time.time()
index = faiss.IndexHNSWFlat(d, 32)
index.add(xb)
t1 = time.time()
print(
f"\t single bulk add(): {index.ntotal} added in {t1 - t0:6.3f}s"
f" = {index.ntotal / (t1 - t0):.0f}/s"
)
for retain_locks in [False, True]:
index = faiss.IndexHNSWFlat(d, 32)
index.retain_locks = retain_locks
t0 = time.time()
t1 = None
t2 = None
for i in range(0, len(xb), batch_size):
t1 = time.time()
index.add(xb[i : i + batch_size])
t2 = time.time()
if i > 2 and t2 - t0 > 2:
break
assert t1 and t2
dt = t2 - t0
print(
f"\t {retain_locks=:1}: {index.ntotal} added in {t2 - t0:6.3f}s"
f" = {index.ntotal / (t2 - t0):.0f}/s"
)
+13 -153
View File
@@ -7,7 +7,6 @@
#include <faiss/IndexBinaryHNSW.h>
#include <omp.h>
#include <cassert>
#include <cmath>
#include <cstdio>
@@ -39,140 +38,6 @@
namespace faiss {
/**************************************************************
* add / search blocks of descriptors
**************************************************************/
namespace {
void hnsw_add_vertices(
IndexBinaryHNSW& index_hnsw,
size_t n0,
size_t n,
const uint8_t* x,
bool verbose,
bool preset_levels = false) {
HNSW& hnsw = index_hnsw.hnsw;
size_t ntotal = n0 + n;
double t0 = getmillisecs();
if (verbose) {
printf("hnsw_add_vertices: adding %zd elements on top of %zd "
"(preset_levels=%d)\n",
n,
n0,
int(preset_levels));
}
int max_level = hnsw.prepare_level_tab(n, preset_levels);
if (verbose) {
printf(" max_level = %d\n", max_level);
}
auto& locks = index_hnsw.locks;
locks.prepare(ntotal);
// add vectors from highest to lowest level
std::vector<int> hist;
std::vector<int> order(n);
{ // make buckets with vectors of the same level
// build histogram
for (size_t i = 0; i < n; i++) {
HNSW::storage_idx_t pt_id =
static_cast<HNSW::storage_idx_t>(i + n0);
int pt_level = hnsw.levels[pt_id] - 1;
while (pt_level >= static_cast<int>(hist.size())) {
hist.push_back(0);
}
hist[pt_level]++;
}
// accumulate
std::vector<int> offsets(hist.size() + 1, 0);
for (size_t i = 0; i < hist.size() - 1; i++) {
offsets[i + 1] = offsets[i] + hist[i];
}
// bucket sort
for (size_t i = 0; i < n; i++) {
HNSW::storage_idx_t pt_id =
static_cast<HNSW::storage_idx_t>(i + n0);
int pt_level = hnsw.levels[pt_id] - 1;
order[offsets[pt_level]++] = pt_id;
}
}
{ // perform add
RandomGenerator rng2(789);
size_t i1 = static_cast<int>(n);
for (int pt_level = static_cast<int>(hist.size()) - 1;
pt_level >= int(!index_hnsw.init_level0);
pt_level--) {
size_t i0 = i1 - hist[pt_level];
if (verbose) {
printf("Adding %zu elements at level %d\n", i1 - i0, pt_level);
}
// random permutation to get rid of dataset order bias
for (size_t j = i0; j < i1; j++) {
std::swap(
order[j],
order[j + rng2.rand_int(static_cast<int>(i1 - j))]);
}
#pragma omp parallel
{
std::unique_ptr<VisitedTable> vt = VisitedTable::create(ntotal);
std::unique_ptr<DistanceComputer> dis(
index_hnsw.get_distance_computer());
bool do_display = verbose && omp_get_thread_num() == 0;
size_t prev_display = 0;
#pragma omp for schedule(dynamic)
for (int64_t i = i0; i < i1; i++) {
HNSW::storage_idx_t pt_id = order[i];
dis->set_query(
(float*)(x + (pt_id - n0) * index_hnsw.code_size));
hnsw.add_with_locks(
*dis,
pt_level,
pt_id,
locks,
*vt,
index_hnsw.keep_max_size_level0 && (pt_level == 0));
if (do_display && i - i0 > prev_display + 10000) {
prev_display = i - i0;
printf(" %zu / %zu\r", i - i0, i1 - i0);
fflush(stdout);
}
}
}
i1 = i0;
}
if (index_hnsw.init_level0) {
FAISS_ASSERT(i1 == 0);
} else {
FAISS_ASSERT((i1 - hist[0]) == 0);
}
}
if (verbose) {
printf("Done in %.3f ms\n", getmillisecs() - t0);
}
if (!index_hnsw.retain_locks) {
locks.clear();
}
}
} // anonymous namespace
/**************************************************************
* IndexBinaryHNSW implementation
**************************************************************/
@@ -272,28 +137,23 @@ void IndexBinaryHNSW::add(idx_t n, const uint8_t* x) {
bool preset_levels = hnsw.levels.size() == static_cast<size_t>(ntotal);
if (hnsw_deterministic_build) {
hnsw_add_vertices_deterministic(
hnsw,
n0,
n,
d,
init_level0,
keep_max_size_level0,
preset_levels,
verbose,
[this] { return get_distance_computer(); },
[this, x, n0](DistanceComputer& dc, HNSW::storage_idx_t pt_id) {
dc.set_query((const float*)(x + (pt_id - n0) * code_size));
});
} else {
hnsw_add_vertices(*this, n0, n, x, verbose, preset_levels);
}
hnsw_add_vertices_deterministic(
hnsw,
n0,
n,
d,
init_level0,
keep_max_size_level0,
preset_levels,
verbose,
[this] { return get_distance_computer(); },
[this, x, n0](DistanceComputer& dc, HNSW::storage_idx_t pt_id) {
dc.set_query((const float*)(x + (pt_id - n0) * code_size));
});
}
void IndexBinaryHNSW::reset() {
hnsw.reset();
locks.clear();
storage->reset();
ntotal = 0;
}
-6
View File
@@ -11,7 +11,6 @@
#include <faiss/IndexBinaryFlat.h>
#include <faiss/impl/HNSW.h>
#include <faiss/impl/hnsw/LockVector.h>
#include <faiss/utils/utils.h>
namespace faiss {
@@ -41,11 +40,6 @@ struct IndexBinaryHNSW : IndexBinary {
// used when GpuIndexBinaryCagra::copyFrom(IndexBinaryHNSW*) is called.
bool keep_max_size_level0 = false;
// Per-node locks for HNSW graph construction.
LockVector locks;
// locks are freed after each call to add() unless this flag is set.
bool retain_locks = false;
explicit IndexBinaryHNSW();
explicit IndexBinaryHNSW(int d, int M = 32);
explicit IndexBinaryHNSW(IndexBinary* storage, int M = 32);
+15 -178
View File
@@ -9,7 +9,6 @@
#include <faiss/IndexRaBitQ.h>
#include <omp.h>
#include <atomic>
#include <cinttypes>
#include <cstdio>
@@ -31,6 +30,7 @@
#include <faiss/impl/FaissException.h>
#include <faiss/impl/ResultHandler.h>
#include <faiss/impl/VisitedTable.h>
#include <faiss/impl/hnsw/LockVector.h>
#include <faiss/impl/hnsw/MinimaxHeap.h>
#include <faiss/utils/random.h>
#include <faiss/utils/sorting.h>
@@ -42,8 +42,6 @@ using NodeDistFarther = HNSW::NodeDistFarther;
HNSWStats hnsw_stats;
bool hnsw_deterministic_build = false;
/**************************************************************
* add / search blocks of descriptors
**************************************************************/
@@ -63,158 +61,6 @@ DistanceComputer* storage_distance_computer(const Index* storage) {
return storage->get_distance_computer();
}
void hnsw_add_vertices(
IndexHNSW& index_hnsw,
size_t n0,
size_t n,
const float* x,
bool verbose,
bool preset_levels = false) {
size_t d = index_hnsw.d;
HNSW& hnsw = index_hnsw.hnsw;
size_t ntotal = n0 + n;
double t0 = getmillisecs();
if (verbose) {
printf("hnsw_add_vertices: adding %zd elements on top of %zd "
"(preset_levels=%d)\n",
n,
n0,
int(preset_levels));
}
if (n == 0) {
return;
}
int max_level = hnsw.prepare_level_tab(n, preset_levels);
if (verbose) {
printf(" max_level = %d\n", max_level);
}
auto& locks = index_hnsw.locks;
locks.prepare(ntotal);
// add vectors from highest to lowest level
std::vector<int> hist;
std::vector<int> order(n);
{ // make buckets with vectors of the same level
// build histogram
for (size_t i = 0; i < n; i++) {
storage_idx_t pt_id = static_cast<storage_idx_t>(i + n0);
int pt_level = hnsw.levels[pt_id] - 1;
while (pt_level >= static_cast<int>(hist.size())) {
hist.push_back(0);
}
hist[pt_level]++;
}
// accumulate
std::vector<int> offsets(hist.size() + 1, 0);
for (size_t i = 0; i < hist.size() - 1; i++) {
offsets[i + 1] = offsets[i] + hist[i];
}
// bucket sort
for (size_t i = 0; i < n; i++) {
storage_idx_t pt_id = static_cast<storage_idx_t>(i + n0);
int pt_level = hnsw.levels[pt_id] - 1;
order[offsets[pt_level]++] = pt_id;
}
}
idx_t check_period = InterruptCallback::get_period_hint(
max_level * index_hnsw.d * hnsw.efConstruction);
{ // perform add
RandomGenerator rng2(789);
size_t i1 = static_cast<int>(n);
for (int pt_level = static_cast<int>(hist.size()) - 1;
pt_level >= int(!index_hnsw.init_level0);
pt_level--) {
size_t i0 = i1 - hist[pt_level];
if (verbose) {
printf("Adding %zu elements at level %d\n", i1 - i0, pt_level);
}
// random permutation to get rid of dataset order bias
for (size_t j = i0; j < i1; j++) {
std::swap(
order[j],
order[j + rng2.rand_int(static_cast<int>(i1 - j))]);
}
bool interrupt = false;
#pragma omp parallel if (i1 > i0 + 100)
{
std::unique_ptr<VisitedTable> vt =
VisitedTable::create(ntotal, hnsw.use_visited_hashset);
std::unique_ptr<DistanceComputer> dis(
storage_distance_computer(index_hnsw.storage));
bool do_display = verbose && omp_get_thread_num() == 0;
size_t prev_display = 0;
size_t counter = 0;
// here we should do schedule(dynamic) but this segfaults for
// some versions of LLVM. The performance impact should not be
// too large when (i1 - i0) / num_threads >> 1
#pragma omp for schedule(static)
for (int64_t i = i0; i < i1; i++) {
storage_idx_t pt_id = order[i];
dis->set_query(x + (pt_id - n0) * d);
// cannot break
if (interrupt) {
continue;
}
hnsw.add_with_locks(
*dis,
pt_level,
pt_id,
locks,
*vt,
index_hnsw.keep_max_size_level0 && (pt_level == 0));
if (do_display && i - i0 > prev_display + 10000) {
prev_display = i - i0;
printf(" %zu / %zu\r", i - i0, i1 - i0);
fflush(stdout);
}
if (counter % check_period == 0) {
if (InterruptCallback::is_interrupted()) {
interrupt = true;
}
}
counter++;
}
}
if (interrupt) {
FAISS_THROW_MSG("computation interrupted");
}
i1 = i0;
}
if (index_hnsw.init_level0) {
FAISS_ASSERT(i1 == 0);
} else {
FAISS_ASSERT((i1 - hist[0]) == 0);
}
}
if (verbose) {
printf("Done in %.3f ms\n", getmillisecs() - t0);
}
if (!index_hnsw.retain_locks) {
locks.clear();
}
}
} // namespace
// Deterministic HNSW graph build inspired by ParlayANN: points are bucketed by
@@ -708,28 +554,23 @@ void IndexHNSW::add(idx_t n, const float* x) {
bool preset_levels = hnsw.levels.size() == static_cast<size_t>(ntotal);
if (hnsw_deterministic_build) {
hnsw_add_vertices_deterministic(
hnsw,
n0,
n,
d,
init_level0,
keep_max_size_level0,
preset_levels,
verbose,
[this] { return storage_distance_computer(storage); },
[this, x, n0](DistanceComputer& dc, HNSW::storage_idx_t pt_id) {
dc.set_query(x + (pt_id - n0) * d);
});
} else {
hnsw_add_vertices(*this, n0, n, x, verbose, preset_levels);
}
hnsw_add_vertices_deterministic(
hnsw,
n0,
n,
d,
init_level0,
keep_max_size_level0,
preset_levels,
verbose,
[this] { return storage_distance_computer(storage); },
[this, x, n0](DistanceComputer& dc, HNSW::storage_idx_t pt_id) {
dc.set_query(x + (pt_id - n0) * d);
});
}
void IndexHNSW::reset() {
hnsw.reset();
locks.clear();
storage->reset();
ntotal = 0;
}
@@ -920,7 +761,7 @@ void IndexHNSW::init_level_0_from_entry_points(
int n,
const storage_idx_t* points,
const storage_idx_t* nearests) {
locks.prepare(ntotal);
LockVector locks(ntotal);
#pragma omp parallel
{
@@ -950,10 +791,6 @@ void IndexHNSW::init_level_0_from_entry_points(
if (verbose) {
printf("\n");
}
if (!retain_locks) {
locks.clear();
}
}
void IndexHNSW::reorder_links() {
-6
View File
@@ -18,7 +18,6 @@
#include <faiss/IndexScalarQuantizer.h>
#include <faiss/impl/HNSW.h>
#include <faiss/impl/Panorama.h>
#include <faiss/impl/hnsw/LockVector.h>
#include <faiss/utils/utils.h>
namespace faiss {
@@ -53,11 +52,6 @@ struct IndexHNSW : Index {
// See impl/VisitedTable.h.
std::optional<bool> use_visited_hashset;
// Per-node locks for HNSW graph construction.
LockVector locks;
// locks are freed after each call to add() unless this flag is set.
bool retain_locks = false;
explicit IndexHNSW(int d = 0, int M = 32, MetricType metric = METRIC_L2);
explicit IndexHNSW(Index* storage, int M = 32);
-76
View File
@@ -969,84 +969,8 @@ void merge_reverse_links_impl(
}
}
template <class C>
void add_with_locks_impl(
HNSW& hnsw,
DistanceComputer& ptdis,
int pt_level,
int pt_id,
LockVector& locks,
VisitedTable& vt,
bool keep_max_size_level0) {
storage_idx_t nearest = hnsw.entry_point;
if (nearest == -1) { // avoid locking after the first point.
#pragma omp critical
if (hnsw.entry_point == -1) { // double-check under lock.
hnsw.max_level = pt_level;
hnsw.entry_point = pt_id;
// leave nearest = -1 to trigger early exit after critical block.
} else {
// else: Another thread set the entry point.
nearest = hnsw.entry_point;
}
}
if (nearest < 0) {
return;
}
locks.lock(pt_id);
int level = hnsw.max_level; // level at which we start adding neighbors
float d_nearest = ptdis(nearest);
// greedy search on upper levels
for (; level > pt_level; level--) {
greedy_update_nearest_impl<C>(hnsw, ptdis, level, nearest, d_nearest);
}
for (; level >= 0; level--) {
add_links_starting_from_impl<C>(
hnsw,
ptdis,
pt_id,
nearest,
d_nearest,
level,
locks,
vt,
keep_max_size_level0);
}
locks.unlock(pt_id);
#pragma omp critical
{
if (pt_level > hnsw.max_level) {
hnsw.max_level = pt_level;
hnsw.entry_point = pt_id;
}
}
}
} // namespace
void HNSW::add_with_locks(
DistanceComputer& ptdis,
int pt_level,
int pt_id,
LockVector& locks,
VisitedTable& vt,
bool keep_max_size_level0) {
if (is_similarity) {
add_with_locks_impl<C_similarity>(
*this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
} else {
add_with_locks_impl<C_distance>(
*this, ptdis, pt_level, pt_id, locks, vt, keep_max_size_level0);
}
}
void HNSW::compute_forward_links_deterministic(
DistanceComputer& ptdis,
int pt_level,
+2 -17
View File
@@ -212,16 +212,6 @@ struct HNSW {
VisitedTable& vt,
bool keep_max_size_level0 = false);
/** add point pt_id on all levels <= pt_level and build the link
* structure for them. */
void add_with_locks(
DistanceComputer& ptdis,
int pt_level,
int pt_id,
LockVector& locks,
VisitedTable& vt,
bool keep_max_size_level0 = false);
/** Deterministic build, phase A: write pt_id's forward links against an
* immutable snapshot, touching only pt_id's own slots. Reciprocal edges
* are collected in `pt_reverse_edges` for phase B, not applied. Requires
@@ -288,7 +278,8 @@ struct HNSW {
void permute_entries(const idx_t* map);
};
/** Deterministic, lock-free HNSW graph build, shared by IndexHNSW and
/** Deterministic, lock-free HNSW graph build. This is the only graph build
* that add() uses, and it is shared by IndexHNSW and
* IndexBinaryHNSW. The callbacks let both share the algorithm:
* `make_distance_computer()` returns a fresh DistanceComputer per thread
* (caller-owned) and `set_query(dc, pt_id)` points it at pt_id's vector. */
@@ -329,12 +320,6 @@ struct HNSWStats {
// global var that collects them all
FAISS_API extern HNSWStats hnsw_stats;
/** Use the lock-free deterministic graph build in add() rather than the
* lock-based one. Transitional: it will become the only path.
*
* NOT thread-safe: set before calling add(). Sampled once per add(). */
FAISS_API extern bool hnsw_deterministic_build;
/// Internal HNSW algorithm helpers. These are not part of the public API; they
/// are exposed here only so that unit tests (and a few cross-TU callers such as
/// the Panorama search variant) can reach them.
-1
View File
@@ -4384,7 +4384,6 @@ class _SwigGlobals:
distance_compute_blas_database_bs: int
distance_compute_min_k_reservoir: int
index_factory_verbose: int
hnsw_deterministic_build: bool
cvar: _SwigGlobals
-2
View File
@@ -612,8 +612,6 @@ void gpu_sync_all_devices()
%include <faiss/IndexIVFAdditiveQuantizer.h>
%include <faiss/impl/hnsw/MinimaxHeap.h>
%include <faiss/impl/HNSW.h>
%ignore faiss::IndexHNSW::locks;
%ignore faiss::IndexBinaryHNSW::locks;
%include <faiss/IndexHNSW.h>
%include <faiss/impl/kmeans1d.h>
-1
View File
@@ -251,7 +251,6 @@ def main() -> None:
# thread count.
faiss.omp_set_num_threads(faiss.omp_get_max_threads())
threads = faiss.omp_get_max_threads()
faiss.cvar.hnsw_deterministic_build = True
print("faiss HNSW deterministic-build benchmark")
rss = RSSPeak()
+2 -28
View File
@@ -49,17 +49,9 @@ class TestHNSW(unittest.TestCase):
nb = 20000
xb = np.random.RandomState(123).random((nb, d)).astype("float32")
# build() mutates the process-global OpenMP thread count and the
# deterministic-build flag; restore both so they do not leak into
# subsequent tests.
# build() mutates the process-global OpenMP thread count; restore it so
# it does not leak into subsequent tests.
self.addCleanup(faiss.omp_set_num_threads, faiss.omp_get_max_threads())
self.addCleanup(
setattr,
faiss.cvar,
"hnsw_deterministic_build",
faiss.cvar.hnsw_deterministic_build,
)
faiss.cvar.hnsw_deterministic_build = True
def build(nthreads):
faiss.omp_set_num_threads(nthreads)
@@ -97,24 +89,6 @@ class TestHNSW(unittest.TestCase):
seg = g8a[int(offs[i]) : int(offs[i + 1])]
self.assertNotIn(i, seg[seg >= 0].tolist())
def test_deterministic_build_recall(self):
# Same recall bar as test_hnsw (which exercises the lock-based default),
# so the deterministic graph is not merely reproducible but as good.
self.addCleanup(
setattr,
faiss.cvar,
"hnsw_deterministic_build",
faiss.cvar.hnsw_deterministic_build,
)
faiss.cvar.hnsw_deterministic_build = True
index = faiss.IndexHNSWFlat(self.xq.shape[1], 16)
index.add(self.xb)
Dhnsw, Ihnsw = index.search(self.xq, 1)
self.assertGreaterEqual((self.Iref == Ihnsw).sum(), 460)
self.io_and_retest(index, Dhnsw, Ihnsw)
def test_range_search(self):
index_flat = faiss.IndexFlat(self.xb.shape[1])
index_flat.add(self.xb)