andygrove opened a new pull request, #1954:
URL: https://github.com/apache/datafusion-ballista/pull/1954

   # Which issue does this PR close?
   
   Closes #1953.
   
   > Stacked on #1951 (`shuffle-inflight-governor`). Until #1951 merges, the 
diff here also shows that PR's commits; it will narrow to just the 
exchange-reuse commits once #1951 lands.
   
   # Rationale for this change
   
   The distributed planner (`ballista/scheduler/src/planner.rs`, 
`plan_query_stages`) inserts shuffle boundaries by walking the physical plan, 
but has **no mechanism to detect and reuse identical repeated subplans** — 
`grep -r "Reuse" ballista/` finds nothing. When a query contains the same 
subtree more than once (common in TPC-H — Q2, Q11, Q15 reference a shared 
aggregate/scan subquery), Ballista plans and **executes each occurrence 
independently**: redundant scans, shuffles, and compute for work that produces 
identical results.
   
   Spark solves this with `ReuseExchange`/`ReuseSubquery`: materialize a shared 
exchange once and fan its output out to every consumer. This PR ports the 
exchange-reuse half of that model onto Ballista's stage DAG.
   
   The key observation is that **Ballista already has the `ReusedExchangeExec` 
indirection built in**. A Ballista exchange is a `ShuffleWriterExec` *stage*; a 
consumer references it *by `stage_id`* via `UnresolvedShuffleExec` → 
`ShuffleReaderExec`, and `ExecutionStageBuilder` already fans one stage out to 
many consumers through `output_links: Vec<usize>`. Reuse never happened only 
because the planner minted a fresh `stage_id` for every writer, even for 
structurally identical subtrees. So no new operator and no output-attribute 
remapping are needed (Ballista physical plans are positional — no exprIds): 
reuse reduces to making identical writer subtrees share one `stage_id`.
   
   # What changes are included in this PR?
   
   1. **A post-planning reuse pass** 
(`ballista/scheduler/src/physical_optimizer/reuse_exchange.rs`, the analog of 
Spark's standalone `ReuseExchangeAndSubquery` rule). `reuse_shuffle_stages` 
deduplicates `ShuffleWriter` stages whose **stage-id-normalized protobuf 
serialization** is byte-identical, and rewires every dropped stage's consumers' 
`UnresolvedShuffleExec` to the surviving `stage_id`.
      - **Identity test = exact protobuf bytes** of a stage-id-normalized 
writer, produced with the configured `PhysicalExtensionCodec` (the same codec 
that already ships stages to executors). Byte-equality is semantic equality 
here because plans are positional. It captures full `DataSourceExec` file 
groups, expressions, partitioning, and nested `stage_id`s — a `Display`-string 
key would elide scan file groups and risk a false merge. If a subtree can't be 
canonicalized (e.g. a custom op the codec can't encode alone), it is treated as 
unique and **never merged** — a missed optimization, never a wrong result.
      - Stages are processed in **ascending `stage_id`** order. Because the 
planner assigns ids bottom-up, a dependency is finalized before any dependent 
is keyed, giving a single-pass fixed point (inner duplicates collapse, then the 
outer subtrees that now carry the collapsed inner id collapse in turn). The 
**root stage is never dropped**.
   
   2. **Wiring.** `StaticExecutionGraph::new` is split into `new` (unchanged 
behavior, delegates with reuse off) and `new_with_reuse`, which runs the pass 
between `plan_query_stages` and `ExecutionStageBuilder::build`. The scheduler 
builds the canonicalizer closure from its configured codec; any serialization 
failure degrades to "not reusable" and never aborts planning.
   
   3. **Config** `ballista.optimizer.reuse_exchange_enabled` (default `true`, 
parity with Spark's `spark.sql.exchange.reuse`).
   
   # Are there any user-facing changes?
   
   One new configuration key, `ballista.optimizer.reuse_exchange_enabled` 
(default `true`), so existing deployments get exchange reuse automatically and 
can turn it off if needed. No public API breakage.
   
   ## How are these changes tested?
   
   - Unit tests on `reuse_shuffle_stages` with synthetic stage DAGs: an 
identical pair collapses; differing input or partitioning is not merged; a 
nested duplicate collapses in a single pass (with a stage-id-sensitive stub 
proven to fail if the pre-key ref-rewrite is skipped); the root is never 
dropped; an un-canonicalizable stage is never merged; and after 
`ExecutionStageBuilder::build`, a shared stage fans out to **distinct** 
consumer stages (`output_links` contains each consumer).
   - A TPC-H detection test plans the real query SQL from 
`benchmarks/queries/qN.sql` against the test context and runs the pass with the 
real protobuf canonicalizer, asserting the exact stage-count delta. Exchange 
reuse fires on **Q2 (19→18 stages), Q11 (12→10), and Q15 (6→5)**; it is 
correctly a no-op on Q14/Q17/Q20/Q21, whose plans contain no 
structurally-identical exchanges. A guard asserts at least one query exercises 
reuse, so a silent no-op regression fails the test.
   


-- 
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]

Reply via email to