Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion datafusion/execution/src/spill_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ pub trait SpillFile: Send + Sync {

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ use crate::aggregates::{

/// Marker for raw rows -> partial state aggregation.
pub(in crate::aggregates) struct PartialMarker;
/// Marker for partial state -> partial state aggregation.
pub(in crate::aggregates) struct PartialReduceMarker;
/// Marker for raw rows -> partial state conversion without aggregation.
pub(in crate::aggregates) struct PartialSkipMarker;
/// Marker for partial state -> final value aggregation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ mod common_ordered;
mod final_table;
mod ordered_final_table;
mod ordered_partial_table;
mod partial_reduce_table;
mod partial_table;

pub(super) use common::{
AggregateHashTable, FinalMarker, PartialMarker, PartialSkipMarker,
AggregateHashTable, FinalMarker, PartialMarker, PartialReduceMarker,
PartialSkipMarker,
};
pub(super) use common_ordered::OrderedAggregateTable;
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

use std::sync::Arc;

use arrow::datatypes::SchemaRef;
use arrow::record_batch::RecordBatch;
use datafusion_common::{Result, internal_err};
use datafusion_expr::EmitTo;

use crate::aggregates::AggregateExec;

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.

I verified that this code is covered by existing tests:

cargo llvm-cov -p datafusion-physical-plan --lib --html -- partial_reduce


use super::common::{
AggregateHashTable, AggregateHashTableBuffer, AggregateHashTableState,
MaterializedAggregateOutput, PartialReduceMarker,
};

/// Methods specific to the aggregate hash table used in the partial-reduce stage.
impl AggregateHashTable<PartialReduceMarker> {
pub(in crate::aggregates) fn new(
agg: &AggregateExec,
partition: usize,
output_schema: SchemaRef,
batch_size: usize,
) -> Result<Self> {
Self::new_with_filters(
agg,
partition,
output_schema,
batch_size,
vec![None; agg.aggr_expr.len()],
)
}

/// Emits the next batch of aggregated group keys and aggregate states.
///
/// The output batch size is determined by `self.batch_size`.
///
/// Returns `Some(batch)` for each emitted batch, `None` when output is
/// exhausted, and an internal error if polled in the `Building` state.
pub(in crate::aggregates) fn next_output_batch(
&mut self,
) -> Result<Option<RecordBatch>> {
let output_schema = Arc::clone(&self.output_schema);
let batch_size = self.batch_size;
// Take ownership of the output state. Note `emit_next_materialized_batch`
// updates state after it emits a materialized slice.
match std::mem::replace(&mut self.state, AggregateHashTableState::Done) {
AggregateHashTableState::Outputting(state) => {
if state.group_values.is_empty() {
return Ok(None);
}

let output =
self.materialize_partial_reduce_output(state, output_schema)?;
Ok(self.emit_next_materialized_batch(output, batch_size))
}
AggregateHashTableState::OutputtingMaterialized(output) => {
Ok(self.emit_next_materialized_batch(output, batch_size))
}
AggregateHashTableState::Done => Ok(None),
AggregateHashTableState::Building(_) => {
internal_err!("next_output_batch must be called in the outputting state")
}
}
}

fn materialize_partial_reduce_output(
&self,
mut state: AggregateHashTableBuffer,
output_schema: SchemaRef,
) -> Result<MaterializedAggregateOutput> {
// `state(EmitTo::All)` consumes accumulator state. Emit all groups once,
// then slice the materialized batch on subsequent polls.
let emit_to_all = EmitTo::All;
let timer = self.group_by_metrics.emitting_time.timer();
let mut output = state.group_values.emit(emit_to_all)?;

for acc in state.accumulators.iter_mut() {
output.extend(acc.state(emit_to_all)?);
}
drop(timer);

let batch = RecordBatch::try_new(output_schema, output)?;
debug_assert!(batch.num_rows() > 0);
Ok(MaterializedAggregateOutput::new(batch))
}

fn emit_next_materialized_batch(
&mut self,
mut output: MaterializedAggregateOutput,
batch_size: usize,
) -> Option<RecordBatch> {
let batch = output.next_batch(batch_size);
if output.is_exhausted() {
self.state = AggregateHashTableState::Done;
} else {
self.state = AggregateHashTableState::OutputtingMaterialized(output);
}
batch
}

pub(in crate::aggregates) fn aggregate_batch(
&mut self,
batch: &RecordBatch,
) -> Result<()> {
let evaluated_batch = self.evaluate_batch(batch)?;
let state = self.state.building_mut();

let timer = self.group_by_metrics.aggregation_time.timer();
for group_values in &evaluated_batch.grouping_set_args {
state
.group_values
.intern(group_values, &mut state.batch_group_indices)?;
let group_indices = &state.batch_group_indices;
let total_num_groups = state.group_values.len();

for (acc, values) in state
.accumulators
.iter_mut()
.zip(evaluated_batch.accumulator_args.iter())
{
acc.merge_batch(values, group_indices, total_num_groups)?;
}
}
drop(timer);

Ok(())
}

pub(in crate::aggregates) fn start_output(&mut self) -> Result<()> {
self.start_outputting();
Ok(())
}
}
118 changes: 118 additions & 0 deletions datafusion/physical-plan/src/aggregates/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::aggregates::{
no_grouping::AggregateStream,
ordered_final_stream::OrderedFinalAggregateStream,
ordered_partial_stream::OrderedPartialAggregateStream,
partial_reduce_stream::PartialReduceHashAggregateStream,
row_hash::GroupedHashAggregateStream,
topk_stream::GroupedTopKAggregateStream,
};
Expand Down Expand Up @@ -81,6 +82,7 @@ mod no_grouping;
pub mod order;
mod ordered_final_stream;
mod ordered_partial_stream;
mod partial_reduce_stream;
mod row_hash;
mod skip_partial;
mod topk;
Expand Down Expand Up @@ -531,6 +533,9 @@ enum StreamType {
/// Partial stage of the hash aggregation
/// Input output scheme: initial input -> partial state
PartialHash(PartialHashAggregateStream),
/// Partial-reduce stage of the hash aggregation
/// Input output scheme: partial state -> partial state
PartialReduceHash(PartialReduceHashAggregateStream),
/// Final stage of the hash aggregation
/// Input output scheme: partial state -> final result
FinalHash(FinalHashAggregateStream),
Expand Down Expand Up @@ -560,6 +565,7 @@ impl From<StreamType> for SendableRecordBatchStream {
match stream {
StreamType::AggregateStream(stream) => Box::pin(stream),
StreamType::PartialHash(stream) => Box::pin(stream),
StreamType::PartialReduceHash(stream) => Box::pin(stream),
StreamType::FinalHash(stream) => Box::pin(stream),
StreamType::OrderedPartialAggregate(stream) => Box::pin(stream),
StreamType::OrderedFinalAggregate(stream) => Box::pin(stream),
Expand Down Expand Up @@ -1048,6 +1054,12 @@ impl AggregateExec {
)?));
}

if self.should_use_partial_reduce_hash_stream(context) {
return Ok(StreamType::PartialReduceHash(
PartialReduceHashAggregateStream::new(self, context, partition)?,
));
}

if self.should_use_ordered_final_aggregate_stream(context) {
return Ok(StreamType::OrderedFinalAggregate(
OrderedFinalAggregateStream::new(self, context, partition)?,
Expand Down Expand Up @@ -1108,6 +1120,19 @@ impl AggregateExec {
&& self.group_by.is_single()
}

fn should_use_partial_reduce_hash_stream(&self, context: &TaskContext) -> bool {
// TODO: implement memory-limited path and remove this limitation
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
return false;
}

self.mode == AggregateMode::PartialReduce
&& self.limit_options.is_none()
&& self.input_order_mode == InputOrderMode::Linear
&& !self.group_by.is_true_no_grouping()
&& self.group_by.is_single()
}

fn should_use_ordered_final_aggregate_stream(&self, context: &TaskContext) -> bool {
// TODO: implement memory-limited path and remove this limitation
if matches!(context.memory_pool().memory_limit(), MemoryLimit::Finite(_)) {
Expand Down Expand Up @@ -3481,6 +3506,99 @@ mod tests {
Ok(())
}

fn partial_reduce_test_aggregate() -> Result<AggregateExec> {
let schema = Arc::new(Schema::new(vec![
Field::new("a", DataType::UInt32, false),
Field::new("b", DataType::Float64, false),
]));
let group_by =
PhysicalGroupBy::new_single(vec![(col("a", &schema)?, "a".to_string())]);
let aggregates: Vec<Arc<AggregateFunctionExpr>> = vec![Arc::new(
AggregateExprBuilder::new(sum_udaf(), vec![col("b", &schema)?])
.schema(Arc::clone(&schema))
.alias("SUM(b)")
.build()?,
)];

let empty_input =
TestMemoryExec::try_new_exec(&[vec![]], Arc::clone(&schema), None)?;
let partial = AggregateExec::try_new(
AggregateMode::Partial,
group_by.clone(),
aggregates.clone(),
vec![None],
empty_input,
Arc::clone(&schema),
)?;
let partial_schema = partial.schema();
let partial_state_batch = RecordBatch::try_new(
Arc::clone(&partial_schema),
vec![
Arc::new(UInt32Array::from(vec![1, 2, 1, 3])),
Arc::new(Float64Array::from(vec![10.0, 20.0, 40.0, 30.0])),
],
)?;
let partial_reduce_input = TestMemoryExec::try_new_exec(
&[vec![partial_state_batch]],
Arc::clone(&partial_schema),
None,
)?;

AggregateExec::try_new(
AggregateMode::PartialReduce,
group_by,
aggregates,
vec![None],
partial_reduce_input,
partial_schema,
)
}

/// For partial-reduce aggregation, ensures `PartialReduceHashAggregateStream`
/// is used when enabled by migration config.
#[tokio::test]
async fn partial_reduce_aggregate_planning() -> Result<()> {
let partial_reduce = partial_reduce_test_aggregate()?;
let task_ctx = Arc::new(
TaskContext::default().with_session_config(
SessionConfig::new()
.set_bool("datafusion.execution.enable_migration_aggregate", true),
),
);

let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::PartialReduceHash(_)));
let stream: SendableRecordBatchStream = stream.into();
let output = collect(stream).await?;
assert_eq!(output.iter().map(RecordBatch::num_rows).sum::<usize>(), 3);

Ok(())
}

/// Spilling behavior is not implemented for partial-reduce stream yet, so fall
/// back to the existing `GroupedHashAggregateStream`
#[tokio::test]
async fn partial_reduce_aggregate_with_memory_limit_planning() -> Result<()> {
let partial_reduce = partial_reduce_test_aggregate()?;
let runtime = RuntimeEnvBuilder::new()
.with_memory_limit(1, 1.0)
.build_arc()?;
let task_ctx =
Arc::new(
TaskContext::default()
.with_session_config(SessionConfig::new().set_bool(
"datafusion.execution.enable_migration_aggregate",
true,
))
.with_runtime(runtime),
);

let stream = partial_reduce.execute_typed(0, &task_ctx)?;
assert!(matches!(stream, StreamType::GroupedHash(_)));

Ok(())
}

/// Ensures for ordered input, `OrderedPartialAggregateStream` is used.
#[tokio::test]
async fn ordered_partial_aggregate_planning() -> Result<()> {
Expand Down
Loading