Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
a3190f1
Add experimental AST JIT for integral add and multiply
thirtiseven Jul 21, 2026
9bb4ff2
Merge remote-tracking branch 'origin/main' into project-ast-jit-infra
thirtiseven Jul 21, 2026
4f6972a
Update copyright years
thirtiseven Jul 21, 2026
f75b0c1
Address AST JIT review feedback
thirtiseven Jul 21, 2026
b4a1182
Enable per-expression legacy AST projection
thirtiseven Jul 24, 2026
6cea0cf
Add signoff
thirtiseven Jul 24, 2026
6ed9b58
Merge branch 'main' into legacy-ast-per-expression
thirtiseven Jul 24, 2026
f3ebe60
small refactor
thirtiseven Jul 24, 2026
b242c73
address comments
thirtiseven Jul 29, 2026
8ffede9
fix tests
thirtiseven Jul 29, 2026
8fc3fb5
address comments
thirtiseven Jul 30, 2026
ce448ef
Document AST compatibility reason invariant
thirtiseven Jul 31, 2026
a98a812
Merge branch 'main' into legacy-ast-per-expression
igorpeshansky Jul 31, 2026
c20b430
Merge branch 'main' into legacy-ast-per-expression
igorpeshansky Aug 1, 2026
3cc28a8
fix scala 2.13 ci
thirtiseven Aug 2, 2026
555dc68
Merge branch 'main' into legacy-ast-per-expression
thirtiseven Aug 3, 2026
8adcdcd
Merge legacy per-expression AST into AST JIT
thirtiseven Aug 3, 2026
9772c07
Merge main into project-ast-jit-infra
thirtiseven Aug 4, 2026
fabcb01
Make Project AST JIT follow expression tiers
thirtiseven Aug 5, 2026
93b73a8
address local comments
thirtiseven Aug 5, 2026
116528e
Fix AST JIT cleanup registration failure
thirtiseven Aug 5, 2026
59e26f2
address comments
thirtiseven Aug 10, 2026
1058db8
address comments
thirtiseven Aug 12, 2026
084c57f
address comments
thirtiseven Aug 17, 2026
c3a1f6d
add nvtx docs
thirtiseven Aug 19, 2026
3372a30
Add multi-output AST JIT project waves
thirtiseven Aug 26, 2026
7dc0992
use new api
thirtiseven Aug 26, 2026
5b45ce1
Bound multi-output AST JIT groups
thirtiseven Sep 1, 2026
9df526a
Reuse AST JIT programs within Spark tasks
thirtiseven Sep 1, 2026
a0a891c
Reuse AST JIT programs for singleton groups
thirtiseven Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions integration_tests/src/main/python/ast_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@
ast_acosh_descr = [(double_gen, not (is_spark_403() or is_spark_412_or_later()))]

_project_ast_enabled_conf = {"spark.rapids.sql.projectAstEnabled": "true"}
_project_ast_jit_enabled_conf = {"spark.rapids.sql.projectAstJitEnabled": "true"}

def assert_gpu_ast(is_supported, func, conf={}):
exist = "GpuProjectAstExec"
Expand Down Expand Up @@ -371,6 +372,37 @@ def test_multiplication(data_descr):
f.lit(-12).cast(data_type) * f.col('b'),
f.col('a') * f.col('b')))

@pytest.mark.parametrize('data_gen', [int_gen, long_gen], ids=idfn)
@disable_ansi_mode
def test_jit_add_multiply(data_gen):
assert_cpu_and_gpu_are_equal_collect_with_capture(
lambda spark: binary_op_df(spark, data_gen).select(
f.col('a') + f.col('b'),
f.col('a') * f.col('b')),
exist_classes=r"GpuProject.*AST_JIT",
non_exist_classes="GpuProjectAst",
conf=_project_ast_jit_enabled_conf)

@disable_ansi_mode
def test_jit_mixed_nested_subexpressions():
assert_cpu_and_gpu_are_equal_collect_with_capture(
lambda spark: binary_op_df(spark, int_gen).select(
(f.col('a') + f.col('b')) - (f.col('a') * f.col('b'))),
exist_classes=r"GpuProject.*AST_JIT.*- AST_JIT",
non_exist_classes="GpuProjectAst",
conf=_project_ast_jit_enabled_conf)

