PG-Strom v5.2リリース

PG-Strom Development Team (14-Jul-2024)

概要

PG-Strom v5.2における主要な変更は点は以下の通りです。

  • GpuJoin Pinned Inner Buffer
  • GPU-Direct SQLの性能改善
  • GPUバッファの64bit化
  • 行単位CPU-Fallback
  • SELECT DISTINCT句のサポート
  • pg2arrow並列モードの性能改善
  • 累積的なバグの修正

動作環境

  • PostgreSQL v15.x, v16.x
  • CUDA Toolkit 12.2 以降
  • CUDA ToolkitのサポートするLinuxディストリビューション
  • Intel x86 64bit アーキテクチャ(x86_64)
  • NVIDIA GPU CC 6.0 以降 (Pascal以降; Turing以降を推奨)

GpuJoin Pinned Inner Buffer

PG-StromのGpuJoinは、Hash-Joinアルゴリズムを基にGPUで並列にJOIN処理を行えるように設計されています。Hash-Joinアルゴリズムの性質上、INNER側のテーブルはJOINの実行前に予めバッファ上に読み出されている必要がありますが、これまでは、GpuJoinはPostgreSQLの内部APIを介して下位の実行プランから一行ずつINNER側テーブルの内容を読み出していました。

この設計は、INNER側が巨大なテーブルを読み出すGpuScanである場合に無駄の多いものでした。

例えば、以下のようなクエリを想定してみる事にします。このクエリはlineitem(882GB)と、orders(205GB)の約1/4をJOINする処理が含まれています。 JOINを含むGpuPreAggの下位ノードにはGpuScanが存在しており、これがordersテーブルをINNERバッファへ読み出すのですが、これには約3.6億回のGpuScan呼び出しが必要となり、また30GBものINNERバッファをGPUに送出しなければいけませんでした。

=# explain select l_shipmode, o_shippriority, sum(l_extendedprice)
             from lineitem, orders
            where l_orderkey = o_orderkey
              and o_orderdate >= '1997-01-01'
            group by l_shipmode, o_shippriority;
                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 HashAggregate  (cost=38665701.61..38665701.69 rows=7 width=47)
   Group Key: lineitem.l_shipmode, orders.o_shippriority
   ->  Custom Scan (GpuPreAgg) on lineitem  (cost=38665701.48..38665701.55 rows=7 width=47)
         GPU Projection: pgstrom.psum((lineitem.l_extendedprice)::double precision), lineitem.l_shipmode, orders.o_shippriority
         GPU Join Quals [1]: (lineitem.l_orderkey = orders.o_orderkey) ... [nrows: 6000244000 -> 1454290000]
         GPU Outer Hash [1]: lineitem.l_orderkey
         GPU Inner Hash [1]: orders.o_orderkey
         GPU-Direct SQL: enabled (GPU-0)
         ->  Custom Scan (GpuScan) on orders  (cost=100.00..10580836.56 rows=363551222 width=12)
               GPU Projection: o_shippriority, o_orderkey
               GPU Pinned Buffer: enabled
               GPU Scan Quals: (o_orderdate >= '1997-01-01'::date) [rows: 1499973000 -> 363551200]
               GPU-Direct SQL: enabled (GPU-0)
(13 rows)

v5.2では、GpuJoinのINNERバッファ初期セットアッププロセスはより無駄の少ないものに再設計されています。

GpuJoin Pinned Inner Buffer

GpuScanはテーブルから読み出した行にWHERE句のチェックを行い、それをホスト側(PostgreSQLバックエンドプロセス)へ書き戻すという役割を担っています。GpuJoinはその結果を読み出してINNERバッファを構築し、これをGPU側へとコピーするわけですが、元々はGpuScanがINNER側テーブルを処理した時点で必要なデータは全てGPUに載っており、これをわざわざホスト側に書き戻し、再びGPU側へコピーする合理性はあまり大きくありません。

GpuJoin Pinned Inner Bufferは、GpuJoinの下位ノードがGpuScanである場合、その処理結果をホスト側に戻すのではなく、次のGpuJoinに備えてGpuScanの処理結果をGPUに留置しておくというものです。これにより、INNER側テーブルのサイズが非常に大きい場合にGpuJoinの初期セットアップ時間を大きく節約する事が可能となります。

一方で、ホスト側でINNERバッファのセットアップ作業を行わないという事は、GpuJoinのINNERバッファが物理的にCPUメモリ上には存在しないという事になり、CPU Fallback処理を必要とする場合には、SQL全体をエラーによってアボートする必要があります。

こういった副作用が考えられるため、GpuJoin Pinned Inner Buffer機能はデフォルトでは無効化されており、以下のようなコマンドを用いて、例えば『推定されるINNERバッファの大きさが100MBを越える場合にはGpuJoin Pinned Inner Bufferを使用する』という設定を明示的に行う必要があります。

=# set pg_strom.pinned_inner_buffer_threshold = '100MB';
SET

行ごとのCPU-Fallback

GPUでSQLワークロードを処理する場合、原理的にGPUでの実行が不可能なパターンのデータが入力される場合があります。 例えば、長い可変長データがPostgreSQLブロックの大きさに収まらず、断片化して外部テーブルに保存される場合(TOAST機構とよばれます)には、外部テーブルのデータを持たないGPUでは処理を継続する事ができません。

PG-StromにはCPU-Fallbackという仕組みが備わっており、こういったデータに対する処理をCPU側で行う仕組みが備わっています。 通常、GpuJoinやGpuPreAggなどの各種処理ロジックは、64MB分のデータ(チャンクと呼ぶ)をテーブルから読み出し、これに対してSQLワークロードを処理するためにGPUカーネルを起動します。

以前の実装では、SQLワークロードの処理中にCPU-Fallbackエラーが発生すると、そのチャンク全体のGPU処理をキャンセルしてCPU側に書き戻すという処理を行っていました。しかし、この戦略は2つの点において問題がありました。 一つは、通常、数十万行のデータを含むチャンクにたった1個の不良データが存在しただけでチャンク全体がGPUでの処理をキャンセルされてしまう事。もう一つは、GpuPreAggのように集計バッファを更新し続けるワークロードにおいては「どこまで集計表に反映されたか分からない」という状態が起こり得る事です。(したがって、v5.1以前ではGpuPreAggのCPU-Fallbackはエラー扱いとなっていました)

Per-tuple CPU-Fallback

PG-Strom v5.2では、これらの問題を解決するためにCPU-Fallbackの実装が改良されています。

CPU-Fallbackエラーが発生した場合、従来のようにチャンク全体の処理をキャンセルするのではなく、通常の処理結果を書き出す「Destination Buffer」の他に「Fallback Buffer」を用意してCPU-Fallbackエラーを起こしたタプルだけを書き込むようにします。 「Fallback Buffer」の内容は後でまとめてCPU側に書き戻され、改めてCPUで評価が行われます。そのため、必要最小限のタプルだけをCPU-Fallback処理すれば良いだけでなく、GpuPreAggの集計バッファが重複して更新されてしまう心配もありません。

GPUバッファの64bit化

現在でこそ、48GBや80GBといったメモリを搭載したGPUが販売されていますが、PG-Stromが内部的に使用するGPUバッファのデータ形式を設計したのはv2.0の開発中である2017年頃。つまり、ハイエンドGPUでも16GBや24GB、それ以外では8GB以下というのが一般的でした。 そういった前提では、物理的にあり得ない大容量のデータを過不足なく表現できるよりも、メモリ使用量を削る事のできるデータ形式が優先でした。

その他の新機能

GPU-Direct SQLの性能改善

NVIDIAのcuFileライブラリは、内部的にDevive Primary Contextを仮定していました。そのため、独自に生成したCUDA Contextを使用していたPG-Strom GPU ServiceからのAPI呼出しに対して、CUDA Contextを切り替えるコストが発生していました。

PG-Strom v5.2では、GPU ServiceもDevice Primary Contextを使用するように設計変更を行い、cuFileライブラリ内のスイッチングコストとそれに付随するCPUのBusy Loopを無くすことで、およそ10%程度の性能改善が行われています。

SELECT DISTINCT句のサポート

PG-Strom v5.2ではSELECT DISTINCT ...句がサポートされました。 以前のバージョンでは、これをGROUP BY句に書き直す必要がありました。

pg2arrow並列モードの性能改善

pg2arrowコマンドで-tオプションと-nオプションを併用した場合、読出し対象のテーブルサイズを調べ、各ワーカースレッドが重複してテーブルを読み出さないようにスキャン範囲を調整してクエリを発行します。

IMPORT FOREIGN SCHEMAで重複列に別名

IMPORT FOREIGN SCHEMApgstrom.arrow_fdw_import_file()関数を用いてArrowファイルをインポートする際、Fieldが重複した名前を持っていた場合、重複した名前を持つ2番目以降の列には別名を付けるようになりました。

部分的な処理結果の返却

GpuJoinやGpuPreAggなど各種の処理でDestination Bufferを使い尽くした場合、GPUカーネルの実行を一時停止し、部分的な処理結果をバックエンドプロセスに返却してから、再度GPUカーネルの実行を再開するようになりました。 これまではDestination Bufferを拡大してチャンクの処理結果を全て保持するような構造になっていたため、入力に対して結果セットが巨大である場合には、CPUやGPUのメモリを過剰に消費してシステムが不安定になるという問題がありました。

累積的なバグの修正

  • [#664] Too much CPU consumption ratio with cuFile/GDS on many threads
  • [#757] wrong varnullingrels at setrefs.c
  • [#762] arrow_fdw: scan-hint needs to resolve expressions using INDEV_VAR
  • [#763] Var-nodes on kvdef->expr was not compared correctly
  • [#764][#757] Var::varnullingrels come from the prior level join are not consistent
  • [#673] lookup_input_varnode_defitem() should not use equal() to compare Var-nodes
  • [#729] update RPM build chain
  • [#765] Add regression test for PostgreSQL 16
  • [#771] Update regression test for PostgreSQL 15
  • [#768] fix dead loop in `gpuCacheAutoPreloadConnectDatabase
  • [#xxx] Wrong GiST-Index based JOIN results
  • [#774] add support of SELECT DISTINCT
  • [#778] Disable overuse of kds_dst buffer in projection/gpupreagg
  • [#752] add KDS_FORMAT_HASH support in execGpuJoinProjection()
  • [#784] CPU-Fallback JOIN didn't handle LEFT/FULL OUTER case if tuple has no matched inner row
  • [#777] Fix the bug of dynamically allocating fallback buffer size and nrooms
  • [#776] Fix the out of range bug in pgfn_interval_um()
  • [#706] gpucache: drop active cache at DROP DATABASE from shmem / gpumem
  • [#791] gpujoin: wrong logic to detect unmatched inner tuple
  • [#794] assertion failure at cost_memoize_rescan()
  • [#xxx] pg2arrow: outer/inner-join subcommand initialization
  • [#xxx] IMPORT FOREIGN SCHEMA renames duplicated field name
  • [#xxx] arrow_fdw: correct pg_type hint parsing
  • [#748] Add support CPU-fallback on GpuPreAgg, and revise fallback implementation
  • [#778] Add XpuCommandTag__SuccessHalfWay response tag