Skip to content

Parallelize the repair process for disconnected nodes during HNSW merge-re-use - #16627

Open
ethbak wants to merge 2 commits into
apache:mainfrom
ethbak:parallel-hnsw-merge-repair
Open

ethbak wants to merge 2 commits into
apache:mainfrom
ethbak:parallel-hnsw-merge-repair

Conversation

@ethbak

@ethbak ethbak commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Overview

Currently, when IncrementalHnswGraphMerger reuses an existing graph, InitializedHnswGraphBuilder copies it, repairs the nodes that lost too many neighbors, and rebalances the levels. This logic runs inside ConcurrentHnswMerger.createBuilder. Which is called before HnswConcurrentMergeBuilder even exists, so the copy/repair/rebalance process runs on the single merge thread without utilizing the worker pool.

This is expensive especially for repair as it does a search per flagged node. Benchmarking with a 500k cohere index at 35% deletes shows repair takes 97.7s of a 174.7s merge. This is slower than the full rebuild process which does use the paralellized worker pool, even though it reduces CPU utilization. (Full rebuild of the same index took 97.7s for ALL steps, while the repair step itself took the same amount of time)

Changes

The three phases used to be one unit where initGraph would copy, repair, and rebalance the graph. With this change, the copy now fires alone (keep this part serial, its difficult/not worth to parallelize) and the other work is deferred to the concurrent merge builder which finishes the process. In practice: InitializedHnswGraphBuilder.copyGraph executes the copy and returns the half-built graph and the list of nodes needing repair per level, then ConcurrentHnswMerger hands both to HnswConcurrentMergeBuilder, which repairs and rebalances inside build(), which utilizes the worker pool.

Repair is now structured to be per node per level work items to allow this parallelization. On each level, the workers take batches of flagged nodes off a shared AtomicInteger until that level's list is drained, one task per worker, and each worker repairs its nodes through its own scorer and searcher.

Levels are repaired top-down with invokeAll serializing the levels (workers dont move on to next level until the current one is complete). This is necessary because nodes with no surviving neighbors have no entry point to search from, so it walks from the entry node to reach the current level, which looks at above levels (that must be completed before this happens). For this pr the rebalance work stays serial on the copy builder afterwards, as its time is negligible to the overall re-use process.

Repairing concurrently needed one lock that a normal insert doesn't. With copied nodes that already inherit connections, its possible one node can be adding a reciprocal link into the same array that another is selecting. During repair, I hold hnswLock.write(level, node) around the selection and snapshot the entry points under hnswLock.read(level, node) before searching. I then release the lock before the per-neighbor loop that takes hnswLock.write(level, nbr). This ensures that a worker holds at most one stripe at a time.

Notes for reviewers

  • The first commit is pure restructuring as I needed to moves fixDisconnectedNodes and addConnections onto HnswGraphBuilder, so it may be useful to look at inter-commit diff while reviewing.
  • I suspect this will conflict with Make HNSW merges abortable during graph initialization and repair #16620 which at the time of writing is approved but not merged, so I will rebase afterwards.

Benchmarking / Testing

In addition to the unit tests added in the PR, I ran before/after benchmarks on cohere to show the difference with/without parallelization.

cohere-1024, M=32, beamWidth=200, 500k vectors indexed as 5x100k segments, 35% delete, forceMerge(1), numMergeWorkers=8, m7g.4xlarge pinned to 8 cores

Arm copy+repair parallel insert total vs rebuild
reuse: before (#16618) 97.7s 77.0s 174.7s 1.79×
reuse: this fix 18.1s 79.2s 97.4s 1.00×
rebuild 3.0s 94.7s 97.7s 1.00×

We see a dramatic speedup in copy+repair time from this fix compared to re-use with no parallelism. Note that in this run specifically, re-use and rebuild are nearly the same, but this is at 35% deletes (near upper limit of the amount of repair work the merge algo needs to do) This shows that we, at worst, restore the wall-time that was lost with the previous unparallel implementation and that with lower delete percents, the speedup will be more visible (at 10% deletes, I see a ~13% speedup with re-use vs rebuild).

Closes #16618

@ethbak
ethbak force-pushed the parallel-hnsw-merge-repair branch from 30cddd3 to 7e430f7 Compare September 4, 2026 13:36
@ethbak

ethbak commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Updated PR to resolve merge conflicts since #16618 was merged.

Comment thread lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/util/hnsw/InitializedHnswGraphBuilder.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/util/hnsw/InitializedHnswGraphBuilder.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java Outdated
Comment thread lucene/core/src/java/org/apache/lucene/util/hnsw/HnswConcurrentMergeBuilder.java Outdated
}

