GTC2017 - Tesla V100 と CUDA9.0

今年のGTCはジェンスン・ファンCEOの基調講演が3日目に設定されており、そこでVolta世代の新製品 Tesla V100 と、それを搭載するDGX-1などサーバ製品とGPUクラウドが発表された。
4日間の日程のうち、情報の解禁が3日目の正午なので、その後は慌ただしくVoltaのアーキテクチャや、関連するCUDA9新機能のセッションに参加。
聞くべきセッションが被っちゃったりもしたので、できれば基調講演は初日の方が日程に余裕はあったかなぁ…。

Tesla V100について

5120CUDAコア、16GB(HBM2; 900GB/s)のメモリを搭載するまさに化け物プロセッサ。

バイス全体で80個のSMを搭載する。SMあたりの構成は64CUDA CoreなのでPascalと同等だが、目新しい基軸としてTensor Coreという新しい計算ユニットが追加されている(SMあたり8個 = 全体で640個)。また、L1 Cacheが128KB x 80 = 10MB、L2 Cacheが6MB搭載されている。

これは4x4行列同士のFMAD演算(←と言っていいの?)を実行する専用のH/Wユニットで、従来であれば、16個のCUDAコアが協調して掛け算/足し算を行うべき箇所を命令一発で実行するというもの。

深層学習のアルゴリズムに関して詳しくないんだけども、ハマる状況であれば綺麗に高速化できるんであろうし、実際、TensorCore機能を使った機械学習ワークロードの実行速度が、P100比で9.3倍に向上したという事はアナウンスされている。

また、6本のリンクを全てフル帯域で使った場合の理論帯域だけども、NVLinkで300GB/sのスループットがあり、NVLink接続のリモートGPUに対するcache coherencyやatomic operationが可能だとのこと。

この辺の発表を聞いていての印象は、15年-20年ほど前からx86 CPUが辿った道のりに似てるなぁ~と。

Intel CPUの場合、マルチメディア処理命令としてMMX/AVXという拡張命令が加わったが、それを自分のプログラムから直接使うという人はあまり多くなく、実態としてはglibcなどのライブラリが内部的にこれらの機能を使うようになったために、それを通じて、ハマるケース限定でハードウェア専用命令を使うようになってきている。
TensorCoreの場合も、cuDNNやcuBLASなどのライブラリ群が内部的にこの機能を使うとのことなので、ユーザプログラムが行列計算や深層学習にこれらのライブラリ群を使っていれば、知らず知らずのうちに恩恵を受けることになる。というか、ラップしてくれないとちょっと使える状況が特殊すぎて辛い。

もう一つ。NVLinkの帯域が向上し、しかもUnified Memoryでアドレス空間の問題が解決し、cache coherencyやatomic operationに対応するとなれば、他のGPUバイス上のメモリをローカルメモリに近い感覚で使うことができるようになるという事だろう。
そうすると、かつてLinuxがNUMA環境への対応を行ったように、メモリの物理的な距離を意識しなければならない状況が生まれつつあるように思える。例えば Tesla V100 は 16GB(HBM2) のメモリを積んでいるが、アプリケーションが20GBのデータをロードして2台のGPUでぶん回したい場合、データ量は明らかに1台のGPUに収まらないので2台に分散ロードする必要がある。
この時、物理GPUページの割り当てが『前半10GBはGPU0、後半10GBはGPU1』みたいなざっくりで上手く動作するのだろうか?もしかしたら、メモリアクセスの局所性が期待できるタイプのワークロードなのか? それとも、全体を舐め回すのでインタリーブ型の物理データ配置を行うべきなのか、それほど簡単な問いではないように思える。


SMの構成は図の通りであるが、L1 Cacheの扱いが少し変わっている。
PascalではL1 Cacheが24KBだったのが、128KBあるL1/Shared Memory共通の領域のうち、共有メモリが使用していない領域をL1 Cacheとして利用できるわけである。
Maxwell以前にL1/Shared Memoryの大きさを16KB/48KB、32KB/32KB、48KB/16KBの中から選んでいたのに通ずるが、もう少しフレキシブルに"早いメモリ"を使ってくれているようである。
ただ、GPUのL1キャッシュの場合(少なくともPascalまでは)、キャッシュの下に転がっているGlobal Memoryの内容がGPU kernel実行中に少なくとも変更されない/Read-Onlyであるという制約があったが、これはおそらくVoltaでも変わらないものと思われる(何も言及無かったし)

また、Pascal以前のGPUと大きく変わってくる点として、プログラムカウンタ(PC)がwarp単位ではなくthread単位になるという違いがある。
従来は、if (...) { ... } else { ... } ブロックを実行するときに、warp中の一部スレッドだけが条件が真、他のスレッドが偽である場合には両方のブロックを逐次的に実行することになるため、他方のスレッド群が遊んでしまい、それ故にGPUは条件分岐に弱いと言われてきたが、各スレッドがPCを持つという事はこうした弱点に手を打ってきたという事でもある。
(とは言え、TrueブロックとFalseブロックの分量があまりにアンバランスだとアカンでしょうが)

5月13日追記:
FBでNVIDIA-JPの成瀬さんから教えていただきました。
『プログラムカウンタ(PC)がwarp単位ではなくthread単位になる』というのは必ずしも間違ってはいないのですが、CPUスレッドのように各CUDAコアが全く独立な命令を実行できるという事ではないようです。

より正確に述べると、PC(ここでは次に実行する命令のアドレスとしましょう)は、Voltaではスレッド毎に持ちます。ただし、同じwarp内のスレッドは、PCが同じであるスレッドだけが、同じクロックで実行されます。warp内で、別PCの命令が実行されることは、これまで通り、ありません。

つまり、if (...) {} else {} ブロックで分岐が発生する場合、従来、trueブロックを実行中のCUDAコアがある時はfalseブロックに分岐したCUDAコアは寝ているしかなかった。Voltaにおいては、例えばDRAMアクセスなどでメモリストールが発生しておりtrueブロックを実行中のCUDAコアが寝ている状況では、その間を利用してfalseブロックに分岐したCUDAコアも先に実行を進めることができる、という事のようです。

他にニッチなところとしては、SMが80個にも増えてアプリケーションによっては全てのSMを使いきれないという事を想定しているのか、MPS(Multi Process Service)を使用した時に複数のアプリケーションからのGPU kernel起動要求を調停して、GPU資源を切り分けるという使い方ができますよーとの事。ただ、MPSはcuStreamAddCallback()のようにコールバック関数を呼び出す事ができなかったハズなので、非同期処理が中心のPG-Stromとは今一つ相性がなぁ…。

CUDA 9について

Voltaアーキテクチャ向けのTensorCoreを使うための機能や、cuBLASなどライブラリのTensorCore対応が行われるらしい。

ただ、最も革命的と言ってよい変化は、今までスレッド間の同期は __syncthreads() を使ったSM内の同期のみであったのが、SMを越えて複数のグリッド間で/複数のGPUバイスで並行動作するカーネル間で同期を取ることが可能になったというもの。

