TadaoYamaokaの開発日記

個人開発しているスマホアプリや将棋AIの開発ネタを中心に書いていきます。

将棋AIの進捗 その19(初期局面集)

自己対局による強化学習を行う際に、対局の開始局面には、初期局面集を使用している。
AlphaZeroでは、固定手数まではノイズを加えルートノードの訪問回数に応じた確率で手を選択することで局面の多様性を確保している。
しかし、この方法ではモデルに依存した限られた序盤進行になってしまうため、初期局面集を使う方が良いと考えている。
実際のところは、どちらが良いかは検証してみないとはっきりはわからない。

他の将棋AIでも決まった方法があるわけではなさそうだ。
Aperyでは、初期局面集に1手ランダムムーブした局面から生成していて、やねうら王では20手までランダムムーブを行っているようだ。

第5回電王トーナメント版のdlshogiを学習したときは、elmo_for_learnで教師局面を生成した。
そのときは、Aperyのroots.hcpにネット集めた棋譜の85手までの局面を足して、重複局面を排除したものを使用した。
elmo_for_learnは、初期局面集からさらに6手ランダムムーブを行っていたので、1~6手の範囲で一様分布で選んだ手数だけランダムムーブするように変更した。
ランダムムーブしない初期局面を1回ずつ使用した局面も生成した。
AlphaZero方式で自己対局を行う際は、Aperyと同じ方法を使うようにしている。

elmo_for_learnで教師局面を生成すると、重複局面が4%近く生成される。
また、ディープラーニングでモデルを学習すると局面を使い切る前に収束する。
このことから、教師局面の多様性があまり確保できていないのではないかという疑いを持っている。

ディープラーニングは序盤は強いが、中終盤で悪手が増えるので、学習局面に中終盤のバリエーションがもっと増やしたいと考えている。
これを初期局面を増やすことで対策できないか試している。

最近の平岡さんのツイートで、初期局面をフィッシャーランダムで生成すると弱くなったという情報があった。


初期局面の多様性は重要だがランダムではかえって弱くなるようだ。
実際の対局で起こりそうな局面に近いことも重要なのかもしれない。

AlphaGoとイセドル九段の対局の第4局の敗北は、Policyの予測漏れに起因している。
これも中終盤の学習局面のバリエーション不足が原因になっている。
AlphaGo Masterでは弱点を克服しているが、どのように学習したかは明らかにされていない。(敵対的ネットワークを使ったとか使ってないとか)

ということで、初期局面集のバリエーションを増やすために、floodgateの2013年から2017年の5年分の棋譜をもとに、開始局面から棋譜上の評価値が1000点以内の手数までの局面を初期局面集に加えた。
重複局面を除くと、17,391,142局面が生成できた。
Aperyのroots.hcpは重複局面も含んでいるので、重複局面を除くと、45,945,496局面が含まれる。
それからすると少ない。Aperyは相当な数の棋譜を集めてroots.hcpを作成しているようだ。

Aperyのroots.hcpとfloodgateの棋譜から生成した局面集を合わせて重複局面を除くと、52,078,876局面になった。
6,133,380局面を増やすことできた。
効果があるか比較検証したいが、自己対局に時間がかかりすぎて比較検証はできそうにない。
検証は将来の課題として、勘を信じることにする。


初期局面集作成に使ったツール類を公開しています。
github.com

将棋AIの進捗 その18(スケーラビリティ)

AWSのp3.8xlargeインスタンスを試験的に借りてGPUを増やした場合の性能を測定しました。
Linuxだとマルチスレッドの性能がでないので、OSはWindowsです。

p3.8xlargeのマシンスペックは以下の通りです。

Tesla V100 GPUs 4
vCPUs 32
Main Memory 244GiB

GPUに割り当てる探索スレッド255として、GPUを増やしながら平手初期局面での探索速度(シミュレーション/秒)を測定しました。

GPU枚数 探索速度(シミュレーション/秒)
1 6344
2 7545
3 7757
4 8168

f:id:TadaoYamaoka:20180404230913p:plain
GPUを増やすほど、探索速度が上がっていますが線形には伸びていません。

スレッド数

GPU4枚で、GPUあたりの探索スレッドを変えて測定しました。

スレッド数 探索速度(シミュレーション/秒)
255 8168
192 8790
168 8923
128 8179

