comphead opened a new issue, #6001:
URL: https://github.com/apache/datafusion-comet/issues/6001

   ### What is the problem the feature request solves?
   
   `CometBaseAggregate.doConvert` refuses any `HashAggregateExec` whose 
`groupingExpressions` **and** `aggregateExpressions` are both empty:
   
   ```scala
   // spark/src/main/scala/org/apache/spark/sql/comet/operators.scala
   if (groupingExpressions.isEmpty && aggregateExpressions.isEmpty) {
     withFallbackReason(aggregate, "No group by or aggregation")
     return None
   }
   ```
   
   Spark plans exactly that operator whenever a **global** aggregate's output 
is unused, which is not an exotic shape:
   
   1. `ColumnPruning` rewrites `Project(_, Aggregate(Nil, [count(1) AS c], 
child))` into `Aggregate(Nil, Nil, child)` (`Optimizer.scala`, `case p @ 
Project(_, a: Aggregate)`), and `RemoveNoopOperators` drops the now-empty 
`Project`.
   2. `RemoveRedundantAggregates` cannot remove the aggregate: it explicitly 
refuses when the lower aggregate is global (`lowerIsGlobalAgg`).
   3. `OptimizeOneRowPlan` cannot remove it either, not even for a single-row 
input, because `Aggregate.groupOnly` requires non-empty `groupingExpressions`.
   4. `AggUtils.planAggregateWithoutDistinct` still emits a partial/final pair, 
and the final requires `AllTuples`, so an `Exchange SinglePartition` lands 
between the two.
   
   Both halves of that pair fall back, and a transition is forced immediately 
above the scan.
   
   **Minimal repro.** Comet `1.1.0-SNAPSHOT` (main @ 58ab5f618e), Spark 4.0.0, 
the documented `spark-shell` setup plus 
`spark.comet.exec.localTableScan.enabled=true` and 
`spark.sql.adaptive.enabled=false`:
   
   ```sql
   SELECT 1 FROM (SELECT count(*) FROM VALUES (1) AS v(a))
   ```
   
   ```
    HashAggregate [COMET: No group by or aggregation]
   +- Exchange
      +-  HashAggregate [COMET: No group by or aggregation]
         +- CometColumnarToRow
            +- CometLocalTableScan
   ```
   
   > Comet accelerated 1 out of 4 eligible operators (25%). Final plan contains 
1 transitions between Spark and Comet.
   
   Not specific to local relations. The same fallback happens over Parquet, 
where the scan is pruned to no columns:
   
   ```
    HashAggregate [COMET: No group by or aggregation]
   +- Exchange
      +-  HashAggregate [COMET: No group by or aggregation]
         +- CometColumnarToRow
            +- CometNativeScan parquet [] ... ReadSchema: struct<>
   ```
   
   The likeliest way to hit this in real code is calling `.count()` on an 
aggregated DataFrame. `Dataset.count()` is `groupBy().count()`, and that outer 
count references nothing from the inner aggregate, so the inner one gets pruned 
to the empty form:
   
   ```scala
   Seq(1, 2).toDF("a").agg(sum($"a")).count()
   ```
   
   ```
   HashAggregate
   +- HashAggregate
      +-  HashAggregate [COMET: No group by or aggregation]
         +- Exchange
            +-  HashAggregate [COMET: No group by or aggregation]
               +- CometColumnarToRow
                  +- CometLocalTableScan
   ```
   
   > Comet accelerated 1 out of 6 eligible operators (16%).
   
   ### Describe the potential solution
   
   Serialize the operator rather than bailing out. Its contract is narrow and 
needs none of the aggregate machinery:
   
   > emit exactly one row with **no columns**, regardless of input cardinality.
   
   The "regardless" is load-bearing, since a global aggregate emits a row even 
for empty input. Verified: `SELECT 1 FROM (SELECT count(*) FROM VALUES (1) AS 
v(a) WHERE a > 5)` plans to the same pair over `CometLocalTableScan <empty>` 
and still returns one row. The two stages differ only in scope, the pre-shuffle 
one emitting one empty row per partition and the post-shuffle one collapsing 
those to a single row.
   
   The awkward part is the zero-column output: the row count cannot ride in a 
column, so this needs a batch with `num_rows = 1` and no arrays to survive the 
JNI boundary and the native `ScanExec`. Whether that is representable end to 
end today is a question for someone closer to the native side, and it decides 
whether this is a small serde addition or needs real plumbing.
   
   Note also that the predicate is duplicated. 
`CometExecRule.canAggregateBeConverted` carries the same 
`groupingExpressions.isEmpty && aggregateExpressions.isEmpty` early return, 
with a comment warning that the two sites must stay in sync, so both would need 
the change.
   
   If native support turns out not to be worth the plumbing, this can be closed 
as documented behaviour, but `df.agg(...).count()` seems common enough to 
measure first.
   
   ### Additional context
   
   Environment: macOS arm64, Spark 4.0.0, Comet `1.1.0-SNAPSHOT` built from 
main @ 58ab5f618e, `spark.comet.exec.localTableScan.enabled=true`, AQE disabled 
so the tree is stable.
   
   One reporting quirk noticed while narrowing this down: which of the two 
nodes carries the `[COMET: ...]` annotation depends on whether Comet shuffle is 
enabled. With `spark.comet.shuffle.enabled=false`, only the aggregate below the 
exchange is annotated; the one above it is never attempted and so falls back 
with no reason recorded. That may be relevant to #2787.
   


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