GPUメモリストア(gstore_fdw)

概要

通常、PG-StromはGPUデバイスメモリを一時的にだけ利用します。クエリの実行中に必要なだけのデバイスメモリを割り当て、その領域にデータを転送してSQLワークロードを実行するためにGPUカーネルを実行します。GPUカーネルの実行が完了すると、当該領域は速やかに開放され、他のワークロードでまた利用する事が可能となります。

これは複数セッションの並行実行やGPUデバイスメモリよりも巨大なテーブルのスキャンを可能にするための設計ですが、状況によっては必ずしも適切ではない場合もあります。

典型的な例は、それほど巨大ではなくGPUデバイスメモリに載る程度の大きさのデータに対して、繰り返し様々な条件で計算を行うといった利用シーンです。これは機械学習やパターンマッチ、類似度サーチといったワークロードが該当します。 S

現在のGPUにとって、数GB程度のデータをオンメモリで処理する事はそれほど難しい処理ではありませんが、PL/CUDA関数の呼び出しの度にGPUへロードすべきデータをCPUで加工し、これをGPUへ転送するのはコストのかかる処理です。

加えて、PostgreSQLの可変長データには1GBのサイズ上限があるため、これをPL/CUDA関数の引数として与える場合、データサイズ自体は十分にGPUデバイスメモリに載るものであってもデータ形式には一定の制約が存在する事になります。

GPUメモリストア(gstore_fdw)は、あらかじめGPUデバイスメモリを確保しデータをロードしておくための機能です。 これにより、PL/CUDA関数の呼び出しの度に引数をセットアップしたりデータを転送する必要がなくなるほか、GPUデバイスメモリの容量が許す限りデータを確保する事ができますので、可変長データの1GBサイズ制限も無くなります。

gstore_fdwはその名の通り、PostgreSQLの外部データラッパ(Foreign Data Wrapper)を使用して実装されています。 gstore_fdwの制御する外部テーブル(Foreign Table)に対してINSERTUPDATEDELETEの各コマンドを実行する事で、GPUデバイスメモリ上のデータ構造を更新する事ができます。また、同様にSELECT文を用いてデータを読み出す事ができます。

外部テーブルを通してGPUデバイスメモリに格納されたデータは、PL/CUDA関数から参照する事ができます。 現在のところ、SQLから透過的に生成されたGPUプログラムは当該GPUデバイスメモリ領域を参照する事はできませんが、将来のバージョンにおいて改良が予定されています。

GPU memory store

初期設定

通常、外部テーブルを作成するには以下の3ステップが必要です。

  • CREATE FOREIGN DATA WRAPPERコマンドにより外部データラッパを定義する
  • CREATE SERVERコマンドにより外部サーバを定義する
  • CREATE FOREIGN TABLEコマンドにより外部テーブルを定義する

このうち、最初の2ステップはCREATE EXTENSION pg_stromコマンドの実行に含まれており、個別に実行が必要なのは最後のCREATE FOREIGN TABLEのみです。

CREATE FOREIGN TABLE ft (
    id int,
    signature smallint[] OPTIONS (compression 'pglz')
)
SERVER gstore_fdw OPTIONS(pinning '0', format 'pgstrom');

CREATE FOREIGN TABLEコマンドを使用して外部テーブルを作成する際、いくつかのオプションを指定することができます。

SERVER gstore_fdwは必須です。外部テーブルがgstore_fdwによって制御されることを指定しています。

OPTIONS句では以下のオプションがサポートされています。

名前 対象 説明
pinning テーブル デバイスメモリを確保するGPUのデバイス番号を指定します。
format テーブル GPUデバイスメモリ上の内部データ形式を指定します。デフォルトはpgstromです。
compression カラム 可変長データを圧縮して保持するかどうかを指定します。デフォストは非圧縮です。

