"Spark 에서 GPU 를 쓴다" 는 말에는 성격이 다른 두 가지가 섞여 있다.
| 구분 | 내용 |
|---|---|
| GPU 자원 스케줄링 | Spark 3.0 부터의 기능. executor 와 task 에 GPU 를 자원으로 할당한다. 할당만 할 뿐, 연산을 GPU 로 옮기지는 않는다 |
| SQL·DataFrame 가속 | RAPIDS Accelerator 플러그인이 물리 계획의 일부 연산자를 GPU 구현으로 바꾼다 |
스케줄링만 켜면 딥러닝 프레임워크처럼 사용자 코드가 직접 GPU 를 쓰는 작업에는 도움이 되지만, 평범한 groupBy · join 은 여전히 CPU 에서 돈다. SQL 을 빠르게 하려는 것이라면 플러그인까지 붙여야 한다.
spark-submit \
--conf spark.executor.resource.gpu.amount=1 \
--conf spark.task.resource.gpu.amount=1 \
--conf spark.executor.resource.gpu.discoveryScript=/opt/spark/getGpusResources.sh \
...
spark.task.resource.gpu.amount 를 1 로 두면 task 하나가 GPU 하나를 통째로 잡는다. GPU 한 장을 여러 task 가 나눠 쓰게 하려면 분수로 준다.
spark.executor.resource.gpu.amount=1
spark.task.resource.gpu.amount=0.25 # task 4개가 GPU 1장을 공유
Spark 는 스크립트의 출력을 JSON 으로 파싱한다. GPU 인덱스를 줄 단위로 찍는 스크립트를 쓰면 executor 기동 단계에서 실패한다.
#!/bin/bash
ADDRS=$(nvidia-smi --query-gpu=index --format=csv,noheader | paste -sd, - | sed 's/[^,]*/"&"/g')
echo "{\"name\": \"gpu\", \"addresses\": [$ADDRS]}"
{"name": "gpu", "addresses": ["0", "1"]}
Spark 배포본에 examples/src/main/scripts/getGpusResources.sh 가 들어 있으므로 그것을 쓰는 편이 안전하다. 스크립트는 executor 가 뜨는 모든 노드에 같은 경로로 있어야 하고 실행 권한이 있어야 한다.
플러그인 jar 를 클래스패스에 올리고 플러그인을 등록한다.
spark-submit \
--jars /opt/spark/jars/rapids-4-spark_2.12-<VERSION>.jar \
--conf spark.plugins=com.nvidia.spark.SQLPlugin \
--conf spark.rapids.sql.enabled=true \
--conf spark.executor.resource.gpu.amount=1 \
--conf spark.task.resource.gpu.amount=1 \
--conf spark.executor.resource.gpu.discoveryScript=/opt/spark/getGpusResources.sh \
--conf spark.sql.files.maxPartitionBytes=512m \
app.py
버전 조합이 까다롭다. 플러그인은 Spark 마이너 버전과 CUDA 버전에 맞는 빌드를 골라야 하고, 노드의 드라이버 버전이 그 CUDA 를 지원해야 한다. 셋 중 하나만 어긋나도 기동하지 않거나 조용히 CPU 로 되돌아간다.
실제로 GPU 로 내려갔는지는 실행 계획으로 확인한다. GPU 연산자는 이름 앞에 Gpu 가 붙는다.
df.explain()
== Physical Plan ==
GpuColumnarToRow
+- GpuHashAggregate
+- GpuShuffleCoalesce
Gpu 가 하나도 없으면 가속이 걸리지 않은 것이다. 어떤 연산자가 왜 CPU 로 남았는지는 다음으로 볼 수 있다.
spark.rapids.sql.explain=NOT_ON_GPU
모든 연산이 GPU 에서 되는 것은 아니다. 지원하지 않는 함수, UDF, 특정 타입이 섞이면 그 구간만 CPU 로 돌아가며 그 경계마다 데이터 변환 비용이 든다. 작은 데이터셋에서는 오히려 느려진다.
세션을 어떤 모드로 띄웠는지에 따라 쓰는 자원이 다르다.
| 모드 | 자원 |
|---|---|
local[*] |
세션 컨테이너에 할당된 CPU·메모리·GPU 안에서만 돈다 |
yarn (client) |
드라이버는 세션 컨테이너, executor 는 YARN 클러스터 |
yarn (cluster) |
드라이버와 executor 모두 YARN |
local[*] 도 컨테이너 안에서 코어 수만큼 병렬로 돈다. 다만 그 컨테이너를 넘어가지 못하므로 데이터가 커지면 의미가 없다.
GPU 를 쓰려면 세션을 만들 때 GPU 를 요청한 프로파일로 띄워야 한다. 컨테이너에 GPU 가 붙어 있지 않으면 local 모드에서 아무리 설정을 넣어도 보이지 않는다. YARN 모드로 GPU 를 쓰려면 YARN 쪽에도 GPU 자원이 등록돼 있어야 한다.
import subprocess; print(subprocess.run(['nvidia-smi'], capture_output=True).stdout.decode())
Py4JError: SparkSession$ does not exist in the JVM
GPU 와 무관하다. pyspark 파이썬 패키지 버전과 SPARK_HOME 의 Spark 버전이 다르거나, py4j 버전이 어긋난 것이다. 둘을 맞춘다.
python -c "import pyspark; print(pyspark.__version__, pyspark.__file__)"
echo "$SPARK_HOME"; "$SPARK_HOME"/bin/spark-submit --version
cudf 를 pip 로 설치하려다 실패하는 경우도 흔하다. cuDF 는 PyPI 의 일반 휠로 배포되지 않으며 CUDA 버전에 맞는 채널에서 받아야 한다. SQL 가속이 목적이라면 cuDF 를 직접 설치할 필요 없이 RAPIDS Accelerator jar 만 있으면 된다.