Fix HNSW allow-replace-deleted mode before shared memory - #1321
Conversation
Signed-off-by: Brennan Cathcart <brennancathcart@gmail.com>
|
|
/assign-reviewers |
|
Reviewers for this PR
Assigned automatically to the least-assigned members of the reviewer pools in |
| // addPoint() routes an existing label to an in-place update. | ||
| algo_->addPoint((T *)record.data(), internal_id, | ||
| algo_->allow_replace_deleted_); | ||
| auto evicted = algo_->addPoint((T *)record.data(), internal_id, |
There was a problem hiding this comment.
This should be hardcoded to false instead of passing algo_->allow_replace_deleted_:
auto evicted = algo_->addPoint((T *)record.data(), internal_id, false);
Passing allow_replace_deleted_ here is actually a bug that could lead to a server crash. If a document is modified but its label is inexplicably missing from label_lookup_ (e.g. due to an RDB anomaly or transient state), passing true gives HNSW permission to steal a vacant tombstone slot. It will then return the tombstone's old label in evicted, which triggers the CHECK(!evicted.has_value()) and crashes the entire server process.
By passing false, you explicitly deny HNSW permission to steal a tombstone. Instead, if the label is missing, HNSW will gracefully allocate a brand new slot for it. evicted will safely evaluate to std::nullopt (preventing the crash), and the module will seamlessly resurrect the missing node back into the graph.
This is also aligned with my fix to main branch.
|
Hi @BCathcart 👋 — flagging this as a P1 launch blocker for valkey-search 1.3 RC1. We're cutting the release branch the morning of Sept 14 (RC1 lands Sept 15), so all P1s need to be merged before then. First-pass reviewer: @neerajr0 — if your first-pass review is already done, please ignore this message; otherwise, please prioritize getting this PR reviewed. Second-pass reviewer: @yairgott — please take a look/followup with the final review and merge once everything looks good. If anything is blocking merge (open changes, CI, design questions), drop a note here so we can unblock quickly. Board: #1346. Thanks so much! 🙏 |
Finishes solving #1282 and solves #1288.
There are three things addressed here:
1. Fully recovering to a healthy state when loading an RDB that contains an HNSW index with duplicate labels.
#1283 prevents new instances of duplicate labels being created. It also prevents the validation logic from rejecting existing RDBs with duplicate labels and restores the HNSW index to the state it was in at the time of save, but that isn't a fully healthy state. There will be dangling vector pointers in the index (see #1282 (comment)). This PR actually repairs the HNSW state so that there are no dangling pointers by enforcing label uniqueness. This gives a clear one-to-one mapping of labels in
tracked_vectors_and labels indata_level0_memory_.2. Cleaning up an overwritten slot's vector from
tracked_vectors_.The whole point of enabling
allow-replace-deletedis to reduce memory usage with the potential risk of reducing the quality of the graph structure (incoming edges lose effectiveness when the vector is arbitrarily replaced). The problem was, when a slot was overwritten, we weren't actually freeing the most significant part of the memory associated with it: the old vector (#1288). Now the HNSW library layer returns the label of an overwritten slot so thattracked_vectors_can be cleaned up.3. Removes tombstoned labels from
label_lookup_.Labels are used by the search module to operate on live vectors. Once a vector is deleted from the keyspace, and the module has told the HNSW library to tombstone the slot with that label, there is no more use for mapping from label -> slot.
label_lookup_now only contains live labels as a simplification and space optimization.