GPU kernelを起動するときは、必要なスレッド数がN個としてN=(ブロックサイズ)×(グリッドサイズ)
という関係になるようスレッド数を与えて起動する。ブロックサイズの上限は 1024 であり、GPU kernelの実行時にはブロックサイズに依らず、特定のブロックは特定のSMで実行される。
(グリッドサイズ > デバイスのSM数 な場合は、ブロックを順番に実行する)

で、大雑把に言えば、従来はH/Wに由来するブロックを単位とする同期の他には、GPU kernel全体の実行完了を待つという同期方法しか取れなかったが、CUDA 9で導入される co-operative group という概念を使えば、H/W的な構造と論理的な処理の同期の単位を切り分けることができる。

これがなぜ嬉しいかと言うと、例えばBitonic-Sortingを実行するときに、要素数が1個のSMで処理するには辛い数(1万個とか)になってくると、ステップ事に一度GPU kernelを完了し、そこで同期した上で改めて次のステップを実行するという処理が必要になる。
これは結構しんどい処理で、PG-StromでGPUでのORDER BY処理の実装を断念した要因の一つでもある。
が、SMを越えた単位で処理を同期できるとなれば、ステップ事に同期ポイントを設定してやればよいだけである。

例えば、以下のような感じで処理を記述する。

__global__ void
particleSim(Particle *p, int N)
{
  grid_group g = this_grid();

  // 1. Jobs prior to sync
  for (i = g.thread_rank(); i < N; i += g.size())
    integrate(p[i]);

  g.sync() // 2. Sync whole grid

  // 3. Jobs post sync
  for (i = g.thread_rank(); i < N; i += g.size())
    collide(p[i], p, N);
}

grid_groupというのはco-operative groupを表現するためのクラスで、this_grid()はこのGPU kernelを起動した時の全てのスレッドを含むオブジェクト。
つまり、g.sync()は複数のSMに跨った全てのスレッドで同期を取ることを意味している。

他にも、従来同様に現在のブロックに含まれるスレッドを表現する thread_group や、逆に複数のデバイスで同時に実行中の各GPU kernelに含まれるスレッドを表現する multi_grid_group が定義されている。
なお、SM間/デバイス間での同期機構を使う場合には、GPU kernelを起動する時に用いるAPIが従来のものとは少し異なっており、Runtime APIでは以下のAPIを用いてGPU kernelをキックしなければならない。

  • grid_groupを使う場合
    • cudaLaunchCooperativeKernel()
  • multi_grid_groupを使う場合
    • cudaLaunchCooperativeKernelMultiDevice()

で、更に面白いことに、このco-operative groupというのは更に細かい単位にパーティション化する事ができる*1

こんな調子で、thread_group gに含まれるスレッドを tiled_partition() を介して4つのパーティションに分割する事ができるらしい。
なので、真ん中の tile.sync() ではパーティション毎の同期という事になる。

  thread_group g = this_thread_label()
  thread_gruup tile = tiled_partition(g, 4);
        :
  tile.sync();
        :

現状、CUDA 9では等分割のパーティショニングのみだが、将来的には labeled partition という形にコンセプトが拡張されるとの事。
"labeled" というのは、以下のように partition() 関数に与えた整数値によってそのスレッドが属するパーティションを決めるというもの。

  int label = foo() % 4;
  thread_group g = partition(this_thread_block(), label);

何が嬉しいかというと、GROUP BY/集約演算をGPUで実装するときに、GROUP BYで指定したカラムの値が同一のスレッド間でのみ同期を取り、縮約演算を行うというロジックが可能になること。これは、現状atomic演算を用いて実装せざるを得ないことを考えると非常に大きなアドバンテージになる。

CUDA 9.0の登場が待ち遠しいところである。

余談

SM間で同期を取るとき、GPU kernelのブロック数がデバイスのSM数よりも多い場合、ハードウェア的にはSMを明け渡さないといけないはずなので、そのタイミングで共有メモリやレジスタをグローバルメモリに退避する(= そんなに軽い処理じゃないのでは?)と思い、セッション後に発表者を捕まえて質問してみた。
曰く、Pascal世代で(H/W的には)対応したプリエンプションの仕組みを使って実装されており、レジスタや共有メモリの使用量がH/Wリソースを超える場合には確かにそうなる。なので、GPU kernelを起動するときのパラメータチューニングが必要で、SMあたり {(ブロック数)/(デバイスのSM数)} のブロックを保持できる程度のレジスタ/共有メモリの消費に抑えておく必要がある。
一応、その辺は Occupancy を計算するAPIを使って効率的なブロックサイズ/グリッドサイズを求めてほしいとの事。

お願い

S7285 - Unified Memory on the Volta architecture』のセッションが、CUDA9のセッションと被ってて聞けなかった!誰か内容教えて!!

NVIDIA TechBlogにも既に発表内容を踏まえた記事が掲載されています。

(*) 本文中の画像は全て上記ブログからの引用です。

*1:が、セッションの中ではthread_groupの例しか示されてなかったので、grid_groupやmulti_grid_groupでもできるかは要確認

Posters flying on GTC2017

I'm on GTC(GPU Technology Conference) this year again.

I didn't plan to trip because of travel budget, so only posters are submitted and approved.
(Posters are shown regardless of the attendance of the presenter.)

But I got an e-mail below...

Hi Kohei,

After extensive review from multiple committees and nearly 140 posters, your poster has been chosen as a Top 5 Finalist. Congratulations!!

Eventually, I arranged a trip in hurry, then now in San Jose.

The top-5 posters are shown at most conspicuous place of the convention center.

Participants can vote one of their favor poster, then the best poster this year is chosen through the democratic process. :-)

I and other top-20 presenters had a short talk on evening of the 1st day of the conference.
4-minutes are given to each presenter, and we introduce their research in this short time-slot.
(It is shorter than usual lightning talks!)

Our challenge tries to de-define the role of GPU. People usually considers GPU is a powerful tool to tackle computing intensive workloads. It is right, and widespread of workloads utilizes GPUs well like deep learning, simulations and so on.
On the other hands, I/O is one major workloads for database; including data analytics which try to handle massive amount of data.


We designed and implemented a Linux kernel module that intermediates peer-to-peer data transfer between NVMe-SSD and GPU, on top of GPUDirect RDMA feature. It allows to load PostgreSQL's data blocks on NVMe-SSDs to GPU's RAM directly.
Once data blocks get loaded to GPU's RAM, we already have SQL execution engine on GPU devices.
It means we can process earlier stage of SQL execution prior to data loading onto the system main memory. The earlier stage includes WHERE-clause, JOIN and GROUP BY. They likely work to reduce amount of data.
For example, when an WHERE-clause filters out 90% of rows (it is not a strange assumption in reporting queries) on scan of 100GB tables, we can say 90GB of I/O bandwidth and dist cache to be wipe out are consumed by junk data.
Once data blocks are loaded to GPU, it can filter out unnecessary rows (and processes JOIN and GROUP BY), then write back the results of this pre-process to host memory. The data blocks written back contains all valid data because unnecessary data are already dropped off.


