Skip to content

Commit e4aa41d

Browse files
2010YOUY01alamb
andauthored
refactor(hash-aggr): Migrate partial-reduce hash aggregation (#23233)
## Which issue does this PR close? <!-- We generally require a GitHub issue to be filed for all bug fixes and enhancements and this helps us generate change logs for our releases. You can link an issue to this PR using the GitHub syntax. For example `Closes #123` indicates that this PR will close issue #123. --> - Closes #. ## Rationale for this change <!-- Why are you proposing this change? If this is already explained clearly in the issue then this section is not needed. Explaining clearly why changes are proposed helps reviewers understand your changes and offer better suggestions for fixes. --> Part of #22710 This PRs implements the partial-reduce aggregation. Comments at datafusion/physical-plan/src/aggregates/partial_reduce_stream.rs explains the high-level idea. ## What changes are included in this PR? <!-- There is no need to duplicate the description in the issue here but it is sometimes worth providing a summary of the individual changes in this PR. --> - Adds an `AggregateHashTable<PartialReduceMarker>` variant to handle partial-reduce aggregation. - Adds `PartialReduceHashAggregateStream` to implement the partial-reduce state machine. ## Are these changes tested? <!-- We typically require tests for all PRs in order to: 1. Prevent the code from being accidentally broken by subsequent changes 2. Serve as another way to document the expected behavior of the code If tests are not included in your PR, please explain why (for example, are they covered by existing tests)? --> Covered by existing tests ## Are there any user-facing changes? <!-- If there are user-facing changes then we may require documentation to be updated before approving the PR. --> <!-- If there are any breaking changes to public APIs, please add the `api change` label. --> No --------- Co-authored-by: Andrew Lamb <andrew@nerdnetworks.org>
1 parent 094ad31 commit e4aa41d

6 files changed

Lines changed: 658 additions & 2 deletions

File tree

datafusion/execution/src/spill_file.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ pub trait SpillFile: Send + Sync {
4242

4343
/// Writer for spill file backends.
4444
pub trait SpillWriter: std::io::Write + Send {
45-
/// Intended for close/sync/commit operations.
45+
/// Intended for close/sync/commit operations.
4646
fn finish(&mut self) -> Result<()>;
4747
}
4848

datafusion/physical-plan/src/aggregates/aggregate_hash_table/common.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ use crate::aggregates::{
3636

3737
/// Marker for raw rows -> partial state aggregation.
3838
pub(in crate::aggregates) struct PartialMarker;
39+
/// Marker for partial state -> partial state aggregation.
40+
pub(in crate::aggregates) struct PartialReduceMarker;
3941
/// Marker for raw rows -> partial state conversion without aggregation.
4042
pub(in crate::aggregates) struct PartialSkipMarker;
4143
/// Marker for partial state -> final value aggregation.

datafusion/physical-plan/src/aggregates/aggregate_hash_table/mod.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ mod common_ordered;
2020
mod final_table;
2121
mod ordered_final_table;
2222
mod ordered_partial_table;
23+
mod partial_reduce_table;
2324
mod partial_table;
2425

2526
pub(super) use common::{
26-
AggregateHashTable, FinalMarker, PartialMarker, PartialSkipMarker,
27+
AggregateHashTable, FinalMarker, PartialMarker, PartialReduceMarker,
28+
PartialSkipMarker,
2729
};
2830
pub(super) use common_ordered::OrderedAggregateTable;
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
// Licensed to the Apache Software Foundation (ASF) under one
2+
// or more contributor license agreements. See the NOTICE file
3+
// distributed with this work for additional information
4+
// regarding copyright ownership. The ASF licenses this file
5+
// to you under the Apache License, Version 2.0 (the
6+
// "License"); you may not use this file except in compliance
7+
// with the License. You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing,
12+
// software distributed under the License is distributed on an
13+
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14+
// KIND, either express or implied. See the License for the
15+
// specific language governing permissions and limitations
16+
// under the License.
17+
18+
use std::sync::Arc;
19+
20+
use arrow::datatypes::SchemaRef;
21+
use arrow::record_batch::RecordBatch;
22+
use datafusion_common::{Result, internal_err};
23+
use datafusion_expr::EmitTo;
24+
25+
use crate::aggregates::AggregateExec;
26+
27+
use super::common::{
28+
AggregateHashTable, AggregateHashTableBuffer, AggregateHashTableState,
29+
MaterializedAggregateOutput, PartialReduceMarker,
30+
};
31+
32+
/// Methods specific to the aggregate hash table used in the partial-reduce stage.
33+
impl AggregateHashTable<PartialReduceMarker> {
34+
pub(in crate::aggregates) fn new(
35+
agg: &AggregateExec,
36+
partition: usize,
37+
output_schema: SchemaRef,
38+
batch_size: usize,
39+
) -> Result<Self> {
40+
Self::new_with_filters(
41+
agg,
42+
partition,
43+
output_schema,
44+
batch_size,
45+
vec![None; agg.aggr_expr.len()],
46+
)
47+
}
48+
49+
/// Emits the next batch of aggregated group keys and aggregate states.
50+
///
51+
/// The output batch size is determined by `self.batch_size`.
52+
///
53+
/// Returns `Some(batch)` for each emitted batch, `None` when output is
54+
/// exhausted, and an internal error if polled in the `Building` state.
55+
pub(in crate::aggregates) fn next_output_batch(
56+
&mut self,
57+
) -> Result<Option<RecordBatch>> {
58+
let output_schema = Arc::clone(&self.output_schema);
59+
let batch_size = self.batch_size;
60+
// Take ownership of the output state. Note `emit_next_materialized_batch`
61+
// updates state after it emits a materialized slice.
62+
match std::mem::replace(&mut self.state, AggregateHashTableState::Done) {
63+
AggregateHashTableState::Outputting(state) => {
64+
if state.group_values.is_empty() {
65+
return Ok(None);
66+
}
67+
68+
let output =
69+
self.materialize_partial_reduce_output(state, output_schema)?;
70+
Ok(self.emit_next_materialized_batch(output, batch_size))
71+
}
72+
AggregateHashTableState::OutputtingMaterialized(output) => {
73+
Ok(self.emit_next_materialized_batch(output, batch_size))
74+
}
75+
AggregateHashTableState::Done => Ok(None),
76+
AggregateHashTableState::Building(_) => {
77+
internal_err!("next_output_batch must be called in the outputting state")
78+
}
79+
}
80+
}
81+
82+
fn materialize_partial_reduce_output(
83+
&self,
84+
mut state: AggregateHashTableBuffer,
85+
output_schema: SchemaRef,
86+
) -> Result<MaterializedAggregateOutput> {
87+
// `state(EmitTo::All)` consumes accumulator state. Emit all groups once,
88+
// then slice the materialized batch on subsequent polls.
89+
let emit_to_all = EmitTo::All;
90+
let timer = self.group_by_metrics.emitting_time.timer();
91+
let mut output = state.group_values.emit(emit_to_all)?;
92+
93+
for acc in state.accumulators.iter_mut() {
94+
output.extend(acc.state(emit_to_all)?);
95+
}
96+
drop(timer);
97+
98+
let batch = RecordBatch::try_new(output_schema, output)?;
99+
debug_assert!(batch.num_rows() > 0);
100+
Ok(MaterializedAggregateOutput::new(batch))
101+
}
102+
103+
fn emit_next_materialized_batch(
104+
&mut self,
105+
mut output: MaterializedAggregateOutput,
106+
batch_size: usize,
107+
) -> Option<RecordBatch> {
108+
let batch = output.next_batch(batch_size);
109+
if output.is_exhausted() {
110+
self.state = AggregateHashTableState::Done;
111+
} else {
112+
self.state = AggregateHashTableState::OutputtingMaterialized(output);
113+
}
114+
batch
115+
}
116+
117+
pub(in crate::aggregates) fn aggregate_batch(
118+
&mut self,
119+
batch: &RecordBatch,
120+
) -> Result<()> {
121+
let evaluated_batch = self.evaluate_batch(batch)?;
122+
let state = self.state.building_mut();
123+
124+
let timer = self.group_by_metrics.aggregation_time.timer();
125+
for group_values in &evaluated_batch.grouping_set_args {
126+
state
127+
.group_values
128+
.intern(group_values, &mut state.batch_group_indices)?;
129+
let group_indices = &state.batch_group_indices;
130+
let total_num_groups = state.group_values.len();
131+
132+
for (acc, values) in state
133+
.accumulators
134+
.iter_mut()
135+
.zip(evaluated_batch.accumulator_args.iter())
136+
{
137+
acc.merge_batch(values, group_indices, total_num_groups)?;
138+
}
139+
}
140+
drop(timer);
141+
142+
Ok(())
143+
}
144+
145+
pub(in crate::aggregates) fn start_output(&mut self) -> Result<()> {
146+
self.start_outputting();
147+
Ok(())
148+
}
149+
}

datafusion/physical-plan/src/aggregates/mod.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use crate::aggregates::{
2626
no_grouping::AggregateStream,
2727
ordered_final_stream::OrderedFinalAggregateStream,
2828
ordered_partial_stream::OrderedPartialAggregateStream,
29+
partial_reduce_stream::PartialReduceHashAggregateStream,
2930
row_hash::GroupedHashAggregateStream,
3031
topk_stream::GroupedTopKAggregateStream,
3132
};
@@ -81,6 +82,7 @@ mod no_grouping;
8182
pub mod order;
8283
mod ordered_final_stream;
8384
mod ordered_partial_stream;
85+
mod partial_reduce_stream;
8486
mod row_hash;
8587
mod skip_partial;
8688
mod topk;
@@ -531,6 +533,9 @@ enum StreamType {
531533
/// Partial stage of the hash aggregation
532534
/// Input output scheme: initial input -> partial state
533535
PartialHash(PartialHashAggregateStream),
536+
/// Partial-reduce stage of the hash aggregation
537+
/// Input output scheme: partial state -> partial state
538+
PartialReduceHash(PartialReduceHashAggregateStream),
534539
/// Final stage of the hash aggregation
535540
/// Input output scheme: partial state -> final result
536541
FinalHash(FinalHashAggregateStream),
@@ -560,6 +565,7 @@ impl From<StreamType> for SendableRecordBatchStream {
560565
match stream {
561566
StreamType::AggregateStream(stream) => Box::pin(stream),
562567
StreamType::PartialHash(stream) => Box::pin(stream),
568+
StreamType::PartialReduceHash(stream) => Box::pin(stream),
563569
StreamType::FinalHash(stream) => Box::pin(stream),
564570
StreamType::OrderedPartialAggregate(stream) => Box::pin(stream),
565571
StreamType::OrderedFinalAggregate(stream) => Box::pin(stream),
@@ -1048,6 +1054,12 @@ impl AggregateExec {
10481054
)?));
10491055
}
10501056

1057+
if self.should_use_partial_reduce_hash_stream(context) {
1058+
return Ok(StreamType::PartialReduceHash(
1059+
PartialReduceHashAggregateStream::new(self, context, partition)?,
1060+
));
1061+
}
1062+
10511063
if self.should_use_ordered_final_aggregate_stream(context) {
10521064
return Ok(StreamType::OrderedFinalAggregate(
10531065
OrderedFinalAggregateStream::new(self, context, partition)?,
@@ -1108,6 +1120,19 @@ impl AggregateExec {
11081120
&& self.group_by.is_single()
11091121
}
11101122

1123+
fn should_use_partial_reduce_hash_stream(&self, context: &TaskContext) -> bool {
1124+
// TODO: implement memory-limited path and remove this limitation
1125+
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
1126+
return false;
1127+
}
1128+
1129+
self.mode == AggregateMode::PartialReduce
1130+
&& self.limit_options.is_none()
1131+
&& self.input_order_mode == InputOrderMode::Linear
1132+
&& !self.group_by.is_true_no_grouping()
1133+
&& self.group_by.is_single()
1134+
}
1135+
11111136
fn should_use_ordered_final_aggregate_stream(&self, context: &TaskContext) -> bool {
11121137
// TODO: implement memory-limited path and remove this limitation
11131138
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
@@ -3481,6 +3506,99 @@ mod tests {
34813506
Ok(())
34823507
}
34833508

3509+
fn partial_reduce_test_aggregate() -> Result<AggregateExec> {
3510+
let schema = Arc::new(Schema::new(vec![
3511+
Field::new("a", DataType::UInt32, false),
3512+
Field::new("b", DataType::Float64, false),
3513+
]));
3514+
let group_by =
3515+
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
3516+
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
3517+
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
3518+
.schema(Arc::clone(&schema))
3519+
.alias("SUM(b)")
3520+
.build()?,
3521+
)];
3522+
3523+
let empty_input =
3524+
TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema), None)?;
3525+
let partial = AggregateExec::try_new(
3526+
AggregateMode::Partial,
3527+
group_by.clone(),
3528+
aggregates.clone(),
3529+
vec![None],
3530+
empty_input,
3531+
Arc::clone(&schema),
3532+
)?;
3533+
let partial_schema = partial.schema();
3534+
let partial_state_batch = RecordBatch::try_new(
3535+
Arc::clone(&partial_schema),
3536+
vec![
3537+
Arc::new(UInt32Array::from(vec![1, 2, 1, 3])),
3538+
Arc::new(Float64Array::from(vec![10.0, 20.0, 40.0, 30.0])),
3539+
],
3540+
)?;
3541+
let partial_reduce_input = TestMemoryExec::try_new_exec(
3542+
&[vec![partial_state_batch]],
3543+
Arc::clone(&partial_schema),
3544+
None,
3545+
)?;
3546+
3547+
AggregateExec::try_new(
3548+
AggregateMode::PartialReduce,
3549+
group_by,
3550+
aggregates,
3551+
vec![None],
3552+
partial_reduce_input,
3553+
partial_schema,
3554+
)
3555+
}
3556+
3557+
/// For partial-reduce aggregation, ensures `PartialReduceHashAggregateStream`
3558+
/// is used when enabled by migration config.
3559+
#[tokio::test]
3560+
async fn partial_reduce_aggregate_planning() -> Result<()> {
3561+
let partial_reduce = partial_reduce_test_aggregate()?;
3562+
let task_ctx = Arc::new(
3563+
TaskContext::default().with_session_config(
3564+
SessionConfig::new()
3565+
.set_bool("datafusion.execution.enable_migration_aggregate", true),
3566+
),
3567+
);
3568+
3569+
let stream = partial_reduce.execute_typed(0, &task_ctx)?;
3570+
assert!(matches!(stream, StreamType::PartialReduceHash(_)));
3571+
let stream: SendableRecordBatchStream = stream.into();
3572+
let output = collect(stream).await?;
3573+
assert_eq!(output.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);
3574+
3575+
Ok(())
3576+
}
3577+
3578+
/// Spilling behavior is not implemented for partial-reduce stream yet, so fall
3579+
/// back to the existing `GroupedHashAggregateStream`
3580+
#[tokio::test]
3581+
async fn partial_reduce_aggregate_with_memory_limit_planning() -> Result<()> {
3582+
let partial_reduce = partial_reduce_test_aggregate()?;
3583+
let runtime = RuntimeEnvBuilder::new()
3584+
.with_memory_limit(1, 1.0)
3585+
.build_arc()?;
3586+
let task_ctx =
3587+
Arc::new(
3588+
TaskContext::default()
3589+
.with_session_config(SessionConfig::new().set_bool(
3590+
"datafusion.execution.enable_migration_aggregate",
3591+
true,
3592+
))
3593+
.with_runtime(runtime),
3594+
);
3595+
3596+
let stream = partial_reduce.execute_typed(0, &task_ctx)?;
3597+
assert!(matches!(stream, StreamType::GroupedHash(_)));
3598+
3599+
Ok(())
3600+
}
3601+
34843602
/// Ensures for ordered input, `OrderedPartialAggregateStream` is used.
34853603
#[tokio::test]
34863604
async fn ordered_partial_aggregate_planning() -> Result<()> {

0 commit comments

Comments
 (0)