[VL] Improve FlushableHashAggregateRule - #13033
Conversation
The patch improves FlushableHashAggregateRule to apply flushable optimizations in additional cases. The rule code also gets simplified.
There was a problem hiding this comment.
🟡 Changes recommended
Fix protected aggregate tracking and preserve ordering for order-sensitive aggregates.
Get a fresh assessment by requesting another Copilot review.
Pull request overview
This pull request broadens FlushableHashAggregateRule optimizations and adds single-partition regression coverage.
Changes:
- Expands eligible aggregate rewrites.
- Simplifies transformation logic.
- Adds flushable aggregation tests without a shuffle.
File summaries
| File | Summary |
|---|---|
backends-velox/src/test/scala/org/apache/gluten/execution/VeloxAggregateFunctionsSuite.scala |
Adds single-partition flushable aggregation coverage. |
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala |
Broadens and simplifies aggregate conversion logic. |
Review details
Suppressed comments (1)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:42
- This broadens the rewrite to intermediate
SortHashAggregateExecTransformers even when no shuffle separates the partial and final stages. In the existingtest collect_list with orderingpath (the suite uses one shuffle partition), this can turn the sorted partial aggregate intoFlushableHashAggregateExecTransformer;allowFlushmay emit multiple buffers and the finalcollect_listmerge is concatenation, so the input ordering is no longer preserved (and the test's two-SortHashassertion can fail). Keep sort-preserving aggregates regular for order-sensitive functions, or otherwise restrict this new no-exchange case to aggregates whose flush semantics are order-independent.
case agg: SortHashAggregateExecTransformer if isEligible(agg, protectedAggIds) =>
toFlushableAgg(agg)
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
There was a problem hiding this comment.
🟡 Changes recommended
A critical issue affecting join-pushed partial aggregates requires correction and regression coverage.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:83
- Because
transformUpnow visits aggregates that are not below an exchange, this guard is insufficient for grouping-only complete aggregates.MergeTwoPhasesHashBaseAggregatecan collapse a no-exchangeSortAggregateExecinto one node with emptyaggregateExpressionsandrequiredChildDistributionExpressions = None(gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/MergeTwoPhasesHashBaseAggregate.scala:143-152);forallis then vacuously true andisGroupingOnlyFinalAggreturns false. The complete deduplication node is converted toallowFlush=true, which can emit duplicate grouping keys and makeSELECT DISTINCTor grouping without aggregate functions incorrect. Keep merged complete nodes regular or determine that a downstream merge exists, and add a no-exchange grouping-only regression test.
!isGroupingOnlyFinalAgg(agg) &&
agg.aggregateExpressions.forall(p => p.mode == Partial || p.mode == PartialMerge) &&
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Address the grouping-partitioning guard and preserve protected distinct-aggregation nodes during rewriting.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (1)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:85
protectedAggIdsis collected from the original tree, buttransformUprewrites children before evaluating their parents. When the partial aggregate below this protectedPartialMergenode is converted, Spark rebuilds the parent aggregate with a new plan-node id, so its original id is no longer inprotectedAggIdsand it can be converted to flushable. This breaks the one-distinct invariant that the existing test below expects (the protected partial merge must remain regular). Preserve the protection structurally during the rewrite (for example, use the prior top-down/boundary-aware traversal or carry the protection marker through rebuilt nodes) rather than matching stale ids.
private def isEligible(
agg: HashAggregateExecTransformer,
protectedAggIds: Set[Int]): Boolean = {
!isGroupingOnlyFinalAgg(agg) &&
agg.aggregateExpressions.forall(p => p.mode == Partial || p.mode == PartialMerge) &&
!protectedAggIds.contains(agg.id) &&
!aggregatesNotSupportFlush(agg.aggregateExpressions)
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
3ceffcf to
25e6506
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
One or more issues must be addressed before approval.
Review details
Suppressed comments (1)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:86
- The new top-down traversal now converts aggregates even when their child is already partitioned by the aggregate keys. In that case a flushable aggregate can emit duplicate grouping keys from a partition that Spark has deliberately arranged to contain the complete grouping set, changing the semantics of downstream partial aggregation (for example, when grouping keys are a superset of the input partitioning keys). The analogous Bolt rule explicitly skips this case via
child.outputPartitioning.satisfies(ClusteredDistribution(groupingExpressions)); retain the same guard here before converting the aggregate.
private def isEligible(
agg: HashAggregateExecTransformer,
protectedAggIds: Set[Int]): Boolean = {
!isGroupingOnlyFinalAgg(agg) &&
agg.aggregateExpressions.forall(p => p.mode == Partial || p.mode == PartialMerge) &&
!protectedAggIds.contains(agg.id) &&
!aggregatesNotSupportFlush(agg.aggregateExpressions)
- Files reviewed: 2/2 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
The rewrite can duplicate join-aggregate partial rows and multiply join matches without a downstream merge.
Get a fresh assessment by requesting another Copilot review.
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
The aggregate rewrite may violate distribution assumptions and affect join-side partial aggregates.
Review details
Suppressed comments (2)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:43
- This traversal now converts every eligible partial/partial-merge aggregate, including one whose child already satisfies
ClusteredDistributionon its grouping keys. Such an aggregate can be the first stage after an exchange; making it flushable allows duplicate grouping keys to be emitted even though Spark/native planning relies on that input being clustered, which can make downstream partial counts or joins overcount. The existing Bolt implementation explicitly skips this case inisAggInputAlreadyDistributedWithAggKeys(backends-bolt/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:92-95,127-139). Retain the distribution guard here (while preserving the new no-exchange case) before converting the aggregate.
case agg: RegularHashAggregateExecTransformer if isEligible(agg, protectedAggIds) =>
toFlushableAgg(agg)
case agg: SortHashAggregateExecTransformer if isEligible(agg, protectedAggIds) =>
toFlushableAgg(agg)
backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala:43
- This traversal now rewrites every eligible partial aggregate, including the ordinary
HashAggregateExeccreated byImplementJoinAggregate.planPartialPhase. That phase is explicitly packed in aProjectExecand then consumed by the join (ImplementJoinAggregate.scala:107-115,126-137,194), so allowing it to flush can emit duplicate grouping keys that multiply join rows; the previous exchange-bounded traversal did not reach this join-side aggregate. Preserve an exclusion/boundary for join-aggregate partial phases (or otherwise prove their outputs are re-aggregated before the join).
plan.transformDown {
case agg: RegularHashAggregateExecTransformer if isEligible(agg, protectedAggIds) =>
toFlushableAgg(agg)
case agg: SortHashAggregateExecTransformer if isEligible(agg, protectedAggIds) =>
toFlushableAgg(agg)
- Files reviewed: 42/42 changed files
- Comments generated: 0 new
- Review effort level: Lite
The patch improves FlushableHashAggregateRule to apply flushable optimizations in additional cases.
The rule code also gets simplified.