In the results, it performs nearly wired speed of NVMe-SSD.
In case of single SSD benchmark, query execution throughput by vanilla PostgreSQL is around 700MB/s. On the other hands, SSD-to-GPU Direct SQL-execution runs same query with 2.2GB/s; which is almost identical with catalog spec of Intel SSD 750 (SeqRead: 2200MB/s).
In case of multi SSDs cases, it also increases the performance, however, deeper investigation *1 and improvement for md0-raid support are needed.


We intend to position this technology to make summary and pre-process of raw data/logs generated by sensor devices, mobile devices and so on. Once logs get summarized, we can apply statistical-analytics / machine-learning algorithms.
This type of workloads are advantaged with PL/CUDA which is one other our feature to utilize GPUs in-database analytics.
Eventually, GPU-accelerated PostgreSQL will be capable to handle entire life-cycle of data utilization (generation - gathering - summarizing/pre-process - analytics/machine-learning - visualization).
It makes system configuration much simple, reduce cost for production and operation, and deliver the power of analytics to wider and wider people.

My poster is below (enlarge by click):

If you are favored with our development, don't forget to vote.

Enjoy your GTC2017!

(Comment to the above photo by my wife: it looks like a zombie.)

*1:At that time, we measured with Teska K80; that is a bit old.

進捗)SSD-to-GPU ダイレクトSQL実行機能

ここ暫くブログでまとめていなかった、SSD-to-GPUダイレクトSQL実行機能の進捗について。

この機能をかいつまんで言うと、NVMe-SSDに格納されているPostgreSQLのデータブロックをGPU RAMに直接転送し、そこでSQLのWHERE句/JOIN/GROUP BYを実行することで見かけ上のI/O量を削減するという代物である。
NVIDIAのTesla/Quadro GPUが対応するGPUDirect RDMA機能を使い、SSD<=>GPU間のデータ転送を仲介するLinux kernel moduleを使えば、CPU/RAMにデータをロードする前にGPU上での処理を行うことができる。

しばらく前からScan系の処理には対応していたが、JOIN/GROUP BYへの対応を加え、さらにPostgreSQL v9.6のCPU並列にも追従したということで、簡単なベンチマークなら取れるくらいまで開発は進んできている。

という事で、現時点での実力がどの程度なのか、手元の環境を使って測定してみることにした。
使用したのはDELL R730にTesla K80*1Intel SSD 750(400GB)を2枚搭載したモデル。
GPUDirect RDMAの制約から、SSDGPUが同一のCPU又はPCIeスイッチに接続されている必要があるため、共にCPU2配下のPCIeスロットに接続している。

ベンチマークに使用したのはTPC-Hを簡略化したStar Schema Benchmark(SSBM)と呼ばれるテストで、中核となるlineorderテーブルのサイズが明らかにRAMに収まり切らないようscaling factorを調整している。

SSBMでは何種類かクエリが定義されているが、基本的にはWHERE句/JOIN/GROUP BYという、集計クエリの典型的なものである。例えば以下のようなものである。

SELECT sum(lo_revenue), d_year, p_brand1
  FROM lineorder, date1, part, supplier
 WHERE lo_orderdate = d_datekey
   AND lo_partkey = p_partkey
   AND lo_suppkey = s_suppkey
   AND p_category = 'MFGR#12‘
   AND s_region = 'AMERICA‘
 GROUP BY d_year, p_brand1
 ORDER BY d_year, p_brand1;

それを実行してみた結果が以下の通り。じゃん!

青色がPostgreSQL(ファイルシステム経由I/O)での実行結果、橙色がPG-Stromでの実行結果で、それぞれ色の濃い方がSSD x1枚での実行結果、色の薄い方がSSD x2枚(md-raid0)での実行結果。

これを見る限り、SSD x1枚の場合、PG-Stromは概ね理論限界*2に達しているが、SSD x2枚のスループットである4.4GB/sにはまだ届いていない。ただ、これはGPUがTesla K80というやや古いモデルであった事や、GROUP BYでの集約演算がNumeric型の総和や平均など、Kepler世代のGPUには比較的辛い処理だという事は考慮しなければいけないだろう。(例えばQ4-1やQ4-2ではI/Oよりも集計処理にボトルネックがあるように見える)
一方PostgreSQLの場合はSSD x1枚で600-750MB/s程度、SSD x2枚で1.6GB/s程度のスループットなので、PG-Stromの場合はI/Oネックな集計処理において2.3~3.0倍程度の優位性があるという事になるだろう。

この辺、SSDGPU、あるいはCPUのどこに律速要因があるのか追及するのはなかなか骨の折れる作業ではあるが、近々、最新モデルであるTesla P40で実行できるようになるので、最新世代のGPUではどの程度のスループットまで耐えられるのか、試してみたいところである。

本日の記事のより詳しい内容は、先日、BigData基盤研究会#7の方で喋らせて頂いた資料を公開しているので、そちらも併せて参照していただければ。

www.slideshare.net

*1:ちょっと古い

*2:Intel SSD 750(400GB)のSeqReadスペックは2200MB/s

PCIeスロット接続型NVMe-SSDまとめ(2017年4月時点)

PCIeスロット接続型のNVMe-SSDのスペック等、各社どうだったけな~と探すものの、案外まとまった情報がないので自分でまとめてみた。ブックマーク代わりです。
基本的に各社のWebに掲載されているカタログスペックを転記。手作業なので内容の正確さに関しては保証しかねます。(指摘頂ければ直します)

Intel

コンシューマ向け

モデル Intel SSD 750
400GB(MLC)
Intel SSD 750
800GB(MLC)
Intel SSD 750
1.2TB(MLC)
形状 HHHL HHHL HHHL
PCIe PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4
容量 400GB 800GB 1.2TB
SeqRead 2100MB/s 2100MB/s 2400MB/s
SeqWrite 800MB/s 800MB/s 1200MB/s
RandRead 420kIOPS 420kIOPS 440kIOPS
RandWrite 210kIOPS 210IOPS 290kIOPS
リリース Q3'15 Q3'15 Q2'15

エンタープライズ向け

モデルl DC P3608
1.6TB(MLC)
DC P3608
3.2TB(MLC)
DC P3608
4.0TB(MLC)
DC P3700
400GB(MLC)
DC P3700
800GB(MLC)
DC P3700
1.6TB(MLC)
DC P3700
2.0TB(MLC)
形状 HHHL HHHL HHHL HHHL HHHL HHHL HHHL
PCIe PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4
容量 1.6TB 3.2TB 4.0TB 400GB 800GB 1.6TB 2.0TB
SeqRead 5000MB/s 4500MB/s 5000MB.s 2700MB/s 2800MB/s 2800MB/s 2800MB/s
SeqWrite 2000MB/s 2600MB/s 3000MB/s 1080MB/s 1900MB/s 1900MB/s 1900MB/s
RandRead 850kIOPS 850kIOPS 850kIOPS 450kIOPS 460kIOPS 450kIOPS 450kIOPS
RandWrite 150kIOPS 80kIOPS 50kIOPS 75kIOPS 90kIOPS 150kIOPS 175kIOPS
リリース Q3'15 Q3'15 Q3'15 Q2'14 Q2'14 Q2'14 Q2'14

