Repository navigation
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces a new TopN logical operator and an associated rewrite rule to optimize Sort + Limit operations into a more efficient TopN execution path. The implementation includes a local candidate reduction phase followed by a global merge, supported by a configurable budget to manage resource usage. My feedback suggests refactoring the rewrite function in top_n_rewrite.py to check for non-positive limits earlier, which improves code clarity and prevents invalid TopN operator creation.
b0a574d to
edfd31d
Compare
owenowenisme
left a comment
There was a problem hiding this comment.
High level feedbacks:
- I'd suggest implementing a new operator based on PhysicalOperator (maybe on top of MapOperator, which might save a lot of work), since AllToAllOperator will be deprecated soon, once all our shuffles move to shuffle v2 and v1 is deprecated.
- For
top_n_rewrite_max_limit, I suggest we survey how other engines set this number, or sweep it ourselves. I'd also suggest defaultingtop_n_rewrite_enabledto true. - For the final TopN merge, we could probably fetch the inputs in parts and maintain a top-N heap, instead of fetching all at once. With a native operator (1), this could even be done incrementally as candidates arrive.
- Check
pyarrow.compute.select_k_unstableto avoid sorting the whole block (though we probably need to check what problems the unstable ordering could cause). - I think
LocalTopNshould be able to fuse with upstream.
|
I ran some benchmarks on this branch to get memory numbers and to see how k and the number of blocks affect it. Setup: one node, 32 CPUs, Ray nightly a705161 + this PR. Both sides use SHUFFLE_V2 and only
A few things I noticed:
|
Hello @owenowenisme. Sorry for lately replying and appreciate ur suggestion!
Our benchmarks also show that Top-N remains beneficial at k=100,000. Since Ray's candidate volume depends on k, block count, and row width, I think a candidate-aware budget would be more appropriate than a fixed threshold alone. I'll run additional end-to-end benchmarks before deciding on
Thanks again! Let me know what you think about the PR scope, especially points 1 and 4. Edit: After discussing, we should put point 1, 3, 4, 5 in this PR. |
bc7a37d to
96b0456
Compare
|
Hey @viiccwen, I made a few changes to your pr:
|
|
Re-ran my earlier benchmarks on e705cd6 (nightly ed1d83a + this PR, same 32-CPU machine, both paths on the same base, median of 5 warmed-up runs). Point 2 from my last comment no longer applies: with default config, k=10000 over 256 blocks (2.56M candidates) and k=1000 over 2048 blocks both finish and return the right rows. Point 4 is fixed too, The numbers got better. At 128+ blocks the speedup is now 4-8x (was 3-7x). For 100M rows with a 128-byte payload, sort+limit takes 21.4s and 16.3 GiB of extra worker memory; Top-N now takes 3.5s and 0.5 GiB (was 7.2s and 3.9 GiB on the earlier version). The 1M rows / 32 blocks case is now 0.88s vs 0.80s for sort+limit, so the earlier slowdown there is mostly gone. One thing that moved the other way: in the 100M-row case the object store peak for Top-N went from 6.2 GiB to 9.0 GiB (7.6-9.2 GiB across runs; sort+limit stays around 14 GiB). My guess is it comes from disabling upstream backpressure until all candidates arrive, but I haven't checked. Still single-node only. |
Thanks for the detailed benchmarks! This PR is still a work in progress, and I'm making changes to the implementation. It might be best to hold off on further benchmarking until it's ready for review, to avoid spending time testing intermediate versions. Appreciate your help! |
Add a default-off Shuffle V2 Top-N rewrite with local candidate reduction, a guarded final merge, and focused logical, operator, and execution tests. Signed-off-by: viiccwen <vicwen@apache.org>
Propagate DataContext to the final merge task, preserve non-positive Limit plans, and defer the Polars rewrite until null ordering is aligned by ray-project#66925. Add regression coverage for these cases. Signed-off-by: viiccwen <vicwen@apache.org>
Add Bazel py_test targets for the Top-N rewrite, operator, and execution suites so CI discovers and runs them. Signed-off-by: viiccwen <vicwen@apache.org>
Add the standard direct pytest entry point required by Ray's Bazel py_test format checker. Signed-off-by: viiccwen <vicwen@apache.org>
Signed-off-by: viiccwen <vicwen@apache.org>
Add an exact BlockAccessor Top-N primitive for Arrow and Pandas, with correctness-preserving fallbacks for nulls, NaNs, mixed directions, and unsupported kernels. Reuse it in the local and final V2 Top-N stages. Signed-off-by: viiccwen <vicwen@apache.org>
Track merge tasks, resource usage, output queues, and cancellation through the physical operator lifecycle. Preserve candidate budgets, empty schemas, and upstream object-store backpressure exemptions, with focused lifecycle and remote-option regressions. Signed-off-by: viiccwen <vicwen@apache.org>
Describe the local Top-N stage as a map for fusion compatibility and validate sort keys after upstream transforms. Cover fused read/map plans, schema-changing maps, and incompatible compute or remote arguments while keeping the final merge separate. Signed-off-by: viiccwen <vicwen@apache.org>
Parameterize the existing multi-key execution test over zero, minimum, partial, exact-size, and oversized limits on a ten-row dataset. Assert positive limits use the Top-N execution path. Signed-off-by: viiccwen <vicwen@apache.org>
Signed-off-by: You-Cheng Lin <mses010108@gmail.com>
Signed-off-by: You-Cheng Lin <mses010108@gmail.com>
Signed-off-by: You-Cheng Lin <mses010108@gmail.com>
Signed-off-by: viiccwen <vicwen@apache.org>
e705cd6 to
567ef0b
Compare
Signed-off-by: viiccwen <vicwen@apache.org>
Signed-off-by: viiccwen <vicwen@apache.org>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Reviewed by Cursor Bugbot for commit 6d3a166. Configure here.
| ) | ||
| memory_limit = data_context.execution_options.resource_limits.object_store_memory | ||
| if memory_limit < min_bundle_bytes: | ||
| min_bundle_bytes = max(1, int(memory_limit)) |
There was a problem hiding this comment.
Bundle threshold can stall LocalTopN
Medium Severity
When object_store_memory is below the LocalTopN bundle target, min_bundle_bytes is set to that entire limit. EstimateBytes will not submit until the input queue holds that much data, but those queued bundles already count against the same budget, so LocalTopN can sit below the threshold, refuse more input, and never flush until upstream is fully consumed. Tight limits such as the existing 1024-byte merge test can hang a finite sort().limit() job.
Reviewed by Cursor Bugbot for commit 6d3a166. Configure here.


