Repository navigation
Conversation
|
Before you submit for review:
If you did not complete any of these, then please explain below. |
dd34b6b to
203fda4
Compare
ec42a94 to
551ff6c
Compare
MarkWolters
left a comment
There was a problem hiding this comment.
Just the one comment on ParallelGraphWriter that I don't feel should be a blocker but I would like explained.
| throw (RuntimeException) cause; | ||
| } else { | ||
| throw new RuntimeException("Error building node record", cause); | ||
| private void awaitTasks(List<Future<Void>> futures) throws IOException { |
There was a problem hiding this comment.
this does fix an issue with the code already in place but now every future will run to completion even if one of them fails, then afterwards the exception will be thrown. Not sure if this is intended or not but the entire graph is invalid because of a failure, and there is a lot of work done that then gets thrown away
There was a problem hiding this comment.
Agreed—this was a problem. Fixed: a failing worker immediately signals the other tasks to stop. Queued tasks skip their work, and running tasks stop before the next record. We still join every task before releasing resources and propagating the exception. Regression tests cover both failure and interruption, including a later task failing while an earlier task is blocked.
…n one thread Grid now supplies NVQ/FP features at the final write for every run, but only the parallel writer calls those suppliers from several threads. BenchYAML's default (sequential) writer calls them on one thread, so all final-write vector reads and NVQ encoding run single-threaded after construction. On Cohere-1M build+write went from 74-76 s to 100-104 s, and on OpenAI-3072-1M from 165 s to 264 s. Fails at ec42a94 (PR #747).
Grid moved NVQ/FP feature encoding into the final write for every run, but the sequential writer (BenchYAML's default) calls final suppliers on one thread. Grid now defers features only when every writer is an OnDiskParallelGraphIndexWriter; otherwise it encodes them on the insertion threads and writes them with writeFeaturesInline, as before #747. The docs and release note say so. Fixes TestGridDeferredFeatureParallelism.
awaitTasks joined every task but never stopped them, so an interrupted or failed write serialized every remaining record before reporting. writeL0Records now shares a stop flag with its tasks: awaitTasks sets it on interruption or the first task failure, unstarted tasks return at once, and running tasks stop before their next record. Every task is still joined before the channel and suppliers are released. NodeRecordTask keeps its existing constructor. Fixes TestParallelWriterInterrupt.
The test built each graph on a 3-thread ForkJoinPool, so insertion order, hierarchy-level draws and refinement cleanup varied with thread timing. Its self-match check failed intermittently under full-suite load for the BQ, 768-dimension, DOT_PRODUCT, refined configuration (node 5 missing from its own top 10). Graphs are now built through builder.build(source) with single-worker executors, and the file-backed graph's adjacency must equal a resident-built reference graph at every level. That checks the test's purpose (a shared file source must not overwrite insertion or diversity queries) edge for edge. The self-match check is kept and is now deterministic for the fixed data.
Purpose
Serialize complete graph records in bounded batches after construction, avoiding synchronized per-insertion NVQ/FP feature writes and repeated small final writes. The core writer uses the existing
Featureinterface and preserves V6; it does not depend on a particular vector type.#744 is merged, and this PR targets main. Grid uses its
VectorAccess.openRangeAPI; this PR adds the deferred writer, its integration, documentation, and tests.Changes
withParallelBatchSize(int)optionally overrides 64 on both existing parallel-capable builders. It does not enable parallel writing or affect the sequential selection.The core writer accepts ordinary application-supplied feature-state callbacks. It has no dependency on Grid, BenchYAML, dataset preloading, or #744's buffered hooks. The following change is needed only for callers that currently write features during insertion; callers already supplying all features to the parallel final writer automatically obtain complete-record batching.
Caller usage and production integration
Both examples assume the same trained
nvq, graphbuilder, nodecount,writerWorkers, and output path. The builder's construction scoring is unchanged;its peer vectors are supplied separately.
vectorForOrdinalis the application'slookup of an original input vector, not a harness loader.
Old: write each inline feature during insertion
New: supply the features when writing the completed graph
The second call supplies NVQ payloads during final serialization, together with
adjacency, in batches of up to 64 complete records. No
writeFeaturesInlinepassis needed. The same pattern works for FP using
InlineVectorsandInlineVectors.State, or another supported inline feature. Supply every configuredinline feature to obtain complete-record batching, including fused PQ if configured.
Final suppliers run concurrently.
vectorForOrdinalmust remain available throughwriteand provide independent borrowed vectors per worker, or stable vectors.For an existing
RandomAccessVectorValues,threadLocalSupplier()supplies thoseindependent views; it does not require buffered hooks. A caller can instead return
already encoded feature states from its own storage, without retaining original FP
vectors for this write.
Integrations that already call
writewith every inline feature supplier receivebatching when using the parallel writer. Integrations that write features early
must move those calls to final suppliers to use the deferred path. The incremental
API remains compatible: a one-pass source whose values cannot be obtained again
must retain encoded states for final writing or continue using incremental writes.
Performance
Fresh construction-only BenchYAML/Grid runs on RAID, with existing main native Highway support, automatic
avx3_spr, preferred width 512, JDK 23, and 48 insertion/cleanup/writer workers.Setup for the experiments: GCE VM, 48-core Intel Xeon 6985P-C (96 threads), local SSD RAID10; JDK 23, native Highway. Heap and worker limits are specified per study.
All datasets use DOT_PRODUCT; PQ
mFactor=8, K=256, centering=false, anisotropic threshold=-1; NVQ with two subvectors; degree 32, beam 100, overflow 1.2, hierarchy/refinement enabled, pruning disabled, fused PQ, V6. Every arm trains fresh codebooks and builds a fresh graph. Input file cache was explicitly evicted and verified before each run.Input footprints: Cohere 10M 38.184 GiB, MPNet 10M 28.641 GiB, CAP 6M 34.758 GiB. Ample: 128 GiB heap, no cgroup total-memory cap. Limited: 32 GiB total cgroup memory, 20 GiB heap, zero swap. These are experiment settings, not committed defaults.
The before/after measurements below compare #744’s original inline-writing path (
1bab1881) with the recorded 64-record integration (9194992f). They are separate fresh runs; training and parallel graph variation qualify attribution outside serialization.Limited-memory inline runs were excluded because the existing final-writing path slowed dramatically under the memory limit.
Phase breakdown (seconds)
Pipeline is the sum of these phases. Total process additionally includes dataset preparation, setup, reporting, and shutdown. Final write includes deferred NVQ/FP feature encoding; previously that work occurred during insertion.
Memory (GiB)
RSS is the separately measured Linux child-process peak (
getrusage), not heap plus buffer pool. Mapped buffer capacity is not resident memory, and buffer pool excludes other native allocations. GC reclaimed is cumulative. MPNet’s ample-memory RSS increased; this is a throughput improvement, not a claim of universally lower resident memory.Correctness and compatibility
Usage
See Deferred batch serialization. Applications opt in by supplying their final features to the parallel writer. The source must remain available through serialization and suppliers must support concurrent calls. Existing
writeFeaturesInlinecallers remain supported.The recorded measurements explicitly selected the parallel writer. BenchYAML and AutoBenchYAML now select it by default;
-Djvector.parallel_writer=falsepreserves the sequential path. The library default remains sequential, and programmatic Grid callers retain their configured selection.useDatasetPrefetchingcontrols input loading separately. Worker counts followPhysicalCoreExecutor; no server-specific worker count or memory limit is committed.Final-source confirmation
A fresh MPNet 10M run on
da094d0d(production unchanged by the documentation commits) passed with the same 32 GiB total / 20 GiB heap / zero-swap configuration. Writing performance held near the recorded batch result; the lower total is mostly insertion variation in an independently trained graph.Peak Java heap 15.32 GiB; buffer pool capacity 37.73 GiB; separately measured peak RSS 17.50 GiB; cumulative GC reclaimed 550.05 GiB. Input file cache eviction, native dispatch, fresh training, cgroup membership, and zero swap were verified.
Preloaded V6 validation
A focused MPNet 1M comparison uses ordinary fully resident
ListRandomAccessVectorValuesin both arms, withuseDatasetPrefetching=false. The control is #744's inline path (87bb4530); the candidate is #747 (cb2fc873). Both use native Highway, V6, the same PQ/NVQ and graph settings as above, 48 build/cleanup/writer workers, a 128 GiB heap, and no cgroup memory cap. Input files are on the boot drive; output indexes are on RAID for both arms. Each arm starts cold, preloads the input before construction, and independently trains/builds a fresh graph.Total process time decreased 28.6%; final writing decreased 61.6%. This demonstrates a benefit with resident input and no prefetcher. It is one fresh pair, so graph/training variation qualifies the insertion change; it is not a universal speedup guarantee.
Construction example refresh
DiskIntro, LargerThanMemory, NvqExample, SiftSmall and IPC serialization now supply final FP/NVQ features to the parallel writer. The parallel-write example uses the supported API and verifies all serialized features byte for byte. Tutorials document source lifetime and independent worker views. Java 11 compilation and six native-backed SIFT Small smoke checks passed; sequential and parallel V6 payloads matched. Query algorithms and quantization settings are unchanged.
Review fixes and validation
Thanks to Jonathan Shook for the repros and fixes in #752. All five adopted commits retain his authorship. Follow-up changes:
Validation of the updated source: 432 tests passed, 2 skipped, 0 failures across the core, examples, and native suites with
-eaand the native provider. Production compilation on JDK 23 and base/examples compilation with--release 11passed. Four construction-only BenchYAML SIFT Small smoke runs passed: preload/buffered, each with the default parallel writer and explicit sequential override. These are functional checks, not additional 1M performance measurements. All recorded benchmark values above are unchanged.