※ Optane DCモデルに関してはARKにデータが上がってから更新予定。

Samsung

エンタープライズ向け

モデル PM1725a
1.6TB(V-NAND)
PM1725a
3.2TB(V-NAND)
PM1725a
6.4TB(V-NAND)
PM1725
3.2TB(V-NAND)
PM1725
6.4TB(V-NAND)
形状 HHHL HHHL HHHL HHHL HHHL
PCIe PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8
容量 1.6TB 3.2TB 6.4TB 3.2TB 6.4TB
SeqRead 5840MB/s 6200MB/s 6300MB/s 5600MB/s 5600MB/s
SeqWrite 2100MB/s 2600MB/s 2600MB/s 1800MB/s 1800MB/s
RandRead 1000kIOPS 1000kIOPS 1000kIOPS 1000kIOPS 1000lIOPS
RandWrite 140kIOPS 180kIOPS 180kIOPS 130kIOPS 130kIOPS
リリース Q3'16 Q3'16 Q3'16 Q3'15 Q3'15

HGST

エンタープライズ向け

モデル Ultrastar
SN260(1.6TB)
Ultrastar
SN260(3.2TB)
Ultrastar
SN260(6.4TB)
Ultrastar
SN150(1.6TB)
Ultrastar
SN150(3.2TB)
形状 HHHL HHHL HHHL HHHL HHHL
PCIe PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x4 PCIe 3.0 x4
容量 1.6TB 3.2TB 6.4TB 1.6TB 3.2TB
SeqRead 6100MB/s 6100MB/s 6100MB/s 3000MB/s 3000MB/s
SeqWrite 2200MB/s 2200MB/s 2200MB/s 1600MB/s 1600MB/s
RandRead 1200kIOPS 1200kIOPS 1200kIOPS 743kIPOS 743kIPOS
RandWrite 200kIOPS 200kIOPS 200kIOPS 140kIPS 140kIOPS
リリース Q4'16 Q4'16 Q4'16 Q2'15 Q2'15

Seagate

エンタープライズ向け

モデル Nytro XP7200
7.7TB(MLC)
Nytro XP6500
1.5TB
Nytro XP6500
4.0TB
Nytro XP7102
800GB
Nytro XP7102
1.6TB
形状 FHHL FHHL FHHL HHHL HHHL
PCIe PCIe 3.0 x16 PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x4 PCIe 3.0 x4
容量 7.7TB 1.3TB 3.4TB 800GB 1.6TB
SeqRead 10000MB/s 4.0GB/s 4.0GB/s 2500MB/s 2500MB/s
SeqWrite 2300MB/s 1.5GB/s 2.0GB/s 850MB/s 900MB/s
RandRead 950kIOPS 300kIPOS 275kIOPS 245kIOPS 245kIOPS
RandWrite 64kIPS 100kIOPS 75kIOPS 35kIOPS 40kIOPS
リリース Q3'16 ??? ??? Q3'16 Q3'16

ショックだ・・・Nytro XP7200の3.8TB版がサイトから消えている・・・。 orz

OCZ社

コンシューマ向け

モデル OCZ RD400a
128GB(MLC)
OCZ RD400a
256GB(MLC)
OCZ RD400a
512GB(MLC)
OCZ RD400a
1.0TB(MLC)
形状 HHHL HHHL HHHL HHHL
PCIe PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4
容量 128GB 256GB 512GB 1.0TB
SeqRead 2200MB/s 2600MB/s 2600MB/s 2600MB/s
SeqWrite 620MB/s 1150MB/s 1600MB/s 1550MB/s
RandRead 170kIPS 210kIOPS 190kIOPS 210kIOPS
RandWrite 110kIOPS 140kIPS 120kIOPS 130kIOPS
リリース Q2'16 Q2'16 Q2'16 Q2'16

Plextor

コンシューマ向け

モデル M8Pe(Y)
128GB
M8Pe(Y)
256GB
M8Pe(Y)
512GB
M8Pe(Y)
1.0TB
形状 HHHL HHHL HHHL HHHL
PCIe PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4 PCIe 3.0 x4
容量 128GB 256GB 512GB 1.0TB
SeqRead 1600MB/s 2000MB/s 2300MB/s 2500MB/s
SeqWrite 500MB/s 900MB/s 1300MB/s 1400MB/s
RandRead 120kIPS 210kIOPS 260kIPS 280kIOPS
RandWrite 130kIOPS 230kIPS 250kIOPS 240kIPS
リリース Q4'16 Q4'16 Q4'16 Q4'16

Kingstone社

エンタープライズ向け

モデル DCP1000
800GB
DCP1000
1.6TB
DCP1000
3.2TB
形状 HHHL HHHL HHHL
PCIe PCIe 3.0 x8 PCIe 3.0 x8 PCIe 3.0 x8
容量 800GB 1.6TB 3.2TB
SeqRead 6800MB/s 6800MB/s 6800MB/s
SeqWrite 5000MB/s 6000MB/s 6000MB/s
RandRead 900kIOPS 1100kIPS 1000kIOPS
RandWrite 145kIOPS 200kIOPS 180kIPS
リリース Q1'17 Q1'17 Q1'17

※なぜかメーカーのサイトから消えている・・・。

AWSのP2.*インスタンスで PG-Strom を試す

従前、AWSの提供するGPUインスタンス g2.* に搭載されているGPUはGRID K520というちょっと古いモデルで、PG-Stromは非対応だった。
理由は、一年ほど前にComputing Capability 3.5以降で対応のDynamic Parallelism機能を使うように全面的に作り直したからで、詳細は以下のエントリを参照。

kaigai.hatenablog.com

その後、昨年の10月にAWSは新世代*1GPUインスタンスを新たにリリースした。

japan.zdnet.com

これでPG-Stromの動作要件を満たすようになった上に、特にメモリ搭載量で相応の強化が行われたため、例えばPGconf.ASIAで発表を行った創薬領域の類似度サーチのような、I/Oが支配的でないようなワークロードであれば相応の効果が見込める、ハズである。

発表から少し間が空いてしまったが、p2.*インスタンスで動作検証を行い、当該環境をAMIイメージとして公開してみた。

環境構築

基本的にはAMIを探してポチポチするだけである。


最初に、リージョンを p2.* インスタンス提供済みのリージョンに切り替える。
現状、東海岸の「バージニア北部」、西海岸の「オレゴン」、欧州の「アイルランド」での提供が開始されている模様。東京はまだである*2
とりあえず、今回は「オレゴン」リージョンを使用。

また、AWSアカウントを作成直後は p2.* インスタンスの起動制限数が 0 に設定されているため、p2.xlargeインスタンスを起動できるよう、EC2ダッシュボードの『制限』から『制限緩和のリクエスト』を行う必要がある。豆知識。


