엔터프라이즈 애플리케이션에 대한 요구가 갈수록 커지고 있습니다. 에이전틱 AI는 이러한 요구를 한층 키웠습니다. 방대한 기업 데이터가 필요해졌고, 점점 커지는 데이터셋에 복잡한 쿼리를 자주 실행하게 되었기 때문입니다.
이 요구를 충족하려면 비용이 처리량에 비례해 늘지 않으면서 데이터 처리량을 높여야 합니다. NVIDIA cuDF는 GPU의 대규모 병렬성과 고대역폭 메모리를 데이터 분석에 활용하여 이 문제를 해결합니다. 특히 NVIDIA cuDF plugin for Apache Spark는 빅데이터 처리와 분석 분야의 선도 플랫폼인 Apache Spark에 이러한 가속을 적용하여, 주요 워크로드에서 더 나은 TCO 성능을 달성합니다.
사용자 정의 함수인 UDF는 내부를 알 수 없는 불투명한 특성 때문에 런타임 오버헤드를 일으켜, 가속 쿼리 엔진에 오랫동안 골칫거리였습니다. 더 성능이 좋은 코드로 대체할 수 있는 경우가 많지만, 다시 쓰는 일은 세심한 검증이 필요한 고된 작업이었습니다.
이 글에서는 에이전트로 UDF를 GPU 가속 버전으로 몇 분 만에 다시 작성하여 Apache Spark 작업에서 22배 이상의 속도 향상을 달성하는 방법을 설명합니다. 또한 테스트, 벤치마킹, 통합까지 엔드투엔드 개발 전반에서 UDF 병목을 빠르게 돌파하도록 돕는 스킬 모음인 cuDF skills for Apache Spark를 소개합니다.
UDF의 과제
cuDF가 GPU 가속을 매끄럽게 구현할 수 있는 것은 Apache Spark의 선언형 프로그래밍 인터페이스 덕분이기도 합니다. SQL과 데이터프레임 추상화가 강력한 이유는 쿼리가 무엇을 산출해야 하는지와 시스템이 이를 어떻게 실행할지를 분리하기 때문입니다. 이 구조에서는 cuDF 플러그인이 사용자 코드를 전혀 바꾸지 않고도 GPU의 대규모 SIMD 기능으로 행 단위 연산을 투명하게 병렬화할 수 있습니다.
그러나 Apache Spark는 기본 기능을 확장하는 맞춤 루틴인 UDF도 지원합니다. UDF는 고유한 비즈니스 로직을 구현하는 데 유용하지만 선언형 추상화를 깨뜨립니다. 임의의 사용자 코드가 들어 있어서 시스템이 그 내용을 파악하거나 일반적인 방식으로 병렬화할 수 없기 때문입니다. 그 결과 실행 중에 GPU 가속기는 데이터를 열 기반 형식에서 행 기반 형식으로 변환하고, CPU로 폴백하여 UDF를 행 단위로 실행해야 합니다.

