-
Notifications
You must be signed in to change notification settings - Fork 32
[MOD-17578] Isolate a deleted element from the graph once its repairs are done #1012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
nonirosenfeldredis
wants to merge
3
commits into
main
Choose a base branch
from
sharon-17578-isolate-swap
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+250
−63
Open
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -205,7 +205,7 @@ class HNSWIndex : public VecSimIndexAbstract<DataType, DistType>, | |
| idType id) const; | ||
| void emplaceToHeap(vecsim_stl::abstract_priority_queue<DistType, labelType> &heap, | ||
| DistType dist, idType id) const; | ||
| void removeAndSwap(idType internalId); | ||
| void swapWithLast(idType removedId); | ||
|
|
||
| size_t getVectorRelativeIndex(idType id) const { return id % this->blockSize; } | ||
|
|
||
|
|
@@ -279,6 +279,26 @@ class HNSWIndex : public VecSimIndexAbstract<DataType, DistType>, | |
| void unmarkInProcess(idType internalId); | ||
| HNSWAddVectorState storeNewElement(labelType label, const void *vector_data); | ||
| void removeAndSwapMarkDeletedElement(idType internalId); | ||
| void removeFromGraph(idType internalId); | ||
| // Remove `id` from `level_data`'s links if it is still there. Unlike `removeLink`, tolerates | ||
| // its absence - an element that was already isolated (see `isolateDeletedElement`) holds no | ||
| // links at all, and a repair job running in parallel may have dropped the edge too. | ||
| static void removeLinkIfExists(ElementLevelData &level_data, idType id) { | ||
| for (size_t i = 0; i < level_data.getNumLinks(); i++) { | ||
| if (level_data.getLinkAtPos(i) == id) { | ||
| level_data.removeLink(id); | ||
| return; | ||
| } | ||
| } | ||
| } | ||
| // Take a marked-deleted element out of the graph entirely: drop every edge going out of it and | ||
| // every edge coming into it, at every level, keeping the bookkeeping of the other side | ||
| // consistent. To be called once all the repair jobs created for its deletion are done and | ||
| // before its swap job disposes of it, so that from that point no element refers to it and it | ||
| // refers to no element. | ||
| // Takes the per-element links locks (one at a time), so holding the main index guard for shared | ||
| // ownership is enough. | ||
| void isolateDeletedElement(idType internalId); | ||
| void repairNodeConnections(idType node_id, size_t level); | ||
| // For prefetching only. | ||
| const ElementMetaData *getMetaDataAddress(idType internal_id) const { | ||
|
|
@@ -951,7 +971,8 @@ void HNSWIndex<DataType, DistType>::repairConnectionsForDeletion( | |
| if (isMarkedDeleted(neighbour_id)) { | ||
| // Just remove the deleted element from the neighbor's neighbors list. No need to repair as | ||
| // this change is temporary, this neighbor is about to be removed from the graph as well. | ||
| neighbor_level.removeLink(element_internal_id); | ||
| // The link may already be gone if this neighbor was isolated upon completing its repairs. | ||
| removeLinkIfExists(neighbor_level, element_internal_id); | ||
| return; | ||
| } | ||
|
|
||
|
|
@@ -1541,7 +1562,20 @@ void HNSWIndex<DataType, DistType>::mutuallyRemoveNeighborAtPos(ElementLevelData | |
| // mutually, so it should be sufficient to look at the removed node's incoming edges set | ||
| // alone. | ||
| if (!removed_node_level.removeIncomingUnidirectionalEdgeIfExists(node_id)) { | ||
| node_level.newIncomingUnidirectionalEdge(removed_node); | ||
| // No record of this edge on the other side. Normally that means the edge was bidirectional, | ||
| // but it also happens when the removed node was already isolated: `isolateDeletedElement` | ||
| // clears its incoming edges set together with its links. Check whether it actually points | ||
| // back before recording the remaining direction. | ||
| bool points_back = false; | ||
| for (size_t i = 0; i < removed_node_level.getNumLinks(); i++) { | ||
| if (removed_node_level.getLinkAtPos(i) == node_id) { | ||
| points_back = true; | ||
| break; | ||
| } | ||
| } | ||
| if (points_back) { | ||
| node_level.newIncomingUnidirectionalEdge(removed_node); | ||
| } | ||
| } | ||
| } | ||
|
|
||
|
|
@@ -1644,20 +1678,73 @@ HNSWIndex<DataType, DistType>::~HNSWIndex() { | |
| */ | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::removeAndSwap(idType internalId) { | ||
| void HNSWIndex<DataType, DistType>::isolateDeletedElement(idType internalId) { | ||
| assert(isMarkedDeleted(internalId) && "Only a marked-deleted element may be isolated"); | ||
| auto element = getGraphDataByInternalId(internalId); | ||
| for (size_t level = 0; level <= element->toplevel; level++) { | ||
| // Take the element's edges at this level, and drop its own side of them right away - from | ||
| // here on it points to nothing, so a repair job that runs for it later finds no deleted | ||
| // neighbour and returns without touching it. | ||
| lockNodeLinks(internalId); | ||
| ElementLevelData &level_data = getElementLevelData(element, level); | ||
| auto neighbours = level_data.copyLinks(); | ||
| std::vector<idType> incoming_edges(level_data.getIncomingEdges().begin(), | ||
| level_data.getIncomingEdges().end()); | ||
| level_data.setNumLinks(0); | ||
|
Comment on lines
+1685
to
+1693
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we make this operation mutual? That is, in every iteration, acquire both locks (of the isolated node and the neighbor) in sorted order and update the edge removal. That might reduce the need to iterate over the node's neighbors in the remove-neighbor hot path. |
||
| for (idType incoming_id : incoming_edges) { | ||
| level_data.removeIncomingUnidirectionalEdgeIfExists(incoming_id); | ||
| } | ||
| unlockNodeLinks(internalId); | ||
|
|
||
| // Drop the other side of the outgoing edges: the neighbour either recorded this element as | ||
| // an incoming unidirectional edge (the expected case, as no element points to it anymore), | ||
| // or still points back at it - an incoming edge whose repair job never ran, which is | ||
| // dropped here as well. | ||
| for (idType neighbour_id : neighbours) { | ||
| lockNodeLinks(neighbour_id); | ||
| ElementLevelData &neighbour = getElementLevelData(neighbour_id, level); | ||
| if (!neighbour.removeIncomingUnidirectionalEdgeIfExists(internalId)) { | ||
| // No record on the neighbour's side, so it still points back here. Every *live* | ||
| // element that pointed at this one had a repair job that removed its edge before | ||
| // this point, so this is an edge between two deleted elements - neither of them | ||
| // gets a repair job for the other, hence it is dropped here. | ||
| assert(isMarkedDeleted(neighbour_id) && | ||
| "a live element still points to a fully repaired deleted element"); | ||
| removeLinkIfExists(neighbour, internalId); | ||
| } | ||
| unlockNodeLinks(neighbour_id); | ||
| } | ||
|
|
||
| // Same for any incoming edge that is still registered: remove it from its origin's links. | ||
| for (idType incoming_id : incoming_edges) { | ||
| // Same here: an incoming edge that survived all the repair jobs comes from another | ||
| // deleted element. | ||
| assert(isMarkedDeleted(incoming_id) && | ||
| "a live element still points to a fully repaired deleted element"); | ||
| lockNodeLinks(incoming_id); | ||
| removeLinkIfExists(getElementLevelData(incoming_id, level), internalId); | ||
| unlockNodeLinks(incoming_id); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::removeFromGraph(idType internalId) { | ||
| // Sanity check - the id to remove cannot be the entry point, as it should have been replaced | ||
| // upon marking it as deleted. | ||
| assert(entrypointNode != internalId); | ||
| auto element = getGraphDataByInternalId(internalId); | ||
|
|
||
| // Remove the deleted id form the relevant incoming edges sets in which it appears. | ||
| // Remove the deleted id form the relevant incoming edges sets in which it appears. For an | ||
| // asynchronously deleted element there is nothing to walk here: `isolateDeletedElement` already | ||
| // took all of its edges out when its last repair job completed. | ||
| for (size_t level = 0; level <= element->toplevel; level++) { | ||
| ElementLevelData &cur_level = getElementLevelData(element, level); | ||
| for (size_t i = 0; i < cur_level.getNumLinks(); i++) { | ||
| ElementLevelData &neighbour = getElementLevelData(cur_level.getLinkAtPos(i), level); | ||
| // Note that in case of in-place delete, we might have not accounted for this edge in | ||
| // Note that in case of in-place delete, we might have not accounted for this edge | ||
| // in the unidirectional edges, since there is no point in keeping it there temporarily | ||
| // (we know we will get here and remove this deleted id permanently). | ||
| // . (We know we will get here and remove this deleted id permanently.) | ||
| // However, upon asynchronous delete, this should always succeed since we do update | ||
| // the incoming edges in the mutual update even for deleted elements. | ||
| bool res = neighbour.removeIncomingUnidirectionalEdgeIfExists(internalId); | ||
|
|
@@ -1673,16 +1760,19 @@ void HNSWIndex<DataType, DistType>::removeAndSwap(idType internalId) { | |
|
|
||
| // We can say now that the element has removed completely from index. | ||
| --curElementCount; | ||
| } | ||
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::swapWithLast(idType removedId) { | ||
| // Get the last element's metadata and data. | ||
| // If we are deleting the last element, we already destroyed it's metadata. | ||
| // If we are deleting the last element, we already destroyed its metadata. | ||
| auto *last_element_data = getDataByInternalId(curElementCount); | ||
| DataBlock &last_gd_block = graphDataBlocks.back(); | ||
| auto last_element = (ElementGraphData *)last_gd_block.removeAndFetchLastElement(); | ||
|
|
||
| // Swap the last id with the deleted one, and invalidate the last id data. | ||
| if (curElementCount != internalId) { | ||
| SwapLastIdWithDeletedId(internalId, last_element, last_element_data); | ||
| if (curElementCount != removedId) { | ||
| SwapLastIdWithDeletedId(removedId, last_element, last_element_data); | ||
| } | ||
|
|
||
| // If we need to free a complete block and there is at least one block between the | ||
|
|
@@ -1693,7 +1783,8 @@ void HNSWIndex<DataType, DistType>::removeAndSwap(idType internalId) { | |
|
|
||
| template <typename DataType, typename DistType> | ||
| void HNSWIndex<DataType, DistType>::removeAndSwapMarkDeletedElement(idType internalId) { | ||
| removeAndSwap(internalId); | ||
| removeFromGraph(internalId); | ||
| swapWithLast(internalId); | ||
| // element is permanently removed from the index, it is no longer counted as marked deleted. | ||
| --numMarkedDeleted; | ||
| } | ||
|
|
@@ -1756,7 +1847,8 @@ void HNSWIndex<DataType, DistType>::removeVectorInPlace(const idType element_int | |
| } | ||
| // Finally, remove the element from the index and make a swap with the last internal id to | ||
| // avoid fragmentation and reclaim memory when needed. | ||
| removeAndSwap(element_internal_id); | ||
| removeFromGraph(element_internal_id); | ||
| swapWithLast(element_internal_id); | ||
| } | ||
|
|
||
| // Store the new element in the global data structures and keep the new state. In multithreaded | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a hot code path called many times for every vector insertion/repair job, and we are adding another loop over the node's
Mneighbours. This might have a significant performance impact. Let's see if we can avoid this addition, and if not, we would need to assess the penalty for largeMuse cases.