次に、コミュニティAMIから『PGStrom』で検索すれば、夜なべして作ったAMIイメージがヒットする。
これは PG-Strom v1.0 + PostgreSQL v9.5.5 + CUDA 7.5環境を CentOS7(x86_64) 上に構築したもので、テスト用のデータも既にセットアップ済みのものである。


次に、起動すべきインスタンスを選択する。
p2.*インスタンスは以下の3種類提供されており、今回は『p2.xlarge』タイプを使用した。
なお、Amazon様も『高性能データベース』用途ときちんと書いていてくれておりますw

一応、PG-StromはマルチGPUにも対応しているが、CPU並列を使えないPostgreSQL v9.5系列ではGPUへのデータ供給の方が先にボトルネックになってしまうので、あまり意味はない。
この辺は、現在開発中の PostgreSQL v9.6 ベースの実装でうまくハンドリングできるようになる。

インスタンスの構成を確認して起動。ストレージはroot区画に24GBをアタッチするだけのシンプルなものがデフォルト構成。

しばらく待っているとインスタンスが立ち上がってくる。

『ec2-user』でログインする事ができる。

動作確認

早速動作確認を行う。ec2-userはlocalhost経由でPostgreSQLサーバに接続できるよう設定されている。

[ec2-user@ip-172-31-24-196 ~]$ psql postgres
psql (9.5.5)
Type "help" for help.

postgres=#

Tesla K80 GPUが搭載されている事が分かる。
物理的には一枚のカード上にGPU 2個を搭載するデバイスなので、一枚のカードを他の誰かとシェアしている事になる。

postgres=# SELECT * FROM pgstrom_device_info();
 id |                       property                        |    value
----+-------------------------------------------------------+--------------
  0 | Device name                                           | Tesla K80
  0 | Total global memory size                              | 11519 MBytes
  0 | max threads per block                                 | 1024
  0 | Maximum block dimension X                             | 1024
  0 | Maximum block dimension Y                             | 1024
  0 | Maximum block dimension Z                             | 64
  0 | Maximum grid dimension X                              | 2147483647
  0 | Maximum grid dimension Y                              | 65535
  0 | Maximum grid dimension Z                              | 65535
  0 | Maximum shared memory available per block             | 48 KBytes
  0 | Memory available on device for __constant__           | 64 KBytes
  0 | Warp size in threads                                  | 32
  0 | Maximum pitch in bytes allowed by memory copies       | 2147483647
  0 | Maximum number of 32bit registers available per block | 65536
  0 | Typical clock frequency in kilohertz                  | 823 MHz
  0 | Alignment requirement for textures                    | 512
  0 | Number of multiprocessors on device                   | 13
  0 | Has kernel execution timeout                          | False
  0 | Integrated with host memory                           | False
  0 | Host memory can be mapped to CUDA address space       | True
  0 | Compute mode                                          | Default
  0 | Alignment requirement for surfaces                    | 512
  0 | Multiple concurrent kernel support                    | True
  0 | Device has ECC support enabled                        | True
  0 | PCI bus ID of the device                              | 0
  0 | PCI device ID of the device                           | 30
  0 | Device is using TCC driver model                      | False
  0 | Peak memory clock frequency                           | 2505 MHz
  0 | Global memory bus width                               | 384 bits
  0 | Size of L2 cache in bytes                             | 1536 KBytes
  0 | Maximum threads per multiprocessor                    | 2048
  0 | Number of asynchronous engines                        | 2
  0 | Device shares unified address space                   | True
  0 | PCI domain ID of the device                           | 0
  0 | Major compute capability version number               | 3
  0 | Minor compute capability version number               | 7
  0 | Device supports stream priorities                     | True
  0 | Device supports caching globals in L1                 | True
  0 | Device supports caching locals in L1                  | True
  0 | Maximum shared memory per multiprocessor              | 112 KBytes
  0 | Maximum number of 32bit registers per multiprocessor  | 131072
  0 | Device can allocate managed memory on this system     | True
  0 | Device is on a multi-GPU board                        | False
  0 | Unique id if device is on a multi-GPU board           | 0
(44 rows)

早速、シンプルな集計のクエリを叩いてみるが、その前にメインのテーブルに対してpg_prewarmを実行しておくが吉。
gp2ストレージタイプの場合、バッファに載っていないデータをロードするのに割と時間がかかるようなので、I/Oネックになってしまうと、CPUとかGPUとかいう次元の遥か手前で引っかかってしまう。

postgres=# SELECT pg_prewarm('t0'::regclass);
 pg_prewarm
------------
     833334
(1 row)

実行計画を確認する。JOIN+GROUP BYをGPUで実行するようにプランが選択された事がわかる。

postgres=# EXPLAIN SELECT cat,count(*),avg(ax) FROM t0 NATURAL JOIN t1 GROUP BY cat;
                                         QUERY PLAN
---------------------------------------------------------------------------------------------
 HashAggregate  (cost=3578311.16..3578311.48 rows=26 width=12)
   Group Key: t0.cat
   ->  Custom Scan (GpuPreAgg)  (cost=14139.24..2873626.84 rows=234 width=48)
         Reduction: Local + Global
         GPU Projection: cat, ax
         ->  Custom Scan (GpuJoin) on t0  (cost=10139.24..2838812.04 rows=98599882 width=12)
               GPU Projection: t0.cat, t1.ax
               Depth 1: GpuHashJoin, HashKeys: (t0.aid)
                        JoinQuals: (t0.aid = t1.aid)
                        Nrows (in/out: 98.60%), KDS-Hash (size: 13.47MB, nbatches: 1)
               ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
(11 rows)

1億行のテーブルのJOINとGROUP BYで11.3秒要している。
セッションに接続後の初回実行だと、これに加えて、GPUコンテキストの初期化やGPUコードのJITコンパイルの時間も余分にかかるが、オンプレに比べるとAWSの環境ではこの辺が多少もっさりする印象がある。

postgres=# SELECT cat,count(*),avg(ax) FROM t0 NATURAL JOIN t1 GROUP BY cat;
 cat |  count  |       avg
-----+---------+------------------
 nnn | 3845736 | 49.9122204644341
 ccc | 3842975 | 49.9253161146813
 ddd | 3841209 | 49.9346989307005
 aaa | 3848221 | 49.9265219996308
 kkk | 3843481 | 49.9232348058601
 fff | 3845484 | 49.9434949581969
 iii | 3846743 | 49.9227199719054
 jjj | 3846076 | 49.9292863471083
 qqq | 3845646 |  49.945213365697
 hhh | 3842519 | 49.9266693322143
 ttt | 3846725 | 49.9232478784252
 ooo | 3847927 | 49.9346102129999
 zzz | 3847116 | 49.9320751724648
 lll | 3848447 |  49.927865906661
 www | 3846691 | 49.9537192102014
 bbb | 3845315 | 49.9281298849625
 ppp | 3850842 | 49.9149069792416
 eee | 3846285 |  49.931458202446
 xxx | 3845570 | 49.9577920754281
 ggg | 3845044 | 49.9409383291351
 rrr | 3847816 | 49.9341189910578
 uuu | 3845813 | 49.9295202543591
 vvv | 3849157 | 49.9253944163053
 yyy | 3843414 | 49.9364087656463
 mmm | 3848758 | 49.9033622681507
 sss | 3846990 | 49.9213589517191
