zhidongqu-db commented on code in PR #59615:
URL: https://github.com/apache/spark/pull/59615#discussion_r4365108888


##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/vectorExpressions.scala:
##########
@@ -674,11 +676,74 @@ object VectorAvg {
         newCount: Long): Unit = {
       // Update running average: new_avg = old_avg + (new_value - old_avg) / 
new_count
       val invCount = 1.0f / newCount
+      if (dim < 4) {
+        updateRemainingElements(accBytes, inputArray, 0, dim, invCount)
+      } else {
+        updateElementsInBlocks(accBytes, inputArray, dim, invCount)
+      }
+    }
+
+    private def updateElementsInBlocks(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        dim: Int,
+        invCount: Float): Unit = {
       var i = 0
+      while (i <= dim - 4) {
+        val off = Platform.BYTE_ARRAY_OFFSET + i.toLong * 4
+        val oldAvg0 = Platform.getFloat(accBytes, off)
+        val delta0 = inputArray.getFloat(i) - oldAvg0
+        val oldAvg1 = Platform.getFloat(accBytes, off + 4)
+        val delta1 = inputArray.getFloat(i + 1) - oldAvg1
+        val oldAvg2 = Platform.getFloat(accBytes, off + 8)
+        val delta2 = inputArray.getFloat(i + 2) - oldAvg2
+        val oldAvg3 = Platform.getFloat(accBytes, off + 12)
+        val delta3 = inputArray.getFloat(i + 3) - oldAvg3
+        // A finite sum guarantees finite deltas; sum overflow only triggers a 
scalar fallback.
+        val deltaSum = (delta0 + delta1) + (delta2 + delta3)
+        if (!JFloat.isFinite(deltaSum)) {
+          updateRemainingElements(accBytes, inputArray, i, dim, invCount)
+          return

Review Comment:
   Returning here sends every remaining element of the vector through the 
scalar path. An Inf/NaN average never goes away, so one non-finite element near 
the start of a long vector (e.g. element 0 of a 1536-dim embedding) puts every 
later row of that group on the scalar path for the whole vector.
   
   The check runs before any write in the block, and each element depends only 
on its own old value and input. So it should be safe to handle just this block 
on the scalar path and keep going in blocks, with bit-identical results:
   
   ```scala
   if (!JFloat.isFinite(deltaSum)) {
     updateRemainingElements(accBytes, inputArray, i, i + 4, invCount) // `dim` 
param -> `end`
   } else {
     Platform.putFloat(accBytes, off, oldAvg0 + delta0 * invCount)
     // ... other three puts unchanged
   }
   i += 4
   ```
   
   In a rough standalone microbenchmark (Unsafe-backed arrays rather than 
`ArrayData`, so indicative only) with an Inf at element 0 and dim 1536, this 
was about 25-30% faster than the current code, and unchanged for all-finite 
input.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/vectorExpressions.scala:
##########
@@ -674,11 +676,74 @@ object VectorAvg {
         newCount: Long): Unit = {
       // Update running average: new_avg = old_avg + (new_value - old_avg) / 
new_count
       val invCount = 1.0f / newCount
+      if (dim < 4) {
+        updateRemainingElements(accBytes, inputArray, 0, dim, invCount)
+      } else {
+        updateElementsInBlocks(accBytes, inputArray, dim, invCount)
+      }
+    }
+
+    private def updateElementsInBlocks(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        dim: Int,
+        invCount: Float): Unit = {
       var i = 0
+      while (i <= dim - 4) {
+        val off = Platform.BYTE_ARRAY_OFFSET + i.toLong * 4
+        val oldAvg0 = Platform.getFloat(accBytes, off)
+        val delta0 = inputArray.getFloat(i) - oldAvg0
+        val oldAvg1 = Platform.getFloat(accBytes, off + 4)
+        val delta1 = inputArray.getFloat(i + 1) - oldAvg1
+        val oldAvg2 = Platform.getFloat(accBytes, off + 8)
+        val delta2 = inputArray.getFloat(i + 2) - oldAvg2
+        val oldAvg3 = Platform.getFloat(accBytes, off + 12)
+        val delta3 = inputArray.getFloat(i + 3) - oldAvg3
+        // A finite sum guarantees finite deltas; sum overflow only triggers a 
scalar fallback.
+        val deltaSum = (delta0 + delta1) + (delta2 + delta3)
+        if (!JFloat.isFinite(deltaSum)) {
+          updateRemainingElements(accBytes, inputArray, i, dim, invCount)
+          return
+        }
+        Platform.putFloat(accBytes, off, oldAvg0 + delta0 * invCount)
+        Platform.putFloat(accBytes, off + 4, oldAvg1 + delta1 * invCount)
+        Platform.putFloat(accBytes, off + 8, oldAvg2 + delta2 * invCount)
+        Platform.putFloat(accBytes, off + 12, oldAvg3 + delta3 * invCount)
+        i += 4
+      }
+      if (i < dim) {
+        updateRemainingElements(accBytes, inputArray, i, dim, invCount)
+      }
+    }
+
+    private def updateRemainingElements(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        start: Int,
+        dim: Int,
+        invCount: Float): Unit = {
+      var i = start
       while (i < dim) {
         val off = Platform.BYTE_ARRAY_OFFSET + i.toLong * 4
         val oldAvg = Platform.getFloat(accBytes, off)
-        Platform.putFloat(accBytes, off, oldAvg + (inputArray.getFloat(i) - 
oldAvg) * invCount)
+        val value = inputArray.getFloat(i)
+        val delta = value - oldAvg
+        val newAvg = if (JFloat.isFinite(delta)) {
+          oldAvg + delta * invCount
+        } else if (JFloat.isFinite(oldAvg) && JFloat.isFinite(value)) {
+          // Scale before subtracting when opposite finite values overflow the 
delta.
+          // For oldAvg = Float.MaxValue, value = -Float.MaxValue and invCount 
= 0.5f,
+          // the delta is -Infinity, but scaling first gives the correct 
average of 0.0f.
+          oldAvg + (value * invCount - oldAvg * invCount)

Review Comment:
   This isn't introduced by this PR, but it's related to the overflow claim in 
the migration note: the unchanged `mergeElements` can still overflow on finite 
inputs. `leftWeight` and `rightWeight` are rounded to float independently, so 
they can sum to more than 1. For example, merging two buffers whose average is 
`Float.MaxValue`, with counts 11 and 161043324, gives `leftWeight = 6.83046E-8` 
and `rightWeight = 1.0`, and `MaxValue * leftWeight + MaxValue * rightWeight` 
is `Infinity`. The update path keeps `MaxValue` for the same values.
   
   The Final/PartialMerge stage of a regular aggregation goes through `merge`, 
so partial-aggregation plans can still return Infinity for finite inputs. This 
could be fixed here (e.g. `leftAvg + (rightAvg - leftAvg) * rightWeight` with 
the same overflow handling as the update path) or tracked as a follow-up, with 
the migration note scoped accordingly.



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/vectorExpressions.scala:
##########
@@ -674,11 +676,74 @@ object VectorAvg {
         newCount: Long): Unit = {
       // Update running average: new_avg = old_avg + (new_value - old_avg) / 
new_count
       val invCount = 1.0f / newCount
+      if (dim < 4) {
+        updateRemainingElements(accBytes, inputArray, 0, dim, invCount)

Review Comment:
   nit: The comment still says `/ new_count`, but the code multiplies by 
`invCount`, and the formula now only describes the finite-delta case. Maybe 
something like: `// Running average: new_avg = old_avg + (new_value - old_avg) 
* invCount, with special handling for overflow and non-finite values in 
updateRemainingElements.`
   
   Also, `updateRemainingElements(accBytes, inputArray, 0, dim, invCount)` 
covers the whole vector here, so a name like `updateElementsScalar` might read 
better.



##########
docs/sql-migration-guide.md:
##########
@@ -28,6 +28,7 @@ license: |
 
 ## Upgrading from Spark SQL 4.3 to 4.4
 
+- Since Spark 4.4, `vector_avg` improves overflow and non-finite value 
handling for `ARRAY<FLOAT>` inputs. Extreme finite values that previously 
produced infinity may now produce a finite average. Infinity combined with 
finite values or infinity of the same sign now remains infinity instead of 
producing `NaN`. Opposite infinities still produce `NaN`, and `NaN` inputs 
continue to propagate.

Review Comment:
   A few things on this note:
   - Master is 5.0.0-SNAPSHOT, and there is an "Upgrading from Spark SQL 4.4 to 
5.0" section above this one. Please confirm the target release. If this is 
backported to branch-4.2/branch-4.x (both have the same bug), "Since Spark 4.4" 
would need to change as well.
   - "now remains infinity" reads as if the result was already infinity, but 
previously these produced `NaN`. Suggest: "Infinity combined with a finite 
value, or with infinity of the same sign, now produces infinity instead of 
`NaN`." Strictly speaking, this only changed when the infinity is already in 
the running average; a finite value followed by infinity already produced 
infinity.
   - "Extreme finite values that previously produced infinity may now produce a 
finite average" holds for the update path only. See the comment on 
`mergeElements` overflow on the scale-first branch.
   - This line currently conflicts with master, where a Variant bullet was 
added at the same spot. Keeping both bullets resolves it.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/VectorAggSuite.scala:
##########
@@ -405,6 +405,151 @@ class VectorAggSuite extends SparkFunSuite {
     assert(result(1) === 3.0f)
   }
 
+  test("VectorAvg - opposite extreme values do not overflow") {
+    val (agg, buffer, _) = createVectorAvg()
+    agg.update(buffer, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer))
+
+    val (_, buffer1, _) = createVectorAvg()
+    val (_, buffer2, _) = createVectorAvg()
+    agg.update(buffer1, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer2, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    agg.merge(buffer1, buffer2)
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer1))
+  }
+
+  test("VectorAvg - finite overflow preserves cumulative weights") {
+    val (agg, buffer, _) = createVectorAvg()
+    val first = Array(Float.MaxValue, -Float.MaxValue)
+    agg.update(buffer, createInputRow(first))
+    agg.update(buffer, createInputRow(first))
+    // The third update overflows the delta. An unweighted midpoint would 
incorrectly return zero.
+    agg.update(buffer, createInputRow(first.map(-_)))
+    val expected = (Float.MaxValue.toDouble / 3).toFloat
+    assertFloatArrayEquals(
+      evalAsFloatArray(agg, buffer),
+      Array(expected, -expected),
+      tolerance = math.ulp(expected) * 2)
+  }
+
+  test("VectorAvg - non-finite results do not depend on input order") {
+    val first =
+      Array(Float.PositiveInfinity, 1.0f, Float.NegativeInfinity, 
Float.PositiveInfinity)
+    val second = Array(1.0f, Float.PositiveInfinity, 1.0f, 
Float.NegativeInfinity)
+    def checkResult(result: Array[Float]): Unit = {
+      assertResult(Float.PositiveInfinity)(result(0))
+      assertResult(Float.PositiveInfinity)(result(1))
+      assertResult(Float.NegativeInfinity)(result(2))
+      assert(result(3).isNaN)
+    }
+
+    Seq(Seq(first, second), Seq(second, first)).foreach { vectors =>
+      val (agg, buffer, _) = createVectorAvg()
+      vectors.foreach(v => agg.update(buffer, createInputRow(v)))
+      checkResult(evalAsFloatArray(agg, buffer))
+      agg.update(buffer, createInputRow(Array(2.0f, 2.0f, 2.0f, 2.0f)))
+      checkResult(evalAsFloatArray(agg, buffer))
+
+      val (_, buffer1, _) = createVectorAvg()
+      val (_, buffer2, _) = createVectorAvg()
+      agg.update(buffer1, createInputRow(vectors.head))
+      agg.update(buffer2, createInputRow(vectors(1)))
+      agg.merge(buffer1, buffer2)
+      checkResult(evalAsFloatArray(agg, buffer1))
+    }
+  }
+
+  test("VectorAvg - finite elements around non-finite deltas") {
+    val first = Array(1.0f, Float.MaxValue, 3.0f, Float.PositiveInfinity, 5.0f,
+      Float.NegativeInfinity, 7.0f, Float.PositiveInfinity, 9.0f, Float.NaN, 
11.0f)
+    val second = Array(3.0f, -Float.MaxValue, 5.0f, 1.0f, 7.0f, 1.0f, 9.0f,
+      Float.NegativeInfinity, 11.0f, 2.0f, 13.0f)
+    val expected = Array(2.0f, 0.0f, 4.0f, Float.PositiveInfinity, 6.0f,
+      Float.NegativeInfinity, 8.0f, Float.NaN, 10.0f, Float.NaN, 12.0f)
+
+    Seq(false, true).foreach { useGenericArray =>
+      val (agg, buffer, _) = createVectorAvg()
+      Seq(first, second).foreach { values =>
+        val array = if (useGenericArray) {
+          new GenericArrayData(values)
+        } else {
+          ArrayData.toArrayData(values)
+        }
+        agg.update(buffer, InternalRow(array))
+      }
+      assertResult(expected.map(JFloat.floatToIntBits))(
+        evalAsFloatArray(agg, buffer).map(JFloat.floatToIntBits))
+    }
+  }
+
+  test("VectorAvg - finite updates preserve running-average rounding") {
+    Seq(false, true).foreach { includeInfinity =>
+      val (agg, buffer, _) = createVectorAvg()
+      val first = Array.tabulate(17)(i => if (i % 2 == 0) 1.0f else -1.0f)
+      val last = first.map(_ * 6)
+      val expected = Array.tabulate(17)(i => if (i % 2 == 0) 0x402aaaab else 
0xc02aaaab)
+      if (includeInfinity) {
+        first(8) = Float.PositiveInfinity
+        last(8) = 1.0f
+        expected(8) = 0x7f800000
+      }
+      agg.update(buffer, createInputRow(first))
+      agg.update(buffer, createInputRow(first))
+      agg.update(buffer, createInputRow(last))
+      assertResult(expected)(
+        evalAsFloatArray(agg, buffer).map(JFloat.floatToRawIntBits))

Review Comment:
   nit: This uses `floatToRawIntBits`, while the other new tests use 
`floatToIntBits`. The first new test uses `assertResult(Array(0.0f, 0.0f))`, 
i.e. `==`, which also accepts `-0.0f`. Using `floatToIntBits` throughout would 
be consistent.
   
   Also, the test name says "finite updates", but the `includeInfinity` variant 
covers an infinite element. Maybe "... rounding with and without a non-finite 
element".



##########
sql/catalyst/src/main/scala/org/apache/spark/sql/catalyst/expressions/vectorExpressions.scala:
##########
@@ -674,11 +676,74 @@ object VectorAvg {
         newCount: Long): Unit = {
       // Update running average: new_avg = old_avg + (new_value - old_avg) / 
new_count
       val invCount = 1.0f / newCount
+      if (dim < 4) {
+        updateRemainingElements(accBytes, inputArray, 0, dim, invCount)
+      } else {
+        updateElementsInBlocks(accBytes, inputArray, dim, invCount)
+      }
+    }
+
+    private def updateElementsInBlocks(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        dim: Int,
+        invCount: Float): Unit = {
       var i = 0
+      while (i <= dim - 4) {
+        val off = Platform.BYTE_ARRAY_OFFSET + i.toLong * 4
+        val oldAvg0 = Platform.getFloat(accBytes, off)
+        val delta0 = inputArray.getFloat(i) - oldAvg0
+        val oldAvg1 = Platform.getFloat(accBytes, off + 4)
+        val delta1 = inputArray.getFloat(i + 1) - oldAvg1
+        val oldAvg2 = Platform.getFloat(accBytes, off + 8)
+        val delta2 = inputArray.getFloat(i + 2) - oldAvg2
+        val oldAvg3 = Platform.getFloat(accBytes, off + 12)
+        val delta3 = inputArray.getFloat(i + 3) - oldAvg3
+        // A finite sum guarantees finite deltas; sum overflow only triggers a 
scalar fallback.
+        val deltaSum = (delta0 + delta1) + (delta2 + delta3)
+        if (!JFloat.isFinite(deltaSum)) {
+          updateRemainingElements(accBytes, inputArray, i, dim, invCount)
+          return
+        }
+        Platform.putFloat(accBytes, off, oldAvg0 + delta0 * invCount)
+        Platform.putFloat(accBytes, off + 4, oldAvg1 + delta1 * invCount)
+        Platform.putFloat(accBytes, off + 8, oldAvg2 + delta2 * invCount)
+        Platform.putFloat(accBytes, off + 12, oldAvg3 + delta3 * invCount)
+        i += 4
+      }
+      if (i < dim) {
+        updateRemainingElements(accBytes, inputArray, i, dim, invCount)
+      }
+    }
+
+    private def updateRemainingElements(
+        accBytes: Array[Byte],
+        inputArray: ArrayData,
+        start: Int,
+        dim: Int,
+        invCount: Float): Unit = {
+      var i = start
       while (i < dim) {
         val off = Platform.BYTE_ARRAY_OFFSET + i.toLong * 4
         val oldAvg = Platform.getFloat(accBytes, off)
-        Platform.putFloat(accBytes, off, oldAvg + (inputArray.getFloat(i) - 
oldAvg) * invCount)
+        val value = inputArray.getFloat(i)
+        val delta = value - oldAvg
+        val newAvg = if (JFloat.isFinite(delta)) {
+          oldAvg + delta * invCount
+        } else if (JFloat.isFinite(oldAvg) && JFloat.isFinite(value)) {
+          // Scale before subtracting when opposite finite values overflow the 
delta.
+          // For oldAvg = Float.MaxValue, value = -Float.MaxValue and invCount 
= 0.5f,
+          // the delta is -Infinity, but scaling first gives the correct 
average of 0.0f.
+          oldAvg + (value * invCount - oldAvg * invCount)
+        } else {
+          // IEEE addition handles these cases in either operand order:
+          // Inf + Inf -> Inf; -Inf + (-Inf) -> -Inf.
+          // Inf + (-Inf) -> NaN.
+          // NaN + any value -> NaN.
+          // Inf + finite -> Inf; -Inf + finite -> -Inf.

Review Comment:
   nit: "in either operand order" is a bit confusing because addition is 
commutative. I think the point is that the result doesn't depend on which 
operand is the running average. Maybe: "At least one operand is non-finite. 
Plain addition gives the IEEE result whichever operand is the running average: 
same-sign infinities keep their sign, opposite infinities and any NaN give NaN, 
and an infinity absorbs a finite value." 



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/VectorAggSuite.scala:
##########
@@ -405,6 +405,151 @@ class VectorAggSuite extends SparkFunSuite {
     assert(result(1) === 3.0f)
   }
 
+  test("VectorAvg - opposite extreme values do not overflow") {
+    val (agg, buffer, _) = createVectorAvg()
+    agg.update(buffer, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer))
+
+    val (_, buffer1, _) = createVectorAvg()
+    val (_, buffer2, _) = createVectorAvg()
+    agg.update(buffer1, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer2, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    agg.merge(buffer1, buffer2)
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer1))
+  }
+
+  test("VectorAvg - finite overflow preserves cumulative weights") {
+    val (agg, buffer, _) = createVectorAvg()
+    val first = Array(Float.MaxValue, -Float.MaxValue)
+    agg.update(buffer, createInputRow(first))
+    agg.update(buffer, createInputRow(first))
+    // The third update overflows the delta. An unweighted midpoint would 
incorrectly return zero.
+    agg.update(buffer, createInputRow(first.map(-_)))
+    val expected = (Float.MaxValue.toDouble / 3).toFloat
+    assertFloatArrayEquals(
+      evalAsFloatArray(agg, buffer),
+      Array(expected, -expected),
+      tolerance = math.ulp(expected) * 2)
+  }
+
+  test("VectorAvg - non-finite results do not depend on input order") {
+    val first =
+      Array(Float.PositiveInfinity, 1.0f, Float.NegativeInfinity, 
Float.PositiveInfinity)
+    val second = Array(1.0f, Float.PositiveInfinity, 1.0f, 
Float.NegativeInfinity)
+    def checkResult(result: Array[Float]): Unit = {
+      assertResult(Float.PositiveInfinity)(result(0))
+      assertResult(Float.PositiveInfinity)(result(1))
+      assertResult(Float.NegativeInfinity)(result(2))
+      assert(result(3).isNaN)
+    }
+
+    Seq(Seq(first, second), Seq(second, first)).foreach { vectors =>
+      val (agg, buffer, _) = createVectorAvg()
+      vectors.foreach(v => agg.update(buffer, createInputRow(v)))
+      checkResult(evalAsFloatArray(agg, buffer))
+      agg.update(buffer, createInputRow(Array(2.0f, 2.0f, 2.0f, 2.0f)))
+      checkResult(evalAsFloatArray(agg, buffer))
+
+      val (_, buffer1, _) = createVectorAvg()
+      val (_, buffer2, _) = createVectorAvg()
+      agg.update(buffer1, createInputRow(vectors.head))
+      agg.update(buffer2, createInputRow(vectors(1)))
+      agg.merge(buffer1, buffer2)
+      checkResult(evalAsFloatArray(agg, buffer1))
+    }
+  }
+
+  test("VectorAvg - finite elements around non-finite deltas") {
+    val first = Array(1.0f, Float.MaxValue, 3.0f, Float.PositiveInfinity, 5.0f,
+      Float.NegativeInfinity, 7.0f, Float.PositiveInfinity, 9.0f, Float.NaN, 
11.0f)
+    val second = Array(3.0f, -Float.MaxValue, 5.0f, 1.0f, 7.0f, 1.0f, 9.0f,
+      Float.NegativeInfinity, 11.0f, 2.0f, 13.0f)
+    val expected = Array(2.0f, 0.0f, 4.0f, Float.PositiveInfinity, 6.0f,
+      Float.NegativeInfinity, 8.0f, Float.NaN, 10.0f, Float.NaN, 12.0f)
+
+    Seq(false, true).foreach { useGenericArray =>
+      val (agg, buffer, _) = createVectorAvg()
+      Seq(first, second).foreach { values =>
+        val array = if (useGenericArray) {
+          new GenericArrayData(values)
+        } else {
+          ArrayData.toArrayData(values)
+        }

Review Comment:
   nit: This array-building block is duplicated in "non-finite deltas at 
different positions" (lines 532-536). A small helper such as `def 
toInputRow(values: Array[Float], useGenericArray: Boolean): InternalRow` would 
remove both copies.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/VectorAggSuite.scala:
##########
@@ -86,7 +86,7 @@ class VectorAggSuite extends SparkFunSuite {
   // Asserts that two floats are approximately equal within a tolerance.
   def assertFloatEquals(actual: Float, expected: Float, tolerance: Float = 
1e-5f): Unit = {
     assert(
-      java.lang.Float.isNaN(expected) && java.lang.Float.isNaN(actual) ||
+      JFloat.isNaN(expected) && JFloat.isNaN(actual) ||

Review Comment:
   nit (pre-existing): This assertion fails when `expected` and `actual` are 
the same infinity, because `math.abs(Inf - Inf)` is NaN. Nothing hits it today, 
but it's easy to trip over when adding Inf cases with this helper. Adding 
`actual == expected ||` would fix it.



##########
sql/catalyst/src/test/scala/org/apache/spark/sql/catalyst/expressions/aggregate/VectorAggSuite.scala:
##########
@@ -405,6 +405,151 @@ class VectorAggSuite extends SparkFunSuite {
     assert(result(1) === 3.0f)
   }
 
+  test("VectorAvg - opposite extreme values do not overflow") {
+    val (agg, buffer, _) = createVectorAvg()
+    agg.update(buffer, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer))
+
+    val (_, buffer1, _) = createVectorAvg()
+    val (_, buffer2, _) = createVectorAvg()
+    agg.update(buffer1, createInputRow(Array(Float.MaxValue, -Float.MaxValue)))
+    agg.update(buffer2, createInputRow(Array(-Float.MaxValue, Float.MaxValue)))
+    agg.merge(buffer1, buffer2)
+    assertResult(Array(0.0f, 0.0f))(evalAsFloatArray(agg, buffer1))
+  }
+
+  test("VectorAvg - finite overflow preserves cumulative weights") {

Review Comment:
   This is the only test where the scale-first branch runs with `invCount != 
0.5f`. At count 2, scale-first and plain `oldAvg + value` both give 0 for 
`MaxValue`/`-MaxValue`, so the other tests can't tell them apart. With dim 2, 
it also only runs on the `dim < 4` path. Consider also covering block-sized 
vectors, so the block fallback reaches the scale-first branch with a 1/3 
weight, e.g.:
   
   ```scala
   Seq(2, 4, 5, 8).foreach { dim =>
     val (agg, buffer, _) = createVectorAvg()
     val first = Array.tabulate(dim)(i => if (i % 2 == 0) Float.MaxValue else 
-Float.MaxValue)
     Seq(first, first, first.map(-_)).foreach(v => agg.update(buffer, 
createInputRow(v)))
     val expected = (Float.MaxValue.toDouble / 3).toFloat
     assertFloatArrayEquals(
       evalAsFloatArray(agg, buffer),
       Array.tabulate(dim)(i => if (i % 2 == 0) expected else -expected),
       tolerance = math.ulp(expected) * 2)
   }
   ```



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