Skip to content

[Data] Add task-based shuffle v2 support for sort - #64875

Open
400Ping wants to merge 27 commits into
ray-project:masterfrom
400Ping:data/sort-shuffle-v2
Open

[Data] Add task-based shuffle v2 support for sort#64875
400Ping wants to merge 27 commits into
ray-project:masterfrom
400Ping:data/sort-shuffle-v2

Conversation

@400Ping

@400Ping 400Ping commented Jul 20, 2026

Copy link
Copy Markdown
Member

Description

This PR adds task-based shuffle v2 support for Dataset.sort().

Local benchmark

Benchmark:

release/nightly_tests/dataset/sort_benchmark.py

Environment:

OS: Ubuntu on a Microsoft hypervisor/WSL environment
CPU: Intel Core Ultra 7 265K
Physical cores: 20
Threads per core: 1
Memory: 62 GiB
Ray wheel: 2.58.0
Runs per configuration: 3
Input Output partitions (legacy / v2) Legacy median v2 median Runtime delta Legacy object-store peak (median) v2 object-store peak (median) Memory delta Legacy spill (median) v2 spill (median)
5.96 GiB 64 / 200 6.040 s 8.891 s +47.2% 12.149 GiB 5.983 GiB -50.8% 0.000 GiB 0.000 GiB
11.92 GiB 128 / 200 30.634 s 16.927 s -44.7% 15.727 GiB 11.944 GiB -24.0% 25.997 GiB 0.000 GiB

Raw runtimes:

5.96 GiB legacy: 5.742 s, 6.040 s, 20.551 s
5.96 GiB v2:     8.891 s, 8.915 s, 8.352 s

11.92 GiB legacy: 28.371 s, 30.634 s, 68.221 s
11.92 GiB v2:     16.799 s, 16.974 s, 16.927 s

Additional information

400Ping added 4 commits July 20, 2026 22:52
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
@400Ping

400Ping commented Jul 22, 2026

Copy link
Copy Markdown
Member Author

the microcheck error is unrelated to this PR

@richardliaw richardliaw added the data Ray Data-related issues label Jul 23, 2026
@400Ping 400Ping changed the title [WIP][Data] Add task-based shuffle v2 support for sort [Data] Add task-based shuffle v2 support for sort Jul 24, 2026
@400Ping
400Ping marked this pull request as ready for review July 24, 2026 15:54
@400Ping
400Ping requested a review from a team as a code owner July 24, 2026 15:54

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the task-based shuffle v2 path for sorting, implementing SortShuffleMapOp with online range-boundary sampling and integrating it into the physical planner. It also updates ShuffleReduceOp to optionally preserve partition order and modifies the shuffle map task to handle compatible but non-identical schemas during concatenation. The review feedback highlights two important improvement opportunities: dynamically scaling the fallback tuple in _set_boundaries to support multi-column sort keys, and optimizing table concatenation in _shuffle_map_task by attempting a fast-path pa.concat_tables before falling back to type promotion.

@400Ping 400Ping self-assigned this Jul 24, 2026
@400Ping
400Ping requested a review from owenowenisme July 24, 2026 16:07
@400Ping
400Ping requested a review from bveeramani July 24, 2026 16:38
400Ping added 2 commits July 27, 2026 23:44
Signed-off-by: Jie-Kai Chang <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Comment thread python/ray/data/_internal/planner/plan_all_to_all_op.py
@owenowenisme

Copy link
Copy Markdown
Member

On second thought, I think we should sample all the blocks or we might get unexpected data skews. Could you change the pr to sample all blocks before doing range partitions?

Signed-off-by: 400Ping <jiekaichang@apache.org>

# Conflicts:
#	python/ray/data/_internal/execution/operators/shuffle_operators/shuffle_reduce_operator.py
#	python/ray/data/_internal/logical/rules/operator_fusion.py
#	python/ray/data/_internal/planner/plan_all_to_all_op.py
…sort-shuffle-v2

Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
@400Ping

400Ping commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Hey @owenowenisme please take a look when you have time. Sorry for the late fix, I was at Beijing giving a talk in Community Over Code Asia.

Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>

@owenowenisme owenowenisme left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have we thought about a separate sampling op instead of putting it inside of the same operator?

This could make sampling naturally support pipelining with upstream ops, get rid of a lot of things (e.g. max inflight sampling, _finish_map_inputs ) and fused with upstream map op if we implement some sampling op with the same map operator.