(26 rows)

Time: 11326.834 ms

一方、PG-Stromを無効にした場合は以下のように、同じクエリの実行に51.0秒を要している。トゥットゥルー。

postgres=# EXPLAIN SELECT cat,count(*),avg(ax) FROM t0 NATURAL JOIN t1 GROUP BY cat;
                                 QUERY PLAN
-----------------------------------------------------------------------------
 HashAggregate  (cost=3937016.94..3937017.26 rows=26 width=12)
   Group Key: t0.cat
   ->  Hash Join  (cost=3185.00..3197517.82 rows=98599882 width=12)
         Hash Cond: (t0.aid = t1.aid)
         ->  Seq Scan on t0  (cost=0.00..1833334.00 rows=100000000 width=8)
         ->  Hash  (cost=1935.00..1935.00 rows=100000 width=12)
               ->  Seq Scan on t1  (cost=0.00..1935.00 rows=100000 width=12)
(7 rows)

postgres=# SELECT cat,count(*),avg(ax) FROM t0 NATURAL JOIN t1 GROUP BY cat;
 cat |  count  |       avg
-----+---------+------------------
 nnn | 3845736 | 49.9122204644368
 ccc | 3842975 |  49.925316114681
 ddd | 3841209 |  49.934698930695
 aaa | 3848221 | 49.9265219996321
 kkk | 3843481 | 49.9232348058579
 fff | 3845484 | 49.9434949581975
 iii | 3846743 |    49.9227199719
 jjj | 3846076 | 49.9292863471066
 qqq | 3845646 | 49.9452133657041
 ttt | 3846725 | 49.9232478784215
 hhh | 3842519 |  49.926669332217
 zzz | 3847116 |  49.932075172461
 ooo | 3847927 | 49.9346102129966
 lll | 3848447 | 49.9278659066662
 www | 3846691 | 49.9537192102029
 bbb | 3845315 | 49.9281298849615
 ppp | 3850842 | 49.9149069792369
 eee | 3846285 |  49.931458202444
 xxx | 3845570 | 49.9577920754269
 ggg | 3845044 | 49.9409383291352
 uuu | 3845813 | 49.9295202543602
 rrr | 3847816 | 49.9341189910555
 vvv | 3849157 | 49.9253944163048
 yyy | 3843414 | 49.9364087656454
 mmm | 3848758 | 49.9033622681445
 sss | 3846990 | 49.9213589517182
(26 rows)

Time: 51031.084 ms

p2.* インスタンスの登場でPG-Stromを使用するための環境の準備はずいぶんお手軽になった。デプロイ即利用できるようAMIイメージも用意してみたので、まだPG-Stromを触った事がないという方は、ぜひ試してみて頂ければと思う。

*1:と言っても、Tesla K80なんですが…。

*2:予定なしとか悲しい事は言わないでほしいけど

2017年の開発ロードマップについて考える

あけましておめでとうございました。(やや出遅れ感)

新年という事で、この一年、どういった技術開発に取り組んでいきたいかをざーっと書き出してみる事にする。
これらのうち、いくつかはPostgreSQL本体機能の強化を伴うものであったりするので、ある程度計画的にモノゴトを進めないといけないワケで…。

PG-Strom v2.0

先ず最優先で取り組むのが、PostgreSQL v9.6への対応。
CPUパラレル実行と、新しいオプティマイザへの対応でかなり大きなアーキテクチャ上の変更を伴ったものの、全体としてはよりシンプルな設計に落とし込む事ができている。

ちなみに、現状だとこの程度までは動くようになっている。
集約演算がGroupAggregateGpuPreAggの二段階に分解されており、GpuPreAggGatherの配下で並列に動作している事に注目。

postgres=# EXPLAIN (ANALYZE, VERBOSE)
           SELECT cat, count(*), avg(aid), max(bid)
             FROM t0
            WHERE aid < 50000 and cid > 50000
            GROUP BY cat;

                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 GroupAggregate  (cost=91050.50..91070.84 rows=26 width=48)
                 (actual time=1754.432..1755.056 rows=26 loops=1)
   Output: cat, pgstrom.sum((pgstrom.nrows())),
                pgstrom.favg((pgstrom.pavg((pgstrom.nrows((aid IS NOT NULL))),
                             (pgstrom.psum((aid)::bigint))))),
                max((pgstrom.pmax(bid)))
   Group Key: t0.cat
   ->  Sort  (cost=91050.50..91052.32 rows=728 width=48)
             (actual time=1754.381..1754.524 rows=910 loops=1)
         Output: cat, (pgstrom.nrows()), (pgstrom.pavg((pgstrom.nrows((aid IS NOT NULL))),
                                                       (pgstrom.psum((aid)::bigint)))),
                 (pgstrom.pmax(bid))
         Sort Key: t0.cat
         Sort Method: quicksort  Memory: 160kB
         ->  Gather  (cost=90938.54..91015.89 rows=728 width=48)
                     (actual time=1749.313..1753.732 rows=910 loops=1)
               Output: cat, (pgstrom.nrows()), (pgstrom.pavg((pgstrom.nrows((aid IS NOT NULL))),
                                                             (pgstrom.psum((aid)::bigint)))),
                       (pgstrom.pmax(bid))
               Workers Planned: 4
               Workers Launched: 4
               ->  Parallel Custom Scan (GpuPreAgg) on public.t0  (cost=89938.54..89943.09 rows=182 width=48)
                                                            (actual time=1670.139..1673.636 rows=182 loops=5)
                     Output: cat, (pgstrom.nrows()),
                             pgstrom.pavg((pgstrom.nrows((aid IS NOT NULL))),
                                          (pgstrom.psum((aid)::bigint))),
                             (pgstrom.pmax(bid))
                     Reduction: Local
                     GPU Projection: t0.cat, pgstrom.nrows(), pgstrom.nrows((t0.aid IS NOT NULL)),
                                     pgstrom.psum((t0.aid)::bigint), pgstrom.pmax(t0.bid), t0.aid, t0.cid
                     Outer Scan: public.t0  (cost=4000.00..87793.21 rows=2496387 width=12)
                                            (actualtime=14.663..274.534 rows=500187 loops=5)
                     Outer Scan Filter: ((t0.aid < 50000) AND (t0.cid > 50000))
                     Rows Removed by Outer Scan Filter: 1499813
                     Extra: slot-format
                     Worker 0: actual time=1724.195..1728.058 rows=182 loops=1
                     Worker 1: actual time=1570.952..1573.837 rows=182 loops=1
                     Worker 2: actual time=1738.205..1742.053 rows=182 loops=1
                     Worker 3: actual time=1569.055..1571.961 rows=182 loops=1
 Planning time: 0.907 ms
 Execution time: 1759.557 ms
(25 rows)