スレッド数が168のときに最大となりました。

スレッド数を変えてGPU枚数変更

スレッド数を168として、GPU枚数を変更しました。

GPU枚数 探索速度(シミュレーション/秒)
1 5839
2 9696
3 8921
4 8923

GPU2枚が最大となりました。

GPU2枚でスレッド数を変更

GPU2枚でスレッド数を変えて測定しました。

スレッド数 探索速度(シミュレーション/秒)
255 7545
192 9118
168 9696
128 7263

スレッド数168のときが最大となりました。

考察

GPU枚数とスレッド数を単純に増やしても探索速度は上がりませんでした。
GPU枚数とスレッド数のバランスが取れている時が探索速度が最大となっています。
スケーラビリティが頭打ちになった原因は、スレッド間でノード情報を共有するツリー並列化を行っているため、スレッド数が増えるほど競合が発生するため、効率が悪くなったためと推測できます。
ツリー並列化の方式では、GPUは2枚以上にしても探索速度の向上は期待できそうにないことがわかりました。

スケーラビリティを上げるには、マルチプロセスまたはマルチノードでルート並列化を行う必要がありそうです。

なお、AWSではなく2枚のGPU(Titan Vと1080Ti)にCore i9 10コアのPCだと、GPUあたりの探索スレッド数255で、10779シミュレーション/秒の速度がでています。
5月の大会はGPU2枚のPCでの参加の方向になりそうです。

将来的には、マルチノード構成を検討することにします(5月の大会には間に合いそうにない)。

2018/4/5 追記

自己対局をp3.8xlargeでGPU数のプロセスで実行した場合の生成速度は、7.61+8.85+9.08+6.56=32.1局面/秒となった。
GPU2枚のPCだと2プロセスで11.26+8.40=19.66局面/秒なので、AWSを使うと1.63倍の速度で生成できる。

LinuxとWindowsのマルチスレッド性能

将棋AIをAWSで動かそうとLinux対応しましたが、Linuxでマルチスレッドの性能がでないため、いろいろ実験してみました。

検証している将棋AIではGPUの計算が終わったら、待機中の複数の探索スレッドに通知する処理を行っています。
それを、以下のような処理で実装していましたが、Linux(Ubuntu 16.04)ではWindowsの1/10以下の探索速度になってしまいます。

探索スレッド側
while (uct_node[child_index].evaled == 0)
	this_thread::sleep_for(chrono::milliseconds(0));
GPUスレッド側
uct_node[index].evaled = 1;

探索スレッド数は255です。evaled はatomicです。

探索速度(シミュレーション/秒)の比較は、以下の通りです。

Windows 6305
Linux 561

sleep時間変更

はじめ、GPUまわりのコードを疑いましたが、GPUの処理を外しても変わらないため、マルチスレッドに起因する問題と推測し、探索スレッド側でビジーループしている箇所を0ミリ秒のsleepから1ミリ秒のsleepに変更してみました。
すると以下の通り少しだけ改善されました。

Windows 5190
Linux 1354

Windowsでは逆に探索速度が落ちています。

yieldに変更

今度は、0ミリ秒のsleepの代わりにyiledに変更してみました。

while (uct_node[child_index].evaled == 0)
	this_thread::yield();

今度はだいぶ改善されました。

Windows 6301
Linux 2258

Windowsは0ミリ秒のスリープと変わりません。
Linuxはだいぶ改善されましたが、Windowsの半分以下です。

condition_variableに変更

通知の処理を、ビジーループからcondition_variableに変更しました。

探索スレッド側
std::unique_lock<std::mutex> lk(mtx);
while (uct_node[child_index].evaled == 0)
	cond.wait(lk);
GPUスレッド側
uct_node[index].evaled = 1;
cond.notify_all();

探索速度は、以下の通りとなりました。

Windows 1823
Linux 2769

Linuxは今までで一番よい値になりましたが、やはりWindowsのビジーループの場合の半分以下です。
Windowsはcondition_variableを使うと、ビジーループの場合の29%まで落ち込みました。

考察

以上の実験結果から、Linuxではビジーループよりもcondition_variableの方がマルチスレッドの性能がよさそうです。
しかし、Windowsでビジーループする場合の半分以下の性能しかでません。
Windowsでは、condition_variableを使うと、ビジーループよりも性能が低下します。

