This is an automated email from the ASF dual-hosted git repository.

zhztheplayer pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gluten.git


The following commit(s) were added to refs/heads/main by this push:
     new b9dda4b955 [GLUTEN-13349][VL] Fix flushable aggregation 
double-counting distinct keys (#13350)
b9dda4b955 is described below

commit b9dda4b95536b2a4c72b3820234ca2fffc79c9a1
Author: YangJie <[email protected]>
AuthorDate: Mon Oct 19 07:31:16 2026 -0400

    [GLUTEN-13349][VL] Fix flushable aggregation double-counting distinct keys 
(#13350)
---
 .../extension/FlushableHashAggregateRule.scala     | 16 +++--
 .../execution/VeloxAggregateFunctionsSuite.scala   | 68 +++++++++++++++++++++-
 2 files changed, 78 insertions(+), 6 deletions(-)

diff --git 
a/backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala
 
b/backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala
index d5c1292fd1..7aff5b73ea 100644
--- 
a/backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala
+++ 
b/backends-velox/src/main/scala/org/apache/gluten/extension/FlushableHashAggregateRule.scala
@@ -22,7 +22,8 @@ import org.apache.gluten.execution._
 import org.apache.spark.sql.SparkSession
 import org.apache.spark.sql.catalyst.expressions.aggregate._
 import org.apache.spark.sql.catalyst.rules.Rule
-import org.apache.spark.sql.execution.SparkPlan
+import org.apache.spark.sql.execution.{SortExec, SparkPlan}
+import org.apache.spark.sql.execution.aggregate.BaseAggregateExec
 import org.apache.spark.sql.types.{DataType, DoubleType, FloatType}
 
 /**
@@ -122,7 +123,9 @@ case class FlushableHashAggregateRule(session: 
SparkSession) extends Rule[SparkP
   private def collectProtectedOneDistinctPartialMergeAggIds(plan: SparkPlan): 
Set[Int] = {
     val protectedAggIds = Set.newBuilder[Int]
     plan.foreach {
-      case agg: HashAggregateExecTransformer =>
+      // The distinct-partial aggregate may run in vanilla Spark, e.g. when 
one of its functions is
+      // not offloaded, while the PartialMerge aggregate below it is still 
offloaded.
+      case agg: BaseAggregateExec =>
         findProtectedPartialMergeAgg(agg).foreach {
           protectedAgg => protectedAggIds += protectedAgg.id
         }
@@ -131,9 +134,11 @@ case class FlushableHashAggregateRule(session: 
SparkSession) extends Rule[SparkP
     protectedAggIds.result()
   }
 
-  /** If this aggregate is the distinct-partial stage, return its child 
PartialMerge aggregate. */
+  /**
+   * If this aggregate is the distinct-partial stage, return the PartialMerge 
aggregate below it.
+   */
   private def findProtectedPartialMergeAgg(
-      distinctPartialAgg: HashAggregateExecTransformer): 
Option[HashAggregateExecTransformer] = {
+      distinctPartialAgg: BaseAggregateExec): 
Option[HashAggregateExecTransformer] = {
     if (
       !distinctPartialAgg.aggregateExpressions.exists(
         expr => expr.isDistinct && expr.mode == Partial)
@@ -149,6 +154,9 @@ case class FlushableHashAggregateRule(session: 
SparkSession) extends Rule[SparkP
 
   private def asAggregate(plan: SparkPlan): 
Option[HashAggregateExecTransformer] = plan match {
     case agg: HashAggregateExecTransformer => Some(agg)
+    // A sort-based distinct-partial aggregate reads its child through a local 
sort.
+    case sort: SortExecTransformer if !sort.global => asAggregate(sort.child)
+    case sort: SortExec if !sort.global => asAggregate(sort.child)
     case _ => None
   }
 }
diff --git 
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxAggregateFunctionsSuite.scala
 
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxAggregateFunctionsSuite.scala
index d9c8486154..67f5e79803 100644
--- 
a/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxAggregateFunctionsSuite.scala
+++ 
b/backends-velox/src/test/scala/org/apache/gluten/execution/VeloxAggregateFunctionsSuite.scala
@@ -20,7 +20,7 @@ import org.apache.gluten.config.{GlutenConfig, VeloxConfig}
 import org.apache.gluten.extension.columnar.validator.FallbackInjects
 
 import org.apache.spark.SparkConf
-import org.apache.spark.sql.catalyst.expressions.aggregate.{Final, Partial}
+import org.apache.spark.sql.catalyst.expressions.aggregate.{Final, Partial, 
PartialMerge}
 import org.apache.spark.sql.execution.aggregate.BaseAggregateExec
 import org.apache.spark.sql.execution.exchange.ShuffleExchangeLike
 import org.apache.spark.sql.internal.SQLConf
@@ -1307,7 +1307,8 @@ class VeloxAggregateFunctionsFlushSuite extends 
VeloxAggregateFunctionsSuite {
     withSQLConf(
       SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",
       SQLConf.FILES_MAX_PARTITION_BYTES.key -> "1k") {
-      runQueryAndCompare("select count(distinct l_partkey) from lineitem group 
by l_orderkey") {
+      runQueryAndCompare(
+        "select count(distinct l_partkey), count(*) from lineitem group by 
l_orderkey") {
         df =>
           val executedPlan = getExecutedPlan(df)
           val regularAggCount = executedPlan.count {
@@ -1327,6 +1328,69 @@ class VeloxAggregateFunctionsFlushSuite extends 
VeloxAggregateFunctionsSuite {
     }
   }
 
+  test("flushable aggregate rule - vanilla distinct-partial keeps partial 
merge regular") {
+    withSQLConf(SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false") {
+      withTable("flush_distinct_src") {
+        spark
+          .range(0, 30000, 1, 10)
+          .selectExpr("id % 28000 as z")
+          .selectExpr("z % 3 as k", "z")
+          .write
+          .format("parquet")
+          .saveAsTable("flush_distinct_src")
+        // histogram_numeric is not offloaded, so the distinct-partial 
aggregate runs in vanilla
+        // Spark on top of the offloaded PartialMerge aggregate. The raw 
histogram depends on input
+        // order, so only the sum of its counts is compared.
+        val sql =
+          """
+            |select k, count(distinct z), count(*),
+            |  aggregate(histogram_numeric(distinct z, 5), 0D, (acc, e) -> acc 
+ e.y)
+            |from flush_distinct_src group by k
+            |""".stripMargin
+        // With useObjectHashAggregateExec=false the vanilla aggregates are 
SortAggregateExec, which
+        // reads its child through a local sort; with columnar sort off that 
sort is a SortExec.
+        Seq(
+          Seq(SQLConf.USE_OBJECT_HASH_AGG.key -> "true"),
+          Seq(SQLConf.USE_OBJECT_HASH_AGG.key -> "false"),
+          Seq(
+            SQLConf.USE_OBJECT_HASH_AGG.key -> "false",
+            GlutenConfig.COLUMNAR_SORT_ENABLED.key -> "false")
+        ).foreach {
+          confs =>
+            withSQLConf(confs: _*) {
+              runQueryAndCompare(sql) {
+                df =>
+                  val distinctPartialAggs = getExecutedPlan(df).collect {
+                    case agg: BaseAggregateExec
+                        if agg.aggregateExpressions.exists(
+                          e => e.isDistinct && e.mode == Partial) =>
+                      agg
+                  }
+                  assert(
+                    distinctPartialAggs.nonEmpty &&
+                      
distinctPartialAggs.forall(!_.isInstanceOf[HashAggregateExecTransformer]),
+                    "expected the distinct-partial aggregate to run in vanilla 
Spark"
+                  )
+                  val partialMergeAggs = 
distinctPartialAggs.flatMap(_.collectFirst {
+                    case agg: HashAggregateExecTransformer
+                        if agg.aggregateExpressions.forall(_.mode == 
PartialMerge) =>
+                      agg
+                  })
+                  assert(
+                    partialMergeAggs.nonEmpty,
+                    "expected an offloaded PartialMerge aggregate below the 
distinct-partial one")
+                  assert(
+                    
partialMergeAggs.forall(!_.isInstanceOf[FlushableHashAggregateExecTransformer]),
+                    "the PartialMerge aggregate below the distinct-partial 
aggregate must not be " +
+                      "flushable"
+                  )
+              }
+            }
+        }
+      }
+    }
+  }
+
   test("flushable aggregate rule - distinct feeding a join keeps final agg 
regular") {
     withSQLConf(
       SQLConf.ADAPTIVE_EXECUTION_ENABLED.key -> "false",


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to