perf(vindex): parallelize Parquet reads and multipart index uploads - #736
perf(vindex): parallelize Parquet reads and multipart index uploads#736jerry-024 wants to merge 19 commits into
Conversation
The default_training_vector_count call was only used to populate a timing log field, but it ran unconditionally and propagated errors, introducing a new build failure path even when timing diagnostics were disabled. Compute it only when timing is enabled and fall back to 0 on error instead of failing the build. Co-Authored-By: Claude <noreply@anthropic.com>
The vector index (~2 GB) is serialized as a sequential stream into an opendal writer that uploads its 8 MiB multipart chunks strictly one at a time -- ~244 serial round trips per index on object storage. Add async_writer_with_concurrency and let the index upload keep 4 parts in flight (32 MiB buffer), overlapping serialization with uploads. Parquet and other async_writer users keep the previous serial behavior.
* main: perf: vectorize raw vector search (apache#734) feat(file_index): add predicate evaluation foundation (apache#721) feat(go): add postpone fixed-bucket write bindings (apache#722) perf(vindex): split build timing logs by phase (apache#723) fix(avro): read TIME, BLOB, MULTISET and non-string-key map columns (apache#724) fix(datafusion): surface tag create-time and retention in $tags (apache#728) [core] Support multivalue global index (apache#731) feat: add Java-compatible array predicate pushdown (apache#732) fix: serialize unbounded varchar as string (apache#730) perf(vindex): decouple vector read threads and remove chunk barrier (apache#720) feat(vindex): add DiskANN and IVF-SQ/RQ support (apache#726) # Conflicts: # crates/paimon/src/table/data_file_reader.rs # crates/paimon/src/table/vindex_index_build_builder.rs
A row group whose projected bytes exceed the whole read budget previously clamped to every byte permit, so one oversized row group serialized the scan: wide vector columns project ~294 MiB per row group against the 256 MiB default budget, and parallel row-group reads silently degraded to 1 in flight unless the user hand-tuned max-inflight-bytes. Cap a single acquisition at budget / min(parallelism, 4) instead. Row groups at or below their fair share keep exact accounting (no behavior change for ordinary layouts); oversized row groups admit up to four concurrent reads, matching what the 768 MiB hand-tuned budget achieved (source wait -43.7% on a 10M-row 768-dim build) without configuration. The byte budget thereby becomes a fair-admission mechanism for large row groups rather than a strict projected-byte ceiling; the share divisor is capped at 4 until wider RSS measurements justify more.
shyjsarah
left a comment
There was a problem hiding this comment.
Thanks for the optimization work. I found one issue that I think should be addressed before merge:
Concurrent multipart uploads are not bounded to 32 MiB
async_writer_with_concurrency(4) documents the buffering cost as 4 * 8 MiB, but that is not the behavior of the pinned OpenDAL 0.58 implementation.
MultipartWriter creates ConcurrentTasks with prefetch = 8192. With concurrency greater than one, completed tasks are not continuously reaped during write; each completed task still owns its WriteInput, including the 8 MiB part buffer, until tasks are drained during close(). If uploads complete faster than index serialization, a ~2 GiB index can therefore retain close to the whole index size in completed upload buffers, rather than ~32 MiB. The previous concurrency-1 path executes inline and does not accumulate these buffers.
Please either keep concurrency at 1 for now, use/patch an OpenDAL implementation with strictly bounded completed-task buffering, or introduce a bounded uploader that continuously reaps completed parts. A fast fake multipart backend test asserting peak retained bytes would also help prevent regression.
Two non-blocking diagnostics/benchmark notes:
- For the target 768-dimensional workload, a ~294 MiB row group consumes the entire default 256 MiB read budget, so the parallel path can still run effectively serially unless the option is tuned. It would be useful for the benchmark to print the effective budget and observed peak concurrency, and warn when the peak is 1.
- Parquet diagnostics are collected only after
read_budgetis filtered for parallel-path eligibility. Partial row selections can perform Parquet I/O while reporting zero row-group/projected-byte diagnostics. Consider decoupling diagnostics collection from fast-path eligibility, or naming the fields as parallel-path-only metrics.
| .min(batch_stream_builder.metadata().num_row_groups()) | ||
| }) | ||
| .unwrap_or(1); | ||
| let projected_bytes = read_budget |
There was a problem hiding this comment.
Because read_budget has already been filtered by row_selection.is_none(), any partial-row-range read reaches this block with read_budget == None. Vector-index shard boundaries can cut through a file, so these reads still perform Parquet I/O while reporting parquet_row_group_count and all projected-byte diagnostics as zero. Please compute and record the diagnostic sizes from self.read_budget independently of parallel-path eligibility, or rename these fields to make their parallel-path-only scope explicit.
Purpose
Speed up vector-index builds by removing two object-storage bottlenecks:
Changes
row_rangeswithout a deletion vector as an unfiltered scan, allowing the existing parallel row-group path to run. Partial ranges, deletion vectors, and empty ranges keep their previous semantics.max_inflight_bytesas a strict projected-byte admission limit, and warn once when a row group exceeds it.Reproduce the IVF-PQ build benchmark
The benchmark is in
ivfpq_build_benchmark.rs. It builds an IVF-PQ index with dimension 768, cosine distance,nlist=4096, andpq.m=192on an existing Paimon table:Benchmark
Cohere 10M × 768 dimensions, cosine IVF-PQ (
nlist=4096,pq.m=192, 8 bits), 10 Parquet files / 100 row groups, direct OSS, and no local cache.Parallel Parquet reads
Both baseline and this PR use an explicit 512 MiB Parquet read budget. The metrics below cover only the Parquet-read work changed by this PR.
The current-PR values are from the latest single validation run. The 512 MiB budget is a workload-specific benchmark setting and must be configured explicitly. The default remains 256 MiB.
Concurrent multipart upload
With all other benchmark inputs held constant, four in-flight multipart uploads reduced the serialize/upload phase from 30.3s to 4.6s (−84.8%). The vector-index writer alone uses this concurrency; other writers keep their existing behavior.
Tests
_ROW_IDparity.API and format
No public API or file/index format changes. The concurrent writer helper is crate-private.