-
Notifications
You must be signed in to change notification settings - Fork 301
Add experimental Project AST JIT for integral add and multiply [fast-ut] [databricks] #15312
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 3 commits
a3190f1
9bb4ff2
4f6972a
f75b0c1
b4a1182
6cea0cf
6ed9b58
f3ebe60
b242c73
8ffede9
8fc3fb5
ce448ef
a98a812
c20b430
3cc28a8
555dc68
8adcdcd
9772c07
fabcb01
93b73a8
116528e
59e26f2
1058db8
084c57f
c3a1f6d
3372a30
7dc0992
5b45ce1
9df526a
a0a891c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
| } | ||
|
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 | ||
|
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() | ||
|
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) { | ||
|
igorpeshansky marked this conversation as resolved.
Outdated
|
||
| Option(TaskContext.get()).foreach { taskContext => | ||
| onTaskCompletion(taskContext) { | ||
| close() | ||
| } | ||
| completionRegistered = true | ||
| } | ||
| } | ||
| compiledExpression | ||
| } | ||
|
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 |
|---|---|---|
|
|
@@ -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 | ||
|
igorpeshansky marked this conversation as resolved.
|
||
|
|
||
| final def supportsAstJit: Boolean = selfSupportsAstJit && children.forall { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 { | ||
|
|
||
| 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)) | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.