Skip to content

Add experimental Project AST JIT for integral add and multiply [databricks] - #15312

Merged
thirtiseven merged 39 commits into
NVIDIA:mainfrom
thirtiseven:project-ast-jit-infra
Oct 28, 2026
Merged

thirtiseven merged 39 commits into
NVIDIA:mainfrom
thirtiseven:project-ast-jit-infra

Conversation

@thirtiseven

@thirtiseven thirtiseven commented Jul 21, 2026 •

Copy link
Copy Markdown
Collaborator

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

rapidsai/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.
Project op times, process ranges, and changes from 2026-09-15

All OP values are task-summed milliseconds. Hot values are medians of per-process hot medians.

Rows Case Outputs Regular first OP ms Cold-PCH first OP ms Warm-PCH first OP ms Regular hot OP ms JIT hot OP ms
20M multi_independent 1 65.3 8260.5 1130.3 23.8 27.5
20M multi_independent 2 72.5 8402.8 1353.9 35.2 32.8
20M multi_independent 4 100.1 8719.6 1659.7 61.2 42.6
20M multi_independent 8 137.3 9355.7 2070.0 100.3 53.7
20M multi_independent 16 231.5 10391.5 3330.8 176.3 82.2
20M multi_shared_add 1 65.7 8199.4 1171.7 45.5 33.0
20M multi_shared_add 2 81.7 8420.0 1281.5 51.2 35.6
20M multi_shared_add 4 116.0 8426.9 1437.1 65.8 46.3
20M multi_shared_add 8 188.1 8939.1 1883.0 93.0 61.6
20M multi_shared_add 16 181.5 9659.7 2569.6 150.4 90.6
200M multi_independent 1 215.4 8609.3 1279.9 207.2 202.0
200M multi_independent 2 285.8 8521.0 1498.4 292.3 217.7
200M multi_independent 4 432.6 8817.2 1760.1 438.4 248.5
200M multi_independent 8 807.6 9443.1 2398.8 761.9 327.5
200M multi_independent 16 1443.4 10882.7 3688.4 1411.4 471.2
200M multi_shared_add 1 315.4 8397.9 1381.4 304.8 251.2
200M multi_shared_add 2 390.5 8655.1 1531.8 351.4 267.3
200M multi_shared_add 4 497.9 8876.0 1737.3 472.7 299.9
200M multi_shared_add 8 744.5 9212.1 2127.4 690.0 374.3
200M multi_shared_add 16 1192.8 10024.0 3039.2 1171.2 496.4

Compile ranges are across three processes. Hot ranges describe the three per-process medians, not individual actions.

Rows Case Outputs Cold-PCH compile Warm-PCH compile Regular hot E2E median (range) JIT hot E2E median (range) Regular first E2E Cold-PCH JIT first E2E Warm-PCH JIT first E2E
20M multi_independent 1 1958.1–2016.6 273.2–285.3 194.8 (192.6–200.8) 191.8 (188.8–195.2) 287.3–320.2 2344.2–2401.1 506.2–524.0
20M multi_independent 2 1984.3–2004.4 318.1–332.8 214.8 (208.3–219.3) 203.0 (202.7–205.0) 321.1–324.6 2388.1–2422.7 615.0–616.0
20M multi_independent 4 2047.3–2099.8 377.4–415.1 234.4 (233.0–237.8) 225.3 (220.4–232.1) 357.0–360.6 2481.2–2579.7 709.0–771.5
20M multi_independent 8 2133.0–2259.4 494.0–555.2 278.6 (273.5–282.7) 257.8 (247.1–265.9) 410.2–422.0 2624.4–2766.1 862.4–941.8
20M multi_independent 16 2436.2–2520.3 786.9–799.9 358.5 (358.3–365.2) 332.6 (332.1–333.8) 539.6–586.7 3042.5–3142.7 1253.5–1300.4
20M multi_shared_add 1 1947.1–1973.1 275.6–291.8 234.4 (232.2–235.2) 225.1 (219.5–228.6) 324.0–357.7 2340.7–2412.5 563.1–575.6
20M multi_shared_add 2 1993.9–2060.5 297.3–306.9 244.8 (242.9–252.7) 238.9 (237.5–242.0) 369.9–378.7 2406.5–2525.7 601.6–653.7
20M multi_shared_add 4 1989.3–2039.7 338.2–348.3 263.8 (262.5–271.5) 262.3 (254.4–263.6) 407.0–414.9 2479.5–2551.9 693.9–713.2
20M multi_shared_add 8 2090.0–2173.5 403.0–488.2 302.9 (294.3–305.7) 293.8 (290.0–294.5) 470.0–508.3 2631.2–2697.2 813.2–893.2
20M multi_shared_add 16 2278.7–2301.8 593.3–712.2 382.6 (379.1–386.3) 368.7 (365.2–376.1) 558.8–590.7 2927.0–2962.2 1133.4–1246.7
200M multi_independent 1 1937.7–2052.9 265.1–288.2 1004.8 (994.9–1017.7) 1015.1 (1000.5–1017.7) 1114.0–1146.4 3193.1–3299.2 1382.9–1392.1
200M multi_independent 2 1982.9–2050.2 307.9–316.8 1052.0 (1040.6–1053.4) 1024.8 (1015.9–1030.2) 1190.2–1211.7 3260.6–3351.6 1452.2–1475.9
200M multi_independent 4 2030.6–2064.4 359.0–364.1 1112.2 (1106.4–1133.5) 1070.4 (1068.9–1073.9) 1290.1–1309.2 3367.7–3431.2 1558.5–1583.1
200M multi_independent 8 2168.5–2203.5 489.7–518.8 1275.0 (1257.9–1285.4) 1182.7 (1154.8–1183.2) 1480.5–1499.7 3634.2–3668.3 1812.8–1842.3
200M multi_independent 16 2442.9–2593.4 771.6–793.7 1575.0 (1545.2–1589.5) 1324.5 (1306.4–1337.0) 1793.8–1830.3 4151.8–4315.2 2285.1–2314.3
200M multi_shared_add 1 1939.7–1984.3 279.3–301.2 1406.9 (1404.9–1412.9) 1385.6 (1375.6–1415.9) 1535.8–1575.7 3548.3–3593.4 1742.6–1928.6
200M multi_shared_add 2 1989.2–2065.6 301.0–309.9 1429.8 (1417.7–1430.9) 1407.5 (1404.9–1415.1) 1588.2–1623.0 3669.1–3738.4 1838.9–1871.1
200M multi_shared_add 4 1988.4–2058.3 333.1–347.2 1497.9 (1487.3–1507.5) 1475.0 (1457.7–1476.5) 1677.3–1698.1 3730.4–3797.3 1942.1–1972.7
200M multi_shared_add 8 2092.8–2115.5 417.5–438.0 1607.0 (1604.0–1614.3) 1537.3 (1534.0–1547.7) 1835.6–1855.9 3919.8–3984.2 2136.0–2160.2
200M multi_shared_add 16 2255.2–2370.8 592.6–618.3 1866.8 (1855.7–1880.1) 1702.6 (1697.8–1703.3) 2100.5–2151.1 4324.6–4463.7 2503.4–2529.9

Medians from the 2026-09-15 run → this rerun.

