随着各行各业企业的数据规模不断增长, Apache Parquet 已成为一种重要的数据存储格式。Apache Parquet 是一种列式存储格式,专为大规模高效数据处理而设计。通过按列 (而非行) 组织数据,Parquet 可实现高性能查询和分析,因为它可以只读取查询所需的列,而无需扫描整行数据。Parquet 的高效数据布局使其成为现代分析生态系统中的热门选择,特别是在 Apache Spark 工作负载方面。
基于 cuDF 构建的 RAPIDS Accelerator for Apache Spark 支持 Parquet 作为一种数据格式,用于在 GPU 上以加速方式读取和写入数据。对于许多数据输入大小以 TB 为单位的大规模 Spark 工作负载,高效的 Parquet 扫描对于实现良好的运行时性能至关重要。
在本文中,我们将讨论如何减轻因寄存器使用率较高而引起的占用率限制,并分享 benchmark 结果。
Apache Parquet 数据格式
Parquet 文件格式允许使用组合成行组的列块以列式格式存储数据。元数据不同于数据,可根据需要将列拆分成多个文件 (Figure 1) 。

Parquet 格式支持多种 数据类型 。元数据指定了应如何解释这些类型,从而使这些类型能够表示更复杂的逻辑类型,例如时间戳、字符串、小数等。
您还可以使用 metadata 来指定更复杂的结构,例如 nested types 和 lists。数据可以以各种不同的格式进行编码,例如 plain values、dictionaries、run-length encoding、bit-packing 等。
- BOOLEAN: 1 bit boolean
- INT32: 32 bit signed ints
- INT64: 64 bit signed ints
- INT96: 96 bit signed ints
- FLOAT: IEEE 32-bit floating point values
- DOUBLE: IEEE 64-bit floating point values
- BYTE_ARRAY: arbitrarily long byte arrays
- FIXED_LEN_BYTE_ARRAY: fixed length byte arrays
Parquet on GPU 占用率限制
在用于 Apache Spark 的 RAPIDS 加速器 之前,Parquet 扫描的先前实施是一个整体式 cuDF 内核,它在一组处理代码中支持所有 Parquet 列类型。
随着使用 Parquet 数据的客户越来越多地在 GPU 上采用 Spark,鉴于 Parquet 扫描所代表的性能的关键组成部分,他们投入了更多的时间来了解 Parquet 扫描的性能特征。考虑到核函数的运行效率,有以下几种通用资源:
- 流微处理器 (SMs) :GPU 的主要处理单元,负责执行计算任务。
- 共享内存 :GPU 片上内存,每个线程块分配,以便同一线程块中的所有线程都可以访问同一共享内存。
- 寄存器 :快速的片上 GPU 显存,存储单个线程用于由 SM 执行的计算操作的信息。
在分析 Parquet 扫描结果时,我们发现由于遇到寄存器限制,整体 GPU 占用率低于预期。寄存器使用率取决于 CUDA 编译器如何根据内核逻辑和数据管理生成代码。
对于 Parquet 整体式内核而言,支持所有列类型的复杂性造就了一个大型复杂内核,且共享内存和寄存器占用率很高。虽然单个单一内核可能已将代码整合在一起,但其复杂性限制了可能的优化类型,并导致大规模性能限制。

图 2 表示 GPU 上的 Parquet 数据处理循环。每个块都是大量复杂的 kernel 代码,这些代码可能有自己的共享内存需求。许多块都依赖于类型,这会导致加载到内存中的 kernel 肿。
具体来说,其中一个限制是 Parquet 块在 warps 内的解码方式。warps 有一个串行依赖项,需要等待之前命令的 warps 完成,然后再处理其数据块。这使得解码过程的不同部分能够在不同的 warps 中进行,但对 GPU 的依赖性却很低。
采用块级解码算法对于性能至关重要,但由于其增加了数据共享和同步复杂性,因此会进一步增加寄存器数量并限制占用率。
cuDF 中的 Parquet 微核函数
为了减轻因寄存器使用率较高而造成的占用限制,我们尝试了一个较小的 kernel 的初步想法,用于在 Parquet 中预处理列表类型数据。我们将整体 kernel 中的一段代码分离为自包含的 kernel,结果令人印象深刻。整体基准测试显示运行时间更快,GPU 追踪显示占用率有所提高。
之后,我们针对不同的列类型尝试了相同的方法。各种类型的微核函数使用 C++ 模板来复用功能。这简化了每种类型的维护和调试代码。

Parquet 微核函数利用编译时间优化,仅通过必要的代码路径来处理给定类型。您可以生成多个单独的微核函数,其中仅包含该路径所需的代码,而不是一个包含所有可能代码路径的单一内核。
这可以在编译时使用 if constexpr
完成,以便代码正常读取,但不包括永远不会用于特定数据属性组合 (字符串或固定宽度、列表或无列表等) 的任何代码路径。
这是一个处理固定宽度类型列的简单示例。您可以看到,大多数处理都不需要,并且在新的 microkernel 方法中被跳过。这种类型只需要数据复制。

为解决 warp 间瓶颈,新的 microkernel 可在每个步骤处理整个 block,从而使 warp 能够更高效地独立处理数据。这对于字符串尤为重要,以便在 GPU 上启用包含 128 个线程的完整 block 来复制字符串,而之前的实现仅使用一个 warp 来复制字符串。
我们使用 NVIDIA RTX A5000 GPU 24GB 运行本地基准测试,并在设备缓冲区中预先加载压缩的 Parquet Snappy 512 MB 数据。为了测试分块读取,我们每次读取 500-KB 的数据块。测试数据包括一些变体:
- 基数 0 和 1000
- 运行长度 1 和 32
- 1% 为空
- 在重复数据时使用自适应 dictionary
图 5 显示了使用 GPU 上的新微核方法在 Parquet 列类型之间提高吞吐量的结果。

对列表列分块读取的优化还将 500-KB 读取的吞吐量提高了 117%。
在 GPU 上开始使用 Apache Spark
Parquet 是一种广泛应用于大型数据处理的关键数据格式。GPU 可以通过使用 cuDF 中经过优化的微核来加速 Apache Spark 中的 Parquet 数据扫描。
企业可以利用适用于 Apache Spark 的 RAPIDS 加速器将 Apache Spark 工作负载无缝迁移到 NVIDIA GPU。适用于 Apache Spark 的 RAPIDS 加速器将 RAPIDS cuDF 库的强大功能与 Spark 分布式计算框架的规模相结合,利用 GPU 加速处理。通过使用 RAPIDS 加速器为 Apache Spark 插件 JAR 文件启动 Spark,在不更改代码的情况下在 GPU 上运行现有 Apache Spark 应用程序。
借助 Spark RAPIDS Parquet 加速 Colab notebook,亲身体验 Parquet 扫描处理和适用于 Apache Spark 的 RAPIDS 加速器。