You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Leverage PartialReduce AggregationExec mode to drastically reduce shuffle size #360
When DataFusion 53 is released, we should use the PartialReduce mode of AggregateExec (apache/datafusion#20019, kudos to @njsmith) for maximum local data reduction before shuffling.
The expectation is reducing the amount of data exchanged by up to a factor of n_partition_per_task (for example 16x).
Details
Very schematically, here is how a shuffle happens:
My understanding is that we will send duplicate aggregate keys on the network, where we could actually have reduced them to one line locally before the shuffle.
We should leverage PartialReduce to further reduce data before shuffling:
In this example, we'd reduce data exchanged by 4x (because there are 4 partitions per task).
No strong opinion on whether we should add another RepartitionExec layer like in this graph, or if we should run the AggregateExec(PartialReduce) on the "ready to shuffle" partitions. Since shuffles can have hundreds of partitions, I don't know what is the comparative cost of:
RepartitionExec(Hash, output_partitions=16) + AggregateExec(PartialReduce) on 16 partitions
vs AggregateExec(PartialReduce) on potentially hundreds of partitions
Reacted by Gabriel, Nga Tran, Jonathan Chen, Edson Petry and Kurt Volmar
RepartitionExec(Hash([group_keys], N×T)) ← original, stays in place
AggregateExec(mode=PartialReduce) ← NEW
RepartitionExec(Hash([group_keys], M)) ← NEW inner, merges S partitions by key
AggregateExec(mode=Partial)
...
Idea
Insert a smaller RepartitionExec below PartialReduce to hash-partition by group key, so PartialReduce sees all copies of the same key in one partition and can merge them. The existing RepartitionExec stays above, re-expanding into the shuffle partitions.
Approach 1 is optimal. I created a new run script that compared the bytes transferred for queries with large cardinality. This is done by walking the rewritten plan tree and summing bytes_transferred from all NetworkShuffleExec and NetworkCoalesceExec nodes.
The first approach looks great.
For the second, I think there is an issue in the design that explains the lack of improvement, you suggested:
RepartitionExec(Hash(group_keys, N×T)) ← existing, stays in place
AggregateExec(PartialReduce) ← new
AggregateExec(Partial)
And as you noted it does not help much, because it won't reduce data (Partial already did this job correctly 👍).
What you want to try out is:
AggregateExec(PartialReduce) ← new
RepartitionExec(Hash(group_keys, N×T)) ← existing, stays in place
AggregateExec(Partial)
Now the PartialReduce runs on properly partitioned data and can yield data reduction.
Then you can compare with approach 1 and see how it goes! In theory it should have the same number of bytes exchanged. What is unclear to me is which one is less CPU intensive (or fastest), and your bench is perfect to measure this.
I would also suggest you try out on a larger scale factor (at least 10, ideally 100) so that you can ensure your bench are not too noisy. You can aim for an execution that lasts at least one second, ideally multiple.
Also this will be more visible if each workers have many partitions: you can try out with 16 partitions per worker (I think the default test set up may have 2 to 4), which is also closer to what you'd see in production deployments.
Keep us posted! Excited to see the early improvements.
Thanks for the feedback! I've revised approach 2 to the new design and benchmarked using a scale factor of 50 and 16 partitions per worker. With these changes, I believe both approaches show similar results — leaning towards approach 2 because it adds less complexity?
Awesome, thanks! These numbers look good, I suspect the speedup will be more significant when running on different machines with true network.
Given these I think Approach 2 makes sense, and we can still revise this as needed in the future 👍
Approach 2 with @fmonjalet revision looks good! and those numbers look great. I'd expect to see real latency improvements if running in a "real" scenario, with actual network calls.
Activity
nchu05 commentedon Apr 2, 2026
Take
nchu05 commentedon Apr 3, 2026
Design Document
Approach 1: Add repartition + partial reduce
Plan shape
Idea
Insert a smaller RepartitionExec below PartialReduce to hash-partition by group key, so PartialReduce sees all copies of the same key in one partition and can merge them. The existing RepartitionExec stays above, re-expanding into the shuffle partitions.
Implementation
Approach 2: Add partial reduce only
Plan shape
Idea
Insert PartialReduce between the existing RepartitionExec and Agg(Partial) with no additional repartition. Each partition is reduced independently.
Implementation
NetworkShuffleExecnodes.RepartitionExec → Aggregate(Partial)underneath.PartialReducebelow the repartition.nchu05 commentedon Apr 7, 2026
Local Benchmarking
Approach 1 is optimal. I created a new run script that compared the bytes transferred for queries with large cardinality. This is done by walking the rewritten plan tree and summing bytes_transferred from all NetworkShuffleExec and NetworkCoalesceExec nodes.
Approach 1 vs Baseline - Approach 1 Shows Significant Memory Improvement
Baseline vs Approach 2 - No Improvement
Approach 1 vs Approach 2 - Approach 1 is significantly better
fmonjalet commentedon Apr 8, 2026
Hi Nathan, and thank you for taking this on!
The first approach looks great.
For the second, I think there is an issue in the design that explains the lack of improvement, you suggested:
And as you noted it does not help much, because it won't reduce data (Partial already did this job correctly 👍).
What you want to try out is:
Now the PartialReduce runs on properly partitioned data and can yield data reduction.
Then you can compare with approach 1 and see how it goes! In theory it should have the same number of bytes exchanged. What is unclear to me is which one is less CPU intensive (or fastest), and your bench is perfect to measure this.
I would also suggest you try out on a larger scale factor (at least 10, ideally 100) so that you can ensure your bench are not too noisy. You can aim for an execution that lasts at least one second, ideally multiple.
Also this will be more visible if each workers have many partitions: you can try out with 16 partitions per worker (I think the default test set up may have 2 to 4), which is also closer to what you'd see in production deployments.
Keep us posted! Excited to see the early improvements.
nchu05 commentedon Apr 8, 2026
Thanks for the feedback! I've revised approach 2 to the new design and benchmarked using a scale factor of 50 and 16 partitions per worker. With these changes, I believe both approaches show similar results — leaning towards approach 2 because it adds less complexity?
Bytes Transferred
Query Execution Time
fmonjalet commentedon Apr 10, 2026
Awesome, thanks! These numbers look good, I suspect the speedup will be more significant when running on different machines with true network.
Given these I think Approach 2 makes sense, and we can still revise this as needed in the future 👍
gabotechs commentedon Apr 15, 2026
Approach 2 with @fmonjalet revision looks good! and those numbers look great. I'd expect to see real latency improvements if running in a "real" scenario, with actual network calls.
Leverage PartialReduce AggregationExec mode to reduce shuffle size (#396