LinuxWindowsと同等のマルチスレッドの性能を出す方法は今のところ不明です。
AWSを借りる場合、料金は上がりますが、Windowsを借りることになりそうです・・・。
どなたかLinuxでもマルチスレッドの性能について知見をお持ちの方がいたらアドバイスいただけると嬉しいです。

将棋AIの進捗 その17(AWS対応を検討)

世界コンピュータ選手権の参加者のマシンスペックをみると、マシンスペック高すぎです( ゚Д゚)

GPUを2枚詰んだ個人のPCで参加しようと思っていましたが、GPU8枚とかで来られたらモデルと探索の性能ではどうにもならなそうです。
モンテカルロ木探索は並列化の効果が高く、私の実験でも、GPUを2枚にするだけで、GPSfishに対して勝利が40%から75%(R+261)に上がっています。
8枚になると単純計算で、R+2088です(実際は線形には伸びませんが)。

ということで、AWSでp3.16xlargeを借りて参加することを検討しています。

AWSで参加することを考慮して、cuDNN対応したコードをLinuxで動かせるようにしました。
しかし、Ubuntu 16.04 LTSをCore i9で動かすと、マルチスレッドの性能がでず、探索速度がWindowsの半分になります。
GPUの推論処理をコメントアウトしても変わらないので、Ubuntu 16.04 LTSのカーネルが古いためCore i9に対応していないためと予想しています。
AWSXeonなので、性能劣化しないと思っていますが、事前に実験しておく必要がありそうです。
GPUインスタンスは高いので実験するにもお財布が痛みます・・・


あと、AWSを使う場合は、参加者の皆さんはどのように実行されているのでしょうか。
現地にノートPCを持ち込んで、ノートPC上でsshコマンドをバッチファイルにして、将棋所に登録すればよいと思っていますが、どういう方法がメジャーなのでしょうか。
ノートPCを経由するので多少のラグはありますが、大会のサーバがLAN内にあると、AWSから直接というわけにもいかないと思うので、この方法になるのではないかと思っています。

2018/4/3 追記

AWSを試験的に使ってみたのですが、AWSでも遅かったので原因を調べたところ、Linuxで遅くなる原因が判明しました。
ニューラルネットワークの実行を待機する処理を

	while (uct_node[current].evaled == 0)
		this_thread::sleep_for(chrono::milliseconds(0));

のように記述していましたが、これがWindowsだとスレッドの実行をスイッチしてくれますが、LinuxだとCPUを100%消費してスレッドが切り替わらないためでした。

	while (uct_node[current].evaled == 0)
		this_thread::sleep_for(chrono::milliseconds(1));

にすると、速くなったのですが、1ミリ秒のスリープが無駄なのでスピンロックするのはやめて、condition_variableを使った実装に修正する予定です。

2018/4/4 追記

Twitterでの開発者同士のやり取りで以下の方法で可能ということなので、情報を載せておきます。
SSHのコマンドをバッチファイルに記述することで、リモートでUSIエンジンを動かせるようです。
http://ai65536.hatenablog.com/entry/2015/03/31/190904ai65536.hatenablog.com

将棋AIの進捗 その16(マルチGPU)

将棋AIをChainerを使用した実装からcuDNNを使用した実装に変更できたので、マルチGPUでの性能を測定した。

Chainerを使用した場合

Python経由でChainerを使用しているPythonのGIL機構によってマルチスレッドの性能に制限がある。
Chainerを使用した場合の、マルチGPUによる効果は1.33倍程度であった。

playout/sec
シングルGPU(Titan V) 5724
マルチGPU(Titan V+GeForce 1080 Ti) 7640

cuDNNを直接使用した場合

cuDNNを直接使用した場合の測定結果は、以下の通りであった。

playout/sec
シングルGPU(Titan V) 6266
マルチGPU(Titan V+GeForce 1080 Ti) 10779

cuDNNを直接使用した場合、マルチGPU化により1.72倍の高速化の効果があった。
ChainerをシングルGPUで使用した場合と比べると、1.88倍になった。

棋力への影響

