Skip to content

[CORE][VL] Add columnar EmptyRelationExec offload to Velox backend - #12766

Open
minni31 wants to merge 5 commits into
apache:mainfrom
minni31:oss/empty-relation-exec
Open

[CORE][VL] Add columnar EmptyRelationExec offload to Velox backend#12766
minni31 wants to merge 5 commits into
apache:mainfrom
minni31:oss/empty-relation-exec

Conversation

@minni31

@minni31 minni31 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What changes are proposed in this pull request?

This PR offloads EmptyRelationExec to the Velox backend so that empty relations are executed columnarly instead of forcing a fallback to vanilla row execution.

EmptyRelationExec is a leaf node that AQE's Propagate Empty Relations optimization creates on Spark 4.0+ when a materialized query stage turns out to be empty at runtime (e.g. an INTERSECT or join whose input is empty only at runtime, or an aggregation over an empty runtime stage). A statically-empty predicate such as WHERE 1 = 0 does not reach this path — the logical optimizer folds it to an empty LocalRelation (a LocalTableScan) before physical planning. Because its default supportsColumnar is false, Gluten currently wraps it in ColumnarToRow / RowToColumnar transitions even though it returns zero rows. This PR adds EmptyRelationExecTransformer, a columnar leaf that returns an empty RDD[ColumnarBatch], eliminating those transitions.

EmptyRelationExec only exists on Spark 4.0+ (SPARK-47217), so the node is never referenced from version-agnostic modules. Detection is routed through a new SparkShims.isEmptyRelationExec, which defaults to false and is overridden only in the Spark 4.0 and 4.1 shims. The shared OffloadOthers rule and the SparkPlanExecApi trait therefore continue to compile unchanged against Spark 3.3–3.5.

The offload is gated by a new config spark.gluten.sql.columnar.emptyRelation (default true). The Velox backend implements isSupportEmptyRelationExec; other backends inherit the trait default and keep vanilla execution.

How was this patch tested?

  • New VeloxEmptyRelationSuite: empty-result correctness across various schemas, empty UNION ALL, AQE propagation through joins/aggregations, and side-by-side parity with vanilla Spark (asserted on all supported Spark versions). Plan-shape assertions (transformer present, no residual EmptyRelationExec) and the config-disabled negative case are gated to Spark 4.0+, where the node exists.
  • Re-enabled the upstream SPARK-35585 AQE test on Spark 4.0/4.1 with a Gluten-aware assertion that accepts either EmptyRelationExec or EmptyRelationExecTransformer.

Was this patch authored or co-authored using generative AI tooling?

Generated-by: GitHub Copilot (Claude Opus 4.8)

Offload EmptyRelationExec (a leaf node AQE's Propagate Empty Relations
optimization creates on Spark 4.0+ when it proves a subtree produces no
output) to a native EmptyRelationExecTransformer. The transformer produces
an empty RDD[ColumnarBatch] so surrounding columnar operators no longer
need to be wrapped in ColumnarToRow / RowToColumnar transitions around the
empty relation.

EmptyRelationExec only exists on Spark 4.0+ (SPARK-47217), so the node is
never referenced from version-agnostic modules: detection goes through the
new SparkShims.isEmptyRelationExec, overridden only in the Spark 4.0 and 4.1
shims and defaulting to false elsewhere. The shared OffloadOthers rule and
the SparkPlanExecApi trait therefore compile unchanged against Spark 3.3-3.5.

The offload is gated by spark.gluten.sql.columnar.emptyRelation (default
true). The Velox backend implements isSupportEmptyRelationExec; other
backends inherit the trait default and keep vanilla execution.

Adds VeloxEmptyRelationSuite (empty-result correctness on all supported
Spark versions, plus plan-shape and config-gate assertions gated to Spark
4.0+) and re-enables the upstream SPARK-35585 AQE test on Spark 4.0/4.1 with
a Gluten-aware assertion that accepts either EmptyRelationExec or the
transformer.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 459758d8-f68d-4c1f-8069-b4df4eca2175
Copilot AI lite review requested due to automatic review settings August 14, 2026 04:21
@github-actions github-actions Bot added CORE works for Gluten Core VELOX DOCS labels Aug 14, 2026
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a Spark 4.0+ shim-based detection path and a new columnar leaf transformer for EmptyRelationExec, allowing Gluten (Velox backend) to avoid unnecessary ColumnarToRow / RowToColumnar transitions around proven-empty subtrees produced by AQE’s “Propagate Empty Relations”.

Changes:

  • Add SparkShims.isEmptyRelationExec (default false) with Spark 4.0/4.1 overrides, and wire EmptyRelationExecTransformer into OffloadOthers.
  • Introduce EmptyRelationExecTransformer (empty RDD[ColumnarBatch]) and a new dynamic config spark.gluten.sql.columnar.emptyRelation (default true).
  • Add/adjust Spark 4.0/4.1 Velox tests to validate plan shape + correctness and handle SPARK-35585 expectations.

Reviewed changes

Copilot reviewed 14 out of 14 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
shims/spark41/src/main/scala/org/apache/gluten/sql/shims/spark41/Spark41Shims.scala Adds Spark 4.1 shim detection for EmptyRelationExec.
shims/spark40/src/main/scala/org/apache/gluten/sql/shims/spark40/Spark40Shims.scala Adds Spark 4.0 shim detection for EmptyRelationExec.
shims/common/src/main/scala/org/apache/gluten/sql/shims/SparkShims.scala Adds version-agnostic isEmptyRelationExec API with safe default.
gluten-substrait/src/main/scala/org/apache/spark/sql/execution/EmptyRelationExecTransformer.scala New columnar leaf transformer producing an empty RDD[ColumnarBatch].
gluten-substrait/src/main/scala/org/apache/gluten/extension/columnar/offload/OffloadSingleNodeRules.scala Adds EmptyRelationExec → transformer offload case guarded by shims + backend support.
gluten-substrait/src/main/scala/org/apache/gluten/config/GlutenConfig.scala Adds new dynamic config accessor + entry for empty relation offload.
gluten-substrait/src/main/scala/org/apache/gluten/backendsapi/SparkPlanExecApi.scala Adds backend API hooks to support and create the transformer.
backends-velox/src/main/scala/org/apache/gluten/backendsapi/velox/VeloxSparkPlanExecApi.scala Implements support gate + transformer construction for Velox backend.
docs/Configuration.md Documents the new spark.gluten.sql.columnar.emptyRelation config.
backends-velox/src/test/scala/org/apache/gluten/execution/VeloxEmptyRelationSuite.scala New suite validating correctness + Spark 4.0+ plan-shape behavior and config disablement.
gluten-ut/spark41/src/test/scala/org/apache/spark/sql/execution/adaptive/velox/VeloxAdaptiveQueryExecSuite.scala Adds Gluten-specific SPARK-35585 assertion accepting either raw or transformer node.
gluten-ut/spark41/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala Adjusts included AQE test selection to account for new Gluten-specific coverage.
gluten-ut/spark40/src/test/scala/org/apache/spark/sql/execution/adaptive/velox/VeloxAdaptiveQueryExecSuite.scala Same SPARK-35585 Gluten-specific assertion for Spark 4.0 test module.
gluten-ut/spark40/src/test/scala/org/apache/gluten/utils/velox/VeloxTestSettings.scala Adjusts included AQE test selection for Spark 4.0 module.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread docs/Configuration.md Outdated
… docs

The EmptyRelationExecTransformer is a JVM-side columnar leaf that returns an empty RDD[ColumnarBatch]; it does not invoke native execution. Reword the config doc string and scaladocs accordingly, and regenerate the Configuration.md row so it matches the config doc() string (fixes AllGlutenConfiguration check).
Copilot AI review requested due to automatic review settings August 14, 2026 06:05
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

gluten-substrait/src/main/scala/org/apache/spark/sql/execution/EmptyRelationExecTransformer.scala:52

  • withNewChildrenInternal currently ignores newChildren unconditionally. For a leaf node, it’s safer to enforce the invariant that no children are provided (otherwise a caller attempting a tree rewrite could silently fail to apply updates). Add a require(newChildren.isEmpty, ...) guard (or otherwise validate) before returning this.
  override protected def withNewChildrenInternal(
      newChildren: IndexedSeq[SparkPlan]): SparkPlan = this

SharedSparkSession mixes in Spark's SQLTestUtilsBase, which overrides withSQLConf to return Unit rather than the block value. Capturing the collected rows via the block return type therefore inferred Unit and failed to compile (isEmpty / checkAnswer on Unit). Assign the vanilla result to a var inside the block instead.
Copilot AI review requested due to automatic review settings August 14, 2026 06:47
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Suppressed comments (1)

gluten-substrait/src/main/scala/org/apache/spark/sql/execution/EmptyRelationExecTransformer.scala:53

  • EmptyRelationExecTransformer is intended as a semantic replacement for Spark's EmptyRelationExec, but doExecute() currently throws. If this node ever ends up on a row-based path (e.g., under a non-offloaded parent, or if a transition is missed), the query would fail instead of producing an empty result. Consider returning an empty RDD[InternalRow] here. Also, since this is a leaf exec, extending LeafExecNode lets Spark handle children/withNewChildrenInternal invariants and avoids silently ignoring non-empty newChildren.
  override protected def doExecute(): RDD[InternalRow] =
    throw new UnsupportedOperationException(
      "EmptyRelationExecTransformer does not support row execution.")

  override protected def doExecuteColumnar(): RDD[ColumnarBatch] =
    sparkContext.emptyRDD[ColumnarBatch]

  override def children: Seq[SparkPlan] = Seq.empty

  override protected def withNewChildrenInternal(
      newChildren: IndexedSeq[SparkPlan]): SparkPlan = this

…RelationExec

The plan-shape tests queried 'WHERE 1 = 0', which the logical optimizer folds to an empty LocalRelation (physical LocalTableScan) before physical planning, so no EmptyRelationExec is ever produced and the offload assertion failed (0 transformers; plan was LocalTableScan <empty>). EmptyRelationExec is an AQE-runtime node created by Propagate Empty Relations when a materialized query stage is empty at runtime. Drive it through an AQE INTERSECT whose left side (l_orderkey < 0) is empty only at runtime, mirroring the upstream SPARK-35585 trigger, and traverse with collectWithSubqueries. The config-gate test now exercises the same real EmptyRelationExec instead of passing vacuously.
Copilot AI review requested due to automatic review settings August 14, 2026 08:10
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Extend LeafExecNode with ValidatablePlan (matching ColumnarRangeBaseExec) instead of manually overriding children and withNewChildrenInternal. LeafExecNode/LeafLike supplies an empty children list and a withNewChildrenInternal that enforces the no-children invariant, rather than silently ignoring newChildren and returning this.
Copilot AI review requested due to automatic review settings August 14, 2026 10:40
@github-actions

Copy link
Copy Markdown

Run Gluten Clickhouse CI on x86

@minni31 minni31 changed the title [CORE][VL] Add native EmptyRelationExec offload to Velox backend [CORE][VL] Add columnar EmptyRelationExec offload to Velox backend Aug 14, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 14 out of 14 changed files in this pull request and generated no new comments.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CORE works for Gluten Core DOCS VELOX

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants