andygrove opened a new pull request, #2801:
URL: https://github.com/apache/datafusion-ballista/pull/2801
# Which issue does this PR close?
No issue filed. This follows up @comphead's review question on #2765: when
debugging AQE, it would help to see which rules applied and how they changed
the plan and, as LLVM's optimization remarks do, which optimizations applied or
were skipped and why. Related: #1359 (AQE tracking).
# Rationale for this change
AQE runs its rules over a job's plan many times: once when the job starts,
after every stage finishes, and on each stage as it launches. The only way to
follow that today is DataFusion's own `debug` log, which prints the whole plan
after every rule of every pass, whether or not the rule changed it. It has no
job id, and it misses the coalesce and skew join rules, which the planner runs
on each stage directly. It also can't say why a rule did nothing, and several
AQE rules skip without a word: the skew join rule logs only one of the reasons
it can leave a join alone, and exchange reuse never says why an exchange got no
key.
# What changes are included in this PR?
- **Plan change log** (`state/aqe/plan_change_log.rs`). With `debug` logging
on for that module, each pass of rules logs every rule that changed the job's
plan, with a diff, and then the plan the pass ended with. Each line names the
job and the pass: the initial optimization, the replan after stage N finished,
the replan after reusing a finished stage, or stage N as it launches. A rule
that leaves the plan as it was logs nothing, as with LLVM's `-print-changed` or
Spark's plan change log.
- Plans are compared by their rendering, since the coalesce and skew join
rules change an exchange in place. The rendering is only taken while the log is
enabled, so with `debug` off a pass costs one `log_enabled!` check.
- Lines are matched ignoring indentation, so a node a rule puts above a
subtree shows as one added line rather than the whole subtree re-indented.
- A change that a later rule of the pass undoes is dropped. DataFusion's
`OutputRequirements` adds a node and removes it again in every pass, so without
this every replan would log two diffs and a full plan even when nothing changed.
- A pass that fails still logs what its rules changed before it failed.
- This replaces the planner's "Distributed physical plan (before/after
optimization)" dumps, which had no job id. The replan timing line now names the
job and the pass.
- **Why rules skip:**
- `OptimizeSkewedJoinRule` logs why it left a join alone: the join type
can't be split, an input isn't read from a shuffle, an input is coalesced,
already split, sorted, broadcast, range-partitioned or not hash-partitioned,
the stage passes its partitions through, the inputs have different partition
counts, or no partition was split. The last one gives each side's largest
partition and skew threshold.
- Exchange reuse logs why an exchange can't share a stage. `reuse_key`
returns an `Unshareable` reason instead of `None`: range partitioning, an
operator with a volatile expression, an operator that needs a single consumer,
or a subtree the codec can't encode. The line for a shared stage now includes
the job id.
- `CoalescePartitionsRule` logs the one bail it was silent about, leaves
with different partition counts.
- Join selection already logs each decision with its sizes and thresholds,
so it is unchanged.
- **Docs:** a "Debugging AQE decisions" section in the tuning guide.
Tests:
- `plan_change_log` unit tests cover the diff (context lines, lines left
out, a subtree moved under a new node, a node moved to another parent, a diff
too large to match) and which changes are recorded (changes made in place,
changes a later rule undoes, and a disabled log).
- `test::reuse_exchange` checks that the exchanges of a volatile subplan are
unshareable because of their volatile expression.
- I ran `examples/standalone-broadcast-join.rs` with
`RUST_LOG=ballista_scheduler::state::aqe=debug` on a standalone cluster to
check the output end to end. The sample below and the one in the tuning guide
come from that run, with the temporary file paths shortened. The replan after
stage 0 logs only its timing, since the only rule that changed the plan there
was `OutputRequirements`, adding its node and removing it again.
<details>
<summary>Sample output</summary>
```text
DEBUG ballista_scheduler::state::aqe::execution_plan::dynamic_join: AQE join
decision plan_id=0 action=CollectLeft(broadcast) partition_mode=CollectLeft
build_side=right under_threshold=true ...
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: SelectJoinRule changed the plan:
...
AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1))]
AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1))]
ProjectionExec: expr=[]
- DynamicJoinSelectionExec: plan_id=0, join_type=Inner, on=[(k@0,
k@0)] repartitioned=false
- DataSourceExec: file_groups={1 group: [[big.parquet]]},
projection=[k], file_type=parquet
+ ProjectionExec: expr=[k@1 as k, k@0 as k]
+ HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)]
DataSourceExec: file_groups={1 group: [[small.parquet]]},
projection=[k], file_type=parquet
+ DataSourceExec: file_groups={1 group: [[big.parquet]]},
projection=[k], file_type=parquet
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: OutputRequirements changed the plan:
+OutputRequirementExec: order_by=[], dist_by=Unspecified
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1))]
AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1))]
...
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: CombinePartialFinalAggregate changed the plan:
OutputRequirementExec: order_by=[], dist_by=Unspecified
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
- AggregateExec: mode=Final, gby=[], aggr=[count(Int64(1))]
- AggregateExec: mode=Partial, gby=[], aggr=[count(Int64(1))]
+ AggregateExec: mode=Single, gby=[], aggr=[count(Int64(1))]
ProjectionExec: expr=[]
ProjectionExec: expr=[k@1 as k, k@0 as k]
HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)]
...
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: ProjectionPushdown changed the plan:
OutputRequirementExec: order_by=[], dist_by=Unspecified
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
AggregateExec: mode=Single, gby=[], aggr=[count(Int64(1))]
- ProjectionExec: expr=[]
- ProjectionExec: expr=[k@1 as k, k@0 as k]
- HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)]
+ HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)],
projection=[]
DataSourceExec: file_groups={1 group: [[small.parquet]]},
projection=[k], file_type=parquet
DataSourceExec: file_groups={1 group: [[big.parquet]]},
projection=[k], file_type=parquet
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: OutputRequirements changed the plan:
-OutputRequirementExec: order_by=[], dist_by=Unspecified
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
AggregateExec: mode=Single, gby=[], aggr=[count(Int64(1))]
HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)],
projection=[]
...
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: DistributedExchangeRule changed the plan:
+AdaptiveDatafusionExec: is_final=false, plan_id=1, stage_id=pending,
stage_resolved=false
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
AggregateExec: mode=Single, gby=[], aggr=[count(Int64(1))]
HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)],
projection=[]
...
DEBUG ballista_scheduler::state::aqe::plan_change_log: Job 038DQQWXQR000,
initial optimization: plan changed by SelectJoinRule, OutputRequirements,
CombinePartialFinalAggregate, ProjectionPushdown, OutputRequirements,
DistributedExchangeRule:
AdaptiveDatafusionExec: is_final=false, plan_id=1, stage_id=pending,
stage_resolved=false
ProjectionExec: expr=[count(Int64(1))@0 as count(*)]
AggregateExec: mode=Single, gby=[], aggr=[count(Int64(1))]
HashJoinExec: mode=CollectLeft, join_type=Inner, on=[(k@0, k@0)],
projection=[]
DataSourceExec: file_groups={1 group: [[small.parquet]]},
projection=[k], file_type=parquet
DataSourceExec: file_groups={1 group: [[big.parquet]]},
projection=[k], file_type=parquet
DEBUG ballista_scheduler::state::aqe::planner: Job 038DQQWXQR000: initial
optimization took 2.851ms
DEBUG ballista_scheduler::state::aqe::planner: Job 038DQQWXQR000: replan
after stage 0 finished took 1.645875ms
```
</details>
# Are there any user-facing changes?
Only in `debug` logs. The AQE planner logs plan changes under
`ballista_scheduler::state::aqe::plan_change_log`, and the skew join rule and
exchange reuse log why they skipped. The planner's "Distributed physical plan
(before/after optimization)" debug lines are replaced by the plan change log.
There are no API or configuration changes.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]