棋力にどれだけ影響しているかを確認してみた。
十分な対局数がこなせていないが、elmoで深さ8で生成した11億局面を学習したモデルで、GPSfishと1手3秒で5戦したところ、全勝だった。
CPUはCore i9 10コアなのでGPSfishに不利な条件ではない。
なお、GPSfishは定跡ありで、dlshogi側は定跡を使用していない。
シングルGPUのときは、GPSfishへの勝率は、40%程度だったので探索速度の向上によって棋力が上がっていそうである。
もっと対局数を増やして検証したい。
f:id:TadaoYamaoka:20180327220515p:plain

アピール文章書かないと・・・

2018/3/28 追記

48回連続対局を行ったところ、
dlshogi 36勝(75%) GPSfish 12勝(25%)
という結果だった。
cuDNNを使ったマルチGPU対応により、有意に強くなったことが確認できた。
GPSfishのeloレーティングを2800とすると、dlshogiのeloレーティングは2990.8となる。
\displaystyle
\begin{eqnarray}
R_{dlshogi}&=&R_{GPSfish}-400 \log{10} (\frac{1}{E_{dlshogi}} - 1) \\
&=&2800 - 400\log_{10}(\frac{1}{0.75} - 1) \\
&=&2990.8
\end{eqnarray}

探索の高速化は、まだ改良の余地があるので、もう少し高速化を行いたい。
自己対局による強化学習の方も、cuDNN対応を行う予定。

将棋AIの進捗 その15(cuDNNを使用)

モデルの学習にディープラーニングフレームワークのChainerを使用していますが、対局時にChainerで推論を行うと、Python経由で呼び出すためマルチGPUで動かす際、Python経由だとGILによってマルチスレッドの性能が出なくなる。
また、実行環境にPythonが必要になるため、実行バイナリを配布しても利用者側で環境構築が必要になってしまう。

それらの問題を解決するため、対局時にはChainerを使用しないで、cuDNNを直接しようして推論を行えるようにした。
実装方法の詳細は以下の記事を参照してほしい。
Chainerで学習したモデルを使ってcuDNNで推論する - TadaoYamaokaの開発日記
Chainerで学習したモデルを使ってcuDNNで推論する(時間計測) - TadaoYamaokaの開発日記
Chainerで学習したモデルを使ってcuDNNで推論する(BatchNormalization) - TadaoYamaokaの開発日記
Chainerで学習したモデルを使ってcuDNNで推論する(dropout) - TadaoYamaokaの開発日記
Chainerで学習したモデルを使ってcuDNNで推論する(ResNet) - TadaoYamaokaの開発日記
Chainerで学習したモデルを使ってcuDNNで推論する(マルチGPU) - TadaoYamaokaの開発日記

はじめ、キューに溜まっている要求がバッチサイズ未満でも固定のバッチサイズで推論を行うように実装したら、Python経由でChainerを呼び出すよりも遅くなってしまった。

初期局面の探索速度
playout/sec
Chainer 4542
cuDNN 4095

期待した結果にならなかったので、バッチサイズを可変にして、キューに溜まっているサイズのバッチサイズで推論を行うようにしたところ、Python経由でChainerを使用する場合を上回った。

初期局面の探索速度
playout/sec
Chainer 4542
cuDNN 4854

GPUの並列性を期待して、バッチサイズによらず速度は一定と考えたが、メモリの転送時間とかの影響があるため、バッチサイズは可変にした方が良いことがわかった。

しかし、MNISTの推論で比較した場合に比べて、期待したほど速度が向上していない。
ChainerはPythonで実行している分無駄があるので、もっと差が出てよさそうである。
Chainerは、速度を出すためにチューニングしているため、素朴な実装だとGPUの性能が出せていないと思われる。
速度を出すには、メモリ転送をStreamを使うなどチューニングが必要そうだ。
CUDAのチューニングノウハウをほとんど持っていないので、Chainerのソースをみるなどしてもう少し調査してみるつもりだ。

マルチGPUについては、効果があるはずなので、別途測定を行う予定。

2018/3/27 追記

メモリ転送をストリームを使って並列化してみたが、

playout/sec
直列 4853
ストリーム 4202

という結果になり、かえって遅くなってしまった。

