GPU 加速计算引擎
云原生数据仓库 AnalyticDB MySQL 版(以下简称 AnalyticDB for MySQL)的 Apache Spark 支持通过 NVIDIA cuDF 插件(NVIDIA cuDF plugin for Apache Spark,原 RAPIDS Accelerator for Apache Spark)使用 GPU 加速 DataFrame 与 SQL 计算。在保持 Spark 原生 API 与 SQL 语义基本不变的前提下,扫描、过滤、聚合、Join、排序、Shuffle 等算子可整体下推到 GPU 执行,从而在大数据量 ETL 与交互式分析场景中获得数倍的性能提升与更优的单位成本。
本文介绍 GPU 加速计算引擎的原理、适用场景、前提条件、性能实测数据,以及 SQL 作业与 Jar/Python 作业的完整配置示例。
功能介绍
什么是 cuDF Spark 插件
cuDF Spark 插件是 NVIDIA 提供的一组 Spark 插件,它把 Spark SQL 的物理执行计划中可加速的算子替换为基于 GPU 的实现,底层依赖 RAPIDS cuDF 列式计算库与 UCX 通信库:
cuDF:NVIDIA 开源的 GPU DataFrame 库,提供列式数据的过滤、聚合、Join、排序等原语,与 Apache Arrow 内存格式兼容。
UCX:高性能通信框架,插件内置基于 UCX 的加速 Shuffle,在具备条件的集群中可利用 GPU-to-GPU 通信与 RDMA 能力。
自动回退(Fallback):对于 GPU 暂不支持的算子或数据类型,插件会自动回退到 Spark CPU 实现,作业无需修改即可正常运行。
插件以 spark.plugins=com.nvidia.spark.SQLPlugin 的方式挂载到 Spark 上,通过 rapids-4-spark 插件 JAR 生效。该 JAR 可以预置在 Spark classpath 中,也可以随作业一起提交。
NVIDIA cuDF plugin for Apache Spark 的前身是 RAPIDS Accelerator for Apache Spark,RAPIDS 这一名称将逐步停用,spark-rapids相关的 GitHub 链接会重定向到 cudf-spark,Maven 制品名保持不变,配置项仍以 spark.rapids.*为前缀。
GPU 加速为什么有效
传统 CPU 计算受限于核数与内存带宽,在 AI(DL/ML)驱动的数据分析场景中,既难以满足时延要求,也难以控制成本。GPU 拥有大量并行计算单元与高带宽显存,天然适配列式数据的批量向量化处理:
扫描 Parquet/ORC 等列式文件时,可在 GPU 上完成解压、解码与过滤,减少 CPU 与内存拷贝开销。
Hash Join、Hash Aggregate、Sort 等算子以列批(columnar batch)为单位在显存中执行,避免频繁的行式转换。
配合多线程读(MULTITHREADED reader)与加速 Shuffle,可显著提高 OSS 读取与网络吞吐的利用率。
适用场景
大规模 ETL / 数据加工作业:TPC-H、数据仓库分层加工、宽表构建、明细数据清洗等,扫描量与 Shuffle 量大,GPU 加速收益明显。
交互式分析与 Ad-hoc 查询:需要更低时延的 SQL 分析,尤其是包含多表 Join、大聚合、排序的查询。
AI 与数据分析融合场景:在同一个 Spark 作业中完成数据处理与模型训练/推理前的特征加工,减少 CPU-GPU 数据搬运。
成本敏感场景:在单位算力成本可接受的前提下,通过更短的运行时长降低总成本。
性能与成本实测(TPC-H SF1K)
以下为 AnalyticDB for MySQL Spark 在 TPC-H SF1K(约 1 TB)数据集上的实测数据,数据读取自 OSS(Parquet 格式),warm cache 场景,22 条 Query 串行执行(Query 15 的两个子查询合并为一条逻辑查询统计)。
测试环境与成本模型
项目 | GPU 方案 | CPU 方案 |
计算规格 | 2 × ADB.MLLarge.2 | 2 × 2xlarge |
加速引擎 | NVIDIA cuDF 插件(RAPIDS) | Gluten / Velox |
存储与 Shuffle | ADB LakeCache、RSS | ADB LakeCache、RSS |
整体结论
GPU方案整体性价比是CPU的1.54倍
分 Query 明细
Query | 性价比 |
query1 | 1.62 倍 |
query2 | 1.03 倍 |
query3 | 1.60 倍 |
query4 | 2.30 倍 |
query5 | 0.88 倍 |
query6 | 2.78 倍 |
query7 | 1.66 倍 |
query8 | 0.90 倍 |
query9 | 0.82 倍 |
query10 | 1.65 倍 |
query11 | 0.88 倍 |
query12 | 1.86 倍 |
query13 | 2.07 倍 |
query14 | 2.50 倍 |
query15 | 2.65 倍 |
query16 | 1.47 倍 |
query17 | 1.62 倍 |
query18 | 1.63 倍 |
query19 | 2.60 倍 |
query20 | 1.82 倍 |
query21 | 3.08 倍 |
query22 | 1.78 倍 |
22 条 Query 中有 18 条在 GPU 上实现了成本下降或持平,其中 16 条性价比提升 1.5 倍以上;query5、query8、query9、query11 这 4 条由于 GPU 加速比低于机型单价比,成本略有上升。整体来看,作业越偏向大表扫描、聚合与 Join(如 query21、query15、query19、query14、query6),GPU 收益越大。
前提条件
集群的产品系列为企业版、基础版或湖仓版,且已创建 Job 型资源组。
GPU 加速依赖带 NVIDIA GPU 的计算规格,提交作业时需将
spark.executor.resourceSpec指定为 GPU 规格(本文示例为ADB.MLLarge.2,即搭载 NVIDIA A10 GPU 的规格)。集群内每个 Worker 节点都需要具备 NVIDIA GPU 与相应的 NVIDIA 驱动,AnalyticDB for MySQL Spark 的 GPU 规格已内置驱动与 CUDA 运行环境,您无需自行安装。已创建数据库账号。如果是通过阿里云账号访问,只需创建高权限账号;如果是通过 RAM 用户访问,需要创建高权限账号和普通账号,并且将 RAM 用户绑定到普通账号上。
已为 RAM 用户授予
AliyunADBFullAccess、AliyunADBSparkProcessingDataRole权限以及 AnalyticDB for MySQL 库表的读写权限,且具备作业所访问 OSS Bucket 的读写权限。具体操作,请参见账号授权。已获取 cuDF Spark 插件 JAR(
rapids-4-spark_2.12-<版本号>.jar,本文示例为rapids-4-spark_2.12-26.08.0.jar),并上传至作业可访问的 OSS 路径。
注意事项
算子回退:GPU 不支持的算子、表达式或数据类型会自动回退到 CPU 执行,可能引入 GPU 与 CPU 之间的列式/行式数据转换开销。可通过
spark.rapids.sql.explain=NOT_ON_GPU在 Driver 日志中查看未下推到 GPU 的原因。数据格式:GPU 加速的收益高度依赖列式存储。建议输入与输出均使用 Parquet 或 ORC 格式;文本类格式(CSV、JSON)与行式数据源加速效果有限。
计算结果差异:插件力求与原生 Spark 保持逐位一致,但以下场景存在可预期差异,请评估业务影响。
round、bround:由于二进制浮点无法精确表示部分十进制小数,GPU 与 CPU 的结果可能相差一个最小单位。例如round(1.025, 2)在原生 Spark 中为 1.03,在 cuDF 插件中为 1.02。浮点数学函数(
sin、cos等):结果差异在浮点舍入误差范围内。输出顺序:聚合、Join 等不保证顺序的算子,其输出行顺序可能与 CPU 不同;显式
ORDER BY的语义保持兼容,但在排序键存在并列值时顺序可能不同。排序稳定性:插件默认使用非稳定的 out-of-core 排序(支持显存不足时溢写)。如业务依赖稳定排序,需设置
spark.rapids.sql.stableSort.enabled=true。
显存与内存规划:GPU 显存有限,需合理设置
spark.rapids.sql.concurrentGpuTasks(单 GPU 上并发执行的 GPU Task 数)与spark.rapids.memory.pinnedPool.size(锁页内存池大小),并为宿主机预留足够的 off-heap 内存(spark.executor.memoryOverhead、spark.rapids.memory.host.offHeapLimit.size)用于 Shuffle 缓冲与溢写。显存不足时会触发溢写到主机内存乃至本地磁盘,性能下降,因此建议为 Executor 配置较大的本地盘(spark.adb.executorDiskSize)。动态分配:GPU 规格资源相对稀缺且启动成本较高,本文示例关闭了动态分配(
spark.dynamicAllocation.enabled=false),以固定 Executor 数量运行,便于容量与成本规划。序列化:必须使用 Kryo 序列化并注册插件提供的 Registrator(
spark.kryo.registrator=com.nvidia.spark.rapids.GpuKryoRegistrator),否则 GPU 相关对象无法正常序列化。
使用流程
步骤一:上传插件 JAR 到 OSS
获取 cuDF Spark 插件 JAR 包(rapids-4-spark_2.12-26.08.2-SNAPSHOT-cuda12.jar),上传至作业可访问的 OSS Bucket,本文示例路径为 oss://testBucketName/adb/Spark/jars/rapids-4-spark_2.12-26.08.0.jar。具体操作,请参见简单上传。
JAR 版本需与 Spark 版本、Scala 版本匹配。AnalyticDB for MySQL Spark 3.5 对应 Scala 2.12 制品,即 rapids-4-spark_2.12-*.jar。
步骤二:配置 GPU 资源与插件参数
GPU 加速的配置可分为四类:ADB Spark 资源配置、GPU 资源切分、cuDF 插件核心参数、Shuffle 与内存参数。完整参数说明请参见本文的配置参数说明章节。
步骤三:提交作业
SQL 作业配置示例
登录云原生数据仓库AnalyticDB MySQL控制台,在左上角选择集群所在地域。
在左侧导航栏,单击集群列表,然后单击目标集群 ID。
在左侧导航栏,单击。
在 SQL Console 窗口,选择 Spark 引擎和 Job 型资源组,执行以下配置与 SQL:
set spark.adb.version=3.5; set spark.task.resource.gpu.amount = 0.0625; set spark.rapids.sql.concurrentGpuTasks = 4; set spark.rapids.shuffle.mode = MULTITHREADED; set spark.rapids.shuffle.multiThreaded.reader.threads=32; set spark.rapids.shuffle.multiThreaded.writer.threads=32; set spark.rapids.shuffle.multithreaded.skipMerge=true; set spark.executor.resource.gpu.discoveryScript = /opt/spark/examples/src/main/scripts/getGpusResources.sh; set spark.executor.memoryOverhead = 48gb; set spark.kryo.registrator = com.nvidia.spark.rapids.GpuKryoRegistrator; set spark.rapids.memory.host.spillStorageSize = 32G; set spark.rapids.memory.pinnedPool.size = 16g; set spark.rapids.sql.metrics.level = DEBUG; set spark.executor.resource.gpu.vendor = nvidia.com; set spark.executor.instances = 2; set spark.executor.resourceSpec = ADB.MLLarge.2; set spark.adb.executor.gpu.allocate.unit=1; set spark.driver.resourceSpec = large; set spark.adb.executorDiskSize = 100Gi; set spark.sql.adaptive.advisoryPartitionSizeInBytes = 160mb; set spark.sql.adaptive.coalescePartitions.minPartitionSize = 32mb; set spark.rapids.memory.host.offHeapLimit.size = 45g; set spark.serializer = org.apache.spark.serializer.KryoSerializer; set spark.shuffle.service.enabled = false; set spark.rapids.sql.multiThreadedRead.numThreads = 200; set spark.rapids.memory.host.offHeapLimit.enabled = true; set spark.plugins = com.nvidia.spark.SQLPlugin; set spark.sql.adaptive.enabled = true; set spark.rapids.sql.format.parquet.reader.type = MULTITHREADED; set spark.driver.maxResultSize = 2GB; set spark.dynamicAllocation.enabled = false; set spark.executorEnv.LD_PRELOAD = /usr/local/lib/libjemalloc.so; -- 填写实际的oss路径 add jar oss://path/to/rapids-4-spark_2.12-26.08.0.jar; -- 业务 SQL CREATE DATABASE IF NOT EXISTS test_db; CREATE TABLE IF NOT EXISTS test_db.orders_gpu USING parquet LOCATION 'oss://testBucketName/adb/Spark/tpch/orders' AS SELECT l_orderkey, SUM(l_extendedprice) AS total_price FROM test_db.lineitem WHERE l_shipdate >= DATE '1995-01-01' GROUP BY l_orderkey;
Jar / Python 作业配置示例
通过 Jar 或 Python 方式提交作业时,将插件 JAR 放入 jars,配置项放入 conf:
{
"jars": [
"oss://path/to/rapids-4-spark_2.12-26.08.0.jar"
],
"conf": {
"spark.adb.version": "3.5",
"spark.task.resource.gpu.amount": "0.0625",
"spark.rapids.sql.concurrentGpuTasks": "4",
"spark.rapids.shuffle.mode": "MULTITHREADED",
"spark.rapids.shuffle.multiThreaded.reader.threads": "32",
"spark.rapids.shuffle.multiThreaded.writer.threads": "32",
"spark.rapids.shuffle.multithreaded.skipMerge": "true",
"spark.executor.resource.gpu.discoveryScript": "/opt/spark/examples/src/main/scripts/getGpusResources.sh",
"spark.executor.memoryOverhead": "48gb",
"spark.kryo.registrator": "com.nvidia.spark.rapids.GpuKryoRegistrator",
"spark.rapids.memory.host.spillStorageSize": "32G",
"spark.rapids.memory.pinnedPool.size": "16g",
"spark.rapids.sql.metrics.level": "DEBUG",
"spark.executor.resource.gpu.vendor": "nvidia.com",
"spark.executor.instances": "2",
"spark.executor.resourceSpec": "ADB.MLLarge.2",
"spark.adb.executor.gpu.allocate.unit": "1",
"spark.driver.resourceSpec": "large",
"spark.adb.executorDiskSize": "100Gi",
"spark.sql.adaptive.advisoryPartitionSizeInBytes": "160mb",
"spark.sql.adaptive.coalescePartitions.minPartitionSize": "32mb",
"spark.rapids.memory.host.offHeapLimit.size": "45g",
"spark.serializer": "org.apache.spark.serializer.KryoSerializer",
"spark.shuffle.service.enabled": "false",
"spark.rapids.sql.multiThreadedRead.numThreads": "200",
"spark.rapids.memory.host.offHeapLimit.enabled": "true",
"spark.plugins": "com.nvidia.spark.SQLPlugin",
"spark.sql.adaptive.enabled": "true",
"spark.rapids.sql.format.parquet.reader.type": "MULTITHREADED",
"spark.driver.maxResultSize": "2GB",
"spark.dynamicAllocation.enabled": "false",
"spark.executorEnv.LD_PRELOAD": "/usr/local/lib/libjemalloc.so"
}
}Python(PySpark)作业中,只有 DataFrame/SQL API 的计算能被 GPU 加速;Pandas UDF 与部分 Python UDF 需依赖插件的加速 UDF 能力,普通 Python UDF 会回退到 CPU 执行。
步骤四:验证 GPU 加速是否生效
在 Spark UI 的 SQL 页签中查看物理执行计划,若出现
GpuRowToColumnar、GpuColumnarToRow、GpuHashAggregate、GpuBroadcastHashJoin、GpuSort等以 Gpu 开头的算子,说明算子已成功下推到 GPU。在 Executor 页签中确认 Executor 已分配到 GPU 资源(
Total Cores旁的 GPU 资源列有值)。在 Driver 日志中搜索关键字
RAPIDS或cuDF,确认插件已加载并输出版本信息;如需查看回退原因,临时设置set spark.rapids.sql.explain=NOT_ON_GPU;后重跑作业。通过
spark.rapids.sql.metrics.level=DEBUG打开完整 GPU 指标,在 SQL 页签中查看各算子的 GPU 耗时与数据量,用于定位瓶颈。
配置参数说明
参数 | 示例值 | 描述 |
| 3.5 | AnalyticDB for MySQL Spark 引擎版本,需与插件 JAR 支持的 Spark 版本一致。 |
| ADB.MLLarge.2 | Executor 计算规格。GPU 加速必须选择带 NVIDIA GPU 的规格,更多GPU规格可参考文档:AI节点 中 AI Node节点规格。 |
| large | Driver 计算规格。Driver 不执行 GPU 算子,通常无需 GPU 规格。 |
| 2 | Executor 数量。 |
| 100Gi | Executor 本地盘大小。GPU 显存不足时数据会溢写到主机内存与本地盘,建议配置较大的本地盘。 |
| 1 | 每个 Executor 分配的 GPU 数量单位。 |
| false | 是否启用动态资源分配。GPU 作业建议使用固定 Executor 数量。 |
| nvidia.com | GPU 资源提供方标识。 |
| /opt/spark/examples/src/main/scripts/getGpusResources.sh | Spark 用于发现节点 GPU 资源的脚本路径,AnalyticDB for MySQL Spark 镜像已内置该脚本。 |
| 0.0625 | 单个 Task 占用的 GPU 份额。0.0625 即 1/16,表示一块 GPU 最多可被 16 个 Task 共享,需与 |
| com.nvidia.spark.SQLPlugin | 启用 cuDF Spark 插件,必填。 |
| true(默认) | 是否在 GPU 上执行 SQL 算子。设置为 false 可整体关闭 GPU 加速。 |
| 4 | 单块 GPU 上可并发执行的 GPU Task 数量,直接影响显存占用与吞吐。数值过大可能导致显存不足并触发溢写。 |
| NOT_ON_GPU | 输出未下推到 GPU 的算子及原因。可选值:ALL、NONE、NOT_ON_GPU,默认 NONE。用于排查加速效果不佳的问题。 |
| DEBUG | GPU 执行计划指标的详细程度。可选值:DEBUG、MODERATE 等。DEBUG 会输出全部指标,仅建议在调优时开启。 |
| MULTITHREADED | Parquet 读取方式。MULTITHREADED 使用多线程并行读取,可显著提高 OSS 吞吐;ORC 对应 |
| 200 | 每个 Executor 用于并行读取小文件的线程数上限,Executor 启动后不可修改,需与 MULTITHREADED/COALESCING 读取方式配合。 |
| false(默认) | 是否使用稳定排序。开启后 GPU 会尝试一次性排序单个分区的全部数据,显存压力增大。 |
| MULTITHREADED | 加速 Shuffle 模式。MULTITHREADED 基于多线程读写,无需额外的 UCX 服务端组件。 |
| 32 | 多线程 Shuffle 的读线程数。 |
| 32 | 多线程 Shuffle 的写线程数。 |
| true | 是否跳过 Shuffle 读端的小分区合并,减少读放大,配合 AQE 的分区合并使用。 |
| false | 关闭 External Shuffle Service,由插件的加速 Shuffle 接管。 |
| true | 启用 AQE(自适应查询执行),插件在 GPU 模式下支持 AQE。 |
| 160mb | AQE 合并 Shuffle 分区的目标大小。 |
| 32mb | AQE 合并后的最小分区大小。 |
| 48gb | Executor 堆外内存大小。GPU 作业需要较大的堆外内存承载锁页内存、Shuffle 缓冲与溢写数据。 |
| 16g | 锁页(pinned)内存池大小,用于加速主机内存与显存之间的数据传输。设置为 0 表示关闭。仅在启动时生效。 |
| true | 是否限制插件可用的堆外主机内存。 |
| 45g | 插件可用的堆外主机内存上限。 |
| 32G | 显存数据溢写到本地磁盘前,用于缓冲的主机内存大小。该参数已被 |
| org.apache.spark.serializer.KryoSerializer | 必须使用 Kryo 序列化。 |
| com.nvidia.spark.rapids.GpuKryoRegistrator | 注册插件的 Kryo Registrator,必填。 |
| 2GB | Driver 端可接收的最大结果集。GPU 指标级别设为 DEBUG 时会产生较多指标,建议适当调大。 |
| /usr/local/lib/libjemalloc.so | 预加载 jemalloc 内存分配器,改善主机侧内存分配性能。 |
更多cuDF-Spark(RAPIDS)相关参数可参考文档:configs。
更多ADB Spark相关参数配置可参考文档:Spark应用配置参数说明。
调优建议
优先保证数据格式:将输入表统一为 Parquet/ORC 列式格式,并合理控制文件大小(建议单文件 128 MB ~ 1 GB),避免出现大量小文件。
从
concurrentGpuTasks开始调:先使用较小值(如 2 ~ 4)确认作业稳定,再逐步提高,观察是否出现显存不足(OOM)或大量溢写。该值与spark.task.resource.gpu.amount需匹配:单 GPU 上并发 Task 数不应超过1 / gpu.amount。提高存储吞吐:使用
spark.rapids.sql.format.parquet.reader.type=MULTITHREADED并调大spark.rapids.sql.multiThreadedRead.numThreads,把 OSS 读取带宽打满。实测 GPU 方案的 OSS 读取峰值可达 36 Gbps。匹配主机内存:
spark.executor.memoryOverhead应大于spark.rapids.memory.pinnedPool.size+spark.rapids.memory.host.offHeapLimit.size之外的常规堆外开销,并预留 Shuffle 缓冲空间;溢写频繁时优先扩大本地盘spark.adb.executorDiskSize。先诊断再调参:遇到加速不明显时,设置
spark.rapids.sql.explain=NOT_ON_GPU查看回退算子,并开启spark.rapids.sql.metrics.level=DEBUG分析各算子耗时,定位是数据格式、算子回退还是资源不足导致。按查询特征选择方案:以大表扫描、聚合、Join 为主的作业(如 TPC-H 中的 query21、query15、query19、query14、query6)GPU 收益显著;对于计算量小、以少量 CPU 算子为主的查询,GPU 机型单价较高可能导致成本持平或略升,建议先小流量验证再全量切换。
常见问题
Q1:作业提交后作业一直处于运行中状态?
A1:请打开Spark UI查看是否成功申请到Executor资源,UI -> Executors查看是否成功申请到了Executor,受限于GPU库存,Executor申请的GPU资源不一定能申请到,如未申请到可继续等待。
Q2:作业提交后没有任何 GPU 算子,全部在 CPU 执行?
A2:请依次检查 spark.plugins=com.nvidia.spark.SQLPlugin 与 spark.kryo.registrator 是否设置、插件 JAR 是否通过 add jar 或 jars 正确加载、spark.executor.resourceSpec 是否为 GPU 规格、spark.rapids.sql.enabled 是否为 true。
Q3:作业运行中出现显存不足(GPU OOM)或大量溢写?
A3:调小 spark.rapids.sql.concurrentGpuTasks 与 spark.rapids.sql.batchSizeBytes,调大 spark.executor.memoryOverhead、spark.rapids.memory.host.offHeapLimit.size;同时检查是否存在数据倾斜。
Q4:GPU 与 CPU 的计算结果存在细微差异?
A4:round/bround、部分浮点数学函数在浮点精度范围内可能存在差异,排序与聚合的输出行顺序也可能不同。若业务依赖稳定排序,请设置 spark.rapids.sql.stableSort.enabled=true。
Q5:使用 GPU 后成本反而上升?
A5:GPU 机型单价高于 CPU 机型,只有当性能提升倍数超过单价倍数时成本才会下降。实测 TPC-H SF1K 中 22 条 Query 里有 4 条(query5、query8、query9、query11)出现小幅成本上升。建议按作业特征筛选适合 GPU 的作业,或将 GPU 与 CPU 资源组混合作业分流。