jayzhan211 opened a new pull request, #24585:
URL: https://github.com/apache/datafusion/pull/24585

   ## 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_merge` benchmark (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`: a `poll_count()` helper that applies the epoch check, used for 
both sides in `is_poll_count_gt`. No other behaviour change; the 
non-round-robin path is untouched.
   - `streaming_merge.rs`: regression test 
`test_round_robin_tie_breaker_resets_poll_counts_between_tie_runs`, which fails 
on `main`.
   - `benches/sort_preserving_merge.rs`: new 
`bench_merge_tied_keys_slow_producers` case (see below) covering the scenario 
the tie breaker is for.
   
   ### Performance
   
   `cargo bench --bench sort_preserving_merge` (clean A/B, `--sample-size=20`):
   
   | case | main | this PR | change |
   |---|---|---|---|
   | single_u64_column | 17.5 ms | 17.5 ms | −1% |
   | multiple_u64_columns | 32.0 ms | 32.1 ms | +0.4% (n.s.) |
   | single_large_string_column | 77.3 ms | 89.4 ms | **+16%** |
   | multiple_large_string_columns | 633 ms | 662 ms | **+4.5%** |
   
   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 `main` was 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 in 
`poll_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*: 
`SortPreservingMergeExec` runs 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:
   
   | partitions | main | this PR | change |
   |---|---|---|---|
   | 2 | 132.5 ms | 82.1 ms | **−38%** |
   | 4 | 274.3 ms | 223.3 ms | **−19%** |
   
   (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_runs` merges two 
streams of `(key, tag)` rows:
   
       stream 0:  key=1 ×6,   key=2 ×8     (tag 0)
       stream 1:  key=1 ×12,  key=2 ×8     (tag 1)
   
   Stream 0 runs out of `1`s first, so the first run of ties ends with stream 0 
holding a large poll count while stream 1 drains its remaining `1`s alone; then 
both reach key `2` and a second run of ties starts. Expected `tag` sequence 
(derived by hand from the algorithm, not a snapshot): `[0,1]×6, [1]×6, [0,1]×8`.
   
   On `main` the test fails — once both streams are on key `2`, stream 1 wins 
five times in a row, then stream 0 six times, before alternation starts:
   
       main:     [0,1,0,1,0,1,0,1,0,1,0,1, 1,1,1,1,1,1, 1,1,1,1,1, 0,0,0,0,0,0, 
1,0,1,0,1]
       expected: [0,1,0,1,0,1,0,1,0,1,0,1, 1,1,1,1,1,1, 
0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1]
                                                         ^ stale count from run 
one decides run two
   
   With this PR the output matches exactly. The existing memory-limit based 
`test_round_robin_tie_breaker_success` / `_fail` tests still pass (they only 
bound memory, which is why they did not catch this), as do the full 
`datafusion-physical-plan` unit 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).
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to