Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 25 additions & 9 deletions spark/src/main/scala/org/apache/comet/serde/aggregates.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ package org.apache.comet.serde
import scala.jdk.CollectionConverters._

import org.apache.spark.sql.catalyst.expressions.{Attribute, Cast, Literal}
import org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, ApproximatePercentile, Average, BitAndAgg, BitOrAgg, BitXorAgg, BloomFilterAggregate, CentralMomentAgg, CollectList, CollectSet, Corr, Count, Covariance, CovPopulation, CovSample, First, HyperLogLogPlusPlus, Last, Max, Min, Percentile, StddevPop, StddevSamp, Sum, VariancePop, VarianceSamp}
import org.apache.spark.sql.catalyst.expressions.aggregate.{AggregateExpression, ApproximatePercentile, Average, BitAndAgg, BitOrAgg, BitXorAgg, BloomFilterAggregate, CentralMomentAgg, CollectList, CollectSet, Complete, Corr, Count, Covariance, CovPopulation, CovSample, First, HyperLogLogPlusPlus, Last, Max, Min, Partial, Percentile, StddevPop, StddevSamp, Sum, VariancePop, VarianceSamp}
import org.apache.spark.sql.catalyst.util.ArrayData
import org.apache.spark.sql.comet.CometExecUtils
import org.apache.spark.sql.internal.SQLConf
import org.apache.spark.sql.types.{BinaryType, BooleanType, ByteType, DataType, DateType, DecimalType, DoubleType, FloatType, IntegerType, LongType, NumericType, ShortType, StringType, TimestampNTZType, TimestampType}

