GPUメモリストア(Gstore_Fdw)
この記事は「PostgreSQL Advent Calendar 2020」の 16日目です。
GPU版PostGISの他に、今年のPG-Stromの機能強化のうち比較的大きめのものについてもご紹介したいと思います。
GPUメモリストア(Gstore_Fdw)とは
GPUデバイスメモリ上に予め確保した領域にデータを保存し、これをPostgreSQLのFDW(Foreign Data Wrapper)を通じて読み書きする機能。GpuScan/GpuJoin/GpuPreAggといったPG-Stromの提供する各種ロジックにおいてデータソースとして活用する事ができ、その場合、ストレージやホストRAM上のバッファからデータを読み出す必要がないため、その分の処理を節約する事ができる。
この手の機能を持ったGPU-DBというのは他にもあるが、Gstore_Fdwのポイントは更新系ワークロードもきちんと考慮している点。通常、GPUデバイスメモリを更新するには、PCI-Eバスを経由してデータを転送する必要があるが、これのレイテンシが馬鹿にならない。
大雑把に言って、ホストRAMの更新が数十nsのオーダーである一方、PCI-Eバスを介したGPUデバイスメモリの読み書きには数十usを要する。つまり千倍差である。*1
では Gstore_Fdw ではどうしているのか?
およそ数百~数万行分の更新ログを溜めておいて、あとで一気にGPUへ転送し、GPU側では数千コアを同時に動かして更新ログをGPUデバイスメモリ上のデータストアに適用する。そうする事で、比較的大きなPCI-Eのレイテンシも一行あたりに直せば大した値ではなくなる。
更新ログをGPUに転送するのは以下の3つのタイミング。
3つ目がポイントで、ここでは暗黙の裡に分析クエリの実行頻度は更新クエリよりも遥かに少ないという仮定を置いているが、GPUデバイスメモリ側の更新がどれだけ遅延したとしても、分析クエリの実行より前に最新状態にリフレッシュできていれば問題ない、という事である。
この機能の使い道として想定しているのは、携帯電話や自動車、ドローンなど、GPSによる位置情報を時々刻々更新するようなパターンのワークロードで、GPU版PostGISと組み合わせて利用するパターンを念頭に置いている。
この手のログデータは意外とデータサイズは小さい*2が、更新の頻度が極めて高いという特徴を持つ。
Gstore_Fdw で外部テーブルを定義してみる。
では早速、Gstore_Fdwを用いた外部テーブルを定義してみることにする。
開発用サーバには2台のGPUが搭載されているので、PostgreSQLのパーティション機能を用いて両方のGPUにデータを振り分ける。
まず、パーティションのRootとなるテーブルを定義。
=# create table fpoints ( dev_id int, ts timestamp, x float, y float) partition by hash ( dev_id ); CREATE TABLE
続いて、パーティションのLeafとしてGstore_Fdw外部テーブルを定義。
=# create foreign table fpoints_p0 partition of fpoints for values with (modulus 2, remainder 0) server gstore_fdw options (base_file '/opt/pgdata12/fpoints_p0.base', redo_log_file '/opt/pmem/fpoints_p0.redo', gpu_device_id '0', max_num_rows '8000000', primary_key 'dev_id'); CREATE FOREIGN TABLE =# create foreign table fpoints_p1 partition of fpoints for values with (modulus 2, remainder 1) server gstore_fdw options (base_file '/opt/pgdata12/fpoints_p1.base', redo_log_file '/opt/pmem/fpoints_p1.redo', gpu_device_id '1', max_num_rows '8000000', primary_key 'dev_id'); CREATE FOREIGN TABLE
base_file
というのはデータストアを記録するためのファイルで、再起動やクラッシュ後のリカバリのために使用される。redo_log_file
というのは更新ログを記録するためのファイルで、Persistent Memory領域を使用するのが推奨。今回は、Intel製DCPMM[128GB]をマウントした/opt/pmem
以下のファイルを指定している。gpu_device_id
は使用するGPUを指定し、max_num_rows
はバッファを確保する最大行数を指定する。primary_key
には主キーとして振る舞うカラムを指定する。
これらの外部テーブルを定義すると、ログには以下のように表示され、GPUデバイスメモリをそれぞれ 300MB 程度確保した事がわかる。
2020-12-16 08:51:48.668 UTC [11130] LOG: gstore_fdw: initial load [fpoints_p0] - main 324000640 bytes, extra 0 bytes 2020-12-16 08:51:58.265 UTC [11129] LOG: gstore_fdw: initial load [fpoints_p1] - main 324000640 bytes, extra 0 bytes
初期データの投入と更新クエリの実行
続いて、GPUメモリストアに初期データの投入を行う。これはINSERTでもCOPY FROMでもよい。
=# insert into fpoints (select x, now(), 100*random(), 100*random() from generate_series(0,12000000) x); INSERT 0 12000001
Hashパーティショニングを使用しているため、概ね均等にデータが分散されていることが分かる。
postgres=# select count(*) from fpoints; count ---------- 12000001 (1 row) postgres=# select count(*) from fpoints_p0; count --------- 5998934 (1 row) postgres=# select count(*) from fpoints_p1; count --------- 6001067 (1 row)
このデータに対して、位置情報とタイムスタンプの更新を想定し、以下のようなクエリをpgbenchで実行してみることにする。
tsはタイムスタンプの更新、x,yはそれぞれ倍精度浮動小数点による座標のイメージである。
UPDATE fpoints SET ts = now(), x = random() * 100.0, y = random() * 100.0 WHERE dev_id = (select (random() * 250000)::int * 48 + :client_id)
実行結果は以下のような感じ。
$ pgbench -n -f mytest.sql -c 48 -j 48 -T 15 postgres transaction type: mytest.sql scaling factor: 1 query mode: simple number of clients: 48 number of threads: 48 duration: 15 s number of transactions actually processed: 1471332 latency average = 0.490 ms tps = 97912.340285 (including connections establishing) tps = 97950.056405 (excluding connections establishing)
これに伴い、ログにも溜まった更新ログをGPUへ転送し、これをGPUデバイスメモリ上のデータストアに適用した事が出力されている。
2020-12-16 09:24:27.484 UTC [11129] LOG: gstore_fdw: Log applied (nitems=1905688, length=124817704, pos 521268808 => 612741824) 2020-12-16 09:24:28.294 UTC [11130] LOG: gstore_fdw: Log applied (nitems=2037144, length=131644512, pos 521086144 => 618869072) 2020-12-16 09:24:42.567 UTC [11129] LOG: gstore_fdw: Log applied (nitems=823198, length=76151024, pos 612741824 => 652255360) 2020-12-16 09:24:43.390 UTC [11130] LOG: gstore_fdw: Log applied (nitems=824461, length=76733600, pos 618869072 => 658443232) 2020-12-16 09:24:57.686 UTC [11129] LOG: gstore_fdw: Log applied (nitems=1574755, length=118524736, pos 652255360 => 727843584) 2020-12-16 09:24:58.497 UTC [11130] LOG: gstore_fdw: Log applied (nitems=1436306, length=111847320, pos 658443232 => 727385888)
分析クエリの実行はこのような形で、fpointsテーブル(実際にはその配下のfpoints_p0とfpoints_p1外部テーブル)から、指定範囲内に属する座標を抽出するというような形での利用を想定している。
これは別に単純検索である必要はなく、例えば、前回の記事で紹介したようなGiSTインデックスを用いた多対多の範囲検索であってもよい。
=# SELECT count(*) FROM fpoints WHERE st_contains('polygon ((10 10,90 10,90 12,12 12,12 88,90 88,90 90,10 90,10 10))', st_makepoint(x,y)); count -------- 565047 (1 row) Time: 254.274 ms
Gstore_Fdwの今後
現在、PG-Strom v3.0のリリースに向けてマルチGPUの対応をはじめとした諸々の改善や、テストケースの作成を行っています。
乞うご期待!
ちなみに、Advent Calendarとしてはずいぶんギリギリの公開となってしまいましたが、こういった悲しい事故がありました。
『わはは、そんな挙動さすがに無いだろ。釣り乙。』と思って試してみたら、ブログの下書きを巻き込んで再起動してしまいました。Orz https://t.co/dGrLd0xZfL
— 海外 浩平|KaiGai Kohei🌻 (@kkaigai) 2020年12月16日