/**
* Fixes disconnected nodes at a specific level by searching from each node's existing neighbors

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's preserve the javadocs from the original implementation please, unless we (a human, not the AI, please) read them and think they need correcting. Honestly I'm finding it really tedious reviewing the AI's javadocs. Could you please take a pass over them all yourself and see if you think they are OK

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sorry if you did that, but I'm finding enough things that sounds like AI to me ...

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry about that, I share the same opinion about these AI code comments. As I was trying to get it to improve the net-new comments it wrote it seems it messed with a bunch of others that wasn't my intention. I did this right at the end of my own self review and didn't think to look at the other comments it hadnt touched before. Will double check this in the future.

@ethbak
ethbak force-pushed the parallel-hnsw-merge-repair branch 2 times, most recently from ba08a3a to 8ea4b7d Compare September 8, 2026 14:13
Repair ran serially inside createBuilder and dominated merge time; it now runs across the merge worker pool, one level at a time.
@ethbak
ethbak force-pushed the parallel-hnsw-merge-repair branch from 8ea4b7d to 0c4f49d Compare September 8, 2026 14:16
@msokolov

msokolov commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Please try to avoid force-push if possible, since it loses the commits on which the reviews are based and makes tracking the changes much more difficult

@ethbak
ethbak force-pushed the parallel-hnsw-merge-repair branch 3 times, most recently from 3db8865 to 0c4f49d Compare September 9, 2026 00:22
@ethbak

ethbak commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Please try to avoid force-push if possible, since it loses the commits on which the reviews are based and makes tracking the changes much more difficult

Sorry! I was trying to revise and my agent ended up modifying the PR when I meant to just reconcile and push so I could review it myself first. My error, please do not review yet, I will add a more detailed update when I get a chance to fully review and finalize first.

@Pulkitg64

Copy link
Copy Markdown
Contributor

My error, please do not review yet, I will add a more detailed update when I get a chance to fully review and finalize first.

If this PR is not finalized, then you could mark this PR as draft.

@ethbak
ethbak marked this pull request as draft September 9, 2026 15:20
@ethbak

ethbak commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Thanks all for the feedback from previous reviewers, and sorry for the mess with the force pushing and draft PR stuff, I'm still getting the hang of this github thing for real prod-level development.

Hopefully it is clear by my comment replies and javadocs what was changed and why, but happy to help clarify more if needed.

I am also in the middle of running some more benchmarks on cohere to show the before and after results at some different deletion percents, so I'll add those in an additional comment when they are finished.

Going to reopen this for review now.

@ethbak
ethbak marked this pull request as ready for review September 9, 2026 18:16
@ethbak
ethbak requested a review from msokolov September 9, 2026 18:16
@ethbak

ethbak commented Sep 9, 2026

Copy link
Copy Markdown
Contributor Author

Some benchmarking results:

Cohere-1024, 500k normalized float32 vectors indexed as 5x100k segments, M=32, beamWidth=200, forceMerge(1), eight merge workers, Corretto 25, and 16 pinned CPUs on a Graviton desktop.

1% deletes

Arm copy+repair parallel insert total vs rebuild
reuse: before (#16618) 11.2s 122.8s 134.0s 0.86x
reuse: this fix 5.6s 125.3s 130.9s 0.84x
rebuild 4.2s 151.7s 155.8s 1.00x

5% deletes

Arm copy+repair parallel insert total vs rebuild
reuse: before (#16618) 26.5s 120.1s 146.5s 1.00x
reuse: this fix 8.0s 120.0s 128.0s 0.87x
rebuild 4.0s 142.3s 146.4s 1.00x

20% deletes

Arm copy+repair parallel insert total vs rebuild
reuse: before (#16618) 112.2s 99.2s 211.5s 1.73x
reuse: this fix 20.9s 99.1s 119.9s 0.98x
rebuild 3.5s 119.0s 122.4s 1.00x

35% deletes

Arm copy+repair parallel insert total vs rebuild
reuse: before (#16618) 97.5s 79.3s 176.8s 1.74x
reuse: this fix 17.4s 78.6s 96.0s 0.94x
rebuild 3.0s 98.9s 101.9s 1.00x

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

HNSW merge-reuse graph repair runs single-threaded on the merge thread, slowing down merges

3 participants