TadaoYamaokaの開発日記

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

dlshogiのautomatic mixed precision対応

48先生から頂いたautomatic mixed precision対応のプルリクに対応しました。
Pytorch 1.6 の Automatic Mixed Precision対応 by bleu48 · Pull Request #18 · TadaoYamaoka/DeepLearningShogi · GitHub

automatic mixed precisionは、PyTorch 1.6で追加されたTensorCoreを使用して学習を高速化する機能です。
詳細は↓こちらを参照。
Introducing native PyTorch automatic mixed precision for faster training on NVIDIA GPUs | PyTorch

学習時間

dlshogiの学習で測定した結果は以下の通り。

条件:

  • 27,051,941局面の学習
  • バッチサイズ 1024
  • SWA使用
条件 学習時間 SWAのbn_update 合計
ampなし 2:33:57 0:43:58 3:17:55
ampあり 1:15:53 0:31:52 1:47:45

学習時間とSWAのbn_updateの合計で、54%に短縮されている。

精度

テストデータにはfloodgateの棋譜からサンプリングした856,923局面を使用

条件 訓練損失平均 テスト損失 policy一致率 value一致率
ampなし 1.1555423 1.6557858 0.40078359 0.69063551
ampあり 1.15769344 1.65721888 0.40040172 0.68830110

精度が大幅に落ちるということは起きていない。
わずかに下がっているが、初期値の違いでも起きる差なのでampの影響は小さいと思われる。

補足

ampは、FP16への変換を自動で行い、精度が落ちないように計算中の値を自動でスケーリングする。
勾配がInfやNanになる場合は、そのstepはスキップされ、更新が連続で成功した場合に、自動でスケーリングパラメータを調整している。

dlshogiの初期モデルの学習は、初期値によってはモデルの出力がnanになることが良く起きていたが、上記の仕組みによりそれが発生しなくなった。
今の自己対局の仕組みでは起きていないが、以前は学習が進むと突然nanになるようなこともあったので、それも自動で防げるという効果もありそうだ。

まとめ

automatic mixed precisionを使用することで、精度を落とさずに学習を(上記の測定条件の場合)約1.8倍に高速化できる。


48先生、ありがとうございました。

dlshogiのgo nodes対応

少し前にQhapaqさんからもらったプルリクに対応しました。
add go protocol by qhapaq-49 · Pull Request #17 · TadaoYamaoka/DeepLearningShogi · GitHub

go byoyomiで、探索を打ち切らずに時間いっぱいまで探索するようになりました。
go nodesで、固定ノード数まで探索できるようになりました。
(USIオプションのConst_Playoutも継続利用可)

ついでに、今まで探索の打ち切り(最善手が変わる可能性がない場合に打ち切る)は全スレッドで確認していましたが、わずかですが高速化のために一つのスレッドのみで監視するようにしました。

Qhapaqさん、プルリクありがとうございました。

dlshogiにおける思考時間と強さの関係

MCTSと思考時間と強さの関係について、dlshogiのデータが知りたいという要望があったので測定してみた。

測定条件

対戦相手は、最新のやねうら王Ubuntu 18.04のclangでビルドしたもの+水匠2の評価関数で、1000万ノード固定とする。

dlshogiは、最新のソースに、最新のモデル(WCSOC1からR+76くらい)を使用した。


測定環境は、CPU Xeon E5-2698 v4(2.20GHz)、GPU NVIDIA V100、Ubuntu 18.04(Docker)。

水匠2は2スレッド、dlshogiは3スレッド1GPUを使用し、思考時間を変えて測定。

思考時間は、1秒、2秒、3秒、4秒、8秒、16秒とした(8秒、16秒は測定中)。
ポンダーはなし。
256手で引き分け。

強さは、互角局面集を使用して、先後を交互に入れ替え、250戦行った結果で測る。
引き分けも考慮するため、勝率は、勝ちを1ポイント、引き分けを0.5ポイントとして、ポイントの合計/250で計算する。

水匠2は、評価値-10000で投了、dlshogiは、勝率1%で投了する。

eloレーティングは、水匠2のeloレーティングを0、uを勝率として、
\displaystyle
R=400 \log_{10} \frac{u}{(1-u)}
で計算する。

測定結果

思考時間(秒) 勝ち 引き分け 勝率 R
1 87 9 36.6% -95.44
2 132 7 54.2% 29.25
3 158 3 63.8% 98.44
4 165 2 66.4% 118.33

f:id:TadaoYamaoka:20200810210828p:plain

dlshogiの思考時間ごとの探索ノード数は、以下の通り。