Rows Case Outputs Cold-PCH compile ms Warm-PCH compile ms Regular first ms Cold-PCH JIT first ms Warm-PCH JIT first ms Warm-PCH E2E Regular/JIT Hot E2E Regular/JIT
20M multi_independent 1 2120 → 1963 561 → 273 274 → 303 2448 → 2356 738 → 510 0.371x → 0.594x 1.027x → 1.015x
20M multi_independent 2 2350 → 1995 683 → 327 300 → 322 2698 → 2406 918 → 616 0.327x → 0.523x 1.023x → 1.058x
20M multi_independent 4 2578 → 2071 890 → 395 293 → 359 2965 → 2502 1148 → 711 0.255x → 0.505x 1.087x → 1.040x
20M multi_independent 8 3124 → 2205 1438 → 500 397 → 411 3522 → 2739 1731 → 881 0.229x → 0.467x 1.083x → 1.081x
20M multi_independent 16 4395 → 2473 2642 → 798 482 → 551 4937 → 3092 3020 → 1286 0.160x → 0.428x 1.101x → 1.078x
20M multi_shared_add 1 2240 → 1954 566 → 280 305 → 338 2585 → 2357 806 → 575 0.378x → 0.588x 1.033x → 1.041x
20M multi_shared_add 2 2237 → 1997 709 → 300 331 → 370 2628 → 2450 1002 → 617 0.330x → 0.601x 1.042x → 1.025x
20M multi_shared_add 4 2562 → 1996 795 → 346 346 → 411 3018 → 2492 1074 → 711 0.322x → 0.579x 1.021x → 1.006x
20M multi_shared_add 8 2854 → 2125 1162 → 443 420 → 489 3321 → 2690 1503 → 884 0.280x → 0.553x 1.084x → 1.031x
20M multi_shared_add 16 3536 → 2296 1939 → 616 489 → 560 4094 → 2947 2349 → 1144 0.208x → 0.489x 1.062x → 1.038x
200M multi_independent 1 2204 → 2008 518 → 270 1077 → 1129 3365 → 3257 1549 → 1384 0.695x → 0.816x 1.004x → 0.990x
200M multi_independent 2 2343 → 1983 647 → 308 1162 → 1205 3592 → 3262 1730 → 1453 0.672x → 0.830x 1.014x → 1.027x
200M multi_independent 4 2577 → 2043 945 → 360 1202 → 1292 3843 → 3376 2117 → 1581 0.568x → 0.817x 1.029x → 1.039x
200M multi_independent 8 2967 → 2181 1376 → 505 1383 → 1486 4334 → 3653 2599 → 1832 0.532x → 0.811x 1.100x → 1.078x
200M multi_independent 16 4219 → 2501 2582 → 786 1682 → 1809 5737 → 4154 3962 → 2291 0.425x → 0.789x 1.202x → 1.189x
200M multi_shared_add 1 2162 → 1949 561 → 280 1483 → 1558 3714 → 3567 2002 → 1748 0.741x → 0.891x 1.003x → 1.015x
200M multi_shared_add 2 2274 → 2002 671 → 310 1534 → 1613 3850 → 3676 2141 → 1854 0.717x → 0.870x 1.005x → 1.016x
200M multi_shared_add 4 2388 → 2051 820 → 344 1599 → 1692 4037 → 3786 2368 → 1947 0.675x → 0.869x 1.021x → 1.016x
200M multi_shared_add 8 2873 → 2111 1129 → 421 1755 → 1839 4612 → 3947 2726 → 2154 0.644x → 0.854x 1.052x → 1.045x
200M multi_shared_add 16 3492 → 2276 1921 → 612 1990 → 2108 5399 → 4364 3657 → 2510 0.544x → 0.840x 1.098x → 1.096x

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.

2026-09-15 PCH benchmark on the September cuDF build (superseded by the rerun above)

Verified PCH benchmark: 20M and 200M rows, 1–16 outputs (2026-09-15)