formatオプションで選択可能なパラメータは、現在のところpgstromのみです。これは、PG-Stromがインメモリ列キャッシュの内部フォーマットとして使用しているものと同一です。 純粋にSQLを用いてデータの入出力を行うだけであればユーザが内部データ形式を意識する必要はありませんが、PL/CUDA関数をプログラミングしたり、IPCハンドルを用いて外部プログラムとGPUデバイスメモリを共有する場合には考慮が必要です。

compressionオプションで選択可能なパラメータは、現在のところplgzのみです。これは、PostgreSQLが可変長データを圧縮する際に用いているものと同一の形式で、PL/CUDA関数からはGPU内関数pglz_decompress()を呼び出す事で展開が可能です。圧縮アルゴリズムの特性上、例えばデータの大半が0であるような疎行列を表現する際に有用です。

運用

データのロード

通常のテーブルと同様にINSERT、UPDATE、DELETEによって外部テーブルの背後に存在するGPUデバイスメモリを更新する事ができます。

ただし、gstore_fdwはこれらコマンドの実行開始時にSHARE UPDATE EXCLUSIVEロックを獲得する事に注意してください。これはある時点において1トランザクションのみがgstore_fdw外部テーブルを更新できることを意味します。 この制約は、PL/CUDA関数からgstore_fdw外部テーブルを参照するときに個々のレコード単位で可視性チェックを行う必要がないという特性を得るためのトレードオフです。

また、gstore_fdw外部テーブルに書き込まれた内容は、通常のテーブルと同様にトランザクションがコミットされるまでは他のセッションからは不可視です。 この特性は、トランザクションの原子性を担保するには重要な性質ですが、古いバージョンを参照する可能性のある全てのトランザクションがコミットまたはアボートするまでの間は、古いバージョンのgstore_fdw外部テーブルの内容をGPUデバイスメモリに保持しておかねばならない事を意味します。

そのため、通常のテーブルと同様にINSERT、UPDATE、DELETEが可能であるとはいえ、数行を更新してトランザクションをコミットするという事を繰り返すのは避けるべきです。基本的には大量行のINSERTによるバルクロードを行うべきです。

通常のテーブルとは異なり、gstore_fdwに記録された内容は揮発性です。つまり、システムの電源断やPostgreSQLの再起動によってgstore_fdw外部テーブルの内容は容易に失われてしまいます。したがって、gstore_fdw外部テーブルにロードするデータは、他のデータソースから容易に復元可能な形にしておくべきです。

デバイスメモリ消費量の確認

gstore_fdwによって消費されるデバイスメモリのサイズを確認するにはpgstrom.gstore_fdw_chunk_infoシステムビューを参照します。

postgres=# select * from pgstrom.gstore_fdw_chunk_info ;
 database_oid | table_oid | revision | xmin | xmax | pinning | format  |  rawsize  |  nitems
--------------+-----------+----------+------+------+---------+---------+-----------+----------
        13806 |     26800 |        3 |    2 |    0 |       0 | pgstrom | 660000496 | 15000000
        13806 |     26797 |        2 |    2 |    0 |       0 | pgstrom | 440000496 | 10000000
(2 rows)

nvidia-smiコマンドを用いて、各GPUデバイスが実際にどの程度のデバイスメモリを消費しているかを確認する事ができます。 Gstore_fdwが確保したメモリは、PG-Strom GPU memory keeperプロセスが保持・管理しています。ここでは、上記rawsizeの合計約1100MBに加え、CUDAが内部的に確保する領域を合わせて1211MBが占有されている事が分かります。

$ nvidia-smi
Wed Apr  4 15:11:50 2018
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 390.30                 Driver Version: 390.30                    |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla P40           Off  | 00000000:02:00.0 Off |                    0 |
| N/A   39C    P0    52W / 250W |   1221MiB / 22919MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|    0      6885      C   ...bgworker: PG-Strom GPU memory keeper     1211MiB |
+-----------------------------------------------------------------------------+

