今回は、長編(1000手~)の協力詰めが解けるsolverを作る準備をします。
置換表サイズを思考エンジン設定で設定できるようにする
長編が解けるようにするには、まず置換表がたっぷり要ります。(たぶん)
そこで、思考エンジン設定から設定された置換表サイズの値を反映するようにしてみましょう。
置換表サイズは、USIプロトコルでGUIから以下のようなメッセージが送られてきます。
> setoption name USI_Hash value 1024
しかし、将棋所では2つのソフトを対戦させるときに片側だけこの”USI_Hash”の値を変更するというようなことが出来ないため、実験する上で非常に不便です。(置換表が32GBでも動作するかを検証するために連続自己対戦をさせようと思っても、2つのソフトが置換表だけで32GB×2を消費するとPCの搭載メモリが64GBでも足りなくなってしまう。)
そこで、USI_Hashの値は無視するように設計することをお勧めします。
ここでは、協力詰めの置換表サイズなので”CM_Hash”というオプションで変更できるとしましょう。つまり、GUIからは
> setoption name CM_Hash value 1024
のように送られてきます。
思考エンジンは”usi”コマンドに対する応答時に、自分のエンジンが設定できるオプション名を列挙して返すことになっています。詳しくはUSIプロトコルについてggrks。
それで、このとき表示するオプション名は、やねうら王miniではusi.cppの以下の箇所で自由に追加することが出来るようになっています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// optionのdefault値を設定する。 void init(OptionsMap& o) { // Hash上限。32bitモードなら2GB、64bitモードなら1024GB const int MaxHashMB = Is64Bit ? 1024 * 1024 : 2048; o["Threads"] << Option(4, 1, 128, [](auto& o) { Threads.read_usi_options(); }); o["Hash"] << Option(16, 1, MaxHashMB, [](auto&o) { TT.resize(o); }); o["Ponder"] << Option(false); // 協力詰めsolver #ifdef COOPERATIVE_MATE_SOLVER o["CM_Hash"] << Option(16, 1, MaxHashMB, [](auto&o) { CooperativeMate::TT.resize(o); }); #endif |
見ての通り、
o[“オプション名”] << Option(デフォルト値 , 最小値 , 最大値 , 値が変更されたときに呼び出されるハンドラ );
のように書きます。
こう書いておけば、”usi”コマンドに応答して、そのオプション名を表示して、GUI側からそのオプションの値が変更されたというメッセージが来たときに、ここで設定されたハンドラが呼び出されるいう魔法のような仕組みになっています。ええ、もちろん、この素晴らしい仕組みはStockfishの仕組み、丸パクリですとも。
上のソースコードの例では、”CM_Hash”の値が変更されたときに、
CooperativeMate::TT.resize(o);
が呼び出されます。
これで協力詰め用の置換表をPCのメモリのある限り確保できます。
オプション名にスペースは不可?
上の”CM_Hash”なのですが、最初、”CM Hash”と間にスペースを入れたオプション名にしていました。USIの原案である、(チェスの)UCIプロトコルのほうは、このようにオプション名にスペースを入れることを許容しているのですが、将棋所ではこれは対応していないようです。ShogiGUIのほうは対応しているようです。
ゆえに、スペースを含んだオプション名は使わないほうが無難でしょう。
思考エンジン設定のオプション名の日本語表示
将棋所では、XXX.exeという思考エンジンに対してXXX_ja.txtというファイルを同じフォルダに置いて、例えば以下のように書いておけば、思考エンジン設定のときに日本語で表示されます。
1 2 3 4 5 |
Threads スレッド数 Hash 置換表サイズ[mb] Ponder Ponder Write Debug Log デバッグログ出力 CM_Hash 協力詰め置換表サイズ[mb] |
ShogiGUIにはこの仕組みはないようです。(?)
※ ご存知の方がおられましたらコメント欄で教えてもらえると助かります。
置換表を2のべき乗サイズで確保するの無駄すぎ?
いまどきのPCなら64GBぐらいメモリを搭載していることは珍しくないですが、協力詰めには評価関数は必要ないのでsolverの本体の使用メモリは20MB程度です。まあこのサイズは無視できるとしましょう。
一方、搭載メモリ64GB環境下でOSが1GBほど使用するとして残り63GB。63GBも使えるのに置換表に32GBしか使わないのは無駄過ぎませんか?
搭載メモリ256GBだとして置換表に128GBしか使わないなら127GBほど余ります。さすがに無駄がありすぎでしょう。
今回のプログラムの場合、
TTEntry* const tte = &table[(size_t)key.p(0) & (entryCount – 1)];
ではなく
TTEntry* const tte = &table[(size_t)key.p(0) % entryCount];
のようにしても特に問題ありません。(剰余のほうが少し遅くなるでしょうけど、誤差です…)
つまり、(前回までに書いた置換表のプログラムであれば)TTEntryの位置は(128bitの)hash keyの下位64bitの剰余で求めて、TTEntryにはhash keyの上位48bitを格納しておけば十分です。
ちなみにhash keyが64bitの場合、前者の方法でTTEntry*を求めるのであれば、TTEntryにはhash keyの値としてentryCountで割った値を格納しないと辻褄があわなくなります。(entryCountでの剰余した値を使うというのは、entryCount進数とみなしてその下位1桁を用いているということなので、その上位の桁はentryCountで割ると得られるという理屈です。)
どれくらいのnpsが出ているのか?
npsを計測して、それを表示するコードを追加してみましょう。
npsはここの読者には言うまでもないことですが、nodes per second(1秒あたりの探索ノード数)のことです。探索の速さを計測するときによく使われます。
pos.node_searched()はdo_move()が呼び出されるごとに1増えます。探索node数というのはdo_move()が呼び出された回数であるということですね。
1 2 3 4 5 6 7 8 9 10 11 12 |
pos.set_nodes_searched(0); auto start_time = now(); 反復深化のループ { // 定期的にdepth、nodes、npsを出力する。 auto end_time = now(); sync_cout << "info depth " << depth << " nodes " << pos.nodes_searched() << " nps " << (pos.nodes_searched() * 1000 / ((int64_t)(end_time - start_time +1))) << sync_endl; } |
USIプロトコルでは”info string “のあとに続けて書くとそれをそのまま読み筋として表示してくれます。また”info nps “のあとに続けてnpsを出力するとそれがnpsとしてGUI上のどこかに表示されます。(少なくとも将棋所とShogiGUIでは。) depth、nodesについても同様です。
前回の263手詰めでは4458kN/sec(4.458Mnps)で、探索ノード数は227,359でした。おおよそ0.05秒で解けているようですね。それにしても探索ノード数、少ないですね。このへんの作品は、普通の詰将棋に比べるとほぼほぼ一本道なのでしょう。
時間計測
上のソースコードに出てきたnow()は、次のような仕組みになっています。
1 2 3 4 5 6 7 8 9 10 11 12 |
// -------------------- // Time[ms] wrapper // -------------------- // ms単位での時間計測しか必要ないのでこれをTimePoint型のように扱う。 typedef std::chrono::milliseconds::rep TimePoint; // ms単位で現在時刻を返す inline TimePoint now() { return std::chrono::duration_cast (std::chrono::steady_clock::now().time_since_epoch()).count(); } |
思考エンジンでは[ms](ミリ秒)単位で時間を計測することが多いのでわりと重宝します。USIプロトコルで送られてくる時間も[ms]単位ですし。
ここまでのまとめ
ちなみに、実は、ここまでやっても長編(1000手以上の作品)は一つも解けません。少なくともあと5つぐらいの工夫が必要です。
次回に、詳しく書きます。
つづく
>長編が解けるようにするには、まず置換表がたっぷり要ります。
>・・・
>長編(1000手以上の作品)は一つも解けません。
不思議ですねえ。
置換表のメモリをたっぷり用意して、あとは反復深化の力任せ、、、と思っておりましたが、、、。
浅はかですか。
77 :やねうらお ◆YANEX.qYFA :2016/01/04(月) 06:24:55.24 ID:vSySh0w2
あと、ID:Nb5ffF7+氏、バグの指摘ありがとう。
何者かは知らないけど、きっと名のある人なのだろう。
お返事がない、、、と思っていたら、ナントカCHにご出張でありましたか。
忙しいですね。
なんとかちゃんねるに気になる書き込みがあったもので..
>加藤徹さんの長編協力詰めNo.211a(12693手)の早詰め(12687手)をやねうら王協力詰めsolverが発見したっぽい・・・
着々と成果を出されておられる様で、御同慶の至りであります。
https://twitter.com/yaneuraou/status/684251054930698240?ref_src=twsrc%5Etfw
並列化してもうちょっと早くしようかな…。