Skip to content

Commit 8285ee3

Browse files
committed
Remove 2-pass based distributed planner in favor of just 1 pass
1 parent e623085 commit 8285ee3

11 files changed

Lines changed: 1488 additions & 1246 deletions

src/distributed_planner/distribute_plan.rs

Lines changed: 42 additions & 129 deletions
Original file line numberDiff line numberDiff line change
@@ -1,169 +1,82 @@
1-
use crate::common::require_one_child;
1+
use crate::NetworkBoundaryExt;
2+
use crate::distributed_planner::inject_network_boundaries::inject_network_boundaries;
23
use crate::distributed_planner::insert_broadcast::insert_broadcast_execs;
34
use crate::distributed_planner::partial_reduce_below_network_shuffles::partial_reduce_below_network_shuffles;
4-
use crate::distributed_planner::plan_annotator::{
5-
AnnotatedPlan, PlanOrNetworkBoundary, annotate_plan,
6-
};
7-
use crate::{
8-
DistributedConfig, NetworkBoundaryExt, NetworkBroadcastExec, NetworkCoalesceExec,
9-
NetworkShuffleExec, TaskEstimator,
10-
};
11-
use datafusion::common::DataFusionError;
5+
use crate::distributed_planner::prepare_network_boundaries::prepare_network_boundaries;
126
use datafusion::common::tree_node::TreeNode;
137
use datafusion::config::ConfigOptions;
148
use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec;
159
use datafusion::physical_plan::{ExecutionPlan, ExecutionPlanProperties};
16-
use std::ops::AddAssign;
1710
use std::sync::Arc;
18-
use uuid::Uuid;
1911

