Repository navigation
Plan V2 random access by the segments a selection reads - #10621
joseph-isaacs wants to merge 15 commits into
Conversation
Rebase draft PR #10537 onto develop 92a7db0. Apply selections through a composable planner, resolve list offsets before selected elements, and split substantial fixed-width child projections at stored segment boundaries. The projection planner owns subdivision, byte-budget admission, child task execution and ordered collection. Unsupported splits retain the serial path. Keep Planner and Morsel trait methods unchanged. Replace the experimental completion callbacks, WorkFuture and ChildExecutor traits with one PlannerOutput::Await variant; morsels only publish the assembled result. Preserve develop's global local-file coalescing configuration. The 8 KiB experiment caused tiny-lookup regressions and is not part of this version. Preserve masked lazy lowering, bounded dense-list grouping, shared value caches, validated chunk offsets and the fieldless nullable-struct fix. Retain the diagnostic examples and regression benchmark workflow. Backtracking: previous PR head 8442d46; experimental CI head 60fda25; local pre-review checkpoint a253e5727 and reviewed checkpoint e8fd21a4a. The byte budget covers in-flight decoded work, not retained output or encoded/coalesced buffers. Full performance revalidation remains necessary. Validation: 692 layout and 79 scan library tests passed, followed by all 7 await protocol tests including an additional await/IO regression. Scoped all-target/all-feature clippy passed with -D warnings; formatted with nightly-2026-09-10. Focused E2E measurements follow this checkpoint. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Use the planner-driven V2 ScanBuilder directly for every Vortex random access measurement, including object-store reads. Remove the reader toggle, its configuration plumbing, V1 documentation, and unused reader-cache setup. Remove the experiment-only regression workflow, driver script, and three fixture/selection examples, along with their futures dev dependency. Keep the planner implementation and regression tests. The previous commit c4fa781 preserves the diagnostic harnesses and benchmark context. Inspected the resulting diff and searched for stale switch/example references. No tests, builds, or benchmarks were rerun for this cleanup. Signed-off-by: Joe Isaacs <joe.isaacs@live.co.uk>
Once a ListSource has its offsets, it previously compiled a nested Scan over the element plan. When the selected element ranges reach only segment chunks (Concat/Filter/Zoned/LegacyStats over SegmentScan), the list source now issues those reads itself: each touched segment is requested once, all together, decoded on arrival and sliced or masked into the ranges it holds, in list order. Other element plans (dict, struct, nested lists) keep the nested Scan. No operator, pipeline or driver API changes. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Drop the child projection path and every core change it needed: PlannerOutput::Await and the driver's task tracking, SourceBlocked::Child, the Cx child inbox, Turn::Child and Scan::complete_child, AdaptorStep::Child, Compiler::plan_children, the Children executor and its split planning, and the ProjectionPlanner/ProjectedBatch conversion. List sources now always run serially, reading segment elements directly or through a nested scan. Also restore Reach, Concat::reach and Shares to develop. Scan::try_new now counts shared readers over the row runs the splits select, merged when adjacent, so chunks in unselected gaps stay lazy without a mask-aware Reach variant. Each run counts as a reader, so a segment reused by distinct chunks is still decoded once. Splits with more than 1024 runs count their whole range. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Reduce this branch to a minimal change against develop: drop list support, the selection planner and every compiler and generic trait change. V2 split planning now groups each fixed chunk's selected rows. A selected run joins the group before it while the unselected gap is at most one writer row block (8,192 rows), or 1 MiB of serialized data when that spans more rows. Each group becomes its own planner root over just its rows, with the selection's keep mask, so rows in wider gaps get no planner, plan or read. The random-access benchmark reads files with the V2 scan when it supports their layouts and falls back to the default scan otherwise. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Replace the 8,192-row / 1 MiB gap estimate: within each fixed chunk, a selected run joins the previous group when both reach a stored segment in common, so no segment is read by two groups. Local random-access benchmarks against develop's V2 (3 interleaved passes) show uniform access regressing by 8-14% with both this grouping and the gap estimate: extra planner roots split each chunk's reads across tasks. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Revert the selected-run grouping to develop's one planner root per fixed chunk, and narrow that root to the rows from its first selected row to its last, with the selection mask sliced to match. Develop's compiler already builds nothing for chunks the mask does not touch, so rows outside the span get no plan and no read, without splitting a chunk's reads across tasks. Local random-access benchmarks against develop's V2, 3 interleaved passes each (median, 4-core VM, run-to-run spread up to 20%): - this change: geomean -0.7%, no consistent regression; - grouping by an 8,192-row / 1 MiB gap: geomean +0.0%, uniform +8-11%; - grouping by touched segments: geomean +2.8%, uniform +8-14%. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Merging this PR will regress 1 benchmark
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Simulation | take[routing/grouped_chunks/primitive/nonnull/chunks=32/indices=1000] |
385 µs | 429.5 µs | -10.36% |
| ⚡ | Simulation | chunked_opt_bool_canonical_into[(1000, 10)] |
65.4 µs | 55.1 µs | +18.6% |
| ⚡ | WallTime | starts_with_avx2[64, VarBin] |
60.3 µs | 53.3 µs | +13.03% |
| ⚡ | WallTime | words_gather_dispatch_neon[65536] |
2 µs | 1.8 µs | +10.04% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing ji/charming-cerf-9hlmns (2d5cd2b) with develop (9be4ef1)
Footnotes
-
12 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
|
CodSpeed Performance Analysis is red, but none of its flagged regressions run code this PR changes:
No code fix applies here. Acknowledging the result on CodSpeed is left to a maintainer. The random-access benchmark ( Generated by Claude Code |
Polar Signals Profiling ResultsLatest Run
Previous Runs (2)
Powered by Polar Signals Cloud |
Benchmarks: Random Access 📖Commits: PR How to read Verdict and Engines
vortex / arrow-ipc / ns (1.016x ➖, 2↑ 3↓)
random-access / vortex-file-compressed / ns (0.964x ➖, 5↑ 2↓)
random-access / parquet / ns (1.015x ➖, 1↑ 1↓)
random-access / lance / ns (0.995x ➖, 0↑ 0↓)
|
V2 takes were slower than the default scan on CI. Micro benchmarks of each random-access query, over files in the page cache, traced it to work that does not depend on the selected rows: - Each scan checked support by walking the file's whole layout tree, deserializing every chunk: 245 us on feature vectors before any row. Lowering already fails a layout without a pipeline implementation when a split plans it, so the walk is removed. No list layout is written by default, and V2 neither writes nor reads one. - Fixed 64K-row work chunks cut taxi's 178K-row stored chunks into three, and each split read and decoded the same segments again: 1,002 segment requests for 337 segments on taxi/uniform. Selected runs that read a stored segment in common now share a planner, across chunk boundaries, so every segment is read once. - A planner's single hull split made the pipeline reach, and so lower, every chunk between selected runs: 441 lowered layouts for 9 segments on feature-vectors/correlated. Runs reading exactly the same segments share a masked split; others get splits of their own, all active at once, and the pipeline's shared-segment count reaches each split's rows instead of their hull. 441 lowered layouts become 14. - Lowering a chunked layout reused none of the chunk offsets the layout already holds, reading every chunk's row count again. The random-access benchmark reads every Vortex file with V2, without the support probe or the default scan's layout reader cache. A v2_take divan benchmark runs each query with both scans. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Each V2 scan built a hash map from every segment's byte range to its id, so the IO source could answer range requests. Writers lay segments out in offset order, so the file's location table is searched in place instead, falling back to the map when it is not sorted. An empty scan of the feature-vectors file, 3,917 segments, drops from 85 to 59 us. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
tpchgen-cli 4.0.0, released 2026-10-19, removed the --format and --parquet-compression flags the SQL logic test data script passed to an unpinned `uvx tpchgen-cli`, so the job failed before any test ran. Pin 4.0.0 and use `parquet --compression='ZSTD(3)'`, which writes ZSTD column chunks as before. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
|
SQL logic tests failed on
I found no existing fix, so Generated by Claude Code |
CI showed taxi random access regressing: a column whose segments span most of the file made every selected run share a segment, so the whole take became one planner on one thread. A planner now grows only while it spans at most 256K rows, so taxi/uniform runs as 11 planners. A segment that several planners still reach is read once: the scan's IO counts each segment's planners and hands every planner the same read, dropping it once the last has taken it. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Planner tasks were spawned only when the stream's consumer first polled it, after the scan was handed back from the CPU pool. The first `concurrency` tasks now start on the CPU thread right after planning; the rest still start as earlier ones finish. In memory, taxi takes start their tasks 20 to 34 us sooner. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
The scan planned every planner, reaching every selected run, before it spawned any. Planners are now yielded one at a time: a planner is complete once the run after it is known, so it is spawned while the rest are still planned. Shared reads are registered between neighbouring planners as each closes, since a segment two planners read is read by planners next to each other. In memory, taxi/uniform's first planner task starts at about 370 us instead of 830 us. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
A V2 scan now has one root planner, the file planner, which plans the scan on its first step and hands out one unit per step after that. The driver gains distributing roots, whose children are handed back to the caller instead of run, and can admit a handed-off planner or morsel as a root. A new pool in vortex-scan runs those units. Any spare worker steps the file planner, and each unit it hands out waits in a shared queue until a worker claims it and admits it to its own run. Everything a unit emits stays on that worker's run. A worker steps the file planner before its own work whenever fewer units wait than workers are parked, spawns another worker when a unit has no parked worker to take it, and claims more units while the ones it holds wait for IO. Output is reordered by hand-out order when the scan is ordered, and the file planner stops while the consumer falls behind. This replaces the task per unit and the CPU-pool hop that prepared the scan before any unit was planned. Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
Signed-off-by: Claude <noreply@anthropic.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj
|
Two CI failures on
I'm not widening this PR with that change. I'll re-run the failed jobs once when the workflow run finishes. Generated by Claude Code |
Summary
Makes the V2 scan builder faster than the default scan for random access overall, and switches the random-access benchmark to V2 for every Vortex file. This is an alternative to #10537. The changes stay in the V2 planner, the plans, the selection handling and V2's segment IO. They don't change the
Plannertrait or the planning driver, and they add no layout-reader caching.A V2 take was slower than the default scan because of work that did not depend on the rows it selected:
Scan::try_newcounts shared segments over each split's rows instead of their hull, which finds the same shared segments and readers. Feature-vectors/correlated now lowers 14 layouts.Changes
vortex-layout/src/scan/v2:split_rowsgroups selected runs into bounded planners and pipeline splits by the segments theyreach, and counts segments read by several planners.SegmentScanIoshares those reads. The eager layout support walk is removed.SegmentRangessearches sorted locations in place.vortex-layout/src/scan/execution: a projection planner carries its pipeline splits, runs them all at once, and orders the output pieces by split.vortex-layout/src/plan/physical:Scan::try_newreaches each split's rows instead of their hull.lower_chunkedusesChunkedLayout::chunk_offsets.PlanChildren::lazyallocates once.vortex-bench/src/random_access/take.rs: Vortex takes use V2, as in Add planner-owned selection and bounded ListLayout scans to V2 #10537.benchmarks/random-access-bench/benches/v2_take.rs: a divan benchmark of each query with both scans, plus an empty V2 scan.vortex-sqllogictest/slt/tpch/generate_data.sh: pinstpchgen-cli@4.0.0and uses itsparquetsubcommand. Version 4.0.0 was released on October 19, 2026 and broke the unpinned call on every branch.Tests:
splits_read_each_stored_chunk_oncecovers runs in one stored chunk, runs in one work chunk but different stored chunks, and runs in different work chunks but one stored chunk.sessions_share_a_segment_readcovers the shared reads, andsegment_ranges_find_each_segmentcovers lookup with sorted and unsorted locations. The existingmatches_v1row-index cases check the output order of concurrent splits.CI random-access results
From the
action/bench-random-accessrun on2dab888, against the default scan on the base (cached handle, ns per take). The Vortex geomean is 0.964x, down from 1.235x on the first commit of this PR.2dab888)With the file reopened for every take, V2 is at or below the default scan on every row except nested-lists/uniform and nested-structs/uniform. The remaining gaps are in small taxi selections. There, per-take decoding and plan setup that the default scan's layout-reader cache avoids are still a large share of the cost.
Checks:
cargo test -p vortex-layout --lib(550 passed);cargo clippy -p vortex-layout -p vortex-bench --all-targets --all-features -- -D warnings;cargo clippy -p random-access-bench --all-targets -- -D warnings(thelancefeature needsprotoc, which is not installed here);cargo fmt --checkwith the nightly pinned in CI. The TPC-H generation step runs locally with ZSTD output, and SQL logic tests pass in CI.🤖 Generated with Claude Code
https://claude.ai/code/session_014rtiSjzL4EauVCM7NS84hj