But this is just my hunch, can you dig into that direction and see if this is possible?

Also when you're doing this, could you survey how other frameworks sample input blocks before partitioning ?

Comment thread python/ray/data/_internal/planner/plan_all_to_all_op.py Outdated
@400Ping

400Ping commented Aug 28, 2026

Copy link
Copy Markdown
Member Author

Have we thought about a separate sampling op instead of putting it inside of the same operator?

This could make sampling naturally support pipelining with upstream ops, get rid of a lot of things (e.g. max inflight sampling, _finish_map_inputs ) and fused with upstream map op if we implement some sampling op with the same map operator.

But this is just my hunch, can you dig into that direction and see if this is possible?

Also when you're doing this, could you survey how other frameworks sample input blocks before partitioning ?

Thanks, I dug into the separate sampling operator direction.

A separate zero-copy pass-through SortSamplingOp looks feasible. It could:

  1. Retain each original RefBundle without copying its blocks.
  2. Start a sampling task as each block arrives.
  3. Collect the sample results and compute the global range boundaries.
  4. Release the retained bundles only after the boundaries are ready.
  5. Let the downstream SortShuffleMapOp receive those bundles through the
    normal executor admission path.

This would separate sampling from range partitioning, remove the custom
max-inflight sampling logic, and avoid replaying all buffered bundles into
ShuffleMapOp in one synchronous loop.

There are two limitations that I found:

First, the current MapOperator task protocol doesn't have a driver-side side
output for returning sampling metadata while forwarding the original block
references. A custom sampling operator can pipeline with upstream execution,
but fusing it into an upstream MapOperator without copying blocks would
require extending that protocol and the existing fusion rules.

Second, sort v1 calculates:

samples_per_block =
    num_partitions * SAMPLE_POINTS_PER_PARTITION / total_num_blocks

When the first input block arrives, the final number of input blocks may still
be unknown. Therefore, starting sampling immediately cannot exactly preserve
the current v1 sampling formula unless we introduce a mergeable per-block
sampling sketch or rely on an estimate.

This matches the general structure used by other frameworks:

  • Spark runs a separate reservoir-sampling sketch before range partitioning,
    but it knows the number of RDD partitions in advance.
  • Dask performs an initial approximate-quantile pass and then a second
    shuffle pass.
  • Hadoop creates a partition file with InputSampler before running the
    total-order partitioner.

Ray Data cannot generally recompute an arbitrary upstream UDF safely, so the
sampling stage needs to retain the original object references while samples
are collected.

Would you be comfortable with changing the sampling implementation to a
bounded mergeable per-block sketch so sampling can start immediately, or do
you prefer preserving the exact v1 sampling budget even though that requires
waiting until the input block count is known?

For Ref:

@400Ping
400Ping requested a review from owenowenisme August 31, 2026 08:17
@owenowenisme

owenowenisme commented Aug 31, 2026

Copy link
Copy Markdown
Member

My take is choose the way that integrate with current the best, we don't have to follow everything v1 has, so even if you are only sampling a fixed 20 rows per block I think it's okay.

The current MapOperator task protocol doesn't have a driver-side side
output for returning sampling metadata while forwarding the original block
references.

About this I think we can have a custom op if map operator cannot do that. And we can disable fusing for this pr and figure ways to enable that as followup

Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: Jie-Kai Chang <jiekaichang@apache.org>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale Bugbot comment from a previous run.

Comment thread python/ray/data/tests/test_sort_shuffle_v2.py
Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>
@400Ping

400Ping commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

The microcheck error is unrelated to this PR.

@400Ping

400Ping commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

@owenowenisme please take a look at this PR when you have time.

return boundaries

@staticmethod
def _uninitialized_partition_fn(block):

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not blocker but why we have both this and ensure_boundaries?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ShuffleMapOp needs a picklable partition_fn at construction, but sampled boundaries only exist after SortSamplingOp has seen every block. So the constructor gets a fail-loud placeholder and _ensure_boundaries swaps in the real one on the first input.

Signed-off-by: 400Ping <jiekaichang@apache.org>
Signed-off-by: 400Ping <jiekaichang@apache.org>

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

Reviewed by Cursor Bugbot for commit 5cede2f. Configure here.

Comment thread python/ray/data/_internal/execution/operators/sort_shuffle.py
Signed-off-by: 400Ping <jiekaichang@apache.org>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

data Ray Data-related issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants