SSD-to-GPU Peer-to-Peer DMAとバッファ管理(その1)

昨年の暮れ、JPUGカンファレンスのLTで『SQL+GPU+SSD=∞』と題したスピーチを行った。

www.slideshare.net

これはかいつまんで言えば、ストレージからデータをCPU+RAMへとロードするより前に一旦GPUへとデータを転送し、そこで不要なデータを削ぎ落してからCPU+RAMへと渡してやる事で、CPU負荷の軽減とRAMの有効活用が計れるというアイデアである。
実装としては、PCI-Eバイス間でのP2P DMA機能を利用する事によってNVMe SSDの特定ブロックからGPU RAM上の特定の領域へDMAを実行するというものなので、ここは別に新しくも何ともない。

以下の図は、従来の仕組みにおけるデータの流れを示したもの。
f:id:kaigai:20160214001237p:plain
SSDから読み出されたデータは先ずCPU+RAMにバッファされ、それをGPUに送出してJOIN処理を実行、結果を受け取るという一連の流れである。しかし、①SSD=>RAMにデータが転送される時、本来は検索条件に合致せず削除されるはずのタプルや、問合せの中で参照されないカラムも含まれている。つまり、ゴミデータもRAM上のバッファを消費し、もしかしたらその煽りで別のキャッシュされていたブロックが弾き飛ばされてしまう可能性だってある。

もしPG-StromがSSD-to-GPU P2P DMA機能に対応すればどのように変わるか?
f:id:kaigai:20160214001242p:plain
SSDから読み出されたデータは、CPU+RAMへの転送より前に、先ずGPUへと転送される。ここで検索条件や参照列のチェックを行い、先ず、不要な行と列は削除する。場合によっては、SSDから直接読み出したテーブルと結合する事になるInner側のテーブルが既にCPU側から送られており、不要行・列の削除に留まらず、JOINまで行った状態でCPU+RAMへ結果を転送できるかもしれない。
こうすると、CPU+RAMの視点から見た時、『SSDからデータブロックを読みだしたと思ったら、なんとJOINまで既に終わっていたでござる』という状況を作る事ができるようになる。

ただ、ここで考えねばならないのがバッファ管理。
PostgreSQLの全てのブロックの内容がストレージ(NVMe SSD)と常に同期されていれば話は簡単なのだが、そうは問屋が下ろさない。

PostgreSQLはデータブロックの書き出しをbuffered writeで行うので、ストレージの内容よりも新しいデータが、PostgreSQL自身のバッファ(shared buffer)、あるいはOSのバッファ(page cache)に存在する可能性がある。
したがって、バッファされていない、即ちストレージに最新のデータが存在する場合にはSSD-to-GPU P2P DMAを行うとしても、バッファされているデータとの整合を考慮しなければならない。
つまり、GPUを使おうとするプログラムは、バッファリング状態を確認した上で適切なDMA元を指定してやる必要がある。

f:id:kaigai:20160214001305p:plain

先ず、目的とするブロックがPostgreSQLのshared buffer配下に存在するか否かの確認。これは BufTableLookup() 関数を用いれば確認する事ができ、既に載っていれば、shared bufferをソースとするDMAを実行する。
既にCPU+RAM側で保持しているデータに対しては、GPUでデータの振るい落としを行おうがそもそもRAM使用量の節減効果はない事と、そもそもRAMはSSDよりも速いデバイスだから、という事である。

PostgreSQLのshared bufferに載っていない場合、Linuxのpage cache状態の確認はユーザプロセスからは行えないので、ドライバ側で適切にハンドリングする必要がある。これも find_get_page() 関数を用いれば確認する事ができ、既に載っていれば、page cacheをソースとするDMAを実行する。
この2パターンはどちらも本体RAMをソースとするDMAで、それに該当しない場合、NVMe SSDの該当ブロックをソースとするDMAを実行する事になる。

ちょっと辛いのが、『特定ファイルの特定オフセット位置』をブロックデバイス上のセクタ番号に変換するためのインターフェースがVFSで規定されていない事。なので、NVMe SSDドライバにSSD-to-GPU対応機能を追加するとしても、利用可能なファイルシステムを何個か絞った上で、ファイルオフセット⇒ブロック番号への変換関数を呼びだして対処する必要がある。*1
Ext4ならext4_get_block、XFSならxfs_get_blocks関数なんてものが定義されているので、これらを呼び出せばそう難しいものではないハズだが…。

もう一つ考慮しなければならないのが、競合する更新処理をどのように捌いていくのか。これはエントリを分けて記す事にしたい。

*1:LVMやSoft-RAIDを使用する場合はもう一段階の変換が必要か