また、PG-Strom v2.0では、PostgreSQL v9.6へのキャッチアップだけではなく、いくつか目玉となる機能を準備中である。
一つは、これまで何度か紹介している SSD-to-GPU P2P DMA 機構。そしてもう一つは、BRINインデックスへの対応である。

SSD-to-GPU P2P DMA

SSD-to-GPU P2P DMA (NVMe-Strom) は、NVIDIA社製GPUのGPUDirect RDMA機構を利用したもので、PostgreSQLのデータブロックが格納されているNVMe-SSDのデータブロックからGPUへとダイレクトにデータ転送を行う。ファイルシステムを介する事によるオーバーヘッドや、RAMへの無駄なコピーが発生しないため、スループットを稼げるという特長がある。
現状では、GpuScanワークロード下においてNVMe-SSD 1個から成る区画からのデータ転送に対応しており、シングルプロセス性能で1.4GB/sのスキャン性能を出している。
PostgreSQL v9.6対応の過程で、GpuJoinやGpuPreAggの直下にテーブルスキャンが入る場合、これらのロジックはGpuScanがテーブルをスキャンするための関数を直接呼ぶように改良されているので、特別な事は何もしなくても『ストレージからデータを読んだ時点で既にJOIN/GROUP BYが完了している』という状態を作り出す事はできるはず。

PG-Strom v2.0に向けた課題はSoft-RAID0/1への対応。Linuxの場合、基本的には128KB単位で順番にストライピングがかかっているだけなので、技術的にはそう難しい話ではないと考えている。
DC用途向けに、PCIe x8スロット接続で5~6GB/s程度のSeqRead性能を持つNVMe-SSD製品が各社から出てきているので、計算上は、SSD二枚から全力でGPUにデータを流し込む事ができれば、GPUの持つPCIe x16スロットの帯域を飽和させられる事になる。

BRINインデックス対応

BRINインデックス自体はPostgreSQL v9.5から搭載されている機能で、特に時系列データのように

  1. ある一定範囲の値を持つデータが物理的な近傍に集まっている
  2. データの更新頻度が小さい
  3. データサイズが大きい

といった特徴を備えたデータセットに向いており、例えば、センサデータをPostgreSQLに収集して解析するといったワークロードに有効な機能。

BRINインデックス自体は、永安さんのこちらの記事が詳しいです。
pgsqldeepdive.blogspot.jp

PG-Stromとしては、搭載RAMが比較的小さなGPUを使うという事もあり、B-treeのようなランダムメモリアクセスを前提としたインデックスへの対応は厳しい。
ただ、条件句の評価はGPUの数千コアを使って並列処理が可能であるものの、インデックスの選択率が高くなると分の悪い勝負なので『このブロックは明らかに該当行なし』という事が分かっているなら、それを読み飛ばしたい。

GpuScanがBRINインデックスを理解し、必要のないブロックを読み飛ばす事ができるようになれば、例えばIoTのキーワードに絡めてセンサデータの集積・解析用途に、という使い方もできるハズである。
特に、PG-Stromはカラムナーを前提としたDWHではないので、生データをそのまま処理させても高速化できるという点は強みになるだろう。

PL/CUDA

PL/CUDAに関しては、言語バインディングに関してする事は(できる事は)多くないので、その周辺領域を拡充していきたい。

一つは、PostgreSQLにおける可変長データの1GB越え。
現状、全ての可変長データの基盤となっている varlena 構造は、最大でも1GBまでのデータしか持てないため、PL/CUDA関数の引数としてarray-matrixを渡す時には、例えば問題領域をうまく分割してデータサイズを1GB未満に抑えてやらないといけない。

昨年10月のCBI学会で発表した研究でも、1000万件の化合物データ(1.3GB)をロードするにはサイズが大きすぎたので、安全マージンも見て4分割した上でGPU側へロードしている。

しかし、昨今のGPUでは10GB近く、あるいはそれ以上のメモリを搭載するのが常識的になりつつあり、問題領域を1GB以内に抑えねばならない、、、というのはユーザにいかにも不都合である。

先にpgsql-hackersにデザインプロポーザルを投げたところ、Robert Haasから『varlenaとは別の体系で可変長データを保持するフォーマットを作成すべき』とサジェスチョンがあり、自分もこの方針には同意。3月のcommit-festまでにはパッチを投稿し、2018年リリース予定のPostgreSQL v11でのマージを目指したい。

もう一つは、現状、複雑な計算ロジックを個々のユーザ毎に書かねばならないという点である。
2017年1月時点でPL/CUDAを実証できたワークロードとしては、k-NN法類似度サーチや、k-meansクラスタリングがあるが、例えば MADLib のような統計解析パッケージで提供されているアルゴリズムの、全部とは言わないまでも、使用頻度が高く計算負荷の高いものをGPUで計算するようパッケージ化できれば、ユーザの裾野はより広がるだろうし、仮にカスタマイズが必要となっても骨格となるアルゴリズムGPU実装が既に存在する事で省力化が可能となるハズである。

PG-Strom v3.0へ向けた種まき

In-memory Columnar Cache

NVMe-SSDとの密連携の他にI/O系処理を高速化する方策として、通常のPostgreSQLテーブル(行形式)の脇に、予めDMAバッファ上に列形式にデータを再編したキャッシュを持たせる機構を考えている。
このキャッシュはBackground-workerにより非同期で作成され、そのため、スキャンする区間のうち一部領域しか列形式のキャッシュが構築されていないかもしれない。しかし、その場合でもPG-StromはGPUで行形式データを扱えるので(多少のパフォーマンス差に目をつぶれば)大きな問題とはならない。

データ本体とキャッシュを別に持つ場合、必ず一貫性制御が複雑で頭の痛い問題として立ちはだかる。
イデアとしては、これを避けるために ALL_VISIBLE フラグが 1 であるブロックのみを列形式キャッシュに持たせる。
ALL_VISIBLE=1であるブロックは、MVCC制御に関わらず、全てのレコードが全てのトランザクションから可視である事が保証されている。そのため、複雑な同時実行制御に頭を悩ませる必要はなく、全てのレコードの内容を単純に列データとして展開すればよい。

問題は、PostgreSQL側でテーブルが更新され、ALL_VISIBLE フラグが 0 にクリアされた時のinvalidation処理である。
現状、ここにフックを挟む事はできないので、PostgreSQL側の機能強化を行う必要がある。
デザインプロポーザルを出し、この処理を行うにふさわしい場所とフックの仕様を固めていきたい。

GpuSort(+LIMIT)

PG-Strom v2.0では、実はGpuSort機能の廃止を予定している。これは、ソートという問題の性質上、ある程度問題規模が大きくならないとGPUによる処理時間メリットが出てこない一方で、一度にGPUでソートを行う件数が多くなればなるほど、初期データのローディングに時間がかかるようになり、非同期・多重処理のメリットを得にくいからである。
そのため、GPUでソートを行うセグメントサイズを小さくして単位ローディング時間を短くする一方で、CPUでのMergeSort処理の割合が大きくなるか、それとも、セグメントサイズを大きくするかというジレンマに悩まされてきた。
(で、最終的にはそれほど速くならない事が多かったり・・・。)