The completed matrix covers both multi_independent and multi_shared_add, five output widths (1, 2, 4, 8, 16), and regular GPU / cold-PCH JIT / warm-PCH JIT at both input sizes. 180 fresh Spark processes and 1,080 target actions passed validation. Each cell uses three processes; each process runs one first action and five hot actions. The 20M matrix reuses the verified regular/warm-PCH results and adds the missing 15 independent cold-PCH processes. The 200M matrix is a new complete run.

  • Same frozen plugin: cudf-spark 4f907f0b3d0494276ba069325901a1fe5a40ed97, JNI 101de1ba68291f10231c8cdcdbadd6cde6e282c9, cuDF 7edcf282f5e4cea38bb2c18c385e27cdf5a6599b.
  • Spark 3.5.2, Java 17, local[4], one RTX 5880 Ada GPU (CUDA_VISIBLE_DEVICES=0). All arms use NVTX-only Nsight Systems profiling, no CPU sampling, fresh processes and isolated kernel caches; runs are serial.
  • Identical schemas and generation formulas at both scales. The existing 200M input was verified from Parquet metadata: exactly 200,000,000 rows in eight files. Normal Spark file-split planning produces 4 / 18 scan partitions and Project batches at 20M / 200M, respectively.
  • Independent roots are (a + k) * (b + m); shared roots are (a + b) * (c + k), with different literals per output. A GPU aggregate consumes every output.
  • 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.
  • The plugin build passed all 14 modules with mvn clean install -Dbuildver=352 -DskipTests; JNI was already built. The benchmark-only JAR adds old-name aliases for the renamed private version metadata, with unchanged executable payloads.

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 2119.5 560.5 274.2 2448.2 738.2
20M multi_independent 2 2350.4 682.8 300.1 2697.8 917.5
20M multi_independent 4 2578.4 890.2 292.6 2965.0 1147.6
20M multi_independent 8 3123.9 1437.9 396.8 3522.0 1731.1
20M multi_independent 16 4395.2 2641.6 482.1 4936.9 3020.4
20M multi_shared_add 1 2239.6 566.1 304.6 2584.8 805.7
20M multi_shared_add 2 2237.1 708.5 330.6 2628.3 1001.6
20M multi_shared_add 4 2561.5 794.5 345.9 3018.3 1073.9
20M multi_shared_add 8 2853.6 1162.1 420.2 3320.6 1502.6
20M multi_shared_add 16 3536.2 1939.2 489.1 4093.6 2349.2
200M multi_independent 1 2204.2 518.5 1077.0 3365.3 1549.3
200M multi_independent 2 2343.1 646.7 1162.2 3592.4 1730.5
200M multi_independent 4 2577.4 944.5 1201.6 3843.3 2117.2
200M multi_independent 8 2966.6 1376.0 1383.4 4334.5 2598.7
200M multi_independent 16 4218.9 2582.4 1682.1 5737.5 3962.3
200M multi_shared_add 1 2161.5 560.7 1482.8 3714.3 2002.1
200M multi_shared_add 2 2273.7 671.4 1534.4 3850.2 2141.2
200M multi_shared_add 4 2387.9 819.7 1599.3 4037.2 2368.4
200M multi_shared_add 8 2873.0 1129.0 1754.6 4612.3 2725.5
200M multi_shared_add 16 3491.7 1921.0 1989.8 5399.2 3656.8

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.013x 0.051x 0.867x 0.112x 0.371x 1.027x
20M multi_independent 2 0.011x 0.038x 1.117x 0.111x 0.327x 1.023x
20M multi_independent 4 0.009x 0.028x 1.367x 0.099x 0.255x 1.087x
20M multi_independent 8 0.016x 0.034x 1.835x 0.113x 0.229x 1.083x
20M multi_independent 16 0.016x 0.026x 2.157x 0.098x 0.160x 1.101x
20M multi_shared_add 1 0.008x 0.035x 1.182x 0.118x 0.378x 1.033x
20M multi_shared_add 2 0.011x 0.034x 1.177x 0.126x 0.330x 1.042x
20M multi_shared_add 4 0.011x 0.035x 1.223x 0.115x 0.322x 1.021x
20M multi_shared_add 8 0.012x 0.031x 1.494x 0.127x 0.280x 1.084x
20M multi_shared_add 16 0.018x 0.033x 1.834x 0.119x 0.208x 1.062x
200M multi_independent 1 0.023x 0.096x 1.008x 0.320x 0.695x 1.004x
200M multi_independent 2 0.030x 0.106x 1.262x 0.324x 0.672x 1.014x
200M multi_independent 4 0.041x 0.110x 1.681x 0.313x 0.568x 1.029x
200M multi_independent 8 0.064x 0.137x 2.265x 0.319x 0.532x 1.100x
200M multi_independent 16 0.081x 0.132x 2.979x 0.293x 0.425x 1.202x
200M multi_shared_add 1 0.033x 0.121x 1.123x 0.399x 0.741x 1.003x
200M multi_shared_add 2 0.040x 0.130x 1.301x 0.399x 0.717x 1.005x
200M multi_shared_add 4 0.049x 0.139x 1.446x 0.396x 0.675x 1.021x
200M multi_shared_add 8 0.060x 0.149x 1.809x 0.380x 0.644x 1.052x
200M multi_shared_add 16 0.081x 0.145x 2.282x 0.369x 0.544x 1.098x

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

  • Increasing input to 200M amortizes compilation more effectively, but does not reach first-use break-even in this matrix. At 16 outputs, warm-PCH E2E Regular/JIT improves from 0.160x to 0.425x for independent roots, and from 0.208x to 0.544x for shared roots. Both remain below 1.
  • Compiler cost remains similar when row count grows 10x: the 16-output independent program takes 2.64 / 2.58 seconds with warm PCH at 20M / 200M, and the shared program takes 1.94 / 1.92 seconds. This supports a primarily expression-shape-dependent compilation cost.
  • Hot Project benefits are substantially larger than hot E2E benefits. At 200M and 16 outputs, independent Project OP falls from 1,411.2 to 473.7 ms (2.979x), while E2E falls from 1,450.9 to 1,206.5 ms (1.202x). Shared Project OP falls from 1,146.0 to 502.2 ms (2.282x), while E2E falls from 1,741.4 to 1,586.1 ms (1.098x).
  • Read, aggregate, scheduling, and synchronization remain in E2E. These results show dilution of the Project-local benefit but do not isolate how much is specifically due to Parquet reading. Small-width hot gains are modest and should be read with the process ranges below.
  • The new metrics 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. Each task processes one batch; fresh actions build fresh task-local programs while reusing the native kernel cache. A build is not an NVRTC compilation.
  • astJitProgramBuildTime and astJitEvalTime are task-summed host times. In the 200M, 16-output independent warm-PCH case, first-action program-build time is 10.36 seconds accumulated across tasks, while the trace records one 2.58-second compilation. Waiting explains why these metrics cannot be treated as serial query latency.
  • This is the current CUDA source-JIT path. No Row IR/LTO implementation or assumed 30-ms link time is included.

Within each input size, all output-sum checksums matched across arms, rounds, and actions. GPU scan/Project/aggregate and tier/root assertions passed; JIT metrics confirmed the complete row count, and regular runs reported zero for all six JIT metrics. The existing PCH verifier's 10 tests passed during harness validation. Build/runtime compatibility details and earlier measurements are retained below.

Current-run absolute Project op times and dispersion

All OP values below are task-summed milliseconds. Hot values are medians of per-process hot medians.

Rows Case Outputs Regular first OP ms Cold-PCH first OP ms Warm-PCH first OP ms Regular hot OP ms JIT hot OP ms
20M multi_independent 1 115.3 8835.9 2251.8 25.1 29.0
20M multi_independent 2 106.4 9769.9 2795.4 38.4 34.4
20M multi_independent 4 101.2 10713.7 3638.5 57.2 41.8
20M multi_independent 8 200.5 12861.6 5824.6 104.4 56.9
20M multi_independent 16 281.1 17994.4 10683.5 177.9 82.5
20M multi_shared_add 1 79.0 9315.0 2281.1 40.8 34.5
20M multi_shared_add 2 100.5 9307.2 2917.7 46.1 39.2
20M multi_shared_add 4 115.3 10689.4 3253.3 56.6 46.3
20M multi_shared_add 8 145.0 11801.1 4737.3 91.6 61.3
20M multi_shared_add 16 258.5 14562.9 7871.2 147.2 80.3
200M multi_independent 1 219.4 9383.8 2296.8 207.5 205.7
200M multi_independent 2 296.6 9930.8 2810.5 283.2 224.5
200M multi_independent 4 445.6 10900.6 4069.6 445.9 265.2
200M multi_independent 8 804.9 12555.2 5864.6 768.2 339.1
200M multi_independent 16 1436.0 17717.7 10838.9 1411.2 473.7
200M multi_shared_add 1 305.0 9210.2 2514.5 296.3 263.8
200M multi_shared_add 2 385.9 9684.8 2957.9 361.6 277.9
200M multi_shared_add 4 502.2 10174.6 3606.7 469.6 324.8
200M multi_shared_add 8 730.1 12238.9 4914.3 703.1 388.7
200M multi_shared_add 16 1197.5 14793.8 8253.2 1146.0 502.2

Compile ranges are across three processes. Hot ranges describe the three per-process medians, not individual actions.

Rows Case Outputs Cold-PCH compile range ms Warm-PCH compile range ms Regular hot E2E median (range) ms JIT hot E2E median (range) ms
20M multi_independent 1 2106.5–2310.2 499.9–563.3 191.1 (189.4–194.1) 186.2 (183.8–192.9)
20M multi_independent 2 2339.7–2366.9 679.7–693.1 201.4 (201.1–206.5) 197.0 (192.9–204.1)
20M multi_independent 4 2441.8–2784.9 881.8–902.6 218.9 (214.8–223.7) 201.3 (199.6–208.9)
20M multi_independent 8 3105.3–3252.9 1383.9–1458.7 247.8 (247.5–255.5) 228.8 (225.9–237.7)
20M multi_independent 16 4297.8–4429.0 2597.4–2755.2 298.0 (295.0–298.9) 270.6 (265.1–275.7)
20M multi_shared_add 1 2209.1–2262.1 502.4–607.3 228.0 (222.2–231.4) 220.8 (214.8–223.4)
20M multi_shared_add 2 2214.5–2494.0 662.9–734.2 237.8 (228.1–243.3) 228.2 (226.3–238.0)
20M multi_shared_add 4 2458.0–3124.9 756.8–821.4 245.8 (238.5–251.6) 240.7 (240.3–242.0)
20M multi_shared_add 8 2813.8–2987.4 1146.6–1298.6 281.9 (272.1–286.7) 260.1 (257.4–263.5)
20M multi_shared_add 16 3521.8–3875.2 1852.9–1961.5 318.9 (313.8–333.3) 300.3 (298.2–306.7)
200M multi_independent 1 2189.8–2458.6 496.3–549.8 987.6 (987.1–995.6) 983.4 (981.7–990.5)
200M multi_independent 2 2232.4–2389.0 635.5–705.8 1022.2 (1016.5–1028.8) 1008.3 (1007.5–1010.5)
200M multi_independent 4 2571.8–2605.4 879.2–1106.8 1073.1 (1067.4–1074.3) 1042.7 (1040.1–1049.2)
200M multi_independent 8 2959.9–3070.9 1368.9–1439.6 1211.3 (1197.1–1218.0) 1101.2 (1091.6–1106.5)
200M multi_independent 16 4088.2–4515.7 2548.3–2605.2 1450.8 (1442.7–1458.1) 1206.5 (1204.4–1227.6)
200M multi_shared_add 1 2149.8–2259.1 491.7–573.0 1384.2 (1380.5–1388.4) 1380.2 (1372.0–1382.6)
200M multi_shared_add 2 2227.7–2285.3 645.2–739.1 1414.6 (1412.5–1414.8) 1407.4 (1404.4–1409.5)
200M multi_shared_add 4 2380.0–2408.3 769.1–902.2 1462.8 (1457.8–1468.5) 1432.5 (1425.3–1436.2)
200M multi_shared_add 8 2683.1–2907.7 1118.0–1153.3 1561.4 (1557.7–1568.4) 1484.9 (1474.3–1492.6)
200M multi_shared_add 16 3455.5–3527.3 1821.9–2049.8 1741.4 (1740.2–1773.1) 1586.1 (1579.0–1594.4)

First-action ranges are included to expose startup variation that remains after the excluded bootstrap and planning steps.

Rows Case Outputs Regular first E2E range ms Cold-PCH JIT first E2E range ms Warm-PCH JIT first E2E range ms
20M multi_independent 1 250.1–280.1 2436.3–2633.4 691.8–770.1
20M multi_independent 2 288.8–303.9 2679.8–2751.1 893.2–930.0
20M multi_independent 4 284.7–343.3 2821.6–3175.9 1137.5–1161.5
20M multi_independent 8 363.5–403.1 3513.5–3700.3 1672.8–1779.0
20M multi_independent 16 461.1–497.5 4806.0–4954.5 2975.3–3135.7
20M multi_shared_add 1 287.4–308.4 2583.2–2601.4 725.2–854.5
20M multi_shared_add 2 314.9–337.0 2577.2–2894.2 927.4–1018.5
20M multi_shared_add 4 340.7–357.7 2855.1–3561.1 1048.7–1111.1
20M multi_shared_add 8 391.9–420.5 3320.2–3403.4 1494.8–1632.8
20M multi_shared_add 16 471.0–535.7 4057.4–4389.7 2258.3–2373.7
200M multi_independent 1 1076.1–1113.0 3356.5–3634.3 1530.4–1596.0
200M multi_independent 2 1134.6–1167.7 3424.1–3623.7 1708.1–1803.8
200M multi_independent 4 1197.3–1219.7 3810.4–3844.0 2001.9–2272.9
200M multi_independent 8 1378.3–1393.4 4309.1–4425.3 2574.3–2669.9
200M multi_independent 16 1646.8–1706.9 5615.3–6010.7 3928.0–3973.9
200M multi_shared_add 1 1477.6–1496.4 3688.2–3798.5 1931.1–3315.8
200M multi_shared_add 2 1523.6–1567.4 3821.1–3888.1 2113.2–2272.8
200M multi_shared_add 4 1584.4–1611.2 4017.8–4061.9 2268.3–2440.3
200M multi_shared_add 8 1753.0–1764.7 4433.1–4710.2 2722.1–2780.9
200M multi_shared_add 16 1963.9–2008.5 5363.0–5434.7 3625.2–3821.5
Initial verified 20M sweep before independent cold-PCH completion

The regular and warm-PCH cells below are reused in the completed matrix above; they are not additional independent samples. This initial report is retained for provenance.

Verified PCH benchmark: 1–16 outputs (2026-09-15)

Reran the realistic-width sweep at cudf-spark 4f907f0b3d0494276ba069325901a1fe5a40ed97, using JNI 101de1ba68291f10231c8cdcdbadd6cde6e282c9 and cuDF 7edcf282f5e4cea38bb2c18c385e27cdf5a6599b. The plugin was rebuilt with mvn clean install -Dbuildver=352 -DskipTests (all 14 modules successful); JNI was already built. The benchmark JAR adds old-name aliases for the renamed private version-metadata resource so this branch can start with the rebuilt dependency; executable payloads are unchanged.

75 fresh Spark processes / 450 target actions passed plan, checksum, and metric checks. Each process executes one first action and five hot actions. First-action and compiler times are medians across three processes; hot results use the median of the three per-process hot medians.

  • Spark 3.5.2, Java 17, local[4], one RTX 5880 Ada GPU (CUDA_VISIBLE_DEVICES=0).
  • Identical 20-million-row Parquet input, four realized scan partitions, and a GPU aggregate consuming every output.
  • multi_independent: (a + k) * (b + m), with different literals per output.
  • multi_shared_add: (a + b) * (c + k), sharing a + b across outputs.
  • All arms use NVTX-only Nsight Systems profiling, with CPU sampling disabled. Each process has isolated kernel caches; processes run serially.
  • Every JIT process verified NVRTC 12.9, one logical program and one target compiler invocation, and either PCH creation (cold) or PCH reuse (warm). None of the 225 hot JIT actions compiled the target again.
Case Outputs Cold-PCH compile ms Warm-PCH compile ms Regular first ms Warm-PCH JIT first ms Hot Regular/JIT
multi_independent 1 — 560.5 274.2 738.2 1.027x
multi_independent 2 — 682.8 300.1 917.5 1.023x
multi_independent 4 — 890.2 292.6 1147.6 1.087x
multi_independent 8 — 1437.9 396.8 1731.1 1.083x
multi_independent 16 — 2641.6 482.1 3020.4 1.101x
multi_shared_add 1 2239.6 566.1 304.6 805.7 1.033x
multi_shared_add 2 2237.1 708.5 330.6 1001.6 1.042x
multi_shared_add 4 2561.5 794.5 345.9 1073.9 1.021x
multi_shared_add 8 2853.6 1162.1 420.2 1502.6 1.084x
multi_shared_add 16 3536.2 1939.2 489.1 2349.2 1.062x

Hot Regular/JIT > 1 means JIT is faster. A dash means the cold-PCH arm was not measured for that workload. Compile time is target-window rtcx:compile host duration. First/hot action time is wall-clock collect() after planning. Spark startup, the non-JIT GPU bootstrap, the optional distinct one-row PCH probe, and the runtime-version probe are excluded. These are target-action measurements, not total executor-startup latency.

