Conversation
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
This draft explores a post-ingest concurrency opportunity in the CAGRA-HNSW Lucene writer.
Once Lucene has accepted all documents for a segment, the writer has two independent outputs to
produce:
The existing path writes those outputs serially. This change adds an opt-in coordinator that writes
the flat-vector branch on one platform thread while retaining CAGRA/HNSW construction on the caller
thread. Both branches are joined before the flush returns. The serial path remains the default.
This work starts only from data already accepted through
addDocument. It makes no assumptionsabout source files, document counts, document IDs, or vector availability before normal Lucene
ingestion.
Branch and review scope
This is a fork-only draft PR. Its base branch,
pr2476-pr2653-integration-base, pins the locallyvalidated integration of the PR 2476 memory/lifecycle work and the PR 2653 bounded graph-processing
work. The public PR 2653 branch and this integrated validation branch have divergent histories, so
using that public branch directly would obscure this PR with prerequisite changes.
The intended review diff is limited to:
Lucene99AcceleratedHNSWVectorsWriter;InfoStreaminstrumentation used to explain the benchmark result;No branch or commit is pushed to the NVIDIA/rapidsai remote by this work.
Motivation
Phase instrumentation showed that a cold Jasper-10M one-segment flush spent approximately:
The serial flush took 200.5 seconds, almost exactly the sum of those independent branches. Once
document ingestion is complete, running them concurrently changes that portion of the critical path
from approximately
flat + graphto approximatelymax(flat, graph).The graph branch remains on the caller thread intentionally. cuVS resources are thread-local, and
moving graph construction to an arbitrary worker would change resource ownership and cleanup. The
stock Lucene flat-vector writer is the branch moved to the worker in this prototype.
Implementation
Opt-in behavior
The feature is enabled with:
The value is captured when a segment writer is constructed, so a later JVM property mutation cannot
change the behavior of a live writer. When absent or false, flush ordering remains serial.
Flush sequence
For the opt-in path, the writer:
A fatal
Errortakes precedence over an ordinary exception even if the exception was observedfirst. Caller interruption is converted to
InterruptedIOException, the worker is joined, and thecaller's interrupted status is restored before returning.
Instrumentation
Per-segment phase messages cover:
The phase logger treats runtime failures from a custom
InfoStreamas non-authoritative telemetryfailures. Such a failure cannot replace the indexing result or mask an indexing exception.
Benchmark protocol
The promoted post-ingest measurements used:
efSearch=1500; andNo custom IVF-PQ heuristic is part of this change.
Results
Jasper 10M, 1536 dimensions
Times are seconds. "Indexing" is the benchmark's end-to-end CAGRA-HNSW indexing time.
fdatasynchelperOverlap alone reduced indexing time by 41.663 seconds, or 15.68% (1.186x). Overlap plus proactive
whole-file writeback reduced it by 58.612 seconds, or 22.06% (1.283x).
An independent run using the preserved PR 2476 native build reproduced the shape of the result:
fdatasync: 206.898 and 206.877 seconds.The latter pair averaged a 23.23% reduction from that matrix's serial baseline.
Deep1B 10M, 96 dimensions
fdatasynchelperThis control is important: overlap by itself is not universally faster. On Deep1B, it removed about
5.56 seconds from flush and moved about 5.35 seconds into final synchronization. The experiment
therefore identifies writeback scheduling as part of the end-to-end opportunity rather than claiming
that Java concurrency alone is sufficient on every dataset.
What this PR does not include
The proactive
fdatasyncmechanism above was an external benchmark helper that watched completedflat-vector files. It proved that dirty-page writeback explains the serial tail, but it is not
production code and is not included here. Lucene's final file and directory synchronization remained
authoritative in every measured arm and was never removed.
Range-based writeback, larger flat-vector buffers, and
.vexbuffering did not provide a materialgain beyond whole-file writeback and are not proposed:
noise-sized for these runs;
regressed Deep1B; and
overlapped critical path.
Correctness coverage
The new focused tests cover:
CheckIndex, reopen, document-sort order, exact vector-to-document mapping, graph sizes, andvector-search usability; and
Validation completed on an NVIDIA L40S:
cuvs-lucenesuite: 378 tests, zero failures and zero errors (30 pre-existingenvironment/assumption skips);
Relationship to deterministic segment sizing
For a 100M input where four 25M physical segments are desired, an explicit caller-side setting such
as
segmentSizeDocs=25_000_000is a better API than overloadingnumIndexingThreads=4to mean both"four segments" and "four ingestion threads."
Simply mapping 25M to Lucene's
maxBufferedDocsis not sufficient for pipelining. A one-thread DWPTflush is synchronous: the call that adds document 25M blocks while that segment flushes, so document
25,000,001 is not ingested concurrently. Multiple DWPT threads make boundaries nondeterministic and
can retain several 25M buffers.
The caller-side staged design should instead use:
segmentSizeDocs=25_000_000to derive exact contiguous slices;numIndexingThreads=1to mean actualaddDocumentconcurrency within a slice;maxInFlightSegments=2for bounded retention;maxBufferedDocsstrictly greater than its slice size, so the finaladdDocumentdoes not trigger a synchronous flush before handoff;NoMergePolicyandforceMerge=0; andaddIndexespublication using same-filesystem hardlinks.The benchmark harness already has a staged pipeline prototype with most of this scheduling. It still
derives partition count from
numIndexingThreads; decoupling that intosegmentSizeDocsis a harnessfollow-up, not a codec change in this PR.
Existing evidence supports the staged shape: for default-heuristic Deep1B-100M with four physical
segments and no merge, the safe sequential schedule took 301.681 seconds and the safe staged schedule
took 252.269 seconds, a 16.38% reduction. That comparison overlaps ingestion of partition N+1 with
commit of partition N; it is distinct from the within-segment flat/graph overlap implemented here.
Known limitations
This remains a draft for several reasons:
and the persisted tests exercise that implementation. The abstract
FlatVectorsWriterAPI doesnot state a general thread-migration/concurrent-read contract. Before upstreaming, the overlap
should be explicitly constrained to the known implementation or supported by an owned immutable
handoff contract.
should inject a bounded scheduler and define a global resource budget.
preserved, but an early flat failure can waste the remainder of a long graph build.
joins the worker before allowing writer cleanup.
explicit typed option with lifecycle ownership rather than a global property.
implementation, not in the generic codec and not in an external path-scanning helper.
Proposed next work
segmentSizeDocs, keepnumIndexingThreadssemantically independent, avoid constructing the unused whole-dataset prefetch reader on the
partitioned path, and add exact-boundary/remainder/failure-cleanup tests.
snapshot/handoff that does not rely on an abstract implementation detail.
file and directory syncs unconditionally.
including one-segment and exact four-segment/no-merge arms.
settled, then decide whether to split phase instrumentation from the production behavior.
Follow-up: PR-2481 native-FBIN attribution ablation
This follow-up tests a possible confound in the historical comparison: PR-2481 bundled its
native-FBIN/native-flat path with graph-processing optimizations, so a regressive FBIN path could in
principle have been hidden by a larger graph win.
The experiment used a 2x2 matrix on the same PR-2481-era source and native libraries:
All arms used cold source files, default IVF-PQ heuristics, graph degrees 32/48, 16 writer threads,
one indexing thread, one physical segment, no merge, and explicit stored vector IDs. Jasper-10M had
two balanced repetitions per cell; Deep1B-10M had three repetitions per cell after its initial
effects proved noise-sized.
Findings:
Jasper benefit was 6.07%, and all four paired Jasper comparisons had the same beneficial sign.
At 96d the marginal point estimate was 1.12% faster, smaller than the observed run-to-run spread,
so it is effectively throughput-neutral.
direct native accumulation, custom
.vecoutput, and owned writer lifecycle; isolating the readeralone would require another implementation seam.
with
N * dimensions. Peak RSS was consistently lower on the native path.effect was -0.14%. Deep's marginal point estimate was 1.31% slower, also smaller than its
run-to-run spread. In the validated one-at-a-time serializer topology with a 256 GiB heap, the
degree-aware 64 MiB policy did not save enough live payload to justify its extra policy and
synchronization complexity. The prerequisite branch therefore restores PR-2481's fixed
1,048,576-node waves; executor and correctness hardening remain independent.
removal of a net-regressive FBIN path or to wave sizing. They require a matched comparison of
source, heuristic, overlap/writeback, cache state, and segment topology.
The adaptive-wave implementation passed Spotless and the focused graph-equivalence tests,
including byte-for-byte serial/parallel serialization parity. It was used for this ablation, then
rejected after the scale review above. The final fork branch and its prerequisite base use the
original fixed 1,048,576-node policy.