perf(index): prevent two-file shuffle starvation - #8894
Conversation
There was a problem hiding this comment.
❌ Gate recommendation: request changes.
The pre-decode admission bug is fixed, but this revision moves the partition-result bound from individual partitions to whole windows. Keep the window permit shared while emitting completed partition builds incrementally so the configured entry cap continues to bound resident results.
| } | ||
| })) | ||
| .buffer_unordered(concurrency) | ||
| .try_collect::<Vec<_>>() |
There was a problem hiding this comment.
This try_collect retains every PartitionBuildResult in the window until the slowest build completes, before the outer stream can yield even one. A 128 MiB window can span thousands of small partitions, so max_entries = 2 * workers now counts one window while all of its HNSW graph/storage results are resident; this removes the partition-result bound and can drive indexing RSS far beyond the decoded-input budget. Stream completed partition results from each admitted window while sharing its permit, instead of collecting the whole window.
Reproducer
I added a disposable test around the exact map_ok(...).try_flatten() expansion used below: one admitted job (byte and entry cap both 1) returned 8 drop-tracked results. After the first output was yielded, the test expected one resident result and observed 8.
cargo test -p lance index::vector::bounded_partition_stream::tests::expanded_window_results_respect_entry_cap --lib -- --exact
Observed failure: left: 8, right: 1.
…ffle-starvation # Conflicts: # rust/lance/src/index/vector/builder.rs
## What is the performance issue? RQ scale search generates several quantization thresholds per vector dimension and sorts them before selecting the best rescale factor. For RQ8 on 1,536-dimensional vectors, comparison-based tuple sorting was the dominant sampled cost in this part of index training. ## How does this PR improve performance? This PR packs each `(positive finite f32 threshold, dimension index)` pair into a `u64` and sorts the threshold bits with four stable byte-wise radix passes. Positive finite IEEE-754 values have the same ordering as their bit patterns, so this removes comparison-heavy tuple sorting for the common unique-key case. Equal thresholds retain the previous quantizer behavior by falling back to the original comparison sort because their event order can affect the incrementally evaluated floating-point objective. The implementation is isolated from the shuffle reader and scheduling changes in #8894. ## Benchmark The following supporting A/B measurement isolated this implementation on the #8894 benchmark context. Both variants ran on the same AWS `m7i.4xlarge` VM (16 vCPU, 64 GiB RAM, gp3 storage) against the same S3 dataset: 1,000,000 rows, 1,536 `float32` dimensions, 10,000 supplied IVF centroids, and RQ8. Each value is from one fresh process. | Scenario / metric | Comparison sort (`c7508ad49`) | Radix sort (`174663d0a`) | Benefit | | --- | ---: | ---: | ---: | | Shuffle elapsed (lower is better) | 70.219 s | 51.470 s | 1.36x speedup | | Full index build elapsed (lower is better) | 76.831 s | 58.346 s | 1.32x speedup | | Shuffle CPU time (lower is better) | 1,012.95 core-s | 718.29 core-s | 1.41x less CPU time | | S3 read throughput during shuffle (higher is better) | 84.16 MB/s | 117.61 MB/s | 1.40x higher | The two measured commits differed only by the initial RQ radix implementation, but they were on the stacked #8894 context and predate the equal-threshold comparison-sort fallback in this standalone latest-`main` PR. The standalone head has not been remeasured, so the table is supporting implementation evidence rather than a current-head benchmark claim. ## Testing - `cargo test -p lance-index vector::bq::builder::tests --lib --no-fail-fast` (13 passed) - `cargo fmt --all -- --check` - `cargo clippy --all --tests --benches -- -D warnings` The added regression tests verify that radix sorting matches floating-point threshold ordering and that the selected rescale factor is bit-for-bit identical to the comparison-sort reference for RQ1 through RQ8, including zero, duplicate, NaN, and infinite inputs. A targeted equal-threshold regression also verifies the exact rescale factor from the previous comparison-sort behavior.
What is the performance issue?
TwoFileShuffleReaderresolves offsets and reads data independently for every IVF partition. With many partitions and flush groups this creates millions of small ranges. The ordered partition build stream also stops admitting useful work when an early partition is slow, even if later partitions have already completed.How does this PR improve performance?
2 * workersadmitted entries, and exclusive admission for an oversized hotspot partition. A shuffle contained in one window can use the complete entry budget; multiple windows retain per-window reservations to prevent ordered-write starvation.This does not change HNSW graph quality parameters.
Benchmark
The reader benchmark uses the same persistent local files for both implementations and includes a benchmark-local copy of the pre-change reader. It contains 4,096 partitions, 20 flush groups, 262,144 rows, and a 256-dimensional RQ5-like payload, with uniform and CV=3.6001 hotspot distributions.
Environment: Apple arm64, 10 CPU cores / 8 compute workers, 24 GiB RAM, local object store,
release-with-debug, cache-hot diagnostic pass. Values are the median of three fresh processes per implementation at PR head04a64651f. Lower is better for elapsed time, I/O, ranges, and RSS; higher is better for throughput.These measurements cover the two-file shuffle read path, not full IVF-HNSW-RQ build throughput. The 128 MiB reader window is materialized, so the speedup intentionally trades additional bounded memory for much lower I/O amplification. The 512 MiB decoded-input admission budget and entry limit apply independently to each index build; they are not a process-global admission budget or a strict bound on graph/output RSS.
Concurrent full-build resource check
At PR head
9c96131d9(before the single-window and legacy concurrency follow-up), a separate full IVF_RQ check used two independent local datasets, each with 262,144 rows, 4,096 partitions, 256 dimensions, 5-bit RQ, and precomputed centroids. A single fresh process sampled its own RSS and OS thread count while running either one build or two concurrent builds. These are single-run observations underrelease-with-debug, not baseline-versus-PR performance claims, and have not been remeasured at the current head.The observed process resources remained below 2x in this bounded run, but the implementation does not establish a hard process-global limit: decoded-input and CPU admission objects are instantiated per build. Therefore this PR makes no global-admission or arbitrary-concurrency resource-bound claim.
Testing
cargo test -p lance-index vector::v3::shuffler::tests --lib --no-fail-fast(21 passed)cargo test -p lance bounded_partition_stream --lib --no-fail-fast(11 passed)cargo test -p lance index::vector::builder::tests::partition_entry_admission_preserves_input_order -- --exact --nocapturecargo test -p lance index::vector::builder::tests::single_partition_window_uses_full_entry_budget -- --exact --nocapturecargo test -p lance index::vector::builder::tests::fresh_partition_build_runs_multiple_windows_end_to_end -- --exact --nocapturecargo test -p lance-index vector::v3::shuffler::tests::legacy_shuffler_uses_schema_estimate_for_parallel_admission -- --exact --nocapturecargo test -p lance index::vector::ivf::v2::tests::test_optimize_with_empty_partition -- --exact --nocapturecargo test -p lance 'index::vector::ivf::v2::tests::test_knn::test_dataset_too_small::num_deltas_1_1' -- --exact --nocapturecargo test -p lance 'index::vector::ivf::v2::tests::test_knn::test_fewer_than_k_results::num_deltas_1_1' -- --exact --nocaptureuv run pytest -v -s python/tests/test_dataset.py::test_commit_existing_index_segments_accepts_index_metadatacargo fmt --all -- --checkcargo clippy --all --tests --benches -- -D warningscargo bench --profile release-with-debug -p lance-index --bench two_file_shuffle_read -- --test