根本的な原因は、GPUで処理を行ってもソートではデータ件数を減らす事が不可能な点にある。なので、GpuSortを有効に活かすには、何がしか『データ件数を減らせる』パターンでのソートに限った方が利口だ。
例えば、LIMIT句で『上位xx件を取り出す』という事が明らかな場合に限り、GpuSortを使用するというパターンであれば十分に効果を発揮する事ができるだろう。
これにはPostgreSQL本体側で、『LIMIT句でxx件のデータが要求されますよ』という事を下位のノードに伝えてやる仕組みが必要だが、PostgreSQL v10 向けにパッチを出しており、ある程度レビューも進んでいるので間に合うだろうとは踏んでいる。

こんな感じで2017年の開発ロードマップについて考えてみたが、さてさて、大晦日に振り返ってどの程度きちんとやれているでしょうか。

Beyond the 1GB limitation of varlena

This article is a part of the PostgreSQL Advent Calendar 2016.

According to the request by Joe Conway (@josepheconway), I wrote this article in English.

I like to share the discussion we had at the PostgreSQL developer unconference on the day before PGconf.ASIA 2016, and the related idea of mine.

PostgreSQL supports variable length data types; like text, bytea, json, numeric, array and so on.
These data types individually have their own characteristics and own internal format, however, all of them are built on a common structure to represent variable length field; that is called varlena.

It has very simple internal format. Contents of the variable length fields are followed by either 1-byte or 4-bytes header.

We can identify the header type by the least bit of the first 1 byte. If least bit of the first byte, it means 1-byte header, elsewhere, 4-byte header.
In case of 4-byte header, the second bit is also used to show whether it is compressed or not. Therefore, rest of 30-bits can be available to represent the contents length, so, maximum length of the variable length field is 1GB.
A special case exists if the first byte is 00000001. It is an external TOAST reference which consists of OID of TOAST table and unique ID within TOAST table.

Below is the source code comment at include/postgres.h.

    :
 * Bit layouts for varlena headers on little-endian machines:
 *
 * xxxxxx00 4-byte length word, aligned, uncompressed data (up to 1G)
 * xxxxxx10 4-byte length word, aligned, *compressed* data (up to 1G)
 * 00000001 1-byte length word, unaligned, TOAST pointer
 * xxxxxxx1 1-byte length word, unaligned, uncompressed data (up to 126b)
   :

My concern is we have no way to represent a larger variable length datum more than 1GB.

It is quite natural for users who want to process the maximum available data size as long as system capability allows. It is not uncommon for the recent GPU models to have 10GB class device memory or more.
On the other hands, due to the restriction of variable length datum of PostgreSQL, we cannot have a datum larger than 1GB. It also means we cannot provide a large matrix (= 2D-array) onto PL/CUDA function at once. It is an unignorable restriction if a problem user want to solve by PL/CUDA is unavailable or expensive to split into multiple portions.

According to the background, we discussed a few options to support variable length datum larger than 1GB.

64bits varlena header

The first idea was straightforward but got less interest because 99% of existing variable length data types are satisfied with 1GB limitation.
If we have one more varlena header, VARDATA() and VARSIZE() which are widely used in the PostgreSQL core and extensions need to have branch operation inside the macro, and it is not easy to justify the penalty for this niche usage.

Use of large-object and its ID instead

The second idea was a suggestion from audience. Now PostgreSQL has a feature of large object which allows to store up to 4TB data chunk with a unique identifier. If PL/CUDA function supports ID of large object, instead of varlena datum, PL handler can extract larger data chunk by itself.
However, here is a problematic scenario. It requires users to construct large objects preliminary. It is inconvenient when user wants to deal with a large matrix constructed on the fly. For example, the query below constructs a matrix based on the underlying table scan with qualifier. The qualifier can be changed for each execution.

SELECT * FROM matrix_unnest(
  (SELECT my_plcuda_function(Q.matrix, D.matrix)
     FROM (SELECT array_matrix(a,b,c) matrix
             FROM table_q
            WHERE tag LIKE '%abc%') Q,
          (SELECT array_matrix(x,y,z) matrix
             FROM table_d
            WHERE tag LIKE '%xyz%') D
  )
)

In addition, creation of a large object makes unnecessary i/o; which leads performance slow-down.

Special type that includes indirect reference

A overall consensus was to define a special data type which support indirect references to data chunks less than 1GB. If only narrow use-case wants to have a datum larger than 1GB, it is quite natural to define a special data type for the purpose.

My interest is representation of matrix in database.

In case of matrix, it has less necessity to have all the items in a flat memory chunk.

If we have 8GB of matrix, we can split it into 9 portions to keep individual chunk size less than 1GB.

Then, once we define a special matrix type that consists of small metadata and indirect references to the chunks less than 1GB, it is available to represent a big matrix larger than 1GB as a usual SQL data type.

A remaining problem is serialization/deserialization when the matrix data type is saved/loaded.
Right now, PostgreSQL saves contents portion of the variable length datum onto the storage as is, thus, pointers to reference sub-matrix has to be serialized appropriately, however, we have no infrastructure to manipulate type specific data structure on toast_insert_or_update() which set up physical tuple for INSERT/UPDATE.
Likely, pg_type needs to have an optional callback functions for serialization/deserialization. It shall be a mandatory requirement if data structure has indirect reference to other memory chunks.

I expect the serialization callback will use TOAST relation to save a large but less than 1GB chunks, then put its unique ID instead of the pointers. We will be able to have another advantage in this approach, because all the sub-matrix we have to update are the portion actually updated. If some of sub-matrix were not updated, we don't need to delete old toast pages and insert new ones. It will make a performance benefit than existing flat varlena structure.

The timing for deserialization needs a bit more consideration. Because heap_tuple_fetch_attr handles deserialization of the existing flat varlena, but no type OID is supplied to the function. It is not a good option to change the function prototype because many existing code already uses this function without type OID.
We have two another options here. The first one packs type OID within the serialized structure. It needs to define a new VARTAG_* label to distinct from the existing flat varlena. The second one is delayed load because indirectly referenced data chunks will not be used without functions/operators which support the data type. It enables not to load unreferenced chunk, however, it is uncertain whether functions/operators can manipulate a value supplied as an argument. *1

Last, even if we can have variable length datum larger then 1GB from the viewpoint of data format, it is never small data chunks. It involves not a small amount of i/o (or memory copy) stuff.
Therefore, it is a significant to have special optimization based on the knowledge for usual use case of the types.

For example, some workloads takes sparse matrix which have small amount of non-zero values, but mostly zero. In this case, type may be able to assume empty sub-matrix are all-zero instead of data size reduction.

Diagonal matrix is also often used, in case when valid values are located around the diagonal axis only.

I hope making a proof of the concept patch near future, then have a discussion at pgsql-hackers.

*1:If not acceptable, we may need to load sub-matrix multiple times when a particular matrix object is referenced by multiple functions/operators.