Cold-run interpretation and new metrics

  • Verified warm PCH leaves 0.56–2.64 seconds of compilation. First JIT actions take 0.74–3.02 seconds, versus 0.27–0.49 seconds for regular GPU Project. Limiting outputs to 1–16 does not remove the measured first-use penalty.
  • For shared outputs, cold-PCH compiler medians are 2.24–3.54 seconds. Warm PCH reduces compiler medians by approximately 45–75%, depending on shape.
  • The compiler duration is approximately 92–97% of the first-minus-hot action gap in nine cases, and approximately 102% in the one-output independent case. This is consistent with compilation dominating first use; independently aggregated timing medians are not an exact causal decomposition.
  • At 16 outputs, independent/shared shapes have 48/33 unique plugin operations and compile in 2.64/1.94 seconds with warm PCH. Output count alone does not capture compiler complexity. These are different expression shapes, not an isolated CSE on/off comparison; plugin operation counts are not final libcudf Row IR node counts.
  • Hot regular/JIT ratios are 1.021x–1.101x. Gains are modest, and the smallest differences overlap process variation. Regular tiered Project already computes shared a + b once; JIT fusion primarily avoids intermediate-column traffic in that case.
  • Every JIT action reports four astJitProgramBuildAttempts, zero astJitProgramCacheHits, four astJitEvalAttempts, and 20 million astJitEvalRows. Each task processes one batch, leaving no within-task program-reuse opportunity. Repeated actions construct fresh task-local programs while reusing the native kernel cache.
  • astJitProgramBuildTime includes native cache lookup, compilation, and waiting and is summed across tasks. For 16 independent outputs its first-action median is 10.56 seconds across four tasks, while the trace records one 2.64-second compilation. astJitEvalTime is also task-summed host time and excludes explicit program construction; it is not GPU kernel time. Neither metric should be added to query wall time or the enclosing Project metric.

All output-sum checksums matched across arms, rounds, and actions. GPU scan/Project/aggregate and tier/root assertions passed, and all six JIT metrics were zero in regular runs. The existing PCH verifier's 10 tests also passed.

The measured backend remains CUDA source JIT generated from Row IR. Connecting it to precompiled operator fragments is separate work; this benchmark does not measure LTO or assume a 30-ms compile/link time.

Current-run dispersion and cold-PCH first-action timings
Case Outputs Warm-PCH compile min–max ms Regular hot median (process range) ms JIT hot median (process range) ms
multi_independent 1 499.9–563.3 191.1 (189.4–194.1) 186.2 (183.8–192.9)
multi_independent 2 679.7–693.1 201.4 (201.1–206.5) 197.0 (192.9–204.1)
multi_independent 4 881.8–902.6 218.9 (214.8–223.7) 201.3 (199.6–208.9)
multi_independent 8 1383.9–1458.7 247.8 (247.5–255.5) 228.8 (225.9–237.7)
multi_independent 16 2597.4–2755.2 298.0 (295.0–298.9) 270.6 (265.1–275.7)
multi_shared_add 1 502.4–607.3 228.0 (222.2–231.4) 220.8 (214.8–223.4)
multi_shared_add 2 662.9–734.2 237.8 (228.1–243.3) 228.2 (226.3–238.0)
multi_shared_add 4 756.8–821.4 245.8 (238.5–251.6) 240.7 (240.3–242.0)
multi_shared_add 8 1146.6–1298.6 281.9 (272.1–286.7) 260.1 (257.4–263.5)
multi_shared_add 16 1852.9–1961.5 318.9 (313.8–333.3) 300.3 (298.2–306.7)

Hot ranges describe the three per-process medians, not individual actions. Small hot differences should be interpreted with these ranges.

Outputs Cold-PCH compile median (min–max) ms Cold-PCH first action median (min–max) ms Compiler reduction with warm PCH
1 2239.6 (2209.1–2262.1) 2584.8 (2583.2–2601.4) 74.7%
2 2237.1 (2214.5–2494.0) 2628.3 (2577.2–2894.2) 68.3%
4 2561.5 (2458.0–3124.9) 3018.3 (2855.1–3561.1) 69.0%
8 2853.6 (2813.8–2987.4) 3320.6 (3320.2–3403.4) 59.3%
16 3536.2 (3521.8–3875.2) 4093.6 (4057.4–4389.7) 45.2%
Superseded September 11 width sweep and single-sample PCH validation

These earlier results are retained for comparison. The verified three-process sweep above supersedes their cache-state assumptions and one-hot-action estimates.

Realistic output widths: 1–16 (2026-09-11)

A width sweep measures 1, 2, 4, 8, and 16 output expressions over 20 million Parquet rows. multi_independent evaluates (a + k) * (b + m) with different literals per output; multi_shared_add evaluates (a + b) * (c + k), sharing a + b across outputs. Each tested group has one logical JIT program and one actual compiler invocation, including the 16-output cases.

Each value is the median of three fresh Spark processes on one RTX 5880 Ada GPU (CUDA_VISIBLE_DEVICES=0), Spark 3.5.2, Java 17, local[4], and four realized scan partitions. A GPU aggregate consumes every output. Plan assertions check GPU execution, tiers, JIT roots, and shared-subtree placement; a separate 16-output check passed regular/JIT checksum comparison for both workloads.

These measurements used cudf-spark a0a891c7e99d9e14b71e615c34d3a8c51b12d7e8, JNI 101de1ba68291f10231c8cdcdbadd6cde6e282c9, and cuDF 3e810df236a0af2f082d5066f46dc11eb3335cb5. They are dated benchmark evidence, not a rerun of the latest PR head.

“Prewarmed” means a distinct one-row JIT probe ran before the target, while the target kernel remained cold. The September 11 logs record that probe but do not record per-target PCH reuse or the loaded NVRTC version, so this table does not label that state as verified PCH-warm. “No probe” omits the JIT probe. Both use fresh processes and isolated kernel caches.

Case Outputs No-probe compile ms Prewarmed compile ms Regular first ms Prewarmed JIT first ms Hot Regular/JIT
multi_independent 1 — 515.4 270.7 696.8 0.993x
multi_independent 2 — 677.0 307.1 906.9 0.974x
multi_independent 4 — 905.2 323.7 1162.3 1.009x
multi_independent 8 — 1361.9 367.9 1672.5 1.070x
multi_independent 16 — 2613.8 470.7 2975.3 1.137x
multi_shared_add 1 2200.3 512.5 283.4 747.9 1.076x
multi_shared_add 2 2311.0 649.8 329.1 896.7 1.057x
multi_shared_add 4 2502.4 806.7 361.8 1109.7 0.928x
multi_shared_add 8 2848.6 1125.8 411.3 1462.8 1.053x
multi_shared_add 16 3627.4 1957.7 477.2 2359.4 1.016x

Compile times are target-window rtcx:compile host durations; first-action times are wall-clock collect() durations after planning. Spark startup, non-JIT GPU bootstrap, and the optional JIT probe are excluded. “Hot” immediately repeats the target in the same process; Regular/JIT > 1 means JIT is faster. There is only one hot action per process, so small hot differences are directional rather than a robust throughput estimate. “—” means that arm was not measured. JIT samples were collected with NVTX-only Nsight Systems tracing; the regular baseline was not profiled.

