Repository navigation
[FEA] Enable per-expression legacy AST projection [databricks] [fast-ut] - #15377
Conversation
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
|
@greptile full review |
Greptile SummaryThis PR replaces the all-or-nothing
Confidence Score: 5/5This PR is safe to merge. The refactor correctly maintains result semantics for all expression types and execution paths. The change is a well-scoped refactor with thorough test coverage across retry, CSE multi-tier propagation, HOF fusion, and task-completion lifecycle. Resource management follows established withResource/closeOnExcept patterns, the OOM retry flow is unchanged (GpuTieredProject still drives it), and the canThisBeAst refactoring is a pure identity transform. No regressions were found in the logic for mixed AST/non-AST evaluation, null-literal routing, or explain output generation. Files Needing Attention: No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[GpuProjectExecMeta.convertToGpu] --> B{conf.isProjectAstEnabled?}
B -- No --> C[GpuProjectExec with all GPU exprs]
B -- Yes --> D{Per-expression check:\nisFixedWidth AND canThisBeAst\nAND NOT topLevelNullLiteral?}
D -- Yes --> E[wrap in GpuProjectAstExpression]
D -- No --> F[keep as regular GPU expr]
E --> G[GpuProjectExec with mixed expr list]
F --> G
G --> H[GpuTieredProject.buildExprTiers]
H --> I[unwrap AST markers]
I --> J[CSE: GpuEquivalentExpressions]
J --> K[GpuEquivalentExpressions.getExprTiers]
K --> L[rewrapAstTiers: trace AST outputs\nback through CSE tiers]
L --> M[Bound + metric-injected tier expressions]
M --> N[GpuProjectExec.project per tier/batch]
N --> O{hasAstExpressions?}
O -- Yes --> P[tableFromBatch: build shared cuDF Table]
P --> Q[GpuArrayHofFusion.project with evalColumn lambda]
Q --> R{Expression type?}
R -- AST --> S[astExpression.computeColumn table]
R -- Regular --> T[expression.columnarEval cb]
O -- No --> U[GpuArrayHofFusion.project with columnarEval]
S --> V[GpuColumnVector output]
T --> V
U --> V
V --> W[ColumnarBatch result]
Reviews (11): Last reviewed commit: "Merge branch 'main' into legacy-ast-per-..." | Re-trigger Greptile |
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
|
NOTE: release/26.08 has been created from main. Please retarget your PR to release/26.08 if it should be included in the release. |
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
igorpeshansky
left a comment
There was a problem hiding this comment.
LGTM
modulo one remaining missing comment.
|
build |
|
build |
|
build |
|
build |
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
|
build |
1 similar comment
|
build |
|
build |
1 similar comment
|
build |
|
build |
…ricks] (#15312) Contributes to #10640 ### Description This PR adds experimental cuDF AST JIT execution to `GpuProjectExec` and unifies its planning with interpreted (legacy) AST and regular GPU projection. It builds on #15377 and groups compatible JIT outputs for cuDF Row IR CSE, while interpreted AST uses plugin-side CSE and the existing tiered projection lifecycle. JIT operator coverage remains limited to non-ANSI `IntegerType` and `LongType` addition and multiplication. The internal `spark.rapids.sql.projectAstEnabled` and `spark.rapids.sql.projectAstJitEnabled` switches independently enable interpreted AST and JIT; both remain disabled by default. This PR does not solve JIT cold-start cost or enable either backend by default. #### Planning and execution The shared Project planner uses existing per-node operator/type eligibility to discover supported subtrees and build dependency tiers. With tiering enabled, eligible children can be extracted below unsupported roots and from the GPU inputs of CPU bridges without depending on CSE to expose them first. Only deterministic values without side effects move to an earlier tier, where they are computed for every row, so an operand that `AND`, `OR`, `IF`, or `CASE WHEN` would skip is never evaluated early if it can fail. Lambda bodies are not split. With tiering disabled, only complete output expressions are selected. The placement policy is: - Materialize maximal deterministic subtrees shared across backend regions so consumers reuse one computed column; avoid also materializing descendants already covered by that sharing. A value that would otherwise stay inline is not materialized only for sharing when that would leave an interpreted consumer with a single simple arithmetic operation: that consumer would then run on regular GPU, and a JIT consumer would be split into a second program. - Prefer a complete compatible expression after cross-backend sharing. JIT wins when both enabled backends support the complete expression; otherwise, a complete interpreted AST is preferred over splitting it only to use JIT for part of it. - Apply plugin CSE to interpreted AST and regular GPU regions. Interpreted AST expressions can execute separately within one tier. Compatible JIT roots remain together for cuDF CSE within a multi-output group. - Keep standalone add, subtract, and multiply operations with only column/literal inputs on regular GPU rather than interpreted AST. This is checked before boundary selection, treating children read from earlier tiers as columns, and again after CSE. These operations remain supported inside compound AST expressions; JIT eligibility is unchanged. This is a fixed placement policy, not a profitability cost model. Sharing can introduce materialization overhead, and enabling JIT does not establish that compilation will be amortized. Compilation-, execution-, and materialization-aware optimization remains follow-up work (#16384). Each JIT root is compiled explicitly with `AstExpression.compileJit`. Compatible roots use `AstJitProgram.compile` and the reusable program's `computeTable` for multi-output execution. Single-output execution also reuses the task-local program. The internal `spark.rapids.sql.projectAstJitMultiOutputEnabled` switch defaults to true and only applies when JIT is enabled. Project and extracted BHJ/BNLJ build-side projections pass both backend switches explicitly to the tiered binder. Generic binders enable neither Project AST backend and drop existing AST wrappers unless the caller enables the matching backend. The build-side projection that the BHJ hash-build cache retains for rebuilds in later tasks therefore uses regular GPU evaluation; #16509 tracks AST and AST JIT support there. `spark.rapids.sql.explain=ALL` reports final bound tiers as `AST JIT`, `AST Interpreted`, or regular GPU, including selections exposed during tier planning. The Spark plan string shows a backend for a Project output only when that output's final root runs on it; subexpressions extracted to earlier tiers appear only in the final-selection log. #### Retry and resource lifecycle Compiled expressions and JIT programs are created lazily inside the projection retry boundary, including when Project split retry is disabled, reused across retries and batches within a task, and closed at task completion. Multi-output results retain ARM ownership, failure cleanup, output ordering, and null propagation. Extracted BNLJ projections use the same spillable, OOM-retry-aware helper as BHJ. Implementation and validation were assisted by OpenAI Codex. ### cuDF backend status and remaining validation [NVIDIA/cudf#23828](NVIDIA/cudf#23828), which provides the Java/JNI AST JIT bindings, is merged. Local correctness validation below used a locally installed 26.12 snapshot containing these APIs; it does not establish the contents or success of a published Jenkins nightly artifact. The current backend generates CUDA source from Row IR. Cold compilation and precompiled operator-fragment/LTO integration remain separate work (#16383). The 2026-10-27 PCH benchmark below was rerun on the current PR head; the other performance sections retain their original artifact versions and cache-state descriptions and are dated evidence rather than reruns of the latest implementation. ### Testing Local validation on 2026-10-22 used Spark 3.5.2 (`buildver=352`), Java 17, and one RTX 5880 Ada GPU, with the 2026-10-21 26.12 snapshot of cudf-spark-jni `dc2af3a4e358bb8f1c3e32e46502307b49e39792` and cuDF `b83f75be57ddf3850b0178c805dd4d6d9178d339`: - `mvn clean install -Dbuildver=352 -DskipTests`: passed, including test-source compilation and all-module Scalastyle with no errors or warnings. - `GpuProjectAstPlannerSuite`, `GpuProjectAstJitSuite`, `ProjectExprSuite`, `ProjectSplitRetrySuite`, `GpuBroadcastNestedLoopJoinRetrySuite`, `GpuBroadcastHashJoinPostProjectionSuite`, and `GpuArrayHofFusionSuite`: **125 passed**, no failures or aborted suites. - Full `ast_test.py` plus `test_cpu_bridge_with_ast_backends` and `test_cpu_bridge_short_circuit_with_ast_backends`: **247 passed, 1 existing xfail**. The xfail is the unchanged `test_columnar_pow` expectation. Coverage includes independent backend switches, plugin/cuDF CSE placement, maximal cross-backend sharing and its exception for interpreted consumers, simple arithmetic before and after materialization and CSE, compound AST below unsupported roots, CPU bridge regions, skipped operands with side effects under `AND`/`OR`/`IF`/`CASE WHEN`, planning-time wrappers that match the final selection, tiering and multi-output on/off, INT/LONG CPU/GPU equality, serialization/type eligibility, retry, resource cleanup, and the cached BHJ build-side projection. Other Spark shims, Scala 2.13, and Databricks were not run locally. ### Performance #### Verified PCH benchmark rerun on the PR head: 20M and 200M rows, 1–16 outputs (2026-10-27) This reruns the 2026-09-15 matrix below on the current PR head, including the latest planner changes, with the same machine, inputs, and protocol. Its cuDF includes later JIT compile-time improvements, so these numbers replace the September compile and first-use costs. - Plugin: cudf-spark `613cb0635a1ac9bccf5202d60b8c5557f43f5fe5` (PR head), built with `mvn clean install -Dbuildver=352 -DskipTests`; JNI `9bcf05eac4990416cdb5f2741187ad128ebfe811` (2026-10-26 nightly); cuDF `01a7871610d07efd0dbd5828116e9e15eea701b6`. - Spark 3.5.2, Java 17, `local[4]`, one RTX 5880 Ada GPU (`CUDA_VISIBLE_DEVICES=0`), Intel Core i7-9700K (8 cores; each NVRTC compilation runs on one core). All arms use NVTX-only Nsight Systems profiling, no CPU sampling, fresh processes and isolated kernel caches; runs are serial. Inputs and expression shapes are unchanged. - **180 fresh Spark processes and 1,080 target actions passed the original analysis.** Every JIT process verified **NVRTC 12.9**, **one logical program and one actual target compiler invocation**, and PCH creation or reuse for the requested state. None of the **600 hot JIT actions** compiled the target again. Within each input size, output-sum checksums matched across arms, rounds, and actions, and regular runs reported zero for all six JIT metrics. The PCH verifier's 14 tests passed. - The benchmark harness needed two compatibility edits for the current plugin: the tiered binder's new `enableAst` flag, and two metrics that only existed in the LTO prototype. Measurement and verification logic are unchanged. #### Compiler and first-action times Values are medians across three processes. Compiler time is target-window `rtcx:compile` host duration; action time is wall-clock `collect()` after planning. Spark startup, non-JIT GPU bootstrap, the optional distinct one-row PCH probe, and runtime-version verification are excluded. | Rows | Case | Outputs | Cold-PCH compile ms | Warm-PCH compile ms | Regular first ms | Cold-PCH JIT first ms | Warm-PCH JIT first ms | |---|---|---:|---:|---:|---:|---:|---:| | 20M | `multi_independent` | 1 | 1962.6 | 273.3 | 302.8 | 2356.5 | 509.7 | | 20M | `multi_independent` | 2 | 1994.9 | 326.9 | 321.9 | 2406.3 | 615.9 | | 20M | `multi_independent` | 4 | 2071.1 | 395.1 | 359.2 | 2501.6 | 710.9 | | 20M | `multi_independent` | 8 | 2204.7 | 500.1 | 411.4 | 2739.3 | 881.5 | | 20M | `multi_independent` | 16 | 2473.3 | 798.2 | 550.7 | 3092.2 | 1286.0 | | 20M | `multi_shared_add` | 1 | 1953.6 | 279.7 | 338.3 | 2357.0 | 575.2 | | 20M | `multi_shared_add` | 2 | 1997.4 | 299.9 | 370.4 | 2449.9 | 616.8 | | 20M | `multi_shared_add` | 4 | 1995.9 | 345.6 | 411.5 | 2491.9 | 710.5 | | 20M | `multi_shared_add` | 8 | 2124.6 | 442.7 | 489.0 | 2690.4 | 884.1 | | 20M | `multi_shared_add` | 16 | 2296.5 | 616.3 | 560.1 | 2947.2 | 1144.3 | | 200M | `multi_independent` | 1 | 2008.0 | 270.4 | 1129.2 | 3257.1 | 1383.8 | | 200M | `multi_independent` | 2 | 1983.2 | 307.9 | 1205.4 | 3261.8 | 1453.2 | | 200M | `multi_independent` | 4 | 2043.3 | 360.1 | 1292.1 | 3375.7 | 1580.9 | | 200M | `multi_independent` | 8 | 2180.8 | 504.9 | 1486.3 | 3652.7 | 1832.3 | | 200M | `multi_independent` | 16 | 2500.7 | 785.6 | 1808.6 | 4154.2 | 2291.0 | | 200M | `multi_shared_add` | 1 | 1948.8 | 279.6 | 1558.3 | 3567.1 | 1748.0 | | 200M | `multi_shared_add` | 2 | 2001.5 | 309.6 | 1613.0 | 3675.6 | 1853.5 | | 200M | `multi_shared_add` | 4 | 2050.6 | 343.8 | 1691.7 | 3786.1 | 1947.3 | | 200M | `multi_shared_add` | 8 | 2110.9 | 421.4 | 1839.4 | 3947.5 | 2153.8 | | 200M | `multi_shared_add` | 16 | 2276.2 | 611.6 | 2107.8 | 4364.5 | 2510.1 | #### Project op-time and end-to-end speedups All ratios are **Regular / JIT; above 1 means JIT is faster**. - `Cold-PCH OP` = regular **first-action** Project op time / cold-PCH JIT **first-action** Project op time. - `Warm-PCH OP` = the same regular **first-action** Project op time / warm-PCH JIT **first-action** Project op time. - `Hot OP` compares both modes' hot actions. Each process contributes the median of its five hot actions; the reported value is the ratio of the medians across three processes. Hot JIT values use the warm-PCH arm. - E2E ratios use the corresponding action wall times with the same first/hot convention. | Rows | Case | Outputs | Cold-PCH OP Regular/JIT | Warm-PCH OP Regular/JIT | Hot OP Regular/JIT | Cold-PCH E2E Regular/JIT | Warm-PCH E2E Regular/JIT | Hot E2E Regular/JIT | |---|---|---:|---:|---:|---:|---:|---:|---:| | 20M | `multi_independent` | 1 | 0.008x | 0.058x | 0.864x | 0.128x | 0.594x | 1.015x | | 20M | `multi_independent` | 2 | 0.009x | 0.054x | 1.075x | 0.134x | 0.523x | 1.058x | | 20M | `multi_independent` | 4 | 0.011x | 0.060x | 1.437x | 0.144x | 0.505x | 1.040x | | 20M | `multi_independent` | 8 | 0.015x | 0.066x | 1.869x | 0.150x | 0.467x | 1.081x | | 20M | `multi_independent` | 16 | 0.022x | 0.070x | 2.145x | 0.178x | 0.428x | 1.078x | | 20M | `multi_shared_add` | 1 | 0.008x | 0.056x | 1.377x | 0.144x | 0.588x | 1.041x | | 20M | `multi_shared_add` | 2 | 0.010x | 0.064x | 1.441x | 0.151x | 0.601x | 1.025x | | 20M | `multi_shared_add` | 4 | 0.014x | 0.081x | 1.420x | 0.165x | 0.579x | 1.006x | | 20M | `multi_shared_add` | 8 | 0.021x | 0.100x | 1.510x | 0.182x | 0.553x | 1.031x | | 20M | `multi_shared_add` | 16 | 0.019x | 0.071x | 1.661x | 0.190x | 0.489x | 1.038x | | 200M | `multi_independent` | 1 | 0.025x | 0.168x | 1.026x | 0.347x | 0.816x | 0.990x | | 200M | `multi_independent` | 2 | 0.034x | 0.191x | 1.342x | 0.370x | 0.830x | 1.027x | | 200M | `multi_independent` | 4 | 0.049x | 0.246x | 1.764x | 0.383x | 0.817x | 1.039x | | 200M | `multi_independent` | 8 | 0.086x | 0.337x | 2.327x | 0.407x | 0.811x | 1.078x | | 200M | `multi_independent` | 16 | 0.133x | 0.391x | 2.996x | 0.435x | 0.789x | 1.189x | | 200M | `multi_shared_add` | 1 | 0.038x | 0.228x | 1.213x | 0.437x | 0.891x | 1.015x | | 200M | `multi_shared_add` | 2 | 0.045x | 0.255x | 1.315x | 0.439x | 0.870x | 1.016x | | 200M | `multi_shared_add` | 4 | 0.056x | 0.287x | 1.576x | 0.447x | 0.869x | 1.016x | | 200M | `multi_shared_add` | 8 | 0.081x | 0.350x | 1.844x | 0.466x | 0.854x | 1.045x | | 200M | `multi_shared_add` | 16 | 0.119x | 0.392x | 2.359x | 0.483x | 0.840x | 1.096x | `OP` is the target Project's `opTimeLegacy`: accumulated host time across tasks, including construction, evaluation, and waiting. It excludes the surrounding read/aggregate work as timed by their own operators, but is not isolated GPU kernel time. Task-summed OP cannot be subtracted from query wall time or used as an exact decomposition of scan and aggregate latency. The much smaller cold/warm OP ratios reflect compilation/waiting inside Project, compared with regular Project work alone. #### Interpretation - **Target compilation is 1.9–3.3x faster than in September; per-process PCH creation is unchanged and now dominates cold start.** Warm-PCH compiler medians are 0.27–0.80 seconds (September: 0.52–2.64), and cold-PCH medians are 1.95–2.50 seconds (September: 2.12–4.40). In every cell, cold-PCH compilation exceeds warm-PCH compilation by 1.65–1.74 seconds, the one-time cost of creating the PCH in a process. - **First use still does not break even, but the gap is much smaller, especially at 200M.** Warm-PCH E2E Regular/JIT is 0.43–0.60x at 20M and 0.79–0.89x at 200M (September: 0.16–0.38x and 0.43–0.74x). With cold PCH it is 0.13–0.19x and 0.35–0.48x (September: 0.10–0.13x and 0.29–0.40x). - Compiler cost still depends on expression shape rather than row count: with warm PCH, the 16-output independent program compiles in 0.80 / 0.79 seconds at 20M / 200M, and the shared program in 0.62 / 0.61 seconds. - **Hot results are unchanged within noise.** Hot E2E Regular/JIT is 0.99–1.19x (September: 1.00–1.20x). At 200M and 16 outputs, independent Project OP falls from 1,411.4 to 471.2 ms (2.996x), while E2E falls from 1,575.0 to 1,324.5 ms (1.189x). Shared Project OP falls from 1,171.2 to 496.4 ms (2.359x), while E2E falls from 1,866.8 to 1,702.6 ms (1.096x). - The metrics still report 4 / 18 program builds and group evaluations per action at 20M / 200M, exactly 20M / 200M evaluated rows, and zero expression-local program-cache hits. In the 200M, 16-output independent warm-PCH case, first-action program-build time is 3.17 seconds summed across tasks for one 0.79-second compilation: the four concurrently running tasks wait on the same native compilation, and later tasks hit the in-process kernel cache. - Regular first-action times are 4–23% higher than in September. A same-session A/B at 20M with 16 independent outputs gave matching regular times for the September plugin JAR and the PR-head JAR (per-process hot E2E medians 354/354/362 ms and 353/376/359 ms, versus 298 ms in September), so this is drift in the machine environment rather than a regression in the PR head. Every ratio above compares arms measured in the same session. - This is still the CUDA source-JIT path; no Row IR/LTO implementation is included. The measurements below predate the latest planner changes (side-effect-aware tier boundaries, the sharing exception for interpreted consumers, and the interpreted region rule) and were not rerun for them. #### Interpreted AST placement: idle-host A/B rerun (2026-10-10) This comparison isolates the rule that leaves standalone simple arithmetic on regular GPU, on top of the unified planner. The implemented planner matches the tested variant. JIT was disabled throughout; these results do not measure JIT speedup or cold start. The benchmark used 20M Parquet rows, 16 outputs, eight scan partitions/batches, Spark 3.5.2, Java 17, `local[4]`, one RTX 5880 Ada, one concurrent GPU task, a 256 MiB batch target, and a verified 2 GiB pinned pool. Three paired runs used six fresh JVMs in A/B, B/A, A/B order, with two warmups and five timed actions per case/mode: 240 timed actions and 96 warmups. CPU/GPU output aggregates, 49,152 small-input row comparisons, and plan assertions passed. Monitoring found no concurrent native compiler processes. Each absolute time is the median of three per-JVM medians. Reductions are calculated within matched JVM pairs; ranges are the three paired reductions, not confidence intervals. | Case | Baseline legacy Project ms | Updated legacy Project ms | Paired Project reduction % [range] | Baseline query ms | Updated query ms | Paired query reduction % [range] | |---|---:|---:|---:|---:|---:|---:| | cheap | 13.71 | 9.83 | 27.8 [27.1, 29.9] | 189.02 | 185.28 | 2.3 [-0.2, 4.5] | | expensive | 16.65 | 12.22 | 26.5 [26.3, 26.6] | 160.60 | 158.83 | 2.7 [-1.1, 6.8] | | mixed | 62.39 | 53.24 | 14.4 [14.0, 14.7] | 269.36 | 257.67 | 4.5 [0.7, 6.1] | | broad0 (unchanged-plan control) | 21.83 | 21.97 | -1.2 [-2.4, -0.4] | 206.93 | 208.53 | -0.9 [-1.8, -0.1] | The cheap case retained CSE but removed all 17 standalone AST fragments. The expensive case retained one fused shared AST instead of 17 fragments. The mixed case reduced 32 fragments in two tiers to seven compound AST expressions in one tier. The broad control retained 16 AST expressions in one tier. `Project` is task-summed `opTimeLegacy` host time, including preparation and waiting; it is not GPU kernel time and cannot be added to query wall time. Cheap/expensive query reductions span zero, so these measurements do not establish consistent end-to-end speedups. The updated expensive legacy query remained slightly slower than regular GPU despite its improved Project metric. These four warm workloads support the narrow placement rule, not a general claim that legacy AST should be enabled by default. This performance campaign used frozen cudf-spark-jni `16fcee1cd31ad8c07ec22eb8f0eae205f1ab4aa6` and cuDF `c4bb5ea7f70db0ca5201ff534358b7f2f562cc15` artifacts. The newer native artifacts in the Testing section were used for correctness/build validation, not to regenerate these timings. The full historical performance sections follow unchanged. ### Checklists Documentation - [ ] Updated for new or modified user-facing features or behaviors - [x] No user-facing change Testing - [x] Added or modified tests to cover new code paths - [ ] Covered by existing tests - [ ] Not required Performance - [x] Tests ran and results are added in the PR description - [ ] Issue filed with a link in the PR description - [ ] Not required
Related to #8954.
Description
The motivation for this PR is to prepare for AST JIT work.
Legacy AST Project execution is currently all-or-nothing: if any top-level Project output cannot be represented by the legacy AST backend, eligible sibling outputs cannot use AST either.
This change selects the legacy AST backend independently for each top-level Project expression. Supported fixed-width outputs use AST while unsupported outputs in the same Project continue through the regular GPU expression path. Top-level null literals also remain on the regular projection path so their outputs can reuse its cached null vector. It uses the existing
spark.rapids.sql.projectAstEnabledconfiguration and does not change result semantics or add a user-facing configuration.This is independent of the experimental AST JIT work in #15312. It refactors the existing legacy AST path and does not depend on AST JIT, LTO, or precompiled fragments.
This also changes physical-plan rendering: AST projections now appear under GpuProject as
AST(...) AS xinstead of using a dedicatedGpuProjectAstExecnode. This does not change result semantics, APIs, or configuration.Implementation details:
GpuProjectAstExpression.Tableacross all AST outputs evaluated for the same tier and batch.Performance testing:
local[4], plugin26.08.0-SNAPSHOT, and 2 NVIDIA RTX 5880 Ada Generation GPUs.LIBCUDF_JIT_ENABLED=0isolates the legacy AST backend. AST off/on order and case order are reversed on alternating iterations.opTimeLegacy, accumulated across tasks, so it is not directly comparable to E2E wall-clock time.broad_unique_ast[0]/[84]whole_output_duplicates[0,0]/[42,0]cheap_partial_cse[0,0]/[1,84]expensive_partial_cse[0,0]/[1,84]mixed_half[0]/[42]No median regression was observed in the five main benchmark cases in either E2E or Project op time.
Top-level literal routing follow-up
A targeted follow-up compared top-level literals on the regular and AST paths. It used 20 million rows, 84 Project outputs, two warmups, and 15 alternating measured iterations. The benchmark directly executed and consumed the columnar
GpuProject, avoiding Catalyst constant folding and unrelated aggregate, shuffle, or ColumnarToRow work. Speedup is regular / AST; values above 1 mean AST is faster.literal_onlymixed_ast_literalsnull_literal_onlyNon-null literals were neutral in E2E time in both the literal-only and mixed workloads, so there is no evidence for excluding all top-level literals from AST. Null literals were different: the regular projection was 4.1x faster E2E and 49.6x faster in Project op time because it can reuse its cached null vector across outputs, whereas per-expression AST evaluates the null outputs independently. Based on these results, this PR keeps only top-level null literals on the regular projection path and leaves non-null literals AST-eligible.
TPC-H/TPC-DS coverage
We also performed physical-plan sweeps over stock TPC-H/NDS-H and TPC-DS workloads.
These workloads therefore spend nearly all of their time in unrelated scans, joins, and aggregations and do not provide a meaningful performance signal for this change. The performance results above use purpose-built AST-only and mixed-expression Projects so that the affected execution path is exercised directly, while still materializing every projected expression.
Checklists
Documentation
Testing
(Please provide the names of the existing tests in the PR description.)
Performance