20-
/// Inspects the plan, places the appropriate network boundaries, and breaks it down into stages
21-
/// that can be executed in a distributed manner.
12+
/// Transforms a single-node physical plan into a distributed plan by injecting network
13+
/// boundaries between stages.
2214
///
23-
/// It performs the following operations:
15+
/// The pipeline runs four passes in order:
2416
///
25-
/// 1. It prepares the plan for distribution, adding some extra single-node nodes like
26-
/// [BroadcastExec] or [CoalescePartitionsExec] that will signal the following steps to
27-
/// introduce network boundaries in the appropriate places.
17+
/// 1. **Pre-distribution shaping.** A `CoalescePartitionsExec` is wrapped on top of the plan
18+
/// when it has more than one output partition (so [inject_network_boundaries] later sees a
19+
/// partition-collecting parent and injects a `NetworkCoalesceExec` above its child). Then
20+
/// [insert_broadcast_execs] adds `BroadcastExec` nodes on the build side of `CollectLeft`
21+
/// hash joins so those build sides can later be wrapped in `NetworkBroadcastExec`.
2822
///
29-
/// 2. Annotate the plan with [annotate_plan]: adds some annotations to each node about how
30-
/// many distributed tasks should be used in the stage containing them, and whether they
31-
/// need a network boundary below or not.
32-
/// For more information about this step, read [annotate_plan] docs.
23+
/// 2. **Boundary injection.** [inject_network_boundaries] walks the plan, computes a task count
24+
/// for each node, and inserts `NetworkShuffleExec` / `NetworkBroadcastExec` /
25+
/// `NetworkCoalesceExec` above the nodes that delimit a stage (hash `RepartitionExec`s,
26+
/// build-side `BroadcastExec`s, and any node sitting under a `CoalescePartitionsExec` /
27+
/// `SortPreservingMergeExec`).
3328
///
34-
/// 3. Based on the [AnnotatedPlan] returned by [annotate_plan], place all the appropriate
35-
/// network boundaries ([NetworkShuffleExec] and [NetworkCoalesceExec]) with the task count
36-
/// assignation that the annotations required. After this, the plan is already a distributed
37-
/// executable plan.
29+
/// 3. **Boundary preparation.** [prepare_network_boundaries] readies each injected boundary
30+
/// for execution: elides ones that aren't actually needed and finalises the survivors. If
31+
/// no boundary survives, this function returns `None`.
3832
///
39-
/// This function returns None if the plan was left undistributed.
33+
/// 4. **Shuffle-volume optimisation.** [partial_reduce_below_network_shuffles] inserts partial
34+
/// aggregation nodes underneath hash shuffles where it can, so less data crosses the network.
4035
pub(super) async fn distribute_plan(
4136
original: Arc<dyn ExecutionPlan>,
4237
cfg: &ConfigOptions,
4338
) -> datafusion::common::Result<Option<Arc<dyn ExecutionPlan>>> {
44-
// Keep this function idempotent.
39+
// The plan already contains network boundaries set by the user. Just ensure they have nice
40+
// unique identifiers for each stage, and move forward with it.
4541
if original.exists(|plan| Ok(plan.is_network_boundary()))? {
46-
return Ok(None);
42+
// Ensure the stages in the plan have nice unique identifiers.
43+
let plan = prepare_network_boundaries(original)?;
44+
if !plan.exists(|plan| Ok(plan.is_network_boundary()))? {
45+
return Ok(None);
46+
}
47+
return Ok(Some(plan));
4748
}
4849

4950
let mut plan = Arc::clone(&original);
5051

51-
// Add a CoalescePartitionsExec on top of the plan if necessary. The plan annotator will see
52-
// this and will place a NetworkCoalesceExec below it.
52+
// If the plan has multiple output partitions, wrap it in a `CoalescePartitionsExec` so
53+
// `inject_network_boundaries` will recognise the partition-collecting parent and inject a
54+
// `NetworkCoalesceExec` above the original root.
5355
if plan.output_partitioning().partition_count() > 1 {
5456
plan = Arc::new(CoalescePartitionsExec::new(plan));
5557
}
5658

57-
// Insert BroadcastExec nodes in collect left joins so that the plan annotator can inject
58-
// broadcast network boundaries above.
59+
// Insert `BroadcastExec` on the build side of CollectLeft hash joins so that
60+
// `inject_network_boundaries` can wrap them in `NetworkBroadcastExec` afterwards.
5961
plan = insert_broadcast_execs(plan, cfg)?;
6062

61-
// Annotate the plan with network boundary and task count information.
62-
let annotated = annotate_plan(plan, cfg).await?;
63+
// Compute per-node task counts and inject `Network*Exec` nodes at the stage boundaries.
64+
plan = inject_network_boundaries(plan, cfg).await?;
6365

64-
// Based on the annotations, place the actual network boundaries with the appropriate dimensions.
65-
let mut stage_id = 1;
66-
let plan = _distribute_plan(annotated, cfg, Uuid::new_v4(), &mut stage_id)?;
67-
if stage_id == 1 {
66+
// Run some preparations on the network boundaries, like optimizing them out if there is one
67+
// task at both sides of the boundary or assigning it proper unique identifiers.
68+
plan = prepare_network_boundaries(plan)?;
69+
if !plan.exists(|plan| Ok(plan.is_network_boundary()))? {
6870
return Ok(None);
6971
}
7072

71-
// Insert PartialReduce aggregation nodes above hash repartitions to reduce shuffle data volume.
73+
// Push partial aggregation below hash shuffles to shrink the volume of data sent over
74+
// the network.
7275
let plan = partial_reduce_below_network_shuffles(plan, cfg)?;
7376

7477
Ok(Some(plan))
7578
}
7679

77-
/// Takes an [AnnotatedPlan] and returns a modified [ExecutionPlan] with all the network boundaries
78-
/// appropriately placed. This step performs the following modifications to the original
79-
/// [ExecutionPlan]:
80-
/// - The leaf nodes are scaled up in parallelism based on the number of distributed tasks in
81-
/// which they are going to run. This is configurable by the user via the [TaskEstimator] trait.
82-
/// - The appropriate network boundaries are placed in the plan depending on how it was annotated,
83-
/// so new nodes like [NetworkBroadcastExec], [NetworkCoalesceExec] and [NetworkShuffleExec] will be present.
84-
fn _distribute_plan(
85-
annotated_plan: AnnotatedPlan,
86-
cfg: &ConfigOptions,
87-
query_id: Uuid,
88-
stage_id: &mut usize,
89-
) -> Result<Arc<dyn ExecutionPlan>, DataFusionError> {
90-
let d_cfg = DistributedConfig::from_config_options(cfg)?;
91-
let children = annotated_plan.children;
92-
let task_count = annotated_plan.task_count.as_usize();
93-
let max_child_task_count = children.iter().map(|v| v.task_count.as_usize()).max();
94-
let new_children = children
95-
.into_iter()
96-
.map(|child| _distribute_plan(child, cfg, query_id, stage_id))
97-
.collect::<Result<Vec<_>, _>>()?;
98-
match annotated_plan.plan_or_nb {
99-
// This is a leaf node. It needs to be scaled up in order to account for it running in
100-
// multiple tasks.
101-
PlanOrNetworkBoundary::Plan(plan) if plan.children().is_empty() => {
102-
let scaled_up = d_cfg.__private_task_estimator.scale_up_leaf_node(
103-
&plan,
104-
annotated_plan.task_count.as_usize(),
105-
cfg,
106-
);
107-
Ok(scaled_up.unwrap_or(plan))
108-
}
109-
// This is a normal intermediate plan, just pass it through with the mapped children.
110-
PlanOrNetworkBoundary::Plan(plan) => plan.with_new_children(new_children),
111-
// This is a shuffle, so inject a NetworkShuffleExec here in the plan.
112-
PlanOrNetworkBoundary::Shuffle => {
113-
// It would need a network boundary, but on both sides of the boundary there is just 1 task,
114-
// so we are fine with not introducing any network boundary.
115-
if task_count == 1 && max_child_task_count == Some(1) {
116-
return require_one_child(new_children);
117-
}
118-
let node = Arc::new(NetworkShuffleExec::try_new(
119-
require_one_child(new_children)?,
120-
query_id,
121-
*stage_id,
122-
task_count,
123-
max_child_task_count.unwrap_or(1),
124-
)?);
125-
stage_id.add_assign(1);
126-
Ok(node)
127-
}
128-
// DataFusion is trying to coalesce multiple partitions into one, so we should do the
129-
// same with tasks.
130-
PlanOrNetworkBoundary::Coalesce => {
131-
// It would need a network boundary, but on both sides of the boundary there is just 1 task,
132-
// so we are fine with not introducing any network boundary.
133-
if task_count == 1 && max_child_task_count == Some(1) {
134-
return require_one_child(new_children);
135-
}
136-
let node = Arc::new(NetworkCoalesceExec::try_new(
137-
require_one_child(new_children)?,
138-
query_id,
139-
*stage_id,
140-
task_count,
141-
max_child_task_count.unwrap_or(1),
142-
)?);
143-
stage_id.add_assign(1);
144-
Ok(node)
145-
}
146-
// This is a CollectLeft HashJoinExec with the build side marked as being broadcast. we
147-
// need to insert a NetworkBroadcastExec and scale up the BroadcastExec consumer_tasks.
148-
PlanOrNetworkBoundary::Broadcast => {
149-
// It would need a network boundary, but on both sides of the boundary there is just 1 task,
150-
// so we are fine with not introducing any network boundary.
151-
if task_count == 1 && max_child_task_count == Some(1) {
152-
return require_one_child(new_children);
153-
}
154-
let node = Arc::new(NetworkBroadcastExec::try_new(
155-
require_one_child(new_children)?,
156-
query_id,
157-
*stage_id,
158-
task_count,
159-
max_child_task_count.unwrap_or(1),
160-
)?);
161-
stage_id.add_assign(1);
162-
Ok(node)
163-
}
164-
}
165-
}
166-
16780
#[cfg(test)]
16881
mod tests {
16982
use crate::test_utils::in_memory_channel_resolver::InMemoryWorkerResolver;

0 commit comments

Comments
 (0)