-
Notifications
You must be signed in to change notification settings - Fork 299
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 23 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
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,166 @@ | ||
| /* | ||
| * 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.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.GpuMetric.OP_TIME_LEGACY | ||
| 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 canUseAstJit(expression: GpuExpression): Boolean = | ||
| GpuBatchUtils.isFixedWidth(expression.dataType) && | ||
| expression.supportsAstJit && expression.containsAstJitOperator | ||
|
|
||
| private def asAstJit(child: GpuExpression): Option[GpuAstJitExpression] = child match { | ||
| case jitExpression: GpuAstJitExpression => Some(jitExpression) | ||
| case astExpression: GpuProjectAstExpression => asAstJit(astExpression.child) | ||
| case other if canUseAstJit(other) => Some(GpuAstJitExpression(other)) | ||
| case _ => None | ||
| } | ||
|
|
||
| private[rapids] def wrapTierExpression(expression: Expression): Expression = expression match { | ||
| case alias @ GpuAlias(child: GpuExpression, _) => | ||
| asAstJit(child).map(GpuProjectAstExpression.replaceChild(alias, _)).getOrElse(alias) | ||
| case other => other | ||
| } | ||
|
igorpeshansky marked this conversation as resolved.
|
||
|
|
||
| private[rapids] def wrapProjectExpressions( | ||
| expressions: List[NamedExpression]): List[NamedExpression] = { | ||
| expressions.map(wrapTierExpression(_).asInstanceOf[NamedExpression]) | ||
| } | ||
|
|
||
| /** Extracts a Project AST JIT wrapper after unwrapping any top-level aliases. */ | ||
| private[rapids] def extractTopLevel(expression: Expression): Option[GpuAstJitExpression] = | ||
| GpuProjectAstExpressionBase.extractTopLevel(expression).collect { | ||
| case jitExpression: GpuAstJitExpression => jitExpression | ||
| } | ||
|
|
||
| private def hasJitCandidate(expression: Expression): Boolean = expression.find { | ||
| case gpuExpression: GpuExpression => | ||
| gpuExpression.supportsAstJit && gpuExpression.containsAstJitOperator | ||
| case _ => false | ||
| }.isDefined | ||
|
|
||
| private def finalBackend(expression: Expression): String = { | ||
| GpuProjectAstExpressionBase.extractTopLevel(expression) match { | ||
| case Some(_: GpuAstJitExpression) => "Project AST JIT" | ||
| case Some(_: GpuProjectAstExpression) => "legacy Project AST" | ||
|
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. nit: I don't think this explains it very well, and I am not sure a customer is going to understand. If this is not for a customer to follow, then can we make sure it is documented and drop the Project from it? Something like "AST JIT" and "AST Interpreted" feel better to me. |
||
| case _ => "the regular GPU projection" | ||
| } | ||
| } | ||
|
|
||
| private[rapids] def explainFinalSelections( | ||
| expressionTiers: Seq[Seq[Expression]], | ||
| all: Boolean): String = { | ||
| expressionTiers.zipWithIndex.flatMap { case (expressions, tier) => | ||
| val explanations = expressions.iterator.collect { | ||
| case expression if all || | ||
| (extractTopLevel(expression).isEmpty && hasJitCandidate(expression)) => | ||
| s" $expression final backend: ${finalBackend(expression)}\n" | ||
| }.mkString | ||
| if (explanations.nonEmpty) { | ||
| Some(s" TIER $tier\n$explanations") | ||
| } else { | ||
| None | ||
| } | ||
| }.mkString | ||
| } | ||
| } | ||
|
|
||
| case class GpuAstJitExpression(child: GpuExpression) | ||
|
igorpeshansky marked this conversation as resolved.
igorpeshansky marked this conversation as resolved.
|
||
| extends ShimUnaryExpression with GpuProjectAstExpressionBase | ||
| with GpuMetricsInjectable with Retryable with AutoCloseable { | ||
|
|
||
| @transient private[this] var compiledExpression: CompiledExpression = _ | ||
| private[this] var opTime: GpuMetric = NoopMetric | ||
|
|
||
| override def dataType: DataType = child.dataType | ||
|
|
||
| override def nullable: Boolean = child.nullable | ||
|
|
||
| override def toString: String = s"AST_JIT($child)" | ||
|
|
||
| override def injectMetrics(metrics: Map[String, GpuMetric]): Unit = { | ||
| // OP_TIME_LEGACY is the owning operator's non-RDD timing metric, not a legacy AST metric. | ||
| opTime = metrics.getOrElse(OP_TIME_LEGACY, NoopMetric) | ||
| } | ||
|
|
||
| override def checkpoint(): Unit = { | ||
| getCompiledExpression | ||
| } | ||
|
|
||
| // Compiled ASTs are immutable and remain valid across retry attempts. | ||
| override def restore(): Unit = () | ||
|
|
||
| override def close(): Unit = { | ||
| val toClose = synchronized { | ||
| val current = compiledExpression | ||
| compiledExpression = null | ||
| current | ||
| } | ||
| Option(toClose).foreach(_.safeClose()) | ||
| } | ||
|
|
||
| override def columnarEval(batch: ColumnarBatch): GpuColumnVector = { | ||
| withResource(GpuProjectAstExpression.tableFromBatch(batch)) { table => | ||
| computeColumn(table) | ||
| } | ||
| } | ||
|
|
||
| private[rapids] override def computeColumn(table: Table): GpuColumnVector = { | ||
| val compiled = getCompiledExpression | ||
| NvtxIdWithMetrics(NvtxRegistry.PROJECT_AST_JIT, opTime) { | ||
| closeOnExcept(compiled.computeColumnJit(table)) { result => | ||
| GpuColumnVector.from(result, dataType) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| private def getCompiledExpression: CompiledExpression = synchronized { | ||
| if (compiledExpression == null) { | ||
| val compiled = NvtxIdWithMetrics(NvtxRegistry.COMPILE_AST_JIT, opTime) { | ||
| // Force every bound reference to the left table; Project AST has one input table. | ||
| child.convertToAst(Int.MaxValue).compile() | ||
| } | ||
| closeOnExcept(compiled) { _ => | ||
| var completed = false | ||
| Option(TaskContext.get()).foreach { taskContext => | ||
| onTaskCompletion(taskContext) { | ||
| completed = true | ||
| close() | ||
| } | ||
| } | ||
| if (completed) { | ||
| throw new IllegalStateException( | ||
| "Task completed while registering the AST JIT cleanup callback") | ||
| } | ||
| compiledExpression = compiled | ||
| } | ||
| } | ||
| compiledExpression | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -42,6 +42,19 @@ trait GpuBind { | |
|
|
||
| object GpuBindReferences extends Logging { | ||
|
|
||
| private def explainFinalProjectAstJitSelection( | ||
| tieredProject: GpuTieredProject, | ||
| conf: SQLConf): Unit = { | ||
| val explain = RapidsConf.EXPLAIN.get(conf) | ||
| if (!explain.equalsIgnoreCase("NONE")) { | ||
|
igorpeshansky marked this conversation as resolved.
Outdated
|
||
| val explanation = GpuAstJitExpression.explainFinalSelections( | ||
|
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. [Really optional] Unless the user sets "explain=ALL", none of the post-CSE JIT nodes would appear in the log, and thus it would be hard for them to know that the JIT is actually working. It would be useful for the explainer to show counts of the different kinds of nodes (e.g., "20 JIT nodes, 10 legacy AST nodes, 15 regular project") along with the errors/rejected nodes, either unconditionally or with a new "explain=STATS" setting. Definitely out of this PR's scope, so maybe just file a feature request to track?
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. Agreed, filed #15690 |
||
| tieredProject.exprTiers, explain.equalsIgnoreCase("ALL")) | ||
| if (explanation.nonEmpty) { | ||
| logWarning(s"FINAL PROJECT AST JIT SELECTION\n$explanation") | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * An alternative to `Expression.transformDown`, but when a result is returned by `rule` it is | ||
| * assumed that it handled processing exp and all of its children, so rule will not be called on | ||
|
|
@@ -124,18 +137,19 @@ object GpuBindReferences extends Logging { | |
| } | ||
|
|
||
| /** | ||
| * Binding method for tiered expressions without metric injection. | ||
| * This is for use by GpuBind implementations and should not be called directly | ||
| * from SparkPlan nodes. Use the public API that requires metrics instead, except | ||
| * when absolutely needed. | ||
| * Shared implementation for generic and Project-specific tiered binding. | ||
| * | ||
| * @param enableProjectAstJit whether eligible tiers may use Project AST JIT | ||
| */ | ||
| def bindGpuReferencesTieredNoMetrics[A <: Expression]( | ||
| private def bindGpuReferencesTieredNoMetricsInternal[A <: Expression]( | ||
| expressions: Seq[A], | ||
| input: AttributeSeq, | ||
| conf: SQLConf): GpuTieredProject = { | ||
| conf: SQLConf, | ||
| enableProjectAstJit: Boolean): GpuTieredProject = { | ||
|
|
||
| if (RapidsConf.ENABLE_TIERED_PROJECT.get(conf)) { | ||
| val exprTiers = GpuProjectAstExpression.buildExprTiers(expressions, conf) | ||
| val tieredProject = if (RapidsConf.ENABLE_TIERED_PROJECT.get(conf)) { | ||
| val exprTiers = GpuProjectAstExpression.buildExprTiers( | ||
| expressions, conf, enableProjectAstJit) | ||
| val inputTiers = GpuEquivalentExpressions.getInputTiers(exprTiers, input) | ||
| // Update ExprTiers to include the columns that are pass through and drop unneeded columns | ||
| val newExprTiers = exprTiers.zipWithIndex.map { | ||
|
|
@@ -174,8 +188,44 @@ object GpuBindReferences extends Logging { | |
| } | ||
| GpuTieredProject(tiered) | ||
| } else { | ||
| GpuTieredProject(Seq(GpuBindReferences.bindGpuReferencesNoMetrics(expressions, input))) | ||
| val projectExpressions = if (enableProjectAstJit) { | ||
| expressions.map(GpuAstJitExpression.wrapTierExpression) | ||
| } else { | ||
| expressions | ||
| } | ||
| GpuTieredProject(Seq( | ||
| GpuBindReferences.bindGpuReferencesNoMetrics(projectExpressions, input))) | ||
| } | ||
| if (enableProjectAstJit) { | ||
| explainFinalProjectAstJitSelection(tieredProject, conf) | ||
| } | ||
| tieredProject | ||
| } | ||
|
|
||
| /** | ||
| * Binding method for tiered expressions without metric injection. | ||
| * This is for use by GpuBind implementations and should not be called directly | ||
| * from SparkPlan nodes. Use the public API that requires metrics instead, except | ||
| * when absolutely needed. | ||
| */ | ||
| def bindGpuReferencesTieredNoMetrics[A <: Expression]( | ||
|
igorpeshansky marked this conversation as resolved.
|
||
| expressions: Seq[A], | ||
| input: AttributeSeq, | ||
| conf: SQLConf): GpuTieredProject = { | ||
| bindGpuReferencesTieredNoMetricsInternal( | ||
| expressions, input, conf, enableProjectAstJit = false) | ||
| } | ||
|
|
||
| /** | ||
| * Project-specific tiered binding without metric injection. Unlike the generic binder, | ||
| * this path allows configured Project AST JIT selection. | ||
| */ | ||
| private[rapids] def bindGpuProjectReferencesTieredNoMetrics[A <: Expression]( | ||
|
igorpeshansky marked this conversation as resolved.
|
||
| expressions: Seq[A], | ||
| input: AttributeSeq, | ||
| conf: SQLConf): GpuTieredProject = { | ||
| bindGpuReferencesTieredNoMetricsInternal( | ||
| expressions, input, conf, RapidsConf.ENABLE_PROJECT_AST_JIT.get(conf)) | ||
| } | ||
|
|
||
| // ========== Public "Front Door" APIs (for use by SparkPlan nodes) ========== | ||
|
|
@@ -257,12 +307,32 @@ object GpuBindReferences extends Logging { | |
| bound.injectMetrics(metrics) | ||
| bound | ||
| } | ||
|
|
||
| /** | ||
| * Bind Project expressions in a tiered manner and inject metrics. Project AST JIT selection is | ||
| * confined to this entry point so generic tiered binders do not enable it for other operators. | ||
| * @param expressions The expressions to bind | ||
| * @param input The input schema | ||
| * @param conf SQL configuration | ||
| * @param metrics Metrics to inject into the bound expressions | ||
| */ | ||
| def bindGpuProjectReferencesTiered[A <: Expression]( | ||
|
igorpeshansky marked this conversation as resolved.
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 personally don't like the name. This just like If there are good reasons to keep them separate, can we rename this or modify the original API to take in the JIT/AST enable param? To me that is much cleaner and less confusing. |
||
| expressions: Seq[A], | ||
| input: AttributeSeq, | ||
| conf: SQLConf, | ||
| metrics: Map[String, GpuMetric]): GpuTieredProject = { | ||
| val bound = bindGpuProjectReferencesTieredNoMetrics(expressions, input, conf) | ||
| bound.injectMetrics(metrics) | ||
| bound | ||
| } | ||
| } | ||
|
|
||
| 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})" | ||
|
|
||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.