思考時間(秒) 探索ノード数(平均) 探索ノード数(中央値)
1 71971 61518
2 140604 121388
3 207204 179994.5
4 270545 232072

考察

dlshogiの思考時間を増やすことで、強さは対数に近い形で伸びているようだ。
もう少し先の形も確認すればはっきりしそうである(8秒、16秒のデータが測定できたら追記する)。

また、測定目的とは異なるが、水匠2の1000万ノードに対して、dlshogiは10万ノード(1/100)くらいで互角になることがわかった。


αβ探索での思考時間と強さの関係

αβ探索での思考時間と強さの関係についても調べた。

対戦相手は、上記と同様に水匠2の1000万ノード固定とする。

測定側は、水匠2 2スレッドで、思考時間を1秒、2秒、3秒、4秒、8秒、16秒とした(8秒、16秒は測定中)。

測定環境は、CPU Xeon Silver 4210 (2.20GHz)を使用した。

測定結果

思考時間(秒) 勝ち 引き分け 勝率 R
1 7 10 4.8% -518.96
2 18 15 10.2% -377.87
3 26 20 14.4% -309.64
4 55 18 25.6% -185.33

f:id:TadaoYamaoka:20200810214546p:plain

水匠2の思考時間ごとの探索ノード数は、以下の通り。
※ログから集計が面倒だったので1局面だけサンプリング

思考時間(秒) 探索ノード数
1 1298863
2 2252468
3 4359210
4 5334843

考察

2スレッドで測定したため、探索ノード数が1000万ノードよりも低すぎて、右側を見ないと対数の関係なのかどうかが見れないデータになっていた。
スレッド数を8にして測定をやり直すことにする。
測定ができたら別途追記する。

まとめ

MCTSの思考時間と強さの関係は、対数の関係になりそうであることがわかった。
探索深さに対してゲーム木のサイズが指数関数的に増えることと関係していると思われる。

AlphaZeroの論文では、αβよりも思考時間に対する強さの伸びが高いことが報告されている。
f:id:TadaoYamaoka:20200810220020p:plain

同じ対数的な伸びでも、伸び方に差がでている。

αβで思考時間を延ばした場合も測定をしようとしたが、今回は測定条件のミスで使えるデータがとれなかった。
測定できたら、別途記事にしたい。

PythonからC#へポインタ渡しする

C#からPythonへポインタ渡しするとは逆に、Python for .NETを使ってPythonからC#へポインタ渡しする方法について紹介する。

Python for .NET

Python for .NETを使用すると、PythonからC#のコードを呼び出すことができる。
Pythonと同じプロセスで、.NETのランタイムを動かすことができ、C#で記述したコードをPythonのモジュールと同じように扱うことができる。

import clr
from System import String
from System.Collections import *

Python for .NETのインストール

pipコマンドで、インストールする。

pip install pythonnet

C#側のコード作成

.NET Core 3.1を使用して、C#のコードを作成する。

クラスライブラリのプロジェクトを作成する。

dotnet new classlib --name torch_sharp

以下の通りC#のコードを記述する。

using System;

namespace torch_sharp
{
    public class Class1
    {
        public unsafe void func(IntPtr data, int size)
        {
            float* array = (float*)data.ToPointer();
            for (int i = 0; i < size; ++i)
            {
                array[i]  = i;
            }
        }
    }
}
解説

Pythonからは、IntPtr型でポインタを受け取り、それをToPointer()でfloat*に型変換して、float型の配列として扱っている。
Python側でもfloat型になっていることを前提にしている。

Python側のコード作成

以下の通りPythonのコードを記述する。

import clr
clr.AddReference('torch_sharp')
from torch_sharp import Class1
from System import IntPtr

import numpy as np

obj = Class1()

data = np.empty(5, dtype=np.float32)
obj.func(IntPtr.op_Explicit(data.ctypes.data), len(data))

print(data)
解説
clr.AddReference('torch_sharp')
from torch_sharp import Class1

この部分で、C#のクラスライブラリをPythonモジュールとしてimportしている。

ビルドしたC#の.dllファイルは、Pythonモジュールと同じディレクトリに配置しておく必要がある。
C#のプロジェクトファイル(.csproj)で、

  <Target Name="CopyDLL" AfterTargets="AfterBuild">
    <Copy SourceFiles="$(OutDir)/torch_sharp.dll" DestinationFolder="../../torch_sharp/" />
  </Target>

のように記述しておくとよい。

Python側からC#にポインタ渡しするデータは、Numpyで作成する。

IntPtr.op_Explicit(data.ctypes.data)

の部分で、Numpyのデータをポインタとして取り出して、C#のIntPtr型に変換している。
ポインタを数値型のままC#に渡して、C#側でIntPtrにしてもよいが、IntPtrで渡した方が32bit、64bitかを気にしなくてよい。

