動いた!SSD-to-GPU Direct DMA


ここしばらく、NVMe-SSDからGPUへとPeer-to-Peer DMAを行うためのLinux kernelドライバを書いている。

これは昨年末のPGconf.JPのLTでアイデアを先に発表したもので、従来は、例えばテーブルスキャンに際して90%の行がフィルタリングされる場合であっても、データをストレージからRAMにロードしていた。しかし、どうせフィルタリングするのであれば、バッファのために利用したRAMのうち90%は無駄である。

基本的なアイデアは、ストレージからのデータロードに際して、CPU側のRAMではなく、GPU側のRAMへロードし、そこで数百~数千コアの計算能力を使って行のフィルタリングや、あるいは、テーブル同士のJOINや集約演算を行ってしまう。そして、これらの前処理が終わった段階でCPU側へデータを書き戻してやれば、CPUから見ると『ストレージからデータを読出したら、既にJOINもGROUP BYも終わっていた』という状態を作り出す事ができる。

まだPostgreSQL側での対応が済んでいないが、やっとドライバだけは動くようになったので簡単な性能テストを行ってみた。

まず、GPUの情報を確認してみる。
搭載しているのはQuadro K1200で、これはドライバの必要とするGPU Direct DMA機能に対応している。

で、デバイスを nvidia-smi -q で確認すると、つらつらとデバイス情報が表示される。
このモデルはGPU RAMを4GB搭載しているが、実はGPU Direct DMAでは搭載RAMの全ての領域を同時に使用できる訳ではない。
ここでBAR1 Memory Usageと書かれたエリアが256MB存在するが、これがPCIバスを通じてホスト側の物理アドレス空間にマップする事のできる大きさで、いわば4GBのGPU RAMを256MB分の大きさの窓から覗くようなものである。

[kaigai@magro ~]$ nvidia-smi -q

==============NVSMI LOG==============

Timestamp                           : Thu Aug 25 00:49:35 2016
Driver Version                      : 352.93

Attached GPUs                       : 1
GPU 0000:01:00.0
    Product Name                    : Quadro K1200
    Product Brand                   : Quadro
           :
    FB Memory Usage
        Total                       : 4095 MiB
        Used                        : 9 MiB
        Free                        : 4086 MiB
    BAR1 Memory Usage
        Total                       : 256 MiB
        Used                        : 1 MiB
        Free                        : 255 MiB
    Compute Mode                    : Default
           :

なお、このBAR1領域は、Tesla K40/K80やM40といった上位モデルでは、物理アドレスサイズを越える16GBが用意されており、これらのGPUを持っているリッチメンであればBAR1領域の小ささに悩む必要もないハズである。

とりあえず、今は手元にQuadro K1200しかないので、これでトライ。

データ転送のパターンとしては、32MBのバッファを6個確保し、ファイルの内容を順にこれらのバッファに非同期転送を行う。非同期転送が完了したタイミングでCUDAからコールバックが呼び出されるため、バッファが空いたらすかさず次の非同期転送をキューイングしていく。

本来であれば、GPUで何か処理を実行してデータサイズを減らしてからCPU+RAMへ書き戻すのが正しい姿であるが、まだGPU側で処理すべき事を実装できていないので、ここは単純にSSDから読み出した内容を同じバッファへと書き戻す事とした。

通常のVFS経由でのデータロードだと、上記の図のようになる。
read(2)でSSDからバッファにデータをロードし、これをcuMemcpyHtoDAsync()とcuMemcpyDtoHAsync()を使ってCPU RAM⇔GPU RAM間でPCI-Eバスを介して移動させる。

一方、SSD-to-GPU Direct DMAを使用した場合、データの流れはシンプルになる。
SSDからGPUPCI-Eバスを介してPeer-to-Peerでデータ転送が行われ、その後、GPU RAM⇒CPU RAMへの転送が行われる。

かなり乱暴に考えても、CPU RAMに一旦コピーする手間が省ける分は処理性能が稼げるはずである。

これを、OSのキャッシュの影響を避けるため、16GB搭載のマシン上で40GB、100GBの二つのファイルに対して実行してみた。

結果は以下の通り。

性能指標としては、データ転送量を一連の処理時間で割ったスループットを使用している。
SSD-to-GPU Directのケースが最も早く、概ね1.4GB/s程度のスループットを出している。ただし、Intel SSD 750 (400MB)のカタログスペックはシーケンシャルリード 2200MB/s と記載してあるので、これに比べるとまだデバイスの性能を発揮しているとは言い難い。
とりあえず、ドライバが動きましたという段階なのでアレだが、今後、どこで引っかかっているかは調べる必要がある。

一方、VFS経由でデータをロードした場合は1.1GB/s程度のスループットに留まっている。これは、SSD⇒CPU RAMへのデータ転送が余分に必要となっている分、不可避の差とも言える。ただ、これはPostgreSQLでのワークロードを反映しているとは言えない。
というのも、1.1GB/sを記録したケースというのは、read(2)にバッファサイズである32MBを与えた場合であって、システムコール呼び出しに関わるオーバーヘッドが極めて少ないと言えるため。
通常、PostgreSQLのI/Oの単位は8KBで、コンフィグで設定を変えたとしても32KBまでしか増やす事ができない。
こうなると、40GBや100GBといった大きさのファイルをスキャンする際、特にNVMe-SSDのような高速デバイスを使う場合だと、システムコールの呼び出しが律速要因となってしまうため、大幅にスループットが低下しているのが判る。

現在、PostgreSQL側の対応も実装中であるので、単純なI/Oだけでなく、実際にPostgreSQL+PG-Stromにクエリを食わせてみた時の性能値についても追って報告したい。