fix: SortPreservingMerge round-robin tie breaker reads stale poll counts, so tied keys are mostly drained from one partition - #24585
Open
jayzhan211 wants to merge 2 commits into
Conversation
Poll counts are invalidated lazily via an epoch, but only the winner's count was refreshed before `is_poll_count_gt`; the challenger's raw count could belong to an earlier run of ties. The partition with the larger stale count then lost every tie until the other caught up, so whole runs of equal keys were drained from a single partition instead of alternating. Read both counts through an epoch-aware `poll_count()` and add a regression test that fails on main.
Covers the scenario the round-robin tie breaker exists for: inputs with long runs of equal keys whose batches cost CPU to produce. Each input runs in its own task buffered one batch ahead, so draining a single partition through a tie run serialises on that producer.
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #24585 +/- ##
=======================================
Coverage 81.38% 81.38%
=======================================
Files 1116 1116
Lines 397960 398021 +61
Branches 397960 398021 +61
=======================================
+ Hits 323880 323949 +69
+ Misses 55120 55111 -9
- Partials 18960 18961 +1 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
|
@ariel-miculas you might be interested in this. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Rationale for this change
SortPreservingMergeExec's round-robin tie breaker (enable_round_robin_repartition, on by default) is supposed to draw equal-key rows from the tied input partitions in turn, so that no partition's upstream buffer (e.g.RepartitionExec's) grows unbounded while another is drained.It mostly didn't. Poll counts are invalidated lazily with an epoch, but only the winner's count was refreshed before the comparison; the challenger's count was read raw, so a count left over from an earlier run of ties leaked into the next one. The partition with the larger stale count then lost every tie until the other one "caught up", i.e. whole runs of equal keys were drained from a single partition.
On data shaped like our own
sort_preserving_mergebenchmark (3 identical partitions, 5 distinct keys), 260k of 300k rows were emitted in single-partition runs of 20,000 and only 40k rows actually alternated.What changes are included in this PR?
merge.rs: apoll_count()helper that applies the epoch check, used for both sides inis_poll_count_gt. No other behaviour change; the non-round-robin path is untouched.streaming_merge.rs: regression testtest_round_robin_tie_breaker_resets_poll_counts_between_tie_runs, which fails onmain.benches/sort_preserving_merge.rs: newbench_merge_tied_keys_slow_producerscase (see below) covering the scenario the tie breaker is for.Performance
cargo bench --bench sort_preserving_merge(clean A/B,--sample-size=20):The u64 cases have unique keys, so the tie breaker never engages. The string cases are all ties (5 distinct values over 1M rows), and
mainwas fast on them precisely because of the bug: it drained whole key-runs from one buffer sequentially instead of alternating per row. The slowdown is the cost of the fairness the feature exists to provide, not of the two extra loads inpoll_count(which are noise next to the ~300-byte string compare per row).Those cases have their inputs fully materialised in memory, so nothing upstream benefits from balanced consumption. The situation the tie breaker exists for is inputs that are produced concurrently:
SortPreservingMergeExecruns each input in its own task buffered one batch ahead (spawn_buffered(_, 1)), so when the merge drains a single partition through a run of equal keys, that partition's producer is the bottleneck while the others idle. A new bench case,bench_merge_tied_keys_slow_producers(long runs of equal keys, fixed CPU cost per produced batch), shows the fix letting the producers overlap:(With 4 inputs the tie breaker only balances the two sub-tree winners at the root, since ties below the root are still broken by index — a pre-existing limit.)
Follow-up worth considering: alternating at batch or N-row granularity instead of per row would keep this producer overlap while recovering most of the sequential-drain speed on the all-ties in-memory cases.
Are these changes tested?
Yes. The new unit test
test_round_robin_tie_breaker_resets_poll_counts_between_tie_runsmerges two streams of(key, tag)rows:Stream 0 runs out of
1s first, so the first run of ties ends with stream 0 holding a large poll count while stream 1 drains its remaining1s alone; then both reach key2and a second run of ties starts. Expectedtagsequence (derived by hand from the algorithm, not a snapshot):[0,1]×6, [1]×6, [0,1]×8.On
mainthe test fails — once both streams are on key2, stream 1 wins five times in a row, then stream 0 six times, before alternation starts:With this PR the output matches exactly. The existing memory-limit based
test_round_robin_tie_breaker_success/_failtests still pass (they only bound memory, which is why they did not catch this), as do the fulldatafusion-physical-planunit tests, core sort tests and sqllogictest (504 files).Are there any user-facing changes?
Row order among rows with equal sort keys may differ from before when the round-robin tie breaker is enabled (it was never guaranteed in that mode;
with_round_robin_repartition(false)remains stable by partition index).