まとめ

PythonからC#へポインタ渡しする方法について紹介した。

C#のアプリケーションで作成したデータを使って、Pythonでモデルを学習したいというようなケースで、C#のデータをシリアライズして、Python側で読み込みたいような場合に、PythonからC#のコードが呼べると便利である。
モデルに入力するためのデータの前処理を行うとデータ量が増えるため、シリアライズデータは加工前の状態にしたいが、Pythonで加工を行うと遅いため、速度を考慮するとC#で加工を行いたい。
しかし、PythonC#の間で、データのマーシャリングが発生すると呼び出しのオーバーヘッドが発生するので、ここで紹介した方法でポインタ渡しをするとオーバーヘッドを抑えられる。

dlshogiのdf-pnのテスト方法

dlshogiのdf-pnだけを動かす手順を書いておきます。

テスト局面追加

testプロジェクトのtest.cppの
「// DfPnテスト」と書いてあるところのmain()の

	vector<string> sfens = {
		// 詰み
		"9/9/+N8/p1p4p1/6p1p/1P7/3k3PP/2+p5L/6+rGK w R2B2G3Sgs3n3l9p 1",
		"1n1g3+Pl/k1p1s4/1ng5p/pSP1p1pp1/1n3p3/P1K3P1P/1P7/9/L1G5L b 2R2BG2SL5Pn 161", // mate 15
		// (略)
	};

の部分にsfenで局面を追加することでテストできます。

test.cppには、main()がたくさんありますが、テストしたいところを「#if 1」にしています。

https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/test/test.cpp#L359

探索上限変更

上限ノード数は

dfpn.set_max_search_node(1000000);

で変更、深さの上限は、

dfpn.set_maxdepth(29);

で変更できます。

ハッシュサイズ変更

ハッシュの衝突が起きると探索効率が落ちるため、置換表は十分なサイズが必要です。
dfpn.cppの以下の箇所で定数定義しています。

int64_t DfPn::HASH_SIZE_MB = 2048;

https://github.com/TadaoYamaoka/DeepLearningShogi/blob/master/usi/dfpn.cpp#L11

ハッシュの衝突が起きると上書きするため、長手数の詰み探索には向いていません。
長手数の詰み探索には、Small TreeGCとか使う方が正しいです。

ビルド方法

Visual Studioの場合は、ビルド構成をReleaseにしてtestプロジェクトを右クリックしてビルドでビルドできます。
ソリューションのビルドからは外しているため個別にビルドが必要です。

Gccの場合は、testディレクトリにMakefileがあります。

df-pnの関連記事

TLでdf-pnの話が上がっていたので、以前書いた記事のリンクをまとめておきます。

dlshogiのdf-pnの実装は、優越関係、証明駒、反証駒、先端ノードでの3手詰めルーチンを実装していて、ループの問題も処理していますが、対局時の利用に特化しているので、長手数の詰みはノード数上限、深さ上限に引っかかると不詰み扱いになります。
詰め将棋エンジンとしては使えません。

dlshogiでMagic Bitboardを有効にする

やねうら王が飛車と角の利きに、PEXTの代わりにMagic Bitboardを使用することで、Threadripperで高速化できたとのことなので、dlshogiでも高速化できるか試してみた。

dlshogiは合法手生成にAperyのソースを使用しており、AperyはMagic Bitboardを実装していて、HAVE_BMI2を無効にすることで有効化できる(有効にしているとPEXTが使われる)。

自己対局を実行しているPCは、Ryzen Threadripper 3970Xを使用しているので、HAVE_BMI2を無効にしてビルドしなおして、自己対局における局面生成速度を比較した。

現在自己対局では、3枚のGPUで、1GPUあたり3つの探索スレッドと1つの詰み探索スレッドを使用している。
GPUの処理は、効率的なバッチ処理NNキャッシュを使用しており、GPUよりもCPUがボトルネックになっているため、合法手生成の高速化の効果はそのまま局面生成速度に表れると予想している。

比較結果

10000局面を生成した際の局面生成速度を2回測定して比較した。

条件 1回目(NPS) 2回目(NPS) 平均(NPS)
変更前 66.67 66.61 66.64
変更後 75.62 74.44 75.03

平均で、12.6%高速化できた。

まとめ

Ryzen ThreadripperにおけるMagic Bitboardによる高速化は、dlshogiでも同じ効果が得られることがわかった。

大会ではintelのCPUを使用しているので大会での強さには影響なさそうだが、自己対局の生成速度があがるのはすぐにでも恩恵が得られそうである。