Cold-run interpretation

  • Prewarming leaves 0.51–2.61 seconds of compiler work at 1–16 outputs. Prewarmed JIT first actions take 0.70–2.98 seconds, versus 0.27–0.48 seconds for regular GPU Project. Reducing output width therefore does not eliminate the measured first-use penalty.
  • For shared outputs, no-probe compilation takes 2.20–3.63 seconds. The probe reduces measured compiler time by 46–77%; the historical logs alone cannot attribute the entire reduction specifically to PCH.
  • Compiler durations are approximately 95–99% of the first-minus-hot action gap in nine of ten cases; the one-output independent case is approximately 103%. This is consistent with compilation dominating first use, but a ratio of separate timing medians is not an exact causal decomposition.
  • At 16 outputs, the independent and shared shapes contain 48 and 33 unique plugin operations, respectively, and compile in 2.61 and 1.96 seconds after prewarming. Output count alone does not describe compiler complexity. “Unique ops” uses plugin planner equivalence, not a final libcudf Row IR node count.
  • Hot end-to-end gains are modest in these simple workloads. Regular tiered Project already materializes shared a + b once; keeping it inside the JIT group saves intermediate-column traffic. The observed hot ratios span 0.928x–1.137x.
  • Cache state matters: an unrelated probe, an exact target kernel already in the disk cache, and an exact target kernel already in process memory are different controls. These target-action measurements exclude executor startup and should not be described as total fresh-executor latency.

A separate single-sample validation on 2026-09-15 verified NVRTC 12.9 and per-target PCH reuse for the one-output shared case: compiler time was 490.7 ms with verified warm PCH, versus 2177.6 ms with verified PCH creation in the no-probe control. This supports the remaining first-use-cost observation, but is not a replacement for the three-sample width sweep or verification of its historical PCH state.

The current AST-JIT path still generates and compiles CUDA source from Row IR. Connecting it to precompiled operator fragments remains separate work. No current-path LTO timing or 30-ms-per-program assumption is included in these results.

Earlier benchmarks: warm throughput, 60-output stress cases, cache controls, and historical LTO POC

The following results were collected earlier with the workloads and artifacts described in each subsection. They are retained as historical evidence; the 60-output cases are wider stress controls and should not be generalized to the 1–16-output sweep above.

Warm throughput

The following is a same-process warm-throughput comparison, not a fresh-process cold-run measurement:

  • Spark 3.5.2 using local[4];
  • 2 x NVIDIA RTX 5880 Ada Generation;
  • local RAPIDS/cuDF 26.10 snapshot builds;
  • 20 million Parquet rows in eight partitions;
  • 60 add/multiply outputs in the multi-output workloads;
  • one warmup followed by five measured iterations;
  • a GPU aggregate over every output to force Project materialization;
  • tier/JIT-root shape and cross-mode checksum validation.

The modes are regular GPU Project, per-expression AST JIT, and multi-output AST JIT. Regular/Multi and Per-expression/Multi values above 1 mean multi-output JIT is faster. Project op is the cumulative opTimeLegacy SQL metric across parallel tasks; it is supporting evidence and is not directly comparable to wall-clock time.

Case Regular e2e ms Per-expression JIT e2e ms Multi-output JIT e2e ms Regular/Multi Per-expression/Multi Project op Per-expression/Multi
single_chain 184.364 172.747 180.598 1.021x 0.957x 1.333x
multi_independent 524.398 448.535 456.765 1.148x 0.982x 1.157x
multi_shared_add 517.596 505.969 422.693 1.225x 1.197x 3.018x
mixed_waves 516.696 499.072 452.349 1.142x 1.103x 1.797x

single_chain has only one JIT root, so it does not activate multi-output execution and serves as a noise control. Grouping 60 independent roots is approximately neutral versus per-expression JIT at the end-to-end level in this run.

When all 60 outputs share a + b, keeping that subtree visible to cuDF for cross-output CSE and consolidating execution improves E2E by 19.7% over per-expression JIT and 22.5% over regular GPU Project. The cumulative Project-op per-expression/multi ratio is 3.018x. For the three-wave mixed workload, multi-output improves E2E by 10.3% over per-expression JIT and 14.2% over regular GPU Project.

Because all modes share one Spark process after warmup, process-local compiler and kernel caches may be shared. These results support the steady-state wave/grouping policy but do not establish a cold-start improvement or a general cost policy for operators beyond the add/multiply scope.

Cold start and amortization

A separate benchmark measures first-use latency in fresh Spark processes after an excluded non-JIT GPU bootstrap. The 20-million-row and 200-million-row runs use the same expression shapes and 60-output workloads. Normal Spark file-split planning was retained, with four and 18 realized scan partitions respectively. Each value is the median of three fresh processes, and cross-mode checksums passed.

Cold Regular/JIT and PCH Regular/JIT are speedups: values above 1 mean JIT is faster.

Rows Case Regular first ms JIT cold ms PCH warm ms Cold Regular/JIT PCH Regular/JIT
20M single_chain 272.185 2,247.098 630.429 0.121x 0.432x
20M multi_shared_add 861.804 6,895.533 5,118.893 0.125x 0.168x
20M mixed_waves 878.011 6,269.488 4,716.772 0.140x 0.186x
200M single_chain 1,477.554 3,523.115 1,782.703 0.419x 0.829x
200M multi_shared_add 3,113.963 8,553.500 6,962.021 0.364x 0.447x
200M mixed_waves 3,048.092 8,234.821 6,691.231 0.370x 0.456x

At 20 million rows, legacy AST first-use latency was 342.261 ms, 865.344 ms, and 913.246 ms for the three cases, close to regular Project. Empty-cache AST-JIT is 7.1–8.3x slower than regular Project at 20 million rows and remains 2.4–2.7x slower after increasing the input by 10x. PCH removes 25–80% of the cold-to-hot gap depending on expression shape, but does not make first-use JIT faster than regular Project in these workloads.

An earlier NVRTC 12.8 trace separately measures compiler work. Output expressions counts JIT roots. Both arms enable automatic PCH: PCH cold creates it during the first target compilation, while PCH warm first compiles a distinct one-row probe outside the measured target and keeps the exact target kernel keys cold. Each timing is the median of three fresh Spark processes.

Case Output expressions JIT programs Program shapes (roots / plugin unique ops) PCH-cold rtcx::compile ms PCH-warm per-program ms PCH-warm total ms Prewarm reduction
single_chain 1 1 1 / 2 2,078.443 422.917 422.917 79.7%
multi_shared_add 60 2 32 / 65; 28 / 57 5,633.793 2,163.072; 1,842.760 4,062.130 27.9%
mixed_waves 60 3 32 / 32; 8 / 8; 20 / 20 5,711.994 2,131.079; 661.239; 1,192.695 3,980.035 30.3%

rtcx::compile is the host wall time of NVRTC program creation, compilation, compile-log handling, CUBIN extraction, and program destruction on actual compiler cache misses. It excludes Row IR/source generation, cache lookup and waiting, library loading, and kernel execution. Plugin unique ops uses cudf-spark planner equivalence and is not a final libcudf Row IR node count. Per-program medians are calculated independently, so they do not necessarily sum to the median total.

The estimated cache-miss overhead is JIT cold - exact disk-cache first. The estimates at 20 million rows are 1,968.051 ms, 6,080.500 ms, and 5,403.616 ms; at 200 million rows they are 2,058.447 ms, 6,035.906 ms, and 5,344.617 ms. Every estimate changes by less than 5% after increasing the row count by 10x, indicating that source-JIT cache-miss overhead is primarily a fixed per-expression-shape cost.

Cache-state controls and raw measurements
Rows Case Exact disk-cache first ms Exact in-process hot ms Estimated cache-miss overhead ms PCH gap removed
20M single_chain 279.047 229.914 1,968.051 80.1%
20M multi_shared_add 815.033 537.574 6,080.500 27.9%
20M mixed_waves 865.872 602.104 5,403.616 27.4%
200M single_chain 1,464.668 1,360.673 2,058.447 80.5%
200M multi_shared_add 2,517.594 2,183.139 6,035.906 25.0%
200M mixed_waves 2,890.204 2,498.839 5,344.617 26.9%

Every empty-cache sample uses a new LIBCUDF_KERNEL_CACHE_PATH with LIBCUDF_JIT_DISABLE_CUDA_CACHE=1. PCH warm first compiles a distinct one-row probe while leaving the target kernel key cold; NVRTC logs confirmed PCH reuse by every target kernel. Exact disk cache starts a fresh process with the target kernel already present, while Exact in-process hot immediately repeats the target action in the PCH process. Spark startup, the non-JIT bootstrap, PCH probe, and disk-cache population are excluded from measured target latency.

Historical Row IR/LTO POC. An earlier proof of concept precompiled INT32/INT64 ADD and MUL fragments and linked them with a thin runtime topology. For a 100K-row depth-4 expression, isolated fresh-process/fresh-cache first-call latency fell from approximately 2.33 seconds with a semantically equivalent source-JIT control to 0.12 seconds with LTO, saving approximately 2.21 seconds (95%, 20.2x). Hot kernel throughput was unchanged: the 100M-row, 30-iteration trimmed means were 4.096 ms for LTO and 4.098 ms for source JIT. This is POC evidence rather than a measurement of the current cudf-spark path; the source control used an outer IDENTITY node to force source-JIT fallback. The prototype is available on project-ast-jit-ultimate-perf.

Checklists

Documentation

  • Updated for new or modified user-facing features or behaviors
  • No user-facing change

Testing

  • Added or modified tests to cover new code paths
  • Covered by existing tests
  • Not required

Performance

  • Tests ran and results are added in the PR description
  • Issue filed with a link in the PR description
  • Not required

@thirtiseven

Copy link
Copy Markdown
Collaborator Author

@greptile full review

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@greptile-apps

greptile-apps Bot commented Jul 21, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] No blocking issue was found in the reviewed changes.

Summary

Adds experimental Project AST JIT for non-ANSI integer and long addition and multiplication.

  • Project expressions share one planner for choosing execution backends and tiers.
  • Eligible Project expressions can run in grouped cuDF AST JIT programs.
  • Extracted broadcast join projections can use configured AST backends during task work.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Project expressions] --> B[Check enabled backends]
  B --> C[Build dependency tiers]
  C --> D[Regular GPU expressions]
  C --> E[Interpreted AST expressions]
  C --> F[Grouped JIT expressions]
  F --> G[Build or reuse task-local program]
  D --> H[Ordered output batch]
  E --> H
  G --> H
  G --> I[Close at task completion]
Loading

Reviews (12) · Last reviewed commit: "Merge branch 'main' into project-ast-jit..." · Reviewed by Greptile

Comment thread sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuAstJitExpression.scala Outdated
Comment thread sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuAstJitExpression.scala Outdated
Comment thread integration_tests/src/main/python/ast_test.py
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@thirtiseven

Copy link
Copy Markdown
Collaborator Author

Java binding is in 26.10, waiting for main branch to switch...

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@nvauto

nvauto commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

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.

