[GLUTEN-11524][VL] Fix ColumnarAQEShuffleReadExec - #12691
Conversation
There was a problem hiding this comment.
Pull request overview
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Fixes ColumnarAQEShuffleReadExec canonicalization by allowing its delegate to be a generic SparkPlan (including ShuffleExchange during canonicalization), while preserving AQE shuffle reader behavior for runtime execution.
Changes:
- Refactors
ColumnarAQEShuffleReadExecto storedelegate: SparkPlanand adjustschild/output/outputPartitioning/withNewChildInternalaccordingly. - Updates stage execution mode adjustment to construct
ColumnarAQEShuffleReadExecwith the new delegate type and tweaks resize-batches handling. - Updates Velox tests to accommodate the new delegate representation.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
| gluten-substrait/src/main/scala/org/apache/spark/sql/execution/adaptive/ColumnarAQEShuffleReadExec.scala | Refactors delegate handling to avoid canonicalization failures and updates plan plumbing accordingly. |
| backends-velox/src/test/scala/org/apache/gluten/execution/StageExecutionModeSuite.scala | Updates assertions to derive shuffle stages via the new delegate: SparkPlan shape. |
| backends-velox/src/main/scala/org/apache/spark/sql/execution/AdjustStageExecutionMode.scala | Constructs ColumnarAQEShuffleReadExec with the new delegate type and narrows a resize-batches match. |
| backends-velox/src/main/scala/org/apache/gluten/extension/AppendBatchResizeForShuffleInputAndOutput.scala | Enables shuffle-output resize when CUDF columnar mode is on (with TODO note). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
|
Run Gluten Clickhouse CI on x86 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (3)
gluten-substrait/src/main/scala/org/apache/spark/sql/execution/adaptive/ColumnarAQEShuffleReadExec.scala:53
outputPartitioningnow comes fromdelegate.outputPartitioning. Whendelegateis aShuffleQueryStageExec, this can differ from the previously usedAQEShuffleReadExec(...).outputPartitioning(which is derived frompartitionSpecs). That is a semantic change and can affect downstream planning decisions that rely on AQE shuffle-reader partitioning. Consider restoring the previous behavior by derivingoutputPartitioningfromaqeReader.outputPartitioning(and similarly keep outputs consistent with the AQE reader semantics when the delegate is a stage).
override def output: Seq[Attribute] = delegate.output
override lazy val outputPartitioning: Partitioning = delegate.outputPartitioning
gluten-substrait/src/main/scala/org/apache/spark/sql/execution/adaptive/ColumnarAQEShuffleReadExec.scala:85
- The scaladoc explicitly allows
delegateto beShuffleExchangeduring canonicalization, butmetricseagerly depends onaqeReader, which will throw for any non-AQEShuffleReadExec/ShuffleQueryStageExecdelegate. This can reintroduce canonicalization-time failures if Spark touchesmetrics(e.g., for explain/UI/debug) on a canonicalized plan. To make canonicalization robust, consider makingmetrics(and any other accesses that can happen during canonicalization) safe for theShuffleExchangecase (e.g., return an empty metric map or delegate metrics when available), or tighten invariants so a non-AQE/stage delegate cannot reach paths where metrics are evaluated.
private lazy val aqeReader: AQEShuffleReadExec = {
delegate match {
case a: AQEShuffleReadExec => a
case s: ShuffleQueryStageExec =>
// Wrap ShuffleQueryStageExe with dummy PartitionSpecs by creating CoalescedPartitionSpec
// for each partition.
val partitionSpecs =
Array.tabulate(s.shuffle.numPartitions)(i => CoalescedPartitionSpec(i, i + 1))
AQEShuffleReadExec(s, partitionSpecs)
case _ =>
// The child is Exchange during canonicalization.
throw new IllegalStateException(
s"Cannot get aqeReader from delegate class ${delegate.getClass.getSimpleName}.")
}
}
@transient override lazy val metrics: Map[String, SQLMetric] = aqeReader.metrics
backends-velox/src/main/scala/org/apache/spark/sql/execution/AdjustStageExecutionMode.scala:101
- This changes behavior from adjusting
VeloxResizeBatchesExecunconditionally to only adjusting it when its child is a shuffle stage / AQE shuffle read. IfadjustExecutionModeis expected to consistently propagatestageExecutionModethrough the plan, this introduces a special case where the resize node’s stage mode may remain stale while its subtree is updated via the default case. Consider either (a) keeping the unconditional handling, or (b) adding an explicit else branch that still updates theVeloxResizeBatchesExec’s stage mode while recursing, to avoid mixed or inconsistent stage-mode annotations.
case r: VeloxResizeBatchesExec
if r.child.isInstanceOf[ShuffleQueryStageExec] ||
r.child.isInstanceOf[AQEShuffleReadExec] =>
VeloxResizeBatchesExec(
adjustExecutionMode(r.child, stageExecutionMode),
Some(stageExecutionMode))
|
Run Gluten Clickhouse CI on x86 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.
Suppressed comments (5)
backends-velox/src/main/scala/org/apache/spark/sql/execution/AdjustStageExecutionMode.scala:101
- This narrows the previous
VeloxResizeBatchesExecrewrite to only two child types. AnyVeloxResizeBatchesExecwrapping other plans will now fall through to the default case and won’t getSome(stageExecutionMode)set anymore, which is a functional behavior change. If the intent is still to propagate stage execution mode throughVeloxResizeBatchesExecgenerally, keep the rewrite unconditional (as before) or add a separate case to preserve/set the stage mode for other children.
case r: VeloxResizeBatchesExec
if r.child.isInstanceOf[ShuffleQueryStageExec] ||
r.child.isInstanceOf[AQEShuffleReadExec] =>
VeloxResizeBatchesExec(
adjustExecutionMode(r.child, stageExecutionMode),
Some(stageExecutionMode))
gluten-substrait/src/main/scala/org/apache/spark/sql/execution/adaptive/ColumnarAQEShuffleReadExec.scala:81
- The exception message is hard to debug in production because it only prints the simple class name. Consider including
delegate.nodeNameand/or the full class name, and explicitly listing the expected delegate types (e.g.,AQEShuffleReadExecorShuffleQueryStageExec) plus the canonicalization context. That makes failures actionable if this gets triggered outside canonicalization.
// The child is Exchange during canonicalization.
throw new IllegalStateException(
s"Cannot get aqeReader from delegate class ${delegate.getClass.getSimpleName}.")
gluten-substrait/src/main/scala/org/apache/spark/sql/execution/adaptive/ColumnarAQEShuffleReadExec.scala:34
- The Scaladoc parameter description is grammatically incomplete and also refers to
ShuffleExchangewhile the concrete Spark plan type appears to beShuffleExchangeExec(per usage/tests). Please rewrite this line to be precise about the expected delegate plan types and match the actual class naming used in Spark.
* AQEShuffleReadExec or ShuffleQueryStageExec. Or ShuffleExchange during canonicalization.
backends-velox/src/test/scala/org/apache/gluten/execution/StageExecutionModeSuite.scala:126
- This error message is misleading (the match is on
_.delegate, not the child) and drops important context for diagnosing test failures. Consider changing it to mentiondelegateand include the actual class (and/ornodeName) of the unexpected plan.
case _ =>
throw new IllegalArgumentException("Unexpected child of ColumnarAQEShuffleReadExec")
backends-velox/src/main/scala/org/apache/gluten/extension/AppendBatchResizeForShuffleInputAndOutput.scala:36
resizeBatchesShuffleOutputEnablednow represents 'explicit shuffle output resize enabled OR CUDF enabled', which is broader than the name implies. To keep intent clear, consider renaming the variable to reflect the combined condition or split into two booleans (one for the config, one for CUDF) and combine them explicitly at the call site.
// TODO: Move cudf resize batches into shuffle reader.
val resizeBatchesShuffleOutputEnabled =
VeloxConfig.get.veloxResizeBatchesShuffleOutput || VeloxConfig.get.enableColumnarCudf
|
Run Gluten Clickhouse CI on x86 |
|
Run Gluten Clickhouse CI on x86 |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (2)
backends-velox/src/main/scala/org/apache/spark/sql/execution/AdjustStageExecutionMode.scala:103
- This guard ignores the new wrapper (
ColumnarAQEShuffleReadExec). In plans whereVeloxResizeBatchesExecsits on top ofColumnarAQEShuffleReadExec(as asserted in the new test), the case won’t match andVeloxResizeBatchesExecwon’t getSome(stageExecutionMode), potentially leaving the resize node in an inconsistent mode configuration. Includer.child.isInstanceOf[ColumnarAQEShuffleReadExec]in the guard (or remove the guard and handle non-shuffle cases explicitly) so shuffle-resize nodes are consistently annotated.
case r: VeloxResizeBatchesExec
// TODO: This should be removed after merging resize into native shuffle read.
// Only change the execution mode for shuffle reader.
if r.child.isInstanceOf[ShuffleQueryStageExec] ||
r.child.isInstanceOf[AQEShuffleReadExec] =>
VeloxResizeBatchesExec(
adjustExecutionMode(r.child, stageExecutionMode),
Some(stageExecutionMode))
backends-velox/src/test/scala/org/apache/gluten/execution/StageExecutionModeSuite.scala:114
- This assertion is brittle because canonicalization behavior and exact exchange class (
ShuffleExchangeExecvs a different exchange implementation) can vary across Spark versions and planner paths. If the intent is to ensure canonicalization removes query-stages/readers and leaves an exchange-like node, consider asserting on a more stable abstraction (e.g., Spark’s exchange base type) or checkingnodeName/isInstanceOf[Exchange]to reduce version-specific failures.
val canonicalized = reader.canonicalized
// canonicalized plan before applying query stage optimizer rules.
assert(canonicalized.children.forall(_.isInstanceOf[ShuffleExchangeExec]))
|
Run Gluten Clickhouse CI on x86 |
ColumnarAQEShuffleReadExecis a wrapper forShuffleQueryStageExecor a replacement forAQEShuffleReadExec. Its execution mode determines whether the shuffle reader outputs VeloxRowVectorsfor the CPU pipeline orCudfVectorsfor the GPU pipeline.However, during canonicalization, the
delegatefield can be set toShuffleExchange, which currently causes a failure. This PR fixes the issue and adds UT.This PR also fixes adding
ResizeBatchesExecbefore shuffle write when cudf is enabled.Related issue: #11524