import org.apache.comet.CometConf.COMET_EXEC_STRICT_FLOATING_POINT
import org.apache.comet.CometSparkSessionExtensions.{isSpark41Plus, withFallbackReason}
import org.apache.comet.CometSparkSessionExtensions.{isSpark41Plus, isSpark42Plus, withFallbackReason}
import org.apache.comet.expressions.CometEvalMode
import org.apache.comet.serde.QueryPlanSerde.{evalModeToProto, exprToProto, serializeDataType}
import org.apache.comet.shims.{CometCollectShim, CometEvalModeUtil}
Expand Down Expand Up @@ -830,11 +831,19 @@ object CometBloomFilterAggregate extends CometAggregateExpressionSerde[BloomFilt

object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] {

override def getIncompatibleReasons(): Seq[String] = Seq(
"Comet deduplicates NaN values (treats `NaN == NaN`) while Spark treats each NaN as a" +
s" distinct value. When `${COMET_EXEC_STRICT_FLOATING_POINT.key}=true`, `collect_set`" +
" on floating-point types falls back to Spark unless" +
" `spark.comet.expression.CollectSet.allowIncompatible=true` is set.")
override def getIncompatibleReasons(): Seq[String] = {
if (isSpark42Plus) {
Nil
} else {
Seq(
"Before Spark 4.2, Comet deduplicates NaN values (treats `NaN == NaN`) while Spark" +
" treats each NaN as a distinct value. Comet treats -0.0 and 0.0 as distinct while" +
" Spark treats them as equal." +
s" When `${COMET_EXEC_STRICT_FLOATING_POINT.key}=true`, `collect_set` on" +
" floating-point types falls back to Spark on those versions unless" +
" `spark.comet.expression.CollectSet.allowIncompatible=true` is set.")
}
}

override def getSupportLevel(expr: CollectSet): SupportLevel = {
// The native path always drops null inputs. Spark 4.2 added an `ignoreNulls` field to
Expand All @@ -844,12 +853,14 @@ object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] {
// analysis time, and CometCollectShim.ignoreNulls hardcodes true, making this a no-op.
if (!CometCollectShim.ignoreNulls(expr)) {
Unsupported(Some("collect_set with RESPECT NULLS (ignoreNulls = false) is not supported"))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that getIncompatibleReasons() returns Nil on 4.2+, there is nothing left to render on the Spark 4.2 compatibility page, so collect_set will show up there as fully supported with no caveats at all. But RESPECT NULLS still falls back on this line.

Could you add the matching getUnsupportedReasons(), gated the same way so it does not appear on the 3.4 through 4.1 pages where the field does not exist?

override def getUnsupportedReasons(): Seq[String] =
  if (isSpark42Plus) {
    Seq("`collect_set` with `RESPECT NULLS` falls back to Spark, since the native " +
      "implementation always drops null inputs.")
  } else {
    Nil
  }

} else if (isSpark42Plus) {
Compatible()
} else {
SupportLevel
.strictFloatingPointReason(
expr.children.head.dataType,
"collect_set on floating-point types " +
"(Comet deduplicates NaN values while Spark treats each NaN as distinct)")
"(Comet deduplicates NaN values and distinguishes -0.0 from 0.0, unlike Spark)")
.map(reason => Incompatible(Some(reason)))
.getOrElse(Compatible())
}
Expand All @@ -861,7 +872,12 @@ object CometCollectSet extends CometAggregateExpressionSerde[CollectSet] {
inputs: Seq[Attribute],
binding: Boolean,
conf: SQLConf): Option[ExprOuterClass.AggExpr] = {
val child = expr.children.head
val child = aggExpr.mode match {
case Partial | Complete if isSpark42Plus =>

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you say a bit about why the normalization is gated on isSpark42Plus?

Normalizing NaN is a no-op for Comet on every version, since the native set already keys on to_bits() and every NaN Spark can hand it is canonical. So dropping the gate would change exactly one thing on 3.4 through 4.1: -0.0 and 0.0 would collapse to a single entry, which is what Spark does on those versions. That is precisely the divergence your new getIncompatibleReasons() text documents. The NaN difference would remain and collect_set would stay Incompatible there, but users would be one divergence away instead of two.

The cost is that array-typed children would start paying the codegen dispatch from my other comment on the older versions as well, so I can see this going either way. If you would rather not take it on here, a tracking issue linked from this PR works. I would just like the reasoning captured somewhere rather than left implicit in the gate.

CometExecUtils.normalizeFloatingNumbers(expr.children.head)
Comment on lines +876 to +877

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One consequence of reusing normalize that I would like called out somewhere.

For any array-typed child it produces KnownFloatingPointNormalized(ArrayTransform(expr, lambda)), and CometArrayTransform is a CometCodegenDispatch rather than a native serde. So on 4.2+, collect_set over array<float>, array<double>, array<struct<v:double>> and struct<a:array<double>> now compiles Spark's doGenCode into a per-batch JVM kernel instead of running natively end to end. Before this PR those ran purely natively, just with the wrong answer, so this is the right trade. The scalar path is unaffected, since it maps onto the native NormalizeNaNAndZero. The plain struct path is unaffected too, since If, IsNull, CreateNamedStruct and GetStructField all have native serdes. It is specifically the array shapes.

Two follow-ups. Could the PR description mention this so the execution-mode change is on the record? And would you file an issue to teach the native NormalizeNaNAndZero in native/spark-expr/src/math_funcs/internal/normalize_nan.rs to recurse into List and Struct, so the serde can emit a single native node for the nested shapes?

Worth noting in that issue that with spark.comet.exec.scalaUDF.codegen.enabled=false the ArrayTransform serializes to None, which makes convert return None and the whole aggregate fall back to Spark. That degrades safely, which is good, but it is slower than it needs to be.

case _ =>
expr.children.head
}
val childExpr = exprToProto(child, inputs, binding)
val dataType = serializeDataType(expr.dataType)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import scala.reflect.ClassTag

import org.apache.spark.{Partition, SparkContext, TaskContext}
import org.apache.spark.rdd.RDD
import org.apache.spark.sql.catalyst.expressions.{Attribute, NamedExpression, SortOrder}
import org.apache.spark.sql.catalyst.expressions.{Attribute, Expression, NamedExpression, SortOrder}
import org.apache.spark.sql.catalyst.optimizer.NormalizeFloatingNumbers
import org.apache.spark.sql.comet.execution.arrow.CometArrowStream
import org.apache.spark.sql.comet.util.Utils
import org.apache.spark.sql.execution.SparkPlan
Expand All @@ -36,6 +37,10 @@ import org.apache.comet.serde.QueryPlanSerde.{exprToProto, serializeDataType}

object CometExecUtils {

/** Expose Spark's package-private recursive floating-point normalizer to Comet serde. */
def normalizeFloatingNumbers(expr: Expression): Expression =
NormalizeFloatingNumbers.normalize(expr)

/**
* Create an empty RDD with the given number of partitions.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
-- specific language governing permissions and limitations
-- under the License.

-- Config: spark.comet.exec.strictFloatingPoint=true
-- ConfigMatrix: parquet.enable.dictionary=false,true

-- ============================================================
Expand Down Expand Up @@ -147,42 +146,6 @@ SELECT grp, sort_array(collect_set(i)) FROM cs_src_intbig GROUP BY grp ORDER BY
query
SELECT grp, sort_array(collect_set(bi)) FROM cs_src_intbig GROUP BY grp ORDER BY grp

-- ============================================================
-- Float (with NULLs, NaN, Inf, -Inf, +0, -0)
-- Comet deduplicates NaN while Spark does not; with
-- strictFloatingPoint=true collect_set falls back to Spark.
-- ============================================================

statement
CREATE TABLE cs_src_float(v float, grp string) USING parquet

statement
INSERT INTO cs_src_float VALUES
(1.5, 'a'), (2.5, 'a'), (1.5, 'a'), (NULL, 'a'),
(CAST('NaN' AS FLOAT), 'b'), (CAST('NaN' AS FLOAT), 'b'), (1.0, 'b'),
(CAST('Infinity' AS FLOAT), 'c'), (CAST('-Infinity' AS FLOAT), 'c'), (CAST('Infinity' AS FLOAT), 'c'),
(CAST(0.0 AS FLOAT), 'd'), (CAST(-0.0 AS FLOAT), 'd'), (1.0, 'd'), (NULL, 'd')

query expect_fallback(not fully compatible with Spark)
SELECT grp, sort_array(collect_set(v)) FROM cs_src_float GROUP BY grp ORDER BY grp

-- ============================================================
-- Double (with NULLs, NaN, Inf, -Inf, +0, -0)
-- ============================================================

statement
CREATE TABLE cs_src_double(v double, grp string) USING parquet

statement
INSERT INTO cs_src_double VALUES
(1.1, 'a'), (2.2, 'a'), (1.1, 'a'), (NULL, 'a'),
(CAST('NaN' AS DOUBLE), 'b'), (CAST('NaN' AS DOUBLE), 'b'), (1.0, 'b'),
(CAST('Infinity' AS DOUBLE), 'c'), (CAST('-Infinity' AS DOUBLE), 'c'), (CAST('Infinity' AS DOUBLE), 'c'),
(0.0, 'd'), (-0.0, 'd'), (1.0, 'd'), (NULL, 'd')

query expect_fallback(not fully compatible with Spark)
SELECT grp, sort_array(collect_set(v)) FROM cs_src_double GROUP BY grp ORDER BY grp

-- ============================================================
-- String (with NULLs and empty string)
-- ============================================================
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
-- 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.

-- MaxSparkVersion: 4.1
-- Config: spark.comet.exec.strictFloatingPoint=true

statement
CREATE TABLE cs_fallback_float(v float, grp string) USING parquet

statement
INSERT INTO cs_fallback_float VALUES
(1.5, 'a'), (2.5, 'a'), (1.5, 'a'), (NULL, 'a'),
(CAST('NaN' AS FLOAT), 'b'), (CAST('NaN' AS FLOAT), 'b'), (1.0, 'b'),
(CAST('Infinity' AS FLOAT), 'c'), (CAST('-Infinity' AS FLOAT), 'c'),
(CAST('Infinity' AS FLOAT), 'c'),
(CAST(0.0 AS FLOAT), 'd'), (CAST('-0.0' AS FLOAT), 'd'), (1.0, 'd'), (NULL, 'd')

query expect_fallback(not fully compatible with Spark)
SELECT grp, sort_array(collect_set(v))
FROM cs_fallback_float GROUP BY grp ORDER BY grp

statement
CREATE TABLE cs_fallback_double(v double, grp string) USING parquet

statement
INSERT INTO cs_fallback_double VALUES
(1.1, 'a'), (2.2, 'a'), (1.1, 'a'), (NULL, 'a'),
(CAST('NaN' AS DOUBLE), 'b'), (CAST('NaN' AS DOUBLE), 'b'), (1.0, 'b'),
(CAST('Infinity' AS DOUBLE), 'c'), (CAST('-Infinity' AS DOUBLE), 'c'),
(CAST('Infinity' AS DOUBLE), 'c'),
(0.0, 'd'), (CAST('-0.0' AS DOUBLE), 'd'), (1.0, 'd'), (NULL, 'd')

query expect_fallback(not fully compatible with Spark)
SELECT grp, sort_array(collect_set(v))
FROM cs_fallback_double GROUP BY grp ORDER BY grp
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
-- 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.

-- MinSparkVersion: 4.2
-- Config: spark.comet.exec.strictFloatingPoint=true
-- ConfigMatrix: parquet.enable.dictionary=false,true

statement
CREATE TABLE cs_norm_scalar(grp string, f float, d double) USING parquet

statement
INSERT INTO cs_norm_scalar VALUES
('ordinary', CAST(1.5 AS FLOAT), CAST(1.1 AS DOUBLE)),
('ordinary', CAST(2.5 AS FLOAT), CAST(2.2 AS DOUBLE)),
('ordinary', CAST(1.5 AS FLOAT), CAST(1.1 AS DOUBLE)),
('ordinary', CAST(NULL AS FLOAT), CAST(NULL AS DOUBLE)),
('nan', CAST('NaN' AS FLOAT), CAST('NaN' AS DOUBLE)),
('nan', CAST('NaN' AS FLOAT), CAST('NaN' AS DOUBLE)),
('nan', CAST(1.0 AS FLOAT), CAST(1.0 AS DOUBLE)),
('infinity', CAST('Infinity' AS FLOAT), CAST('Infinity' AS DOUBLE)),
('infinity', CAST('-Infinity' AS FLOAT), CAST('-Infinity' AS DOUBLE)),
('infinity', CAST('Infinity' AS FLOAT), CAST('Infinity' AS DOUBLE)),
('zero', CAST('-0.0' AS FLOAT), CAST('-0.0' AS DOUBLE)),
('zero', CAST(0.0 AS FLOAT), CAST(0.0 AS DOUBLE)),
('zero', CAST(1.0 AS FLOAT), CAST(1.0 AS DOUBLE)),
('zero', CAST(NULL AS FLOAT), CAST(NULL AS DOUBLE))

-- Repartition so equal values must also deduplicate across partial buffers.
query
SELECT grp, sort_array(collect_set(f)), sort_array(collect_set(d))
FROM (SELECT /*+ REPARTITION(3) */ * FROM cs_norm_scalar)
GROUP BY grp
ORDER BY grp

-- Exercise the no-GROUP BY aggregate shape while merging partial buffers.
query
SELECT sort_array(collect_set(f)), sort_array(collect_set(d))
FROM (
SELECT /*+ REPARTITION(3) */ f, d
FROM cs_norm_scalar
WHERE grp IN ('nan', 'zero')
)

statement
CREATE TABLE cs_norm_nested(
grp string,
s struct<v:double>,
a array<float>,
deep_a array<struct<v:double>>,
deep_s struct<a:array<double>>) USING parquet

statement
INSERT INTO cs_norm_nested VALUES
('nan',
named_struct('v', CAST('NaN' AS DOUBLE)),
array(CAST('NaN' AS FLOAT)),
array(named_struct('v', CAST('NaN' AS DOUBLE))),
named_struct('a', array(CAST('NaN' AS DOUBLE)))),
('nan',
named_struct('v', CAST('NaN' AS DOUBLE)),
array(CAST('NaN' AS FLOAT)),
array(named_struct('v', CAST('NaN' AS DOUBLE))),
named_struct('a', array(CAST('NaN' AS DOUBLE)))),
('zero',
named_struct('v', CAST('-0.0' AS DOUBLE)),
array(CAST('-0.0' AS FLOAT)),
array(named_struct('v', CAST('-0.0' AS DOUBLE))),
named_struct('a', array(CAST('-0.0' AS DOUBLE)))),
('zero',
named_struct('v', CAST(0.0 AS DOUBLE)),
array(CAST(0.0 AS FLOAT)),
array(named_struct('v', CAST(0.0 AS DOUBLE))),
named_struct('a', array(CAST(0.0 AS DOUBLE)))),
('null',
CAST(NULL AS STRUCT<v:DOUBLE>),
CAST(NULL AS ARRAY<FLOAT>),
CAST(NULL AS ARRAY<STRUCT<v:DOUBLE>>),
CAST(NULL AS STRUCT<a:ARRAY<DOUBLE>>)),
('null',
named_struct('v', CAST(NULL AS DOUBLE)),
array(CAST(NULL AS FLOAT)),
array(CAST(NULL AS STRUCT<v:DOUBLE>)),
named_struct('a', array(CAST(NULL AS DOUBLE)))),
('null',
named_struct('v', CAST(NULL AS DOUBLE)),
array(CAST(NULL AS FLOAT)),
array(CAST(NULL AS STRUCT<v:DOUBLE>)),
named_struct('a', array(CAST(NULL AS DOUBLE))))

query
SELECT grp,
sort_array(collect_set(s)),
sort_array(collect_set(a)),
sort_array(collect_set(deep_a)),
sort_array(collect_set(deep_s))
FROM (SELECT /*+ REPARTITION(3) */ * FROM cs_norm_nested)
GROUP BY grp
ORDER BY grp
Loading