@disable_ansi_mode
def test_jit_mixed_project_expressions():
assert_cpu_and_gpu_are_equal_collect_with_capture(
lambda spark: binary_op_df(spark, int_gen).select(
(f.col('a') + f.col('b')).alias('jit'),
(f.col('a') - f.col('b')).alias('gpu'),
((f.col('a') * f.col('b')) - f.col('a')).alias('mixed')),
exist_classes=r"GpuProject.*AST_JIT.*AS jit.*AS gpu.*AST_JIT.*AS mixed",
non_exist_classes="GpuProjectAst",
conf=_project_ast_jit_enabled_conf)

# Each descriptor contains a list of data generators and a corresponding boolean
Comment thread
thirtiseven marked this conversation as resolved.
# indicating whether that data type is supported by the AST
# all the below desc are not supported by the AST because ANSI mode is on
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.nvidia.spark.rapids

import ai.rapids.cudf.{Scalar, Table}
import ai.rapids.cudf.ast.CompiledExpression
import com.nvidia.spark.Retryable
import com.nvidia.spark.rapids.Arm.{closeOnExcept, withResource}
import com.nvidia.spark.rapids.RapidsPluginImplicits._
import com.nvidia.spark.rapids.ScalableTaskCompletion.onTaskCompletion
import com.nvidia.spark.rapids.shims.ShimUnaryExpression

import org.apache.spark.TaskContext
import org.apache.spark.sql.catalyst.expressions.{Expression, NamedExpression}
import org.apache.spark.sql.types.DataType
import org.apache.spark.sql.vectorized.ColumnarBatch

object GpuAstJitExpression {
private def wrapMaximalSubtrees(expression: Expression): Expression = expression match {
case gpuExpression: GpuExpression
if gpuExpression.supportsAstJit && gpuExpression.containsAstJitOperator =>
GpuAstJitExpression(gpuExpression)
case gpuExpression: GpuExpression =>
gpuExpression.mapChildren {
case child: GpuExpression => wrapMaximalSubtrees(child)
case child => child
}
case other => other
}
Comment thread
igorpeshansky marked this conversation as resolved.

private[rapids] def wrapProjectExpressions(
expressions: List[NamedExpression]): List[NamedExpression] = {
expressions.map(wrapMaximalSubtrees(_).asInstanceOf[NamedExpression])
}
}