内部データ形式

gstore_fdwがGPUデバイスメモリ上にデータを保持する際の内部データ形式の詳細はノートを参照してください。

外部プログラムとのデータ連携

CUDAにはcuIpcGetMemHandle()およびcuIpcOpenMemHandle()というAPIが用意されています。前者を用いてアプリケーションプログラムが確保したGPUデバイスメモリのユニークな識別子を取得し、後者を用いて別のアプリケーションプログラムから同一のGPUデバイスメモリを参照する事が可能となります。言い換えれば、ホストシステムにおける共有メモリのような仕組みを備えています。

このユニークな識別子はCUipcMemHandle型のオブジェクトで、内部的には単純な64バイトのバイナリデータです。 本節ではCUipcMemHandle識別子を利用して、PostgreSQLと外部プログラムの間でGPUを介したデータ交換を行うための関数について説明します。

SQL関数の一覧

gstore_export_ipchandle(reggstore)

本関数は、gstore_fdw制御下の外部テーブルがGPU上に確保しているデバイスメモリのCUipcMemHandle識別子を取得し、bytea型のバイナリデータとして出力します。 外部テーブルが空でGPU上にデバイスメモリを確保していなければNULLを返します。

  • 第1引数(ftable_oid): 外部テーブルのOID。reggstore型なので、外部テーブル名を文字列で指定する事もできる。
  • 戻り値: CUipcMemHandle識別子のbytea型表現。
# select gstore_export_ipchandle('ft');
                                                      gstore_export_ipchandle

------------------------------------------------------------------------------------------------------------------------------------
 \xe057880100000000de3a000000000000904e7909000000000000800900000000000000000000000000020000000000005c000000000000001200d0c10101005c
(1 row)

lo_import_gpu(int, bytea, bigint, bigint, oid=0)

本関数は、外部アプリケーションがGPU上に確保したデバイスメモリ領域をPostgreSQL側で一時的にオープンし、当該領域の内容を読み出してPostgreSQLラージオブジェクトとして書き出します。 第5引数で指定したラージオブジェクトが既に存在する場合、ラージオブジェクトはGPUデバイスメモリから読み出した内容で置き換えられます。ただし所有者・パーミッション設定は保持されます。これ以外の場合は、新たにラージオブジェクトを作成し、GPUデバイスメモリから読み出した内容を書き込みます。

  • 第1引数(device_nr): デバイスメモリを確保したGPUデバイス番号
  • 第2引数(ipc_mhandle): CUipcMemHandle識別子のbytea型表現。
  • 第3引数(offset): 読出し開始位置のデバイスメモリ領域先頭からのオフセット
  • 第4引数(length): バイト単位での読出しサイズ
  • 第5引数(loid): 書き込むラージオブジェクトのOID。省略した場合 0 が指定されたものと見なす。
  • 戻り値: 書き込んだラージオブジェクトのOID

lo_export_gpu(oid, int, bytea, bigint, bigint)

本関数は、外部アプリケーションがGPU上に確保したデバイスメモリ領域をPostgreSQL側で一時的にオープンし、当該領域へPostgreSQLラージオブジェクトの内容を書き出します。 ラージオブジェクトのサイズが指定された書き込みサイズよりも小さい場合、残りの領域は 0 でクリアされます。

  • 第1引数(loid): 読み出すラージオブジェクトのOID
  • 第2引数(device_nr): デバイスメモリを確保したGPUデバイス番号
  • 第3引数(ipc_mhandle): CUipcMemHandle識別子のbytea型表現。
  • 第4引数(offset): 書き込み開始位置のデバイスメモリ領域先頭からのオフセット
  • 第5引数(length): バイト単位での書き込みサイズ
  • 戻り値: 実際に書き込んだバイト数。指定されたラージオブジェクトの大きさがlengthよりも小さな場合、lengthよりも小さな値を返す事がある。