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

jackylee-ch 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 2a8bc38eff [GLUTEN-12748][CORE] Compare transition node name hash 
codes instead of subtracting (#12750)
2a8bc38eff is described below

commit 2a8bc38eff94895d2c36c5de376771081c22ef98
Author: YangJie <[email protected]>
AuthorDate: Thu Aug 13 14:22:37 2026 +0800

    [GLUTEN-12748][CORE] Compare transition node name hash codes instead of 
subtracting (#12750)
---
 .../columnar/transition/TransitionGraph.scala      |  9 ++-
 .../transition/TransitionCostModelSuite.scala      | 87 ++++++++++++++++++++++
 2 files changed, 94 insertions(+), 2 deletions(-)

diff --git 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/transition/TransitionGraph.scala
 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/transition/TransitionGraph.scala
index 7dece0b3f5..77f4d13c7f 100644
--- 
a/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/transition/TransitionGraph.scala
+++ 
b/gluten-core/src/main/scala/org/apache/gluten/extension/columnar/transition/TransitionGraph.scala
@@ -130,8 +130,13 @@ object TransitionGraph {
             if (diff != 0) {
               diff
             } else {
-              // To make the output order stable.
-              nodeNames1.mkString.hashCode - nodeNames2.mkString.hashCode
+              // Break the tie on the node names, so that as long as the names 
distinguish the two
+              // paths the same one wins on every JVM run. Integer.compare 
rather than a
+              // subtraction: hash codes far enough apart overflow Int, and a 
wrapped sign hands the
+              // tie to the other path of the same cost. Two distinct name 
sequences can still share
+              // a hash code, and mkString joins without a separator, so a tie 
remains possible; the
+              // winner then comes down to map iteration order, as it did 
before.
+              Integer.compare(nodeNames1.mkString.hashCode, 
nodeNames2.mkString.hashCode)
             }
         }
     }
diff --git 
a/gluten-core/src/test/scala/org/apache/gluten/extension/columnar/transition/TransitionCostModelSuite.scala
 
b/gluten-core/src/test/scala/org/apache/gluten/extension/columnar/transition/TransitionCostModelSuite.scala
new file mode 100644
index 0000000000..eefd862124
--- /dev/null
+++ 
b/gluten-core/src/test/scala/org/apache/gluten/extension/columnar/transition/TransitionCostModelSuite.scala
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *    http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.gluten.extension.columnar.transition
+
+import org.apache.gluten.extension.columnar.cost.LongCostModel
+
+import org.apache.spark.rdd.RDD
+import org.apache.spark.sql.catalyst.InternalRow
+import org.apache.spark.sql.catalyst.expressions.Attribute
+import org.apache.spark.sql.execution.{SparkPlan, UnaryExecNode}
+
+import org.scalatest.funsuite.AnyFunSuite
+
+class TransitionCostModelSuite extends AnyFunSuite {
+  import TransitionCostModelSuite._
+
+  test("costComparator does not overflow on distant node name hash codes") {
+    // The tiebreaker used to subtract the two hash codes. 
"RowToVeloxColumnar" hashes to
+    // 2056048280 and "CHColumnarToCarrierRow" to -2037667767, so the true 
difference is
+    // 4093716047, past Int.MaxValue, and the subtraction wrapped to 
-201251249. That inverted
+    // sign hands an equal-cost tie to the wrong path in 
FloydWarshallGraph#build. 46 of the 110
+    // ordered pairs of Gluten transition node names invert this way.
+    val comparator = costModel.costComparator()
+    val higherHash = costOf(plan => RowToVeloxColumnar(plan))
+    val lowerHash = costOf(plan => CHColumnarToCarrierRow(plan))
+
+    assert("RowToVeloxColumnar".hashCode - "CHColumnarToCarrierRow".hashCode < 
0)
+    assert(comparator.compare(higherHash, lowerHash) > 0)
+    assert(comparator.compare(lowerHash, higherHash) < 0)
+  }
+
+  test("costComparator prefers the cheaper cost before consulting node names") 
{
+    // Aaaa sorts and hashes below Zzzz, so a comparator that ignored the base 
cost would order
+    // these the other way around.
+    val comparator = costModel.costComparator()
+    val cheap = costOf(plan => Zzzz(plan))
+    val expensive = costOf(plan => Aaaa(Aaaa(plan)))
+    assert(comparator.compare(cheap, expensive) < 0)
+    assert(comparator.compare(expensive, cheap) > 0)
+  }
+
+  test("costComparator treats the same transition as equal to itself") {
+    val comparator = costModel.costComparator()
+    assert(comparator.compare(costOf(plan => Zzzz(plan)), costOf(plan => 
Zzzz(plan))) == 0)
+  }
+}
+
+object TransitionCostModelSuite {
+  private def costModel: FloydWarshallGraph.CostModel[Transition] =
+    TransitionGraph.asTransitionCostModel(TestCostModel)
+
+  /** The cost of a transition, whose node names are those of the nodes it 
wraps the input in. */
+  private def costOf(f: SparkPlan => SparkPlan): FloydWarshallGraph.Cost =
+    costModel.costOf((plan: SparkPlan) => f(plan))
+
+  /** Charges one per node, so a deeper plan costs more. */
+  private object TestCostModel extends LongCostModel {
+    override def selfLongCostOf(node: SparkPlan): Long = 1L
+  }
+
+  /** Node names below are the class names: Spark only strips a trailing 
"Exec". */
+  private case class RowToVeloxColumnar(child: SparkPlan) extends Wrapper
+  private case class CHColumnarToCarrierRow(child: SparkPlan) extends Wrapper
+  private case class Zzzz(child: SparkPlan) extends Wrapper
+  private case class Aaaa(child: SparkPlan) extends Wrapper
+
+  private trait Wrapper extends UnaryExecNode {
+    override protected def doExecute(): RDD[InternalRow] = throw new 
UnsupportedOperationException()
+    override def output: Seq[Attribute] = Nil
+    override protected def withNewChildInternal(newChild: SparkPlan): 
SparkPlan =
+      throw new UnsupportedOperationException()
+  }
+}


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

Reply via email to