読者です 読者をやめる 読者になる 読者になる

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

GPU PostgreSQL

従前、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:予定なしとか悲しい事は言わないでほしいけど