隨著各行各業企業的數據規模不斷增長, 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 加速器。
?