sunchao commented on code in PR #7155:
URL: https://github.com/apache/datafusion-comet/pull/7155#discussion_r4347757342
##########
spark/src/main/scala/org/apache/comet/rules/CometExecRule.scala:
##########
@@ -1325,37 +1411,69 @@ case class CometExecRule(session: SparkSession,
queryStagePrep: Boolean = false)
allExecs.get(op.getClass).exists(_.enabledConfig.forall(_.get(op.conf)))
/**
- * Tags each `SerializeFromObjectExec` whose output an operator above it can
stop reading early
- * with that operator, so `transform` does not convert it. Spark computes
the rows of a typed
- * Dataset operation one at a time, as the operator above reads them, while
the conversion fills
- * a whole Arrow batch first. Below a limit, a `mapPartitions` function such
as `_.take(1)`, or
- * code reading `Dataset.rdd`, the conversion would run the user function on
rows Spark never
- * reaches, and a function that throws on one of them would fail a query
that succeeds in Spark.
- * An operator that reads all of its input before it returns a row ends the
search, since Spark
- * computes every row below it anyway: an exchange, a sort, a hash
aggregate, or a top-k over
- * input that is not already sorted.
+ * Tags operators whose output a consumer can skip evaluating. Spark
computes rows as the
+ * consumer reads them, while native expressions and typed Dataset
conversion fill a whole Arrow
+ * batch first. Below a limit, a `mapPartitions` function such as
`_.take(1)`, or code reading
+ * `Dataset.rdd`, an assertion or user function could throw on rows Spark
never reaches. Joins
+ * can also skip an input, and generated filters, samples and joins can
defer projected fields
+ * until a row passes the condition. Conversely, native joins can skip input
Spark would
+ * evaluate. An operator that reads all of its input before it returns a row
ends the search,
+ * since Spark computes every row below it anyway: an exchange, a sort, a
hash aggregate, or a
+ * top-k over input that is not already sorted.
*
* Conversion is bottom-up, so this runs first. TreeNode tags survive the
child copies made
- * during transformUp, while an identity set would not.
+ * during transformUp, while an identity set would not. Retain established
tags when AQE visits
+ * a fragment without its limiting ancestor.
*/
- private def tagPartiallyReadTypedDatasetOutputs(plan: SparkPlan): Unit = {
- def visit(op: SparkPlan, partialReader: Option[String]): Unit = {
- val childReader = op match {
- case serialize: SerializeFromObjectExec =>
- partialReader.foreach(
- serialize.setTagValue(CometExecRule.TYPED_DATASET_PARTIAL_READER,
_))
- partialReader
- case _: CollectLimitExec | _: LocalLimitExec | _: GlobalLimitExec =>
Some("a limit")
+ private def tagPartiallyReadOutputs(plan: SparkPlan): Unit = {
+ import CometExecRule.PartialReader
+
+ def visit(op: SparkPlan, partialReader: Option[PartialReader]): Unit = {
+ // AQE can revisit a fragment without the ancestor that could skip whole
rows. A new
+ // field-only reader must not weaken that established context or its
child propagation.
+ val inheritedReader = op
+ .getTagValue(CometExecRule.PARTIAL_READER)
+ .filter(_.canSkipInput)
+ .orElse(partialReader)
+ // A repeated transform can already contain Comet limits and consumption
barriers.
+ val original = op match {
+ case comet: CometExec => comet.originalPlan
+ case other => other
+ }
+ val childReader = original match {
+ case _: CollectLimitExec | _: LocalLimitExec | _: GlobalLimitExec =>
+ Some(PartialReader("a limit"))
// A top-k reads only its first rows when its input is already sorted.
case topK: TakeOrderedAndProjectExec
if SortOrder.orderingSatisfies(topK.child.outputOrdering,
topK.sortOrder) =>
- Some("a limit")
- case _: MapPartitionsExec => Some("a mapPartitions function")
+ Some(PartialReader("a limit"))
+ case _: MapPartitionsExec => Some(PartialReader("a mapPartitions
function"))
+ // These consumers defer projected fields, but still read every input
object. Keep
+ // typed Dataset conversion eligible unless an ancestor can skip whole
input rows.
+ case _: FilterExec =>
+ inheritedReader.orElse(Some(PartialReader("a filter", canSkipInput =
false)))
+ case _: SampleExec =>
+ inheritedReader.orElse(Some(PartialReader("a sample", canSkipInput =
false)))
+ case _: BroadcastHashJoinExec | _: ShuffledHashJoinExec | _:
SortMergeJoinExec |
+ _: BroadcastNestedLoopJoinExec =>
+ Some(PartialReader("a join"))
case _: Exchange | _: SortExec | _: HashAggregateExec | _:
ObjectHashAggregateExec |
_: TakeOrderedAndProjectExec =>
None
- case _ => partialReader
+ case _ => inheritedReader
Review Comment:
[P2] Preserve deferred assertions across adjacent projections. `ProjectExec`
can conditionally consume a child field, but this catch-all leaves its child
without a partial-reader tag. With whole-stage codegen enabled, put
`AssertNotNull(IF(id = 0, NULL, id)) AS checked` and `rand(0) AS nonce` in a
lower projection, then select `IF(id = 0, 0L, checked), nonce` above it for `id
= 0`. The nondeterministic sibling prevents projection collapse. Spark returns
the row without evaluating `checked`, whereas the native lower projection
throws `NOT_NULL_ASSERT_VIOLATION`. Before this PR, the unsupported assertion
kept these projections in Spark. Please treat such projection consumers as
field-only partial readers, with corresponding own-reader handling, or
otherwise preserve lazy child evaluation, and add a two-projection regression.
Evidence: Registered `ann` as Catalyst `AssertNotNull` in Spark 4.1.3 and
executed: `SELECT IF(id=0,CAST(0 AS BIGINT),checked) AS result, nonce FROM
(SELECT id, ann(IF(id=0,CAST(NULL AS BIGINT),id)) AS checked, rand(0) AS nonce
FROM range(1))`. With AQE disabled and whole-stage codegen enabled, optimized
and executed plans retain both projections and return `[0,0.7604953758285915]`.
Disabling whole-stage codegen raises the assertion. A bounded native
reproduction using exact-head `AssertNotNull`, `IfExpr`, `RandExpr`, and two
DataFusion 55.2.0 `ProjectionExec`s raises `NOT_NULL_ASSERT_VIOLATION` for the
same single-row expressions. `CometProjectExec.convert` preserves the two
native projections, and `jni_api.rs` executes the constructed plan without
merging them. Enable `spark.comet.exec.range.enabled=true` to provide native
input for this SQL example. Reproduction sources and logs are under
`/tmp/comet7155-head-review/`. A complete Comet JVM query was not run.
--
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]