Velox로 Spark 연산 가속하기
Foundry의 네이티브 가속은 Spark 실행 계획의 지원되는 연산을 C++ 기반 Velox로 넘겨 처리합니다. 전체 파이프라인을 다른 엔진으로 바꾸는 방식과 달리, 지원되는 연산만 가속하고 나머지는 Spark 경로로 실행할 수 있습니다. Python Spark 변환과 Pipeline Builder에서 사용할 수 있습니다.
Python 변환에서 활성화
- Python 저장소를 최신 지원 버전으로 업그레이드합니다.
- 실행기 메모리 프로필과 off-heap 분율 프로필을 함께 구성합니다.
- 계산 백엔드를
ComputeBackend.VELOX로 지정합니다.
다음은 sensor_id와 숫자형 temperature 열을 가진 실제 입력에서 온도가 3을 초과하는 행을 선택하는 예제입니다. 경로는 실제 데이터셋으로 바꾸고 두 프로필을 프로젝트에 가져옵니다.
from transforms.api import ComputeBackend, Input, Output, configure, transform_df
@configure(
["EXECUTOR_MEMORY_MEDIUM", "EXECUTOR_MEMORY_OFFHEAP_FRACTION_HIGH"],
backend=ComputeBackend.VELOX,
)
@transform_df(
Output("/examples/native/filtered_sensors"),
source_df=Input("/examples/native/sensors"),
)
def filter_sensors(source_df):
return source_df.filter("temperature > 3").select("sensor_id", "temperature")
이미 작성한 Spark 로직도 연산을 다시 구현하지 않고 설정으로 가속할 수 있습니다. 어떤 부분을 실제로 넘길 수 있는지는 실행 계획과 지원 연산에 따라 결정됩니다. 프로필 가져오기와 적용은 프로필 사용에서 설명합니다.
Gluten과 Velox의 역할
Foundry는 Apache Gluten을 통해 Spark와 Velox를 연결합니다. Velox는 Arrow 형식의 열 데이터를 처리하는 연산 프리미티브를 제공하고, Gluten은 Spark 연산을 대응하는 네이티브 구현으로 매핑합니다.
일반 빌드처럼 Spark 계획을 만든 뒤 추가 최적화 규칙으로 각 연산을 검사합니다. Velox에 해당 구현이 있고 Gluten에 매핑이 있으면 그 부분을 네이티브 실행 대상으로 표시합니다. SELECT, FILTER, JOIN에 해당하는 연산자 수준에서 판단하므로 계획 전체가 지원될 필요는 없습니다.
태스크 스케줄링, 실행기 조율과 수명주기 관리는 Spark가 계속 담당합니다. 실행기가 네이티브로 표시된 구간에 도달하면 Velox 구현을 호출합니다. 일반적인 사용자 작성 UDF는 대응하는 네이티브 구현이 없어 가속 대상에서 제외됩니다.
실행 계획에서 확인
Spark details의 Query Plan > Physical Plan에서 네이티브 구간과 변환 경계를 확인합니다. 다음은 계획의 형태 예시입니다.
== Physical Plan ==
AdaptiveSparkPlan
+- == Final Plan ==
Execute InsertIntoHadoopFsRelationCommand
+- WriteFiles
+- CollectMetrics
+- VeloxColumnarToRowExec
+- ^ ProjectExecTransformer
+- ^ InputIteratorTransformer
+- ^ InputAdapter
+- ^ RowToVeloxColumnar
+- ^ HashAggregate
+- ^ VeloxColumnarToRowExec
+- ^ AQEShuffleRead
+- ^ ShuffleQueryStage
+- ColumnarExchange
+- ^ ProjectExecTransformer
+- ^ RowToVeloxColumnar
+- * ColumnarToRow
+- BatchScan parquet
ProjectExecTransformer는 일반 ProjectExec와 달리 Velox로 실행하는 연산입니다. 이 형태의 계획에서는 넘긴 노드를 ^로 표시합니다. RowToVeloxColumnar와 VeloxColumnarToRowExec는 Spark 행 표현과 네이티브 열 표현을 바꾸는 경계입니다.
이 직렬화와 역직렬화에도 비용이 있습니다. 가속되는 노드 비율이 낮거나 변환 경계가 자주 반복되면 이점이 작아질 수 있습니다. Pipeline Builder의 체크포인트 같은 기능으로 함께 네이티브 실행할 구간을 묶는 방법도 검토할 수 있지만, 추가 저장 및 실행 비용을 포함하여 실제 계획과 지표를 비교합니다.
성능 차이가 생기는 이유
Spark와 JVM도 코드 생성과 C2 JIT 컴파일러로 최적화합니다. Velox는 다른 방식의 최적화 여지를 제공합니다.
- C++ 코드를 기계어로 미리 컴파일하면서 컴파일 시점의 최적화를 적용합니다.
- 네이티브 데이터 처리 영역에서는 JVM 가비지 컬렉션 부담을 줄일 수 있습니다.
- SSE, AVX와 SIMD 같은 하드웨어 벡터 연산 및 저수준 기능을 명시적으로 활용할 수 있습니다.
이 이점이 모든 파이프라인에서 더 빠른 결과로 이어지는 것은 아닙니다. 지원 연산의 비중과 표현 변환 비용, 데이터 크기에 따라 전체 결과가 달라집니다.
On-heap과 off-heap 메모리
일반 Foundry Spark 구성에서는 기본적으로 off-heap 메모리를 활성화하지 않습니다. Velox가 수정하는 데이터는 JVM 밖에서 접근할 수 있어야 하므로 네이티브 가속에는 off-heap 구성이 필요합니다.
처음에는 EXECUTOR_MEMORY_OFFHEAP_FRACTION_HIGH를 검토할 수 있습니다. 이 프로필의 분율은 80%이며 반드시 EXECUTOR_MEMORY_X 계열을 함께 지정해야 합니다. 프로필 참조의 분율과 실제 실행 설정을 확인하여 조정합니다.
스케줄링, 오케스트레이션과 빌드 관리 등은 여전히 JVM에서 실행되므로 on-heap 메모리도 충분히 남겨야 합니다. off-heap을 최대한 크게 잡는 것만으로 최적 구성이 되지 않습니다. 두 메모리 영역과 변환 경계의 비용을 함께 관측합니다.