Description
Add an opt-in Top-N path for Ray Data
sort(...).limit(k)underSHUFFLE_V2. It selects local candidates and merges them incrementally to avoid the full range shuffle.Before: Sort samples boundaries, sorts and partitions all input rows, then merges partitions. Limit can stop consumption before all reducers finish.
After:
TopNRewriteRulereplaces eligible Sort + Limit plans. Each block contributes at mostkcandidates. One active merge task maintains the running Top-N. The operator emits the sorted result after input completion.flowchart TD subgraph Before subgraph BE["Execution / shuffle: V2 sort"] B1[SortSamplingOp] --> B2[SortShuffleMapOp] B2 --> B3[ShuffleReduceOp] --> B4[LimitOperator] end end subgraph After subgraph AE["Execution: Top-N operators"] A1[LocalTopN] --> A2["TopNMergeOperator: batch + running Top-N"] A2 -->|"next batch"| A2 A2 --> A3["Sorted result after input completion"] end end classDef removed fill:#ffcdd2,stroke:#c62828,color:#000 classDef added fill:#c8e6c9,stroke:#2e7d32,color:#000 class B1,B2,B3,B4 removed class A1,A2,A3 addedArrow uses
select_k_unstablewhere supported. The fallback sorts keys and gathers selected rows. Pandas preserves its sort semantics. The native operator usesDataOpTaskfor resource accounting, backpressure, and input cleanup.Batch thresholds are 32 blocks or 128 MiB. Partial batches can flush after 100 ms. Admission can exceed a threshold by one bundle. These are not hard memory limits; the running Top-N must fit in one worker.
The feature stays private and default-off. Eligibility requires positive
k, no custom boundaries, and a configurable maximumkof 10,000 by default. LocalTopN stays unfused. Boundary ties can select different row identities.Benchmark
Workload:
range -> map_batches(SplitMix64 key) -> sort(["key", "row_id"]) -> limit(k) -> materialize.Narrow rows contain two int64 columns. Wide rows add a 128-byte payload. Block counts are requested counts; 128 produces 129 candidate blocks.
Related issue
closes #66831.