따라서 그 외 구간이 가속되는 쿼리에서도 UDF에는 다음과 같은 단점이 있습니다.
- 폴백 때문에 데이터를 왕복 전송하는 오버헤드가 발생합니다.
- GPU 가속 프리미티브로 작성했다면 얻을 수 있었던 연산 속도 향상을 포기하게 됩니다.
UDF 가속하기
적절한 프리미티브를 쓰면 작고 목적이 분명한 코드 수정만으로 많은 UDF를 GPU로 가속할 수 있는 형태로 다시 쓸 수 있습니다. UDF를 다시 쓰면 가속기 폴백에 따른 오버헤드가 사라지고, 20배가 넘는 속도 향상도 얻을 수 있습니다.
UDF를 다시 쓰는 세 가지 접근법은 다음과 같습니다.
| 접근법 | 복잡도 | 적합한 용도 |
|---|---|---|
| SQL | 낮음, 가장 높은 추상화 수준 | 단순한 관계형 로직. 예: 작거나 인라인화된 함수 |
| cuDF | 중간, 실행에 대한 더 많은 유연성과 제어력 | 대부분의 경우. 예: 문자열 파싱, 중첩 타입 등 적당히 복잡한 로직 |
| CUDA | 높음, 최대의 유연성과 커널 수준 제어 | 가장 복잡하거나 성능이 중요한 로직 |
이 접근법들을 구체적으로 살펴보기 위해, 섭씨 온도를 화씨로 변환하는 간단한 Scala UDF를 예로 들어 보겠습니다.
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
""")
SQL로 UDF 다시 쓰기
단순한 UDF에 가장 직접적인 해법은 UDF를 동일한 Spark SQL 표현식으로 대체하는 것입니다.
spark.sql("""
SELECT
*,
celsius * 9.0 / 5.0 + 32.0 AS fahrenheit
FROM temperatures
""")
이렇게 하면 함수가 Spark의 표현식 트리에 드러납니다. 쿼리 계획 단계에서 cuDF 플러그인이 이 표현식을 대응하는 GPU 구현으로 교체하므로, 데이터와 실행이 모두 GPU에 머뭅니다.
cuDF로 UDF 다시 쓰기
SQL 프리미티브로 부족하거나 SQL을 쓰고 싶지 않다면, cuDF 플러그인이 제공하는 규약인 RapidsUDF 인터페이스로 UDF가 GPU 열에서 직접 동작하게 할 수 있습니다. 이 규약에서 필요한 메서드는 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
}
}
cuDF 플러그인은 RapidsUDF 인터페이스의 인스턴스를 인식하여 GPU에 상주하는 ColumnVector를 evaluateColumnar 함수에 전달합니다. 이 함수는 cuDF 연산용 Java API의 연산을 조합해 열을 병렬로 처리합니다. 이 접근법은 더 복잡한 처리를 위한 풍부한 API를 제공합니다.
CUDA로 UDF 다시 쓰기
기존 cuDF 연산을 조합해서는 효율적으로 구현할 수 없는 매우 복잡한 로직에서는, RapidsUDF가 Java Native Interface인 JNI로 커스텀 CUDA 커널을 호출할 수 있습니다. 코드는 간결함을 위해 생략했으며, CUDA 변환 예시는 cuDF examples for Spark UDFs에서 확인할 수 있습니다.
AI 에이전트로 효율적으로 다시 쓰기
UDF를 다시 쓰면 성능은 좋아지지만, 상당한 엔지니어링 노력이 필요합니다. API를 신중하게 고르고, 원래 UDF와 동작이 같은지 검증하고, 성능을 측정해야 합니다. 벡터화 프로그래밍에 익숙하지 않은 실무자에게는 열 기반 API도 낯설 수 있습니다.
다행히 이 작업의 상당 부분은 코딩 에이전트에 적합합니다. 적절한 가드레일만 갖추면 에이전트는 낯선 API를 검색하고, 재작성 후보를 구현하고, 빠르게 반복할 수 있습니다. 각 변경을 채택할지는 실행 가능한 테스트와 성능 측정 결과로 판단합니다.
제공되는 스킬
cuDF skills for Apache Spark는 재작성 과정을 네 개의 집중된 피드백 루프로 나눕니다. 각 스킬은 수행할 작업과 다음 단계로 넘어가기 전에 필요한 근거를 함께 정의합니다.

스캐폴딩과 설계
에이전트가 안정적으로 실행되려면 안정적인 개발 환경이 먼저 갖춰져야 합니다. 스킬은 엔드투엔드 개발에 필요한 cuDF 및 Spark 전용 스캐폴딩을 담은 완성형 프로젝트 템플릿을 제공합니다. 여기에는 테스트와 벤치마크 하니스, 출력 검증용으로 코드화한 루틴, 빌드 구성이 포함됩니다. 이러한 지식을 코드에 담아 두었으므로, 에이전트는 특화된 인프라를 매번 다시 구성하지 않고 UDF에 집중할 수 있습니다.
스킬은 메모리 관리와 구성을 포함해 cuDF로 GPU 프로그래밍을 할 때의 모범 사례도 제공합니다. 이는 참고용 팁 모음 역할을 합니다. 프런티어 모델의 역량을 최대한 활용하도록, 에이전트는 주변 맥락에 따라 다양한 구현을 자유롭게 탐색하고 측정된 정확성과 성능을 기준으로 접근법을 고릅니다.
성능 결과
한 대형 소비자 기술 기업은 이 워크플로로 프로덕션 데이터 파이프라인에서 쓰던 UDF 30개 이상을 GPU 가속 구현으로 변환했습니다. 재작성한 UDF는 기존 CPU UDF의 동작을 그대로 유지하면서, 그 외에는 GPU로 가속되던 워크로드에서 비용이 큰 폴백을 없앴습니다. 다음 벤치마크는 그러한 워크로드 중 하나에 쓰인 UDF의 전후 성능을 예로 보여 줍니다.

이 UDF는 3,000개가 넘는 작업에 영향을 주고 있었으며, 이 작업에서는 cuDF를 적용한 RapidsUDF로 구현하자 22배 빨라졌습니다. 에이전트 덕분에 이런 성능 향상을 훨씬 쉽게 얻을 수 있어, 실무자는 정확성을 해치지 않으면서 며칠이나 몇 주가 아닌 몇 시간 만에 재작성을 반복할 수 있습니다.
시작하기
UDF를 GPU 구현으로 변환하면 큰 속도 향상을 얻고, GPU 가속 파이프라인 전반의 비용 절감에도 기여할 수 있습니다. 에이전트 스킬을 쓰면 재작성 과정이 쉬워질 뿐 아니라 신뢰할 수도 있습니다.
시작하려면 GitHub에서 cuDF skills for Apache Spark를 직접 설치하거나 npx 스킬 설치 도구로 설치하십시오.
npx skills add NVIDIA/cudf-spark --skill ‘*’
워크플로를 자세히 설명하고 구축 과정에서 얻은 실용적인 교훈을 다룬 Data + AI Summit 세션 Coding with AI Agents to Supercharge Your Spark UDFs with GPU Acceleration도 참고하십시오.
Apache Spark의 GPU 가속을 더 알아보려면 cuDF plugin for Apache Spark 시작 가이드를 따라 Spark 파이프라인 전반의 성능과 TCO를 개선해 보십시오. NVIDIA cuDF에 대한 자세한 내용은 NVIDIA cuDF: GPU-Accelerated Structured Data Processing 웹페이지를 방문하거나 NVIDIA cuDF plugin for Apache Spark 문서를 참고하십시오.