Skip to content

Commit cb6a017

Browse files
ddrcoderfacebook-github-bot
authored andcommitted
Add optional persistent locks to IndexHNSW for incremental adds
Summary: `IndexHNSW` allocates an initializes locks for `ntotal+n` nodes on every call to `add()`. This makes batched insertion very costly, and incremental insertion prohibitively so. This diff introduces optional persistent locks for `IndexHNSW` to improve incremental `add()` performance. Previously, `omp_lock_t` arrays of size `ntotal+n` were created/destroyed on each `add()` call. Now locks can be retained via a new `retain_locks` flag (default: false), using a new `HNSW::Lock` RAII wrapper with geometric growth. RFC: Instead of `retain_locks` being the only way to opt into this new behavior, this could be inferred on the first incremental add. That is, clear the locks after insertion iff `n0 == 0`. Workloads which call `add()` once would be unaffected, but workloads which call `add()` repeatedly would 1) forego the clearing of the lock vector after `add()` call #2, and reuse locks for all subsequent calls. The downside would be the lack of the ability to reclaim the locks after insertion without HNSW-specific behavior at the call site. Differential Revision: D98232750
1 parent aef066a commit cb6a017

File tree

7 files changed

+75
-28
lines changed

7 files changed

+75
-28
lines changed

benchs/bench_hnsw.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,3 +189,34 @@ def evaluate(index):
189189
print("search_L", search_L, end=' ')
190190
index.nsg.search_L = search_L
191191
evaluate(index)
192+
193+
194+
if "hnsw_locks" in todo:
195+
196+
batch_size = xb.shape[0] // 100
197+
print(
198+
f"Testing HNSW Flat: add with {batch_size=}, "
199+
"with and without retaining locks"
200+
)
201+
202+
for retain_locks in [False, True]:
203+
index = faiss.IndexHNSWFlat(d, 32)
204+
index.hnsw.efConstruction = 40
205+
index.retain_locks = retain_locks
206+
207+
t0 = time.time()
208+
t1 = None
209+
t2 = None
210+
for i in range(0, len(xb), batch_size):
211+
t1 = time.time()
212+
index.add(xb[i : i + batch_size])
213+
t2 = time.time()
214+
if i > 2 and t2 - t0 > 2:
215+
break
216+
217+
assert t1 and t2
218+
dt = t2 - t0
219+
print(
220+
f"\t {retain_locks=:1}: {index.ntotal} added in {t2 - t0:6.3f}s"
221+
f" = {index.ntotal/(t2-t0):.0f}/s"
222+
)

faiss/IndexBinaryHNSW.cpp

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,7 @@ void hnsw_add_vertices(
6161
printf(" max_level = %d\n", max_level);
6262
}
6363

64-
std::vector<omp_lock_t> locks(ntotal);
65-
for (size_t i = 0; i < ntotal; i++) {
66-
omp_init_lock(&locks[i]);
67-
}
64+
HNSW::Locks locks(ntotal);
6865

6966
// add vectors from highest to lowest level
7067
std::vector<int> hist;
@@ -156,10 +153,6 @@ void hnsw_add_vertices(
156153
if (verbose) {
157154
printf("Done in %.3f ms\n", getmillisecs() - t0);
158155
}
159-
160-
for (size_t i = 0; i < ntotal; i++) {
161-
omp_destroy_lock(&locks[i]);
162-
}
163156
}
164157

165158
} // anonymous namespace

faiss/IndexHNSW.cpp

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,8 @@ void hnsw_add_vertices(
8282
printf(" max_level = %d\n", max_level);
8383
}
8484

85-
std::vector<omp_lock_t> locks(ntotal);
86-
for (size_t i = 0; i < ntotal; i++) {
87-
omp_init_lock(&locks[i]);
88-
}
85+
auto& locks = index_hnsw.locks;
86+
locks.resize(ntotal);
8987

9088
// add vectors from highest to lowest level
9189
std::vector<int> hist;
@@ -199,9 +197,8 @@ void hnsw_add_vertices(
199197
if (verbose) {
200198
printf("Done in %.3f ms\n", getmillisecs() - t0);
201199
}
202-
203-
for (size_t i = 0; i < ntotal; i++) {
204-
omp_destroy_lock(&locks[i]);
200+
if (!index_hnsw.retain_locks) {
201+
locks = HNSW::Locks{};
205202
}
206203
}
207204

@@ -366,6 +363,7 @@ void IndexHNSW::reset() {
366363
hnsw.reset();
367364
storage->reset();
368365
ntotal = 0;
366+
locks = HNSW::Locks{};
369367
}
370368

371369
void IndexHNSW::reconstruct(idx_t key, float* recons) const {
@@ -526,10 +524,7 @@ void IndexHNSW::init_level_0_from_entry_points(
526524
int n,
527525
const storage_idx_t* points,
528526
const storage_idx_t* nearests) {
529-
std::vector<omp_lock_t> locks(ntotal);
530-
for (idx_t i = 0; i < ntotal; i++) {
531-
omp_init_lock(&locks[i]);
532-
}
527+
locks.resize(ntotal);
533528

534529
#pragma omp parallel
535530
{
@@ -547,7 +542,7 @@ void IndexHNSW::init_level_0_from_entry_points(
547542
dis->set_query(vec.data());
548543

549544
hnsw.add_links_starting_from(
550-
*dis, pt_id, nearest, (*dis)(nearest), 0, locks.data(), vt);
545+
*dis, pt_id, nearest, (*dis)(nearest), 0, locks, vt);
551546

552547
if (verbose && i % 10000 == 0) {
553548
printf(" %d / %d\r", i, n);
@@ -559,8 +554,9 @@ void IndexHNSW::init_level_0_from_entry_points(
559554
printf("\n");
560555
}
561556

562-
for (idx_t i = 0; i < ntotal; i++) {
563-
omp_destroy_lock(&locks[i]);
557+
if (!retain_locks) {
558+
locks = HNSW::Locks{};
559+
FAISS_THROW_IF_NOT(locks.capacity() == 0);
564560
}
565561
}
566562

faiss/IndexHNSW.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,17 @@ struct IndexHNSW : Index {
5252
// See impl/VisitedTable.h.
5353
std::optional<bool> use_visited_hashset;
5454

55+
// Per-node locks for HNSW graph construction.
56+
HNSW::Locks locks;
57+
// locks are freed after each call to add() unless this flag is set.
58+
bool retain_locks = false;
59+
5560
explicit IndexHNSW(int d = 0, int M = 32, MetricType metric = METRIC_L2);
5661
explicit IndexHNSW(Index* storage, int M = 32);
5762

5863
~IndexHNSW() override;
5964

65+
/// Adds vectors to the index. May not be called concurrently.
6066
void add(idx_t n, const float* x) override;
6167

6268
/// Trains the storage if needed

faiss/impl/HNSW.cpp

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
#include <cinttypes>
1111
#include <cstddef>
12+
#include <cstdlib>
1213

1314
#include <faiss/IndexHNSW.h>
1415

@@ -504,7 +505,7 @@ void HNSW::add_links_starting_from(
504505
storage_idx_t nearest,
505506
float d_nearest,
506507
int level,
507-
omp_lock_t* locks,
508+
Locks& locks,
508509
VisitedTable& vt,
509510
bool keep_max_size_level0) {
510511
std::priority_queue<NodeDistCloser> link_targets;
@@ -543,7 +544,7 @@ void HNSW::add_with_locks(
543544
DistanceComputer& ptdis,
544545
int pt_level,
545546
int pt_id,
546-
std::vector<omp_lock_t>& locks,
547+
Locks& locks,
547548
VisitedTable& vt,
548549
bool keep_max_size_level0) {
549550
storage_idx_t nearest = entry_point;
@@ -580,7 +581,7 @@ void HNSW::add_with_locks(
580581
nearest,
581582
d_nearest,
582583
level,
583-
locks.data(),
584+
locks,
584585
vt,
585586
keep_max_size_level0);
586587
}

faiss/impl/HNSW.h

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
#include <faiss/impl/DistanceComputer.h>
1818
#include <faiss/impl/FaissAssert.h>
1919
#include <faiss/impl/maybe_owned_vector.h>
20-
#include <faiss/impl/platform_macros.h>
2120
#include <faiss/utils/Heap.h>
2221
#include <faiss/utils/random.h>
2322

@@ -91,6 +90,26 @@ struct HNSW {
9190
int count_below(float thresh);
9291
};
9392

93+
/// RAII wrapper for `omp_lock_t`.
94+
struct Lock : omp_lock_t {
95+
Lock() {
96+
omp_init_lock(this);
97+
}
98+
~Lock() {
99+
omp_destroy_lock(this);
100+
}
101+
// Copy is a no-op, defined to enable std::vector resize.
102+
// Lock is assumed to not be held.
103+
Lock(const Lock&) : Lock() {}
104+
Lock& operator=(const Lock&) {
105+
return *this;
106+
}
107+
108+
Lock(Lock&&) = delete;
109+
Lock& operator=(Lock&&) = delete;
110+
};
111+
using Locks = std::vector<Lock>;
112+
94113
/// to sort pairs of (id, distance) from nearest to farthest or the reverse
95114
struct NodeDistCloser {
96115
float d;
@@ -196,7 +215,7 @@ struct HNSW {
196215
storage_idx_t nearest,
197216
float d_nearest,
198217
int level,
199-
omp_lock_t* locks,
218+
Locks& locks,
200219
VisitedTable& vt,
201220
bool keep_max_size_level0 = false);
202221

@@ -206,7 +225,7 @@ struct HNSW {
206225
DistanceComputer& ptdis,
207226
int pt_level,
208227
int pt_id,
209-
std::vector<omp_lock_t>& locks,
228+
Locks& locks,
210229
VisitedTable& vt,
211230
bool keep_max_size_level0 = false);
212231

faiss/python/swigfaiss.swig

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ void gpu_sync_all_devices()
557557
%include <faiss/IndexIVFSpectralHash.h>
558558
%include <faiss/IndexIVFAdditiveQuantizer.h>
559559
%include <faiss/impl/HNSW.h>
560+
%ignore faiss::IndexHNSW::locks;
560561
%include <faiss/IndexHNSW.h>
561562

562563
%include <faiss/impl/kmeans1d.h>

0 commit comments

Comments
 (0)