GPU計算中にメモリ転送できる箇所が入力層と出力層に一部しかなく、cudaStreamSynchronizeを実行する分、遅くなってしまったようだ。
ストリームに対応させる際に、ホストメモリの確保をC++のデフォルトヒープから、cudaHostAllocによって割り当てられるピンメモリに変更したが、そのことによって速くなることはなかった。

以上から、推論の実行時間のほとんどはGPUの計算に費やされており、Python経由でChainerから実行しても、cuDNNで直接実行しても差は限定的ということが言えそうだ。それでも、Chainerよりも7%程速くなっているので、少しは効果があった。

Chainerで学習したモデルを使ってcuDNNで推論する(マルチGPU)

前回実装した、Chainerで学習したモデルを使用してcuDNNで推論するコードを、マルチGPUで並列に動作するようにした。

cuDNNをマルチスレッドで、スレッドに別々のGPUを割り当てて使用する場合、それぞれのスレッドでcudaSetDevice()を呼び出し、GPU IDを指定する。
cudaSetDevice()を呼び出し後に、cudnnCreate()を呼び出しcuDNNのハンドルを取得し、スレッドごとに異なるcuDNNのハンドルを使用する。

前回までのコードは、cudnnCreate()をNNクラスの静的クラス変数の初期化で呼び出していたが、上記の仕様に対応するため、NNクラスの初期化時に呼び出し、
スレッドごとにNNクラスのインスタンスを作成するように変更した。

以上の変更で、マルチGPUで動作できるようになった。

マルチGPUによって、高速化の効果があるか確認するために、以下の通り測定を行った。

測定条件

  • スレッド内で初期化処理は時間に含めず、推論のみの時間を測定
  • MNISTのtest set images(1000画像)のすべてを20回推論するのに要する時間を測定
  • 3回測定した平均時間
  • ミニバッチサイズは、100
  • モデルは畳み込み1層+畳み込み1層のResNetブロック1つ+畳み込み1層+全結合層2層、BatchNormalizationあり
  • GPUは2枚(TitanVとGeForce GTX 1080 Ti)
  • CUDA 9.0、cuDNN 7.0を使用

測定結果

Chainer(シングルGPU) 4767 ms
シングルGPU 1657 ms
マルチGPU 915 ms

比較対象として、Chainerでシングルスレッドで推論した時間、cuDNNでシングルスレッドシングルGPUで推論した時間を記載している。

マルチGPUは、シングルGPUの約1.8倍高速になっている。

以前に将棋AIの推論でChainerを使ってマルチGPUに対応させたが、マルチGPU化の効果はほとんどなかった。
ChainerをPython経由で使用しているため、PythonのGIL機構によりマルチスレッドでの性能がでないためである。

cuDNNを直接使用するとPythonの制約がないため、マルチGPUにより高速化できている。

GPUごとの推論時間

マルチGPUの場合の、GPUごとの推論時間も計測した。

mnistCUDNN\mnistCUDNN>..\x64\Release\mnistCUDNN.exe 2
There are 2 CUDA capable devices on your machine :
device 0 sms 80 Capabilities 7.0, SmClock 1455 Mhz, MemSize (Mb) 12288, MemClock 850 Mhz, Ecc=0, boardGroupID=0
device 1 sms 28 Capabilities 6.1, SmClock 1670.5 Mhz, MemSize (Mb) 11264, MemClock 5505 Mhz, Ecc=0, boardGroupID=1
gpu:0
1000 iterations
867 [ms]
gpu:1
1000 iterations
751 [ms]
total time = 894 [ms]

gpu:0がTitan Vで、推論に867 [ms]を使っている。
gpu:1がGeForce GTX 1080 Tiで、推論に751 [ms]を使っている。
Titan Vの方が遅いため、トータルの時間は、Titan Vの実行時間+αになっている。

Titan Vの方が遅いのは、クロック数の違いによる。

ベースクロック (MHz) メモリクロック (MHz)
Titan V 1200 850
Geforce GTX 1080 Ti 1480 1100

モデルのサイズ、バッチサイズが小さい場合は、Geforce GTX 1080 Tiの方が高速に動作する。
Titan Vの性能を引き出せるのは、サイズの大きいモデルでバッチサイズを大きくして学習する場合である。
推論の速度だけであれば、モデルによってはクロック数の高いGeforce GTX 1080 Tiを複数枚用意した方がよさそうである。


測定に使用したソースコードを公開しました。
github.com