Skip to content

Keep the TableCacheQueryStageExec on the CPU but convert the child plan - #13944

Closed
razajafri wants to merge 6 commits into
NVIDIA:release/25.12from
razajafri:SP-13878-table-cache-query-stage-fix
Closed

Keep the TableCacheQueryStageExec on the CPU but convert the child plan#13944
razajafri wants to merge 6 commits into
NVIDIA:release/25.12from
razajafri:SP-13878-table-cache-query-stage-fix

Conversation

@razajafri

@razajafri razajafri commented Dec 4, 2025

Copy link
Copy Markdown
Collaborator

Fixes #13878

Description

A customer has reported a bug where writing to a Delta table after caching Parquet scan results in an exception. The fix keeps the TableCacheQueryStageExec on the CPU but brings the child plan InMemoryTableScanExec on the GPU.

Checklists

  • This PR has added documentation for new or modified features or behaviors.
  • This PR has added new tests or modified existing tests to cover new code paths.
    (Please explain in the PR description how the new code paths are tested, such as names of the new/existing tests that cover them.)
  • Performance testing has been performed and its results are added in the PR description. Or, an issue has been filed with a link in the PR description.

jihoonson and others added 5 commits November 17, 2025 14:07
…ting partition count in GpuOptimizeWriteExchangeExec (NVIDIA#13804)

Backport of NVIDIA#13780. Cherry picked from commit f02e77d.

### Description

`GpuOptimizeWriteExchangeExec` computes the `actualNumPartitions` based
on the partition count of its input. The computed `actualNumPartitions`
is used to dynamically optimize the partitioning. This logic is
currently missing handling of the case when the input partition count is
0, which can cause the ArithmeticException error. This PR adds a proper
handling of the case.

### Checklists

- [ ] This PR has added documentation for new or modified features or
behaviors.
- [x] This PR has added new tests or modified existing tests to cover
new code paths.
(Please explain in the PR description how the new code paths are tested,
such as names of the new/existing tests that cover them.)
- [ ] Performance testing has been performed and its results are added
in the PR description. Or, an issue has been filed with a link in the PR
description.

Signed-off-by: Jihoon Son <ghoonson@gmail.com>
…VIDIA#13782)

This is a cherry-pick of
NVIDIA#13775 for 25.10.

### Description

The PR is a clean cherry-pick of a commit we are adding in 25.12 to
circumvent an exception in the state machine. See
NVIDIA#13774 for more info.

### Checklists

- [x] This PR has added documentation for new or modified features or
behaviors.
- [ ] This PR has added new tests or modified existing tests to cover
new code paths.
(Please explain in the PR description how the new code paths are tested,
such as names of the new/existing tests that cover them.)
- [ ] Performance testing has been performed and its results are added
in the PR description. Or, an issue has been filed with a link in the PR
description.

Signed-off-by: Alessandro Bellina <abellina@nvidia.com>
…DIA#13831)

This cherry picks NVIDIA#13806 to
25.10

- [ ] This PR has added documentation for new or modified features or
behaviors.
- [ ] This PR has added new tests or modified existing tests to cover
new code paths.
(Please explain in the PR description how the new code paths are tested,
such as names of the new/existing tests that cover them.)
- [ ] Performance testing has been performed and its results are added
in the PR description. Or, an issue has been filed with a link in the PR
description.

---------

Signed-off-by: Alessandro Bellina <abellina@nvidia.com>
Signed-off-by: Raza Jafri <raza.jafri@gmail.com>
@greptile-apps

greptile-apps Bot commented Dec 4, 2025

Copy link
Copy Markdown
Contributor

Greptile Overview

Greptile Summary

This PR attempts to fix a bug where writing to a Delta table after caching Parquet scan results throws a RapidsHostColumnVector cannot be cast to GpuColumnVector exception. The approach adds a new shim method handleTableCacheInOptimizeAdaptiveTransitions intended to keep TableCacheQueryStageExec on the CPU without recursively processing its children.

  • Added handleTableCacheInOptimizeAdaptiveTransitions method to SparkShims trait with default returning None
  • Overrode in Spark350PlusNonDBShims to return Some(tcqs) for TableCacheQueryStageExec, which would prevent child processing
  • Modified default case in optimizeAdaptiveTransitions to check the new handler before processing children

Critical Issue: The new handler appears to be unreachable for TableCacheQueryStageExec because an earlier case at line 177-179 already matches it via getTableCacheNonQueryStagePlan(p).nonEmpty. Both shim methods use identical pattern matching for TableCacheQueryStageExec, so the earlier case always wins and recursively processes children regardless of the new handler.

Confidence Score: 2/5

  • This PR has a potential logic issue where the new code path may never be executed due to pattern matching order
  • The new handleTableCacheInOptimizeAdaptiveTransitions method is placed in the default case but TableCacheQueryStageExec is already matched by an earlier case at line 177-179. This means the fix may not work as intended. The shim additions are clean but the integration point in GpuTransitionOverrides appears to be dead code for the target use case.
  • GpuTransitionOverrides.scala - the new handler placement appears incorrect; it should either be moved before line 177 or line 177-179 should be modified

Important Files Changed

File Analysis

Filename Score Overview
sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuTransitionOverrides.scala 2/5 Added handler for TableCacheQueryStageExec in default case, but this code appears unreachable since line 177-179 already matches TableCacheQueryStageExec first
sql-plugin/src/main/scala/com/nvidia/spark/rapids/SparkShims.scala 4/5 Added new shim method handleTableCacheInOptimizeAdaptiveTransitions with default returning None; clean addition to trait
sql-plugin/src/main/spark350/scala/com/nvidia/spark/rapids/shims/Spark350PlusNonDBShims.scala 4/5 Override returns Some(tcqs) for TableCacheQueryStageExec to prevent child processing; implementation is correct

Sequence Diagram

sequenceDiagram
    participant AQE as AdaptiveQueryExec
    participant OAT as optimizeAdaptiveTransitions
    participant TCQS as TableCacheQueryStageExec
    participant IMTS as InMemoryTableScanExec

    AQE->>OAT: Process plan with TableCacheQueryStageExec
    OAT->>OAT: Match case at line 177-179<br/>(getTableCacheNonQueryStagePlan.nonEmpty)
    OAT->>TCQS: Get children (returns wrapped plan)
    OAT->>OAT: Recursively process child<br/>InMemoryTableScanExec
    Note over OAT: New handler at line 245-249<br/>is never reached for TCQS
    OAT->>TCQS: withNewChildren(optimized children)
    TCQS-->>AQE: Return modified TCQS
Loading

@greptile-apps greptile-apps Bot 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.

10 files reviewed, no comments

Edit Code Review Agent Settings | Greptile

@sameerz
sameerz requested a review from a team December 4, 2025 14:28
@sameerz sameerz added the bug Something isn't working label Dec 4, 2025
@razajafri

Copy link
Copy Markdown
Collaborator Author

build

@revans2 revans2 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I still don't totally understand what is happening that caused this to be a problem, but making sure that the cached table wrapper stays on the CPU is confusing. Do we end up converting the data to and from the CPU around it?

Could we make sure that we test this on databricks, because you did not update the shims for databricks.

df2 = unary_op_df(spark, data_gen).withColumnRenamed("a", "r_a").cache()
df1.count()
df2.count()
return df1.join(df2, df1.a == df2.r_a, 'Outer')

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: I don't think that a and r_a will ever be equal. At least statistically it should be fairly rare.

@ignore_order(local=True)
@allow_non_gpu("CollectLimitExec", "ShuffleExchangeExec", "ColumnarToRowExec")
@pytest.mark.parametrize('enable_vectorized_conf', enable_vectorized_confs, ids=idfn)
@allow_non_gpu_conditional(is_spark_350_or_351(), "InMemoryTableScanExec")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is there a follow on issue for the cache not being on the GPU?

).persist(StorageLevel.MEMORY_AND_DISK)
minNbrs1.count()

