|
1 | | -use crate::common::require_one_child; |
| 1 | +use crate::NetworkBoundaryExt; |
| 2 | +use crate::distributed_planner::inject_network_boundaries::inject_network_boundaries; |
2 | 3 | use crate::distributed_planner::insert_broadcast::insert_broadcast_execs; |
3 | 4 | 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; |
12 | 6 | use datafusion::common::tree_node::TreeNode; |
13 | 7 | use datafusion::config::ConfigOptions; |
14 | 8 | use datafusion::physical_plan::coalesce_partitions::CoalescePartitionsExec; |
15 | 9 | use datafusion::physical_plan::{ExecutionPlan, ExecutionPlanProperties}; |
16 | | -use std::ops::AddAssign; |
17 | 10 | use std::sync::Arc; |
18 | | -use uuid::Uuid; |
19 | 11 |
|
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. |
22 | 14 | /// |
23 | | -/// It performs the following operations: |
| 15 | +/// The pipeline runs four passes in order: |
24 | 16 | /// |
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`. |
28 | 22 | /// |
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`). |
33 | 28 | /// |
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`. |
38 | 32 | /// |
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. |
40 | 35 | pub(super) async fn distribute_plan( |
41 | 36 | original: Arc<dyn ExecutionPlan>, |
42 | 37 | cfg: &ConfigOptions, |
43 | 38 | ) -> 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. |
45 | 41 | 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)); |
47 | 48 | } |
48 | 49 |
|
49 | 50 | let mut plan = Arc::clone(&original); |
50 | 51 |
|
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. |
53 | 55 | if plan.output_partitioning().partition_count() > 1 { |
54 | 56 | plan = Arc::new(CoalescePartitionsExec::new(plan)); |
55 | 57 | } |
56 | 58 |
|
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. |
59 | 61 | plan = insert_broadcast_execs(plan, cfg)?; |
60 | 62 |
|
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?; |
63 | 65 |
|
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()))? { |
68 | 70 | return Ok(None); |
69 | 71 | } |
70 | 72 |
|
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. |
72 | 75 | let plan = partial_reduce_below_network_shuffles(plan, cfg)?; |
73 | 76 |
|
74 | 77 | Ok(Some(plan)) |
75 | 78 | } |
76 | 79 |
|
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 | | - |
167 | 80 | #[cfg(test)] |
168 | 81 | mod tests { |
169 | 82 | use crate::test_utils::in_memory_channel_resolver::InMemoryWorkerResolver; |
|
0 commit comments