case class GpuAstJitExpression(child: Expression)
extends ShimUnaryExpression with GpuExpression with Retryable with AutoCloseable {
require(child.isInstanceOf[GpuExpression], "AST JIT child must be a GPU expression")

@transient private[this] var compiledExpression: CompiledExpression = _
@transient private[this] var completionRegistered = false

override def dataType: DataType = child.dataType

override def nullable: Boolean = child.nullable

override def disableTieredProjectCombine: Boolean = true
Comment thread
igorpeshansky marked this conversation as resolved.

override def toString: String = s"AST_JIT($child)"

override def checkpoint(): Unit = {
getCompiledExpression
}

override def restore(): Unit = closeCompiledExpression()

override def close(): Unit = closeCompiledExpression()
Comment thread
thirtiseven marked this conversation as resolved.
Outdated

override def columnarEval(batch: ColumnarBatch): GpuColumnVector = {
withResource(tableFromBatch(batch)) { table =>
closeOnExcept(getCompiledExpression.computeColumnJit(table)) { result =>
GpuColumnVector.from(result, dataType)
}
}
}

private def getCompiledExpression: CompiledExpression = synchronized {
if (compiledExpression == null) {
compiledExpression = child.asInstanceOf[GpuExpression]
.convertToAst(Int.MaxValue)
.compile()
}
if (!completionRegistered) {
Comment thread
igorpeshansky marked this conversation as resolved.
Outdated
Option(TaskContext.get()).foreach { taskContext =>
onTaskCompletion(taskContext) {
close()
}
completionRegistered = true
}
}
compiledExpression
}
Comment thread
thirtiseven marked this conversation as resolved.
Outdated

private def closeCompiledExpression(): Unit = synchronized {
Option(compiledExpression).foreach(_.safeClose())
compiledExpression = null
}

private def tableFromBatch(batch: ColumnarBatch): Table = {
if (batch.numCols() != 0) {
GpuColumnVector.from(batch)
} else {
withResource(Scalar.fromBool(false)) { falseScalar =>
withResource(ai.rapids.cudf.ColumnVector.fromScalar(falseScalar, batch.numRows())) {
falseColumn => new Table(falseColumn)
}
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2025, NVIDIA CORPORATION.
* Copyright (c) 2019-2026, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -268,6 +268,8 @@ case class GpuBoundReference(ordinal: Int, dataType: DataType, nullable: Boolean
(val exprId: ExprId, val name: String)
extends GpuLeafExpression with ShimExpression {

override def selfSupportsAstJit: Boolean = true

override def toString: String =
s"input[$ordinal, ${dataType.simpleString}, $nullable]($name#${exprId.id})"

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,21 @@ trait GpuExpression extends Expression {
def convertToAst(numFirstTableColumns: Int): ast.AstExpression =
throw new IllegalStateException(s"Cannot convert ${this.getClass.getSimpleName} to AST")

def selfSupportsAstJit: Boolean = false

def selfIsAstJitOperator: Boolean = false
Comment thread
igorpeshansky marked this conversation as resolved.

final def supportsAstJit: Boolean = selfSupportsAstJit && children.forall {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I get that you are being conservative right now. But I am concerned that this is adding in a lot of code that we are going to have to rip out when we actually do it right. Currently If I have an expression tree like A + B + C that can all but JIT, then we do the JIT. But if I have (A + B + C) / D why would we not want to do the JIT for A + B + C still? This is why I think a two pass optimization is a much better path for this. First pass would be to go through each expression and see if (it by itself) could be JIT or AST or neither. The second pass would be to do cost reduction estimation. For the Bridge it is all about data movement. Here it would be about data materialization cost and possibly reduce JIT vs execution costs. I know that will take a lot of experimentation to understand these costs, but having the framework in place is much better than just do it if we can with all or nothing.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Totally agreed. If we could partially enable the AST JIT inside the expression, that would be better. Also, since the multi-output and CSE NVIDIA/cudf#23621 have been merged, we might need to adjust some design decisions here. I'm converting this to a draft now to test more solutions...

case child: GpuExpression => child.supportsAstJit
case _: AttributeReference => true
case _ => false
}

final def containsAstJitOperator: Boolean = selfIsAstJitOperator || children.exists {
case child: GpuExpression => child.containsAstJitOperator
case _ => false
}

/** Could evaluating this expression cause side-effects, such as throwing an exception? */
def hasSideEffects: Boolean =
children.exists {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1247,6 +1247,12 @@ val GPU_COREDUMP_PIPE_PATTERN = conf("spark.rapids.gpu.coreDump.pipePattern")
.booleanConf
.createWithDefault(false)

val ENABLE_PROJECT_AST_JIT = conf("spark.rapids.sql.projectAstJitEnabled")
.doc("Enable the experimental cuDF JIT backend for supported project AST expressions.")
.internal()
.booleanConf
.createWithDefault(false)

val ENABLE_TIERED_PROJECT = conf("spark.rapids.sql.tiered.project.enabled")
.doc("Enable tiered projections.")
.internal()
Expand Down Expand Up @@ -3639,6 +3645,8 @@ class RapidsConf(conf: Map[String, String]) extends Logging {

lazy val isProjectAstEnabled: Boolean = get(ENABLE_PROJECT_AST)

lazy val isProjectAstJitEnabled: Boolean = get(ENABLE_PROJECT_AST_JIT)

lazy val isTieredProjectEnabled: Boolean = get(ENABLE_TIERED_PROJECT)

lazy val isCombinedExpressionsEnabled: Boolean = get(ENABLE_COMBINED_EXPRESSIONS)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ class GpuProjectExecMeta(
// Force list to avoid recursive Java serialization of lazy list Seq implementation
val gpuExprs = childExprs.map(_.convertToGpu().asInstanceOf[NamedExpression]).toList
val gpuChild = childPlans.head.convertIfNeeded()
if (conf.isProjectAstJitEnabled) {
val jitProjectList = GpuAstJitExpression.wrapProjectExpressions(gpuExprs)
if (jitProjectList.exists(_.find(_.isInstanceOf[GpuAstJitExpression]).isDefined)) {
return GpuProjectExec(jitProjectList, gpuChild)
}
}
if (conf.isProjectAstEnabled) {
// cuDF requires return column is fixed width
val allReturnTypesFixedWidth = gpuExprs.forall(e => GpuBatchUtils.isFixedWidth(e.dataType))
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2019-2025, NVIDIA CORPORATION.
* Copyright (c) 2019-2026, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -645,6 +645,8 @@ object GpuLiteral {
*/
case class GpuLiteral (value: Any, dataType: DataType) extends GpuLeafExpression {

override def selfSupportsAstJit: Boolean = true

// Assume this came from Spark Literal and no need to call Literal.validateLiteralValue here.

override def foldable: Boolean = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,11 @@ abstract class GpuAddBase extends CudfBinaryArithmetic with Serializable {
override def binaryOp: BinaryOp = BinaryOp.ADD
override def astOperator: Option[BinaryOperator] = Some(ast.BinaryOperator.ADD)

override def selfSupportsAstJit: Boolean =
!failOnError && (dataType == IntegerType || dataType == LongType)

override def selfIsAstJitOperator: Boolean = selfSupportsAstJit
Comment thread
igorpeshansky marked this conversation as resolved.
Outdated

override def hasSideEffects: Boolean =
(failOnError && GpuAnsi.needBasicOpOverflowCheck(dataType)) || super.hasSideEffects

Expand Down Expand Up @@ -768,6 +773,11 @@ case class GpuMultiply(
override def binaryOp: BinaryOp = BinaryOp.MUL
override def astOperator: Option[BinaryOperator] = Some(ast.BinaryOperator.MUL)

override def selfSupportsAstJit: Boolean =
!failOnError && (dataType == IntegerType || dataType == LongType)
Comment thread
igorpeshansky marked this conversation as resolved.
Outdated

override def selfIsAstJitOperator: Boolean = selfSupportsAstJit

private def multiplyOverflowError(msg: String): ArithmeticException = {
RapidsErrorUtils.arithmeticOverflowError(msg, origin)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Copyright (c) 2026, NVIDIA CORPORATION.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.nvidia.spark.rapids

import org.scalatest.funsuite.AnyFunSuite

import org.apache.spark.sql.catalyst.expressions.AttributeReference
import org.apache.spark.sql.rapids.{GpuAdd, GpuMultiply, GpuSubtract}
import org.apache.spark.sql.types.{FloatType, IntegerType, LongType}

class GpuProjectAstJitSuite extends AnyFunSuite {
private def reference(ordinal: Int, dataType: org.apache.spark.sql.types.DataType) =
AttributeReference(s"c$ordinal", dataType, nullable = true)()

private def alias(expression: GpuExpression, name: String) = GpuAlias(expression, name)()

test("project AST JIT is disabled by default") {
assert(!new RapidsConf(Map.empty[String, String]).isProjectAstJitEnabled)
}

test("project AST JIT supports non-ANSI integral add and multiply") {
val left = reference(0, LongType)
val right = reference(1, LongType)
val expression = alias(
GpuMultiply(
GpuAdd(left, right, failOnError = false)(),
right,
failOnError = false)(),
"result")

val wrapped = GpuAstJitExpression.wrapProjectExpressions(List(expression))
val jit = wrapped.head.asInstanceOf[GpuAlias].child.asInstanceOf[GpuAstJitExpression]
assert(jit.child.isInstanceOf[GpuMultiply])
assert(jit.child.find(_.isInstanceOf[GpuAstJitExpression]).isEmpty)
}

test("project AST JIT wraps maximal nested subtrees independently") {
val left = reference(0, IntegerType)
val right = reference(1, IntegerType)
val expression = alias(
GpuSubtract(
GpuAdd(left, right, failOnError = false)(),
GpuMultiply(left, right, failOnError = false)(),
failOnError = false)(),
"result")

val wrapped = GpuAstJitExpression.wrapProjectExpressions(List(expression))
val subtract = wrapped.head.asInstanceOf[GpuAlias].child.asInstanceOf[GpuSubtract]
assert(subtract.left.asInstanceOf[GpuAstJitExpression].child.isInstanceOf[GpuAdd])
assert(subtract.right.asInstanceOf[GpuAstJitExpression].child.isInstanceOf[GpuMultiply])
}

test("project AST JIT excludes ANSI and floating point arithmetic") {
val intLeft = reference(0, IntegerType)
val intRight = reference(1, IntegerType)
val floatLeft = reference(0, FloatType)
val floatRight = reference(1, FloatType)

val ansiAdd = alias(GpuAdd(intLeft, intRight, failOnError = true)(), "ansi_sum")
val floatMultiply = alias(
GpuMultiply(floatLeft, floatRight, failOnError = false)(), "float_product")

val wrapped = GpuAstJitExpression.wrapProjectExpressions(List(ansiAdd, floatMultiply))
assert(wrapped.forall(_.find(_.isInstanceOf[GpuAstJitExpression]).isEmpty))
}
}
Loading