ee.join(minNbrs1, "src")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is a noop and should be deleted


override def handleTableCacheInOptimizeAdaptiveTransitions(plan: SparkPlan,
parent: Option[SparkPlan]): Option[SparkPlan] = {
plan match {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

So effectively we are saying for Spark 3.5.0+ (non-DB) don't do into TableCacheQueryStageExec? It is already handled.

@razajafri
razajafri changed the base branch from branch-25.08 to branch-25.10 December 4, 2025 22:20

@greptile-apps greptile-apps Bot 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.

Additional Comments (1)

  1. sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuTransitionOverrides.scala, line 245-249 (link)

    logic: The new handleTableCacheInOptimizeAdaptiveTransitions call appears to be unreachable for TableCacheQueryStageExec because line 177-179 already matches it first:

    // Line 177-179 (earlier in the match):
    case p if SparkShimImpl.getTableCacheNonQueryStagePlan(p).nonEmpty =>
      p.withNewChildren(p.children.map(c => optimizeAdaptiveTransitions(c, Some(p))))

    Both getTableCacheNonQueryStagePlan and handleTableCacheInOptimizeAdaptiveTransitions match TableCacheQueryStageExec, so the earlier case wins.

    If the goal is to prevent child processing for TableCacheQueryStageExec, the handler should be placed before line 177, or line 177-179 should be modified to use the new shim method's behavior.

3 files reviewed, 1 comment

Edit Code Review Agent Settings | Greptile

@razajafri
razajafri changed the base branch from branch-25.10 to release/25.12 December 5, 2025 04:45
@gerashegalov

Copy link
Copy Markdown
Collaborator

Please please up-merge to HEAD of release/25.12

@razajafri

Copy link
Copy Markdown
Collaborator Author

This is not needed as the bug is not reproducible in 25.10+

@razajafri razajafri closed this Dec 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants