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

viirya pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/datafusion-comet.git


The following commit(s) were added to refs/heads/main by this push:
     new fd8e09e699 bench: measure nested types as native shuffle hash 
partitioning keys (#5788)
fd8e09e699 is described below

commit fd8e09e699e4e5e8a399616083d292dfb49cbb78
Author: Liang-Chi Hsieh <[email protected]>
AuthorDate: Thu Sep 10 04:58:13 2026 +0800

    bench: measure nested types as native shuffle hash partitioning keys (#5788)
    
    * bench: measure nested hash partitioning keys in native shuffle
    
    Nothing covered the shapes admitted by
    `spark.comet.shuffle.native.partitioning.hash.nested.enabled`.
    `shuffleArrayBenchmark` and `shuffleStructBenchmark` do repartition on a 
nested
    column but have no native-shuffle case, because the gate rejected nested 
keys when
    they were written, and `shuffleDeeplyNestedBenchmark` calls 
`repartition(n)` with
    no key, which is round robin rather than hash partitioning.
    
    Adds `shuffleNestedHashKeyBenchmark` with a native-shuffle case that 
enables the
    config, so it measures the native hashing path rather than a silent 
fallback. The
    shapes separate the two code paths in the native Murmur3 kernel: a list 
whose
    elements are primitives is vectorized, while a list whose elements are 
nested falls
    through to `hash_list_array!`, which slices a one-element array and 
re-enters
    `create_murmur3_hashes` for every element.
    
    Measured on an Apple M4 Max, local[5], 1M rows (best time, relative to 
Spark doing
    the whole shuffle):
    
      struct<int, string>              5 partitions   79ms ->  41ms   1.9x
      struct<int, string>            201 partitions  126ms ->  89ms   1.4x
      array<int>                       5 partitions   76ms ->  52ms   1.5x
      array<int>                     201 partitions  145ms -> 101ms   1.4x
      struct<array<int>, string>       5 partitions  100ms ->  68ms   1.5x
      array<struct<int, string>>       5 partitions  164ms -> 561ms   0.3x
      array<struct<int, string>>     201 partitions  250ms -> 647ms   0.4x
      struct<map<string, int>, int>    5 partitions  121ms ->  97ms   1.2x
      struct<map<string, int>, int>  201 partitions  160ms -> 155ms   1.0x
    
    The native case is not silently falling back to Spark's shuffle. Rerunning 
the
    `array<struct<..>>` case at 5 partitions separates the three Comet cases, 
at 761ms
    (Spark shuffle), 1059ms (JVM shuffle) and 2340ms (native shuffle), so the 
native path is
    distinct and is the slow one. Absolute times move with machine load, but 
the ordering and
    the set of shapes that win or lose reproduce.
    
    So `array<struct<..>>` is the one shape that loses, at 0.3-0.4x of leaving 
the shuffle to
    Spark, which is what the config defaults to off for. Every other shape 
measured is faster
    natively. That is the fallback path tracked in #5777, so these numbers are 
the before side
    of that comparison rather than a standing property of the kernel. A map 
nested in a struct
    is not affected, because the kernel specializes common scalar key/value 
pairs.
    
    Co-authored-by: Claude Code <[email protected]>
    
    * bench: verify nested shuffle admission and expand map coverage
    
    Skip native map-key cases before Spark 4.0 and verify the native exchange 
before timing. Compare against the JVM shuffle baseline, add variable-size maps 
in both input orders, and provide a nested-hash-only entry point.
    
    Validated Spark 4.1.3 with 18 groups / 72 benchmark cases and native plan 
checks. Spark 3.5.9 clean build and 14-case smoke run confirm native struct 
admission and explicit native map skips at 5 and 201 partitions.
    
    ---------
    
    Co-authored-by: Claude Code <[email protected]>
---
 .../sql/benchmark/CometShuffleBenchmark.scala      | 140 +++++++++++++++++++++
 1 file changed, 140 insertions(+)

diff --git 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometShuffleBenchmark.scala
 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometShuffleBenchmark.scala
index 9f5aaef306..64d9d0237a 100644
--- 
a/spark/src/test/scala/org/apache/spark/sql/benchmark/CometShuffleBenchmark.scala
+++ 
b/spark/src/test/scala/org/apache/spark/sql/benchmark/CometShuffleBenchmark.scala
@@ -27,17 +27,20 @@ import scala.util.Random
 import org.apache.spark.SparkConf
 import org.apache.spark.benchmark.Benchmark
 import org.apache.spark.sql.{Column, SaveMode, SparkSession}
+import org.apache.spark.sql.comet.execution.shuffle.{CometNativeShuffle, 
CometShuffleExchangeExec}
 import org.apache.spark.sql.internal.SQLConf
 import org.apache.spark.sql.types._
 
 import org.apache.comet.CometConf
 import org.apache.comet.CometSparkSessionExtensions
+import org.apache.comet.CometSparkSessionExtensions.isSpark40Plus
 import org.apache.comet.testing.{DataGenOptions, FuzzDataGenerator, 
SchemaGenOptions}
 
 // spotless:off
 /**
  * Benchmark to measure Comet shuffle performance. To run this benchmark:
  * `SPARK_GENERATE_BENCHMARK_FILES=1 make 
benchmark-org.apache.spark.sql.benchmark.CometShuffleBenchmark`
+ * Add `-- --nested-hash-only` to run just the nested hash key cases.
  * Results will be written to 
"spark/benchmarks/CometShuffleBenchmark-**results.txt".
  */
 // spotless:on
@@ -475,7 +478,142 @@ object CometShuffleBenchmark extends CometBenchmarkBase {
     }
   }
 
+  /**
+   * Nested hash partitioning keys, which native shuffle admits only when
+   * `spark.comet.shuffle.native.partitioning.hash.nested.enabled` is on.
+   *
+   * Primitive arrays use the typed element path; arrays of structs exercise 
recursive hashing.
+   * Map cases include a singleton control and variable cardinalities in both 
input key orders,
+   * covering normalization as well as the specialized scalar key/value hash 
loop.
+   *
+   * Compare native with Comet JVM shuffle to evaluate the default `auto` mode 
with nested hashing
+   * disabled. The all-Spark arm also changes scan and projection execution. 
These are end-to-end
+   * shuffle measurements, not isolated hash-kernel timings.
+   */
+  def shuffleNestedHashKeyBenchmark(
+      name: String,
+      keyExpr: String,
+      values: Int,
+      partitionNum: Int): Unit = {
+    val benchmark =
+      microBenchmark(s"Nested hash key: $name ($partitionNum Partition)", 
values)
+
+    withTempPath { dir =>
+      withTempTable("parquetV1Table") {
+        // `tbl`'s `value` spans the full Long range, so a direct cast to INT 
overflows under ANSI
+        // mode. `pmod` keeps the key varied (a constant would hash every row 
alike, which would
+        // not measure partitioning at all) while staying in range.
+        prepareTable(dir, spark.sql(s"SELECT CAST(pmod(value, 1000000) AS INT) 
AS c1 FROM $tbl"))
+        val query = s"SELECT $keyExpr AS k, c1 FROM parquetV1Table"
+
+        benchmark.addCase("Spark") { _ =>
+          withSQLConf(CometConf.COMET_ENABLED.key -> "false") {
+            spark.sql(query).repartition(partitionNum, Column("k")).noop()
+          }
+        }
+
+        benchmark.addCase("Comet (Spark Shuffle)") { _ =>
+          withSQLConf(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_SHUFFLE_ENABLED.key -> "false") {
+            spark.sql(query).repartition(partitionNum, Column("k")).noop()
+          }
+        }
+
+        benchmark.addCase("Comet (JVM Shuffle)") { _ =>
+          withSQLConf(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_SHUFFLE_ENABLED.key -> "true",
+            CometConf.COMET_SHUFFLE_MODE.key -> "jvm") {
+            spark.sql(query).repartition(partitionNum, Column("k")).noop()
+          }
+        }
+
+        def containsMap(dataType: DataType): Boolean = dataType match {
+          case _: MapType => true
+          case ArrayType(elementType, _) => containsMap(elementType)
+          case StructType(fields) => fields.exists(f => 
containsMap(f.dataType))
+          case _ => false
+        }
+
+        // Spark 3.x does not normalize map partitioning keys for native 
hashing.
+        if (containsMap(spark.sql(query).schema("k").dataType) && 
!isSpark40Plus) {
+          val message = s"Skipping native shuffle for $name: map keys require 
Spark 4.0+"
+          benchmark.out.println(message)
+        } else {
+          val nativeConfigs = Seq(
+            CometConf.COMET_ENABLED.key -> "true",
+            CometConf.COMET_EXEC_ENABLED.key -> "true",
+            CometConf.COMET_SHUFFLE_ENABLED.key -> "true",
+            CometConf.COMET_SHUFFLE_MODE.key -> "native",
+            
CometConf.COMET_SHUFFLE_NATIVE_HASH_PARTITIONING_NESTED_ENABLED.key -> "true")
+          // Check outside the timer: enabling the gate alone does not prove 
native admission.
+          withSQLConf(nativeConfigs: _*) {
+            val plan =
+              spark.sql(query).repartition(partitionNum, 
Column("k")).queryExecution.executedPlan
+            val nativeExchanges = collect(plan) {
+              case exchange: CometShuffleExchangeExec
+                  if exchange.shuffleType == CometNativeShuffle =>
+                exchange
+            }
+            require(
+              nativeExchanges.size == 1,
+              s"Expected one native shuffle for $name, found 
${nativeExchanges.size}:\n$plan")
+            benchmark.out.println(
+              s"Verified native exchange for $name ($partitionNum 
partitions):")
+            benchmark.out.println(plan.treeString)
+          }
+          benchmark.addCase("Comet (Native Shuffle)") { _ =>
+            withSQLConf(nativeConfigs: _*) {
+              spark.sql(query).repartition(partitionNum, Column("k")).noop()
+            }
+          }
+        }
+
+        benchmark.run()
+      }
+    }
+  }
+
+  private def runNestedHashKeyBenchmarks(): Unit = {
+    runBenchmarkWithTable("Nested hash partitioning key", 1024 * 1024 * 1) { v 
=>
+      val shapes = Seq(
+        "struct<int, string>" -> "named_struct('a', c1, 'b', CAST(c1 AS 
STRING))",
+        "array<int>" -> "ARRAY_REPEAT(c1, 10)",
+        "struct<array<int>, string>" ->
+          "named_struct('a', ARRAY_REPEAT(c1, 10), 'b', CAST(c1 AS STRING))",
+        "array<struct<int, string>>" ->
+          "ARRAY_REPEAT(named_struct('a', c1, 'b', CAST(c1 AS STRING)), 10)",
+        "struct<map<string, int>, int>" ->
+          "named_struct('m', MAP(CAST(c1 AS STRING), c1), 'i', c1)")
+      // Distinct keys, variable entry counts, and opposite input orders 
exercise map sorting.
+      val mapShapes = for {
+        maxEntries <- Seq(10, 50)
+        reverse <- Seq(false, true)
+      } yield {
+        val indices = s"sequence(1, 2 + pmod(c1, ${maxEntries - 1}))"
+        val ordered = if (reverse) s"reverse($indices)" else indices
+        val map = s"map_from_arrays(transform($ordered, x -> CAST(c1 + x AS 
STRING)), " +
+          s"transform($ordered, x -> c1 + x))"
+        val order = if (reverse) "reversed" else "forward"
+        s"struct<map<string, int>, int> (2-$maxEntries entries, $order)" ->
+          s"named_struct('m', $map, 'i', c1)"
+      }
+      (shapes ++ mapShapes).foreach { case (name, keyExpr) =>
+        Seq(5, manyPartitions).foreach { partitionNum =>
+          shuffleNestedHashKeyBenchmark(name, keyExpr, v, partitionNum)
+        }
+      }
+    }
+  }
+
   override def runCometBenchmark(mainArgs: Array[String]): Unit = {
+    if (mainArgs.contains("--nested-hash-only")) {
+      runNestedHashKeyBenchmarks()
+      return
+    }
 
     // nested type shuffle
     val numRows = 1000
@@ -492,6 +630,8 @@ object CometShuffleBenchmark extends CometBenchmarkBase {
       }
     }
 
+    runNestedHashKeyBenchmarks()
+
     runBenchmarkWithTable("Shuffle on array", 1024 * 1024 * 1) { v =>
       benchmarkTypes.foreach { dataType =>
         Seq(5, manyPartitions).foreach { partitionNum =>


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

Reply via email to