Article summary — original
Rewriting Apache Spark UDFs for GPU Acceleration Using AI Agents and NVIDIA cuDF
NVIDIA cuDF plugin accelerates Apache Spark workloads, but CPU-bound user-defined functions (UDFs) break the declarative execution model and trigger slow fallbacks. By using specialized AI agent skills, developers can automate the conversion of UDFs to GPU-accelerated forms, achieving substantial performance gains.
CPU UDFs force Spark to convert GPU columnar batches to row format and fall back to the CPU, causing round-trip transfers and eliminating GPU compute speedups.
UDFs can be rewritten using three approaches depending on complexity: native Spark SQL expressions, cuDF columnar operations via the RapidsUDF interface, or custom CUDA kernels via JNI.
The cuDF skills for Apache Spark provide an agentic workflow across four feedback loops: testing, conversion, benchmarking, and optimization.
In a production deployment at a major consumer technology company, rewriting a CPU UDF across a 600-billion-row workload reduced operator time from 274.55 hours to 12.78 hours—a 22x speedup affecting over 3,000 jobs.
The Spark UDF Bottleneck
Apache Spark's declarative SQL and DataFrame APIs allow the NVIDIA cuDF plugin to parallelize row operations on GPUs transparently. However, opaque user-defined functions (UDFs) cannot be optimized by query planners, forcing data to convert from columnar to row-wise format and execute row-by-row on the CPU. This creates round-trip transfer overhead and forfeits GPU acceleration.
Three Paths to GPU-Accelerated Rewrites
Developers can eliminate fallbacks through three primary rewrite approaches based on logic complexity. Simple operations can be replaced with direct Spark SQL expressions to stay within Spark's expression tree on the GPU. Moderately complex logic can implement the RapidsUDF interface using cuDF's evaluateColumnar method and Java API. Highly complex logic can dispatch to custom CUDA kernels via the Java Native Interface (JNI).
AI Agent Skills and Production Results
Vectorized APIs and manual verification often make rewriting UDFs labor-intensive. The cuDF skills for Apache Spark provide project templates, test harnesses, benchmark infrastructure, and best practices to guide AI agents across testing, conversion, benchmarking, and optimization loops. A major consumer technology company used this framework to rewrite more than 30 production UDFs, achieving a 22x speedup on a 600-billion-row job affecting over 3,000 workloads.
Saved original article
Enterprise applications have become increasingly demanding. Agentic AI has compounded the need for vast amounts of enterprise data with frequent, complex queries across increasingly large datasets.
Meeting that demand requires greater data throughput without a proportional increase in cost.
NVIDIA cuDF
addresses this challenge by leveraging the massive parallelism and high-bandwidth memory of GPUs for data analytics. Particularly, the
NVIDIA cuDF plugin for Apache Spark
brings this acceleration to
Apache Spark
, the industry-leading platform for big data processing and analytics, to achieve
better TCO performance
in major workloads.
User-defined functions (UDFs)
have long been problematic for accelerated query engines,
introducing runtime overheads due to their opaque nature. While UDFs can often be replaced with more performant code, rewriting UDFs has long been a laborious process requiring careful validation.
This post describes how to use agents to rewrite UDFs into GPU-accelerated equivalents within minutes, achieving 22x+ speedups in Apache Spark jobs
. We’ll introduce the
cuDF skills for Apache Spark
, a set of skills to quickly break through the UDF bottleneck for end-to-end development, from testing, to benchmarking, through integration.
The UDF challenge
cuDF achieves seamless GPU acceleration in part due to Apache Spark’s declarative programming interface. The SQL / Dataframe abstractions are powerful because they separate what a query should produce from how the system should execute it. This enables the cuDF plugin to transparently parallelize row-wise operations using the massive
SIMD
capabilities of GPUs, without any change in user code.
Apache Spark, however, also supports UDFs: custom routines to extend built-in functionality. While useful for implementing bespoke business logic, UDFs break the declarative abstraction—they contain arbitrary user code which the system cannot reason about or parallelize in any general sense. During execution, the GPU accelerator must convert the data from a columnar to row-wise format, and fall back to the CPU to execute the UDF row-by-row:
Figure 1.A CPU UDF forces an otherwise GPU-resident pipeline to convert columnar batches to rows, execute on the CPU, and convert back to columnar format
Thus in an otherwise accelerated query, UDFs have the following drawbacks:
The UDF incurs a round-trip data transfer overhead due to fallback.
The UDF forgoes potential compute speedup, had it been written using GPU-accelerated primitives.
Accelerating UDFs
With a small, targeted code change, many UDFs can be rewritten into a GPU-accelerable form by using the right primitives. Rewriting UDFs eliminates the overhead associated with accelerator fallback, and can yield speedups of
over 20x
.
We outline three approaches to rewrite UDFs:
Approach
Complexity
Best For
SQL
Low, highest level of abstraction
Simple relational logic, e.g. small or inlined functions
cuDF
Medium, more flexibility and control over execution
Most cases, e.g. moderately complex logic like string parsing, nested types
CUDA
High, maximum flexibility and kernel-level control
Most complex or performance critical logic
Table 1. Comparison of SQL, cuDF, and CUDA approaches for rewriting Spark UDFs, organized by complexity and best-fit use case
To make these approaches concrete, consider a simple Scala UDF that converts temperature from Celsius to Fahrenheit:
final class ToFahrenheit
extends Function1[java.lang.Double, java.lang.Double]
with Serializable {
override def apply(celsius: java.lang.Double): java.lang.Double = {
if (celsius == null) null
else celsius * 9.0 / 5.0 + 32.0
}
}
spark.udf.register("to_fahrenheit", new ToFahrenheit)
spark.sql("""
SELECT
*,
to_fahrenheit(celsius) AS fahrenheit
FROM temperatures
""")
Rewriting the UDF with SQL
The most direct solution for simple UDFs is to replace the UDF with an equivalent Spark SQL expression:
spark.sql("""
SELECT
*,
celsius * 9.0 / 5.0 + 32.0 AS fahrenheit
FROM temperatures
""")
This makes the function visible in Spark’s expression tree. During query planning, the cuDF plugin will replace these expressions with their corresponding GPU implementations, keeping the data and execution fully on the GPU.
Rewriting the UDF with cuDF
When SQL primitives are insufficient or undesirable, the UDF can instead operate directly on GPU columns using the
RapidsUDF
interface, a contract provided by the cuDF plugin. The contract requires a single method,
evaluateColumnar
:
import ai.rapids.cudf.{ColumnVector, Scalar}
import com.nvidia.spark.RapidsUDF
import scala.util.Using
final class ToFahrenheit
extends Function1[java.lang.Double, java.lang.Double]
with Serializable
with RapidsUDF {
override def apply(celsius: java.lang.Double): java.lang.Double = {
if (celsius == null) null
else celsius * 9.0 / 5.0 + 32.0
}
override def evaluateColumnar(
numRows: Int,
args: ColumnVector*): ColumnVector = {
Using.Manager { use =>
val scale = use(Scalar.fromDouble(9.0 / 5.0))
val offset = use(Scalar.fromDouble(32.0))
val scaled = use(args.head.mul(scale))
scaled.add(offset)
}.get
}
}
The cuDF plugin recognizes instances of the
RapidsUDF
interface and passes a GPU-resident
ColumnVector
to the
evaluateColumnar
function. The function then composes operations from the
Java API for cuDF
operations to process the column in parallel. This approach provides a richer API for more complex processing.
Rewriting the UDF with CUDA
For highly complex logic that cannot be efficiently composed from existing cuDF operations, a
RapidsUDF
can invoke a custom CUDA kernel through the
Java Native Interface (JNI)
. We’ve elided the code for brevity, but examples of CUDA conversions can be found in the
cuDF examples for Spark UDFs
.
Efficient rewrites with AI agents
Despite the performance benefits, UDF rewrites can require significant engineering effort. They demand careful API selection, semantic validation, and performance measurement, while columnar APIs can be unfamiliar to practitioners with less experience in vectorized programming.
Fortunately, much of this work is well suited to coding agents. With proper guardrails, an agent can search unfamiliar APIs, implement candidate rewrites, and iterate quickly while executable tests and performance measurements determine whether each change is acceptable.
Available skills
The cuDF skills for Apache Spark decompose the rewrite into four focused feedback loops. Each skill defines both the work to perform and the evidence required before proceeding to the next stage:
Figure 2. Four skills guide agents through testing, conversion, benchmarking, and optimization, with verification-driven feedback loops at each stage
Scaffolding and design
Reliable agent execution begins with a reliable development environment. The skills provide a complete project template containing the cuDF- and Spark-specific scaffolding required for end-to-end development. This includes test and benchmark harnesses, encoded routines for output verification, and build configuration. By encoding this knowledge into code, the agent can focus on the UDF rather than repeatedly reconstructing specialized infrastructure.
The skills also provide best practices for GPU programming with cuDF, including memory management and configuration. This serves as an advisory tips and tricks reference. To best leverage the power of frontier models, the agent remains free to explore different implementations based on surrounding context and select an approach based on measured correctness and performance.
Performance results
A major consumer technology company used this workflow to convert 30+ UDFs used in production data pipelines into GPU-accelerated implementations. The rewrites preserved the behavior of the original CPU UDFs while removing costly fallbacks from otherwise GPU-accelerated workloads. As an example, the following benchmark shows the resulting before-and-after performance for a UDF in one such workload:
Figure 3. Rewriting a production CPU-based UDF with cuDF reduced operator time from 274.55 to 12.78 hours, a 22x speedup across a 600-billion-row workload
This particular UDF was affecting
more than 3,000+ jobs
, and in this job,
saw a 22x speedup
when implemented as a cuDF-enabled RapidsUDF. With the help of agents, these performance gains are much more attainable, enabling practitioners to iterate on rewrites on the order of hours rather than days or weeks, without compromising correctness.
Get started
Converting UDFs to GPU implementations can unlock major speedups and contribute to overall cost savings in GPU-accelerated pipelines. With agent skills, the rewrite process can be made both
easy and reliable.
To get started, install the
cuDF skills for Apache Spark on GitHub
, either directly or using the
npx
skill installer:
npx skills add NVIDIA/cudf-spark --skill ‘*’
For a deeper walkthrough of the workflow and practical lessons from building it, see the Data + AI Summit session,
Coding with AI Agents to Supercharge Your Spark UDFs with GPU Acceleration
.
To learn more about GPU acceleration for Apache Spark, follow the
cuDF plugin for Apache Spark getting-started guide
to unlock better performance and TCO across your Spark pipelines. Visit our webpage to learn more about
NVIDIA cuDF: GPU-Accelerated Structured Data Processing
, or you can read the docs for more information on
NVIDIA cuDF plugin for Apache Spark
.