@sameerz sameerz added the performance A performance related task/issue label Jul 27, 2026
thirtiseven and others added 9 commits July 29, 2026 17:20
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
thirtiseven added a commit that referenced this pull request Aug 4, 2026
…ut] (#15377)

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.projectAstEnabled`
configuration 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 x` instead of using a dedicated
`GpuProjectAstExec` node. This does not change result semantics, APIs,
or configuration.

Implementation details:

- Represent each eligible output with a lightweight
`GpuProjectAstExpression`.
- Keep top-level null literals on the regular projection path so
multiple null outputs can reuse its cached null vector; non-null
literals remain AST-eligible.
- Share one cuDF input `Table` across all AST outputs evaluated for the
same tier and batch.
- Preserve TieredProject common-subexpression elimination for duplicate
outputs and shared subtrees.
- Route fused higher-order-function projections through the same
expression evaluator so AST outputs retain the shared input Table.
- Close compiled legacy AST expressions at task completion, including
retry execution paths.

Performance testing:

- Benchmark script:
[project_ast_per_expression_perf.scala](https://gist.github.com/thirtiseven/f20355b1e8c32444fb11a39fa3717545)
- Environment: Spark 3.5.2 with `local[4]`, plugin `26.08.0-SNAPSHOT`,
and 2 NVIDIA RTX 5880 Ada Generation GPUs.
- Workload: 20 million Parquet rows in 8 partitions, 84 projected
outputs, one warmup, and five measured iterations. A global GPU
aggregate materializes every projected output.
- `LIBCUDF_JIT_ENABLED=0` isolates the legacy AST backend. AST off/on
order and case order are reversed on alternating iterations.
- Speedup is AST off / AST on; values above 1 mean AST is faster. The
Project metric is `opTimeLegacy`, accumulated across tasks, so it is not
directly comparable to E2E wall-clock time.

| Case | Expressions | Coverage / CSE shape | AST tiers (off/on) | AST
outputs (off/on) | AST off/on E2E median (ms) | E2E speedup | AST off/on
Project op median (ms) | Project op speedup |
|---|---|---|---|---:|---:|---:|---:|---:|
| `broad_unique_ast` | 84 unique AST-compatible | 42 AST operator
families | `[0]` / `[84]` | 0 / 84 | 670.249 / 589.848 | 1.136x |
684.796 / 356.251 | 1.922x |
| `whole_output_duplicates` | 42 AST expressions, each projected twice |
whole-output CSE | `[0,0]` / `[42,0]` | 0 / 84 | 601.863 / 526.478 |
1.143x | 355.906 / 155.668 | 2.286x |
| `cheap_partial_cse` | 84 AST-compatible | one shared add | `[0,0]` /
`[1,84]` | 0 / 84 | 516.149 / 467.530 | 1.104x | 214.336 / 189.046 |
1.134x |
| `expensive_partial_cse` | 84 AST-compatible | one shared
transcendental subtree | `[0,0]` / `[1,84]` | 0 / 84 | 476.142 / 440.932
| 1.080x | 239.745 / 175.069 | 1.369x |
| `mixed_half` | 42 AST-compatible + 42 regular GPU | mixed execution |
`[0]` / `[42]` | 0 / 42 | 793.670 / 712.257 | 1.114x | 1167.327 /
868.695 | 1.344x |

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.

| Case | Expressions | Regular/AST E2E median (ms) | E2E speedup |
Regular/AST Project op median (ms) | Project op speedup |
|---|---|---:|---:|---:|---:|
| `literal_only` | 84 unique non-null long literals | 61.418 / 60.952 |
1.008x | 117.137 / 104.202 | 1.124x |
| `mixed_ast_literals` | 42 AST-compatible expressions + 42 unique
non-null long literals | 231.422 / 230.816 | 1.003x | 241.066 / 239.654
| 1.006x |
| `null_literal_only` | 84 duplicate double null literals | 33.257 /
137.528 | 0.242x | 8.224 / 407.906 | 0.020x |

Non-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.

- TPC-H/NDS-H with the standard Decimal schema produced no legacy
Project AST expressions across 24 physical plans.
- TPC-DS produced only three AST expressions across 102 physical plans.
Two operate on a small filtered date dimension, and the remaining
expression is in a post-aggregation Project.

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
- [ ] 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
(Please provide the names of the existing tests in the PR description.)
- [ ] 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

---------

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Co-authored-by: Igor Peshansky <7594381+igorpeshansky@users.noreply.github.com>
@thirtiseven thirtiseven self-assigned this Aug 4, 2026
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@thirtiseven thirtiseven changed the title Add experimental per-expression AST JIT for integral add and multiply Add experimental Project AST JIT for integral add and multiply Aug 4, 2026
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@thirtiseven

Copy link
Copy Markdown
Collaborator Author

@greptile full review

@thirtiseven

Copy link
Copy Markdown
Collaborator Author

I filed #15853 as a follow-up to revisit the multi-output AST JIT group limits once the feature is more mature and supports more operators. For now, this PR uses conservative defaults of 384 unique operations and 32 outputs per group as safety guards.

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@igorpeshansky

Copy link
Copy Markdown
Collaborator

This is still in "draft" — @thirtiseven, is this ready for review?

@thirtiseven

Copy link
Copy Markdown
Collaborator Author

This is still in "draft" — @thirtiseven, is this ready for review?

No, I'm still working on it. I updated the solution and it needs more tests. It also depends on NVIDIA/cudf#23828

rapids-bot Bot pushed a commit to NVIDIA/cudf that referenced this pull request Sep 15, 2026
…on (#23828)

This PR exposes the libcudf AST JIT capabilities introduced by #23615 and #23621 through the Java API, and adds a reusable Java binding for `cudf::transform_program` introduced by #23648.

The execution backend is selected when an expression is compiled:

- `AstExpression.compile()` preserves the existing behavior and produces an expression compatible with default AST consumers.
- `AstExpression.compileJit()` produces a JIT-specific expression. Each `CompiledExpression` owns only the native AST tree and literal representation required by its compilation mode.
- `CompiledExpression.computeColumn(Table)` executes using the compiled expression's mode, avoiding a separate `computeColumnJit` API and avoiding duplicate native AST trees.
- `CompiledExpression.computeTableJit(Table, CompiledExpression...)` evaluates multiple JIT-compiled expressions in one `compute_table_jit` call. Output order follows the supplied expression order, and libcudf Row IR can eliminate structurally equivalent subexpressions across outputs.

For JIT-compiled expressions, literals are converted to one-row columns once during compilation and retained for repeated evaluation. Temporary scalar owners are released after construction completes. Default-compiled expressions retain the existing scalar-backed representation and do not incur the JIT literal-column allocation cost.

The new `AstJitProgram` API exposes reusable AST JIT execution:

- `AstJitProgram.compile(Table, CompiledExpression...)` lowers one or more JIT expressions and retrieves a `cudf::transform_program` specialized to the referenced input-column types and physical nullability.
- `AstJitProgram.computeTable(Table)` reuses the lowered program and retrieved kernel across compatible input tables. Row counts and unreferenced columns may differ between evaluations.
- The program does not retain the schema table or source expressions and remains valid after they are closed.

To support this ownership contract, `cudf::transform_program` now copies `scalar_column_view` literal inputs into program-owned columns. Both default- and JIT-compiled expressions can retain device-backed literal values, so compiled expressions in both modes, along with reusable programs, are registered with the RMM cleanup path.

Reusable programs amortize repeated AST lowering, kernel lookup, and warm dispatch-side overhead. They do not reduce first-use or cold JIT compilation latency.

The binding also:

- rejects compilation-mode mismatches, including passing default-compiled expressions to JIT APIs;
- prevents JIT-compiled expressions from being passed to default-only consumers such as join and scan predicates;
- validates null, closed, and schema-incompatible inputs;
- preserves the lifetime of input tables and compiled expressions across JNI calls.

The `compileJit` and direct multi-output APIs are consumed by NVIDIA/cudf-spark#15312 to execute compatible Project expressions as multi-output AST JIT waves.

Authors:
  - Haoyang Li (https://github.com/thirtiseven)
  - MithunR (https://github.com/mythrocks)

Approvers:
  - Igor Peshansky (https://github.com/igorpeshansky)

URL: #23828
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@nvauto

nvauto commented Sep 29, 2026

Copy link
Copy Markdown
Collaborator

NOTE: release/26.10 has been created from main. Please retarget your PR to release/26.10 if it should be included in the release.

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@thirtiseven
thirtiseven marked this pull request as ready for review October 16, 2026 09:40
@thirtiseven

Copy link
Copy Markdown
Collaborator Author

This PR is ready for review again. It now supports running eligible subtrees with JIT, using cuDF JIT’s CSE, and mixing JIT with legacy AST in the same Project.

I opened #16385 to track the broader AST-JIT work, with #16383 covering cold-start costs and #16384 covering cost-based backend selection.

If this PR feels too large to review, I’m happy to split it up.

@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.

It is looking a lot better, but my AI and I did find two blocking issues.

Comment thread sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuProjectAstPlanner.scala Outdated
Comment thread sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuAstJitExpression.scala Outdated
Comment thread sql-plugin/src/main/scala/com/nvidia/spark/rapids/GpuAstJitExpression.scala Outdated
Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
…tion

- Judge interpreted simple arithmetic on the region left after
  materialization
- Do not materialize a value only for sharing when it would reduce an
  interpreted consumer to simple arithmetic
- Keep planning-time wrappers only when the output's final root runs
  on that backend
- Enable Project AST backends only through explicit binder flags
- Remove Retryable from GpuAstJitExpression
- Report unselected JIT candidates only when JIT is enabled

Signed-off-by: Haoyang Li <haoyangl@nvidia.com>
@thirtiseven thirtiseven changed the title Add experimental Project AST JIT for integral add and multiply [fast-ut] [databricks] Add experimental Project AST JIT for integral add and multiply [databricks] Oct 27, 2026
@thirtiseven

Copy link
Copy Markdown
Collaborator Author

build

@thirtiseven

Copy link
Copy Markdown
Collaborator Author

I'm going to merge this PR. If there are any new comments, I'm happy to address them in follow-ups.

I also reran the cold-start benchmark on the current PR head with the 2026-10-26 cuDF nightly. JIT compilation is now 2–3x faster than in September. The gain comes from cuDF's JIT improvements, not from changes in this PR.

20M rows, multi_independent 2026-09-15 2026-10-27
Warm-PCH compile, 1 output 561 ms 273 ms (2.1x faster)
Warm-PCH compile, 16 outputs 2,642 ms 798 ms (3.3x faster)
Cold-PCH compile, 16 outputs 4,395 ms 2,473 ms (1.8x faster)

PR description updated.

@thirtiseven
thirtiseven merged commit 98d4c78 into NVIDIA:main Oct 28, 2026
71 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

performance A performance related task/issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants