Skip to content

Commit c67411b

Browse files
Michael Norrisfacebook-github-bot
authored andcommitted
faiss HNSW: make graph construction deterministic by default (remove lock-based build) (facebookresearch#5486)
Summary: TLDR: makes the deterministic HNSW graph build the default (and only) float build path, and removes the legacy lock-based one. Inspired by ParlayANN. The deterministic build is reproducible AND faster than the lock-based build at every scale and thread count we measured. -- 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: - original ParlanANN targets flat graphs like Vamana - re-uses Faiss HNSW pruning in `shrink_neighbor_list` --- AI (with a bunch of edits) explanation in more detail: -- What changed - `IndexHNSW::add` now always uses the deterministic, lock-free build. The lock-based `hnsw_add_vertices` (float) and the opt-in `deterministic_build` flag are removed. - 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. - The binary `IndexBinaryHNSW` keeps its own independent lock-based build (it has no deterministic variant). 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 removed 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`: - `deterministic_build` was never serialized (zero references), so removing it is format-neutral. It was a runtime build flag, like `retain_locks`. - `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). ## 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 removed 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. Differential Revision: D112025877
1 parent 4d74915 commit c67411b

10 files changed

Lines changed: 962 additions & 233 deletions

File tree

.github/actions/build_cmake/action.yml

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ inputs:
3232
description: 'Upload test artifacts. Prevents collisions when multiple jobs need to run build_cmake.'
3333
required: false
3434
default: 'true'
35+
sanitizers:
36+
description: 'Build with AddressSanitizer + UndefinedBehaviorSanitizer and run the tests under them.'
37+
required: false
38+
default: 'OFF'
3539
runs:
3640
using: composite
3741
steps:
@@ -201,7 +205,7 @@ runs:
201205
- name: Setup ccache
202206
uses: hendrikmuhs/ccache-action@v1
203207
with:
204-
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.opt_level }}-gpu${{ inputs.gpu }}-cuvs${{ inputs.cuvs }}-rocm${{ inputs.rocm }}-svs${{ inputs.svs }}
208+
key: ${{ runner.os }}-${{ runner.arch }}-${{ inputs.opt_level }}-gpu${{ inputs.gpu }}-cuvs${{ inputs.cuvs }}-rocm${{ inputs.rocm }}-svs${{ inputs.svs }}-san${{ inputs.sanitizers }}
205209
max-size: 2G
206210
update-package-index: true
207211
- name: Setup macOS Metal environment
@@ -225,6 +229,8 @@ runs:
225229
-DFAISS_ENABLE_ROCM=${{ inputs.rocm }} \
226230
-DFAISS_OPT_LEVEL=${{ inputs.opt_level }} \
227231
-DFAISS_ENABLE_SVS=${{ inputs.svs }} \
232+
-DFAISS_ENABLE_ASAN=${{ inputs.sanitizers }} \
233+
-DFAISS_ENABLE_UBSAN=${{ inputs.sanitizers }} \
228234
-DFAISS_ENABLE_C_API=ON \
229235
-DPYTHON_EXECUTABLE=$CONDA/bin/python \
230236
-DCMAKE_BUILD_TYPE=Release \
@@ -251,6 +257,11 @@ runs:
251257
- name: C++ tests
252258
if: inputs.metal != 'ON'
253259
shell: bash
260+
env:
261+
# Leaks are expected (SWIG/BLAS/OpenMP own long-lived allocations), so
262+
# only hunt for memory errors / UB. Harmless on non-sanitizer builds.
263+
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
264+
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
254265
run: |
255266
conda list --show-channel-urls
256267
export GTEST_OUTPUT="xml:$(realpath .)/test-results/googletest/"
@@ -269,7 +280,7 @@ runs:
269280
shell: bash
270281
run: python3 -m pytest faiss/gpu_metal/test/test_metal_python.py -v
271282
- name: C++ perf benchmarks
272-
if: inputs.rocm == 'OFF' && inputs.metal != 'ON'
283+
if: inputs.rocm == 'OFF' && inputs.metal != 'ON' && inputs.sanitizers != 'ON'
273284
shell: bash
274285
run: |
275286
conda list --show-channel-urls
@@ -319,12 +330,31 @@ runs:
319330
# Confirm torch links against ROCm 7.2 (matches the system /opt/rocm-7.2.0).
320331
python -c "import torch; print('torch', torch.__version__, 'hip', torch.version.hip)"
321332
- name: Python tests (CPU only)
322-
if: inputs.gpu == 'OFF' && inputs.metal != 'ON'
333+
if: inputs.gpu == 'OFF' && inputs.metal != 'ON' && inputs.sanitizers != 'ON'
323334
shell: bash
324335
run: |
325336
conda list --show-channel-urls
326337
pytest --junitxml=test-results/pytest/results.xml tests/test_*.py
327338
pytest --junitxml=test-results/pytest/results-torch.xml tests/torch_*.py
339+
- name: Python tests (CPU only, sanitizers)
340+
if: inputs.gpu == 'OFF' && inputs.metal != 'ON' && inputs.sanitizers == 'ON'
341+
shell: bash
342+
env:
343+
ASAN_OPTIONS: detect_leaks=0:abort_on_error=1
344+
UBSAN_OPTIONS: print_stacktrace=1:halt_on_error=1
345+
run: |
346+
conda list --show-channel-urls
347+
# faiss.so / _swigfaiss*.so are built with -fsanitize=address, so a
348+
# non-instrumented CPython must load the ASan (then UBSan) runtime first
349+
# or the import aborts ("ASan runtime does not come first"). Preload them
350+
# from the same conda toolchain that built faiss.
351+
GCC=x86_64-conda-linux-gnu-gcc
352+
export LD_PRELOAD="$($GCC -print-file-name=libasan.so):$($GCC -print-file-name=libubsan.so)"
353+
# This job targets the HNSW build determinism bug; it is a dynamic-
354+
# dispatch build, so the NONE/AVX2/AVX512 variants of these suites all
355+
# run under the sanitizers. Broaden to tests/test_*.py once green.
356+
pytest --junitxml=test-results/pytest/results.xml \
357+
tests/test_graph_based.py tests/test_index_binary.py
328358
- name: Python tests (CPU + GPU)
329359
if: inputs.gpu == 'ON'
330360
shell: bash
@@ -349,7 +379,7 @@ runs:
349379
if: inputs.upload_artifacts == 'true'
350380
uses: actions/upload-artifact@v4
351381
with:
352-
name: test-results-arch=${{ runner.arch }}-opt=${{ inputs.opt_level }}-gpu=${{ inputs.gpu }}-cuvs=${{ inputs.cuvs }}-rocm=${{ inputs.rocm }}-svs=${{ inputs.svs }}
382+
name: test-results-arch=${{ runner.arch }}-opt=${{ inputs.opt_level }}-gpu=${{ inputs.gpu }}-cuvs=${{ inputs.cuvs }}-rocm=${{ inputs.rocm }}-svs=${{ inputs.svs }}-san=${{ inputs.sanitizers }}
353383
path: test-results
354384
- name: Check installed packages channel
355385
if: inputs.metal != 'ON'

.github/workflows/build-pull-request.yml

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,19 @@ jobs:
124124
uses: ./.github/actions/build_cmake
125125
with:
126126
opt_level: dd
127+
linux-x86_64-DD-ASAN-UBSAN-cmake:
128+
name: Linux x86_64 Dynamic Dispatch ASan+UBSan (cmake)
129+
needs: [linux-x86_64-cmake, changes]
130+
if: ${{ needs.changes.outputs.has_cpu_changes == 'true' || github.event_name != 'pull_request' }}
131+
runs-on: faiss-aws-m7i.large
132+
steps:
133+
- name: Checkout
134+
uses: actions/checkout@v4
135+
- name: Build and Test (cmake)
136+
uses: ./.github/actions/build_cmake
137+
with:
138+
opt_level: dd
139+
sanitizers: ON
127140
linux-x86_64-GPU-cmake:
128141
name: Linux x86_64 GPU (cmake)
129142
needs: [linux-x86_64-cmake, changes]

CMakeLists.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ option(FAISS_ENABLE_PYTHON "Build Python extension." ON)
9595
option(FAISS_ENABLE_C_API "Build C API." OFF)
9696
option(FAISS_ENABLE_EXTRAS "Build extras like benchmarks and demos" ON)
9797
option(FAISS_USE_LTO "Enable Link-Time optimization" OFF)
98+
option(FAISS_ENABLE_ASAN "Build with AddressSanitizer (-fsanitize=address)." OFF)
99+
option(FAISS_ENABLE_UBSAN
100+
"Build with UndefinedBehaviorSanitizer (-fsanitize=undefined)." OFF)
98101
if(APPLE AND CMAKE_SYSTEM_PROCESSOR STREQUAL "arm64")
99102
set(_FAISS_METAL_DEFAULT ON)
100103
else()
@@ -106,6 +109,20 @@ option(FAISS_ENABLE_SVS "Enable SVS (Intel(R) Scalable Vector Search) integratio
106109
set(FAISS_SVS_RUNTIME_VERSION "v0" CACHE STRING "Version of the SVS runtime API to use")
107110
set_property(CACHE FAISS_SVS_RUNTIME_VERSION PROPERTY STRINGS "v0")
108111

112+
# Sanitizer builds (opt-in, driven by the ASan+UBSan CI job). Applied globally
113+
# so every faiss target plus the test binaries and the Python extension are
114+
# instrumented. Keep frame pointers for readable reports; make UBSan abort so
115+
# CI fails on the first violation instead of only logging it.
116+
if(FAISS_ENABLE_ASAN)
117+
add_compile_options(-fsanitize=address -fno-omit-frame-pointer)
118+
add_link_options(-fsanitize=address)
119+
endif()
120+
if(FAISS_ENABLE_UBSAN)
121+
add_compile_options(-fsanitize=undefined -fno-omit-frame-pointer
122+
-fno-sanitize-recover=all)
123+
add_link_options(-fsanitize=undefined)
124+
endif()
125+
109126
if(FAISS_ENABLE_GPU)
110127
if(FAISS_ENABLE_ROCM)
111128
enable_language(HIP)

faiss/IndexBinaryHNSW.cpp

Lines changed: 11 additions & 134 deletions
Original file line numberDiff line numberDiff line change
@@ -43,136 +43,6 @@ namespace faiss {
4343
* add / search blocks of descriptors
4444
**************************************************************/
4545

46-
namespace {
47-
48-
void hnsw_add_vertices(
49-
IndexBinaryHNSW& index_hnsw,
50-
size_t n0,
51-
size_t n,
52-
const uint8_t* x,
53-
bool verbose,
54-
bool preset_levels = false) {
55-
HNSW& hnsw = index_hnsw.hnsw;
56-
size_t ntotal = n0 + n;
57-
double t0 = getmillisecs();
58-
if (verbose) {
59-
printf("hnsw_add_vertices: adding %zd elements on top of %zd "
60-
"(preset_levels=%d)\n",
61-
n,
62-
n0,
63-
int(preset_levels));
64-
}
65-
66-
int max_level = hnsw.prepare_level_tab(n, preset_levels);
67-
68-
if (verbose) {
69-
printf(" max_level = %d\n", max_level);
70-
}
71-
72-
auto& locks = index_hnsw.locks;
73-
locks.prepare(ntotal);
74-
75-
// add vectors from highest to lowest level
76-
std::vector<int> hist;
77-
std::vector<int> order(n);
78-
79-
{ // make buckets with vectors of the same level
80-
81-
// build histogram
82-
for (size_t i = 0; i < n; i++) {
83-
HNSW::storage_idx_t pt_id =
84-
static_cast<HNSW::storage_idx_t>(i + n0);
85-
int pt_level = hnsw.levels[pt_id] - 1;
86-
while (pt_level >= static_cast<int>(hist.size())) {
87-
hist.push_back(0);
88-
}
89-
hist[pt_level]++;
90-
}
91-
92-
// accumulate
93-
std::vector<int> offsets(hist.size() + 1, 0);
94-
for (size_t i = 0; i < hist.size() - 1; i++) {
95-
offsets[i + 1] = offsets[i] + hist[i];
96-
}
97-
98-
// bucket sort
99-
for (size_t i = 0; i < n; i++) {
100-
HNSW::storage_idx_t pt_id =
101-
static_cast<HNSW::storage_idx_t>(i + n0);
102-
int pt_level = hnsw.levels[pt_id] - 1;
103-
order[offsets[pt_level]++] = pt_id;
104-
}
105-
}
106-
107-
{ // perform add
108-
RandomGenerator rng2(789);
109-
110-
size_t i1 = static_cast<int>(n);
111-
112-
for (int pt_level = static_cast<int>(hist.size()) - 1;
113-
pt_level >= int(!index_hnsw.init_level0);
114-
pt_level--) {
115-
size_t i0 = i1 - hist[pt_level];
116-
117-
if (verbose) {
118-
printf("Adding %zu elements at level %d\n", i1 - i0, pt_level);
119-
}
120-
121-
// random permutation to get rid of dataset order bias
122-
for (size_t j = i0; j < i1; j++) {
123-
std::swap(
124-
order[j],
125-
order[j + rng2.rand_int(static_cast<int>(i1 - j))]);
126-
}
127-
128-
#pragma omp parallel
129-
{
130-
std::unique_ptr<VisitedTable> vt = VisitedTable::create(ntotal);
131-
132-
std::unique_ptr<DistanceComputer> dis(
133-
index_hnsw.get_distance_computer());
134-
bool do_display = verbose && omp_get_thread_num() == 0;
135-
size_t prev_display = 0;
136-
137-
#pragma omp for schedule(dynamic)
138-
for (int64_t i = i0; i < i1; i++) {
139-
HNSW::storage_idx_t pt_id = order[i];
140-
dis->set_query(
141-
(float*)(x + (pt_id - n0) * index_hnsw.code_size));
142-
143-
hnsw.add_with_locks(
144-
*dis,
145-
pt_level,
146-
pt_id,
147-
locks,
148-
*vt,
149-
index_hnsw.keep_max_size_level0 && (pt_level == 0));
150-
151-
if (do_display && i - i0 > prev_display + 10000) {
152-
prev_display = i - i0;
153-
printf(" %zu / %zu\r", i - i0, i1 - i0);
154-
fflush(stdout);
155-
}
156-
}
157-
}
158-
i1 = i0;
159-
}
160-
if (index_hnsw.init_level0) {
161-
FAISS_ASSERT(i1 == 0);
162-
} else {
163-
FAISS_ASSERT((i1 - hist[0]) == 0);
164-
}
165-
}
166-
if (verbose) {
167-
printf("Done in %.3f ms\n", getmillisecs() - t0);
168-
}
169-
if (!index_hnsw.retain_locks) {
170-
locks.clear();
171-
}
172-
}
173-
174-
} // anonymous namespace
175-
17646
/**************************************************************
17747
* IndexBinaryHNSW implementation
17848
**************************************************************/
@@ -270,13 +140,20 @@ void IndexBinaryHNSW::add(idx_t n, const uint8_t* x) {
270140
storage->add(n, x);
271141
ntotal = storage->ntotal;
272142

273-
hnsw_add_vertices(
274-
*this,
143+
bool preset_levels = hnsw.levels.size() == static_cast<size_t>(ntotal);
144+
hnsw_add_vertices_deterministic(
145+
hnsw,
275146
n0,
276147
n,
277-
x,
148+
d,
149+
init_level0,
150+
keep_max_size_level0,
151+
preset_levels,
278152
verbose,
279-
hnsw.levels.size() == static_cast<size_t>(ntotal));
153+
[this] { return get_distance_computer(); },
154+
[this, x, n0](DistanceComputer& dc, HNSW::storage_idx_t pt_id) {
155+
dc.set_query((const float*)(x + (pt_id - n0) * code_size));
156+
});
280157
}
281158

282159
void IndexBinaryHNSW::reset() {

0 commit comments

Comments
 (0)