以前、iPadでやねうら王やふかうら王を動作させるためのビルド手段を構築しました。今回は、これらを同時に動作させることに挑戦します。やねうら王(NNUE評価関数)は主にCPUを利用し、ふかうら王(DL評価関数)は主にNeural Engineを利用して思考するため、同時に動作させてその結果を合議させることで、より強い指し手を得られる可能性があります。ただし、今回利用するハードであるiPad(第9世代)ではDLの強さがNNUEよりもかなり劣る(レート400程度)ため、NNUEに対して作られた定跡を惑わせる程度の効果しかないと思われます。より新しいハードでは、CPUの性能向上よりもNeural Engineの性能向上の方が大きいため、強さの差が縮まっていき合議の意味が出てくることを期待しています。
iPadでやねうら王とふかうら王を同時に動作させる場合に難しい点は、アプリ内で複数のプロセスを起動することができない点です。Windows環境であれば、合議を行うプロセスがやねうら王のプロセスとふかうら王のプロセスを起動して通信すれば容易に実現できるのですが、iPad (iPadOS/iOS)では、アプリは単一のプロセスのみで構成する必要があります。そのため、やねうら王とふかうら王を同一プロセス内に共存させる特殊なテクニックが必要です。解くべき課題は2点あります。1点は、C++のシンボル名が重複する点です。やねうら王にはアルファベータ法で探索を行うMainThread::search関数があり、一方でふかうら王にはMCTSで探索を行うMainThread::search関数があります。やねうら王をビルドしたオブジェクトファイルとふかうら王をビルドしたオブジェクトファイルを単純にリンクすると名前が重複してしまい、リンクエラーとなります。もう1点は、どちらのエンジンも指し手の出力に標準入出力を用いるため、指し手がどちらのエンジンから出力されたのか区別がつかない点です。本記事では、まず1点目の解決策を示します。
合議システムの構成
合議システムの構成は以下のようにします。アプリのメインスレッドはSwift言語で実装され、GUIを制御します。GUI上の操作でやねうら王のスレッド、ふかうら王のスレッドを起動します。これらのスレッドはMac上の合議エンジンとTCPソケットで通信します。やねうら王とふかうら王は別々のソケットを用い、干渉しません。本当はiPadアプリ内で合議を行うのが理想ですが、Python言語で記述した合議エンジンをC++またはSwiftへ移植することまでは開発期間の都合で実現していません。
シンボル名の衝突を回避する
名前空間を用いる
やねうら王とふかうら王は同じソースツリーで構成されており、違いはビルドに含めるcppファイルの組み合わせおよびマクロ定義です。やねうら王とふかうら王では、同じ名前の関数として、マクロ定義の違いにより異なる実装がビルドされます。これらの関数名を書き換えて、やねうら王とふかうら王で異なる名前にすれば衝突は解消します。そのための手段として、名前空間を用います。あらゆる関数・クラス等の定義をやねうら王、ふかうら王で異なる名前空間の中で行えば干渉しません。
元のコードが以下のようになっていたとします。
yaneuraou-search.cpp
(やねうら王でビルドに含まれる)
class MainThread { /* やねうら王の探索 */ };
YaneuraOu_dlshogi_bridge.cpp
(ふかうら王でビルドに含まれる)
class MainThread { /* ふかうら王の探索 */ };
これらのビルド結果をリンクすると、MainThread
の実装が重複しているためエラーとなります。これを回避するため、以下のようにソースを書き換えます。
yaneuraou-search.cpp
(やねうら王でビルドに含まれる)
namespace yaneuraou { class MainThread { /* やねうら王の探索 */ }; }
YaneuraOu_dlshogi_bridge.cpp
(ふかうら王でビルドに含まれる)
namespace fukauraou { class MainThread { /* ふかうら王の探索 */ }; }
こうすれば、名前空間が異なっているためエラーとなりません。また、名前空間X
の中で関数f
を呼び出すと、X::f
があればそれを呼び出しますので、別の名前空間Y
にY::f
が存在しても干渉しません。これにより、やねうら王とふかうら王の実装を共存させつつ分離することが可能となります。
ソースコードの自動書き換え
概念的には先述のように名前空間を分ければよいのですが、具体的な実装には留意点がいくつかあります。
名前空間名にマクロを使用
名前空間の名前にはマクロYANEURAOU_GOUGI_NAMESPACE
を用い、コンパイラオプションで、やねうら王の場合とふかうら王の場合で異なる名前に置き換えるようにします。つまり、ソースコードとしては以下のように記載します。
namespace YANEURAOU_GOUGI_NAMESPACE { /* 実装 */ }
やねうら王をビルドする場合:
clang++ -DYANEURAOU_GOUGI_NAMESPACE=yaneuraou_gougi_nnue -DYANEURAOU_ENGINE_NNUE ...
ふかうら王をビルドする場合:
clang++ -DYANEURAOU_GOUGI_NAMESPACE=yaneuraou_gougi_deep -DYANEURAOU_ENGINE_DEEP ...
このようにすることで、単一のソースファイルでビルドごとに異なる名前空間名を用います。
標準ライブラリはnamespace内に入れられない
もっとも簡単な方法である、cppコードの全体をnamespace
で囲うことはうまくいきません。
ns.cpp
:
namespace x { #include <iostream> void hello() { std::cout << "hello" << std::endl; } } int main() { x::hello(); }
このコードは、ビルドに失敗します。
% g++ ns.cpp In file included from /usr/include/c++/7/bits/postypes.h:40:0, from /usr/include/c++/7/iosfwd:40, from /usr/include/c++/7/ios:38, from /usr/include/c++/7/ostream:38, from /usr/include/c++/7/iostream:39, from ns.cpp:3: /usr/include/c++/7/cwchar:64:11: error: ‘::mbstate_t’ has not been declared using ::mbstate_t; ^~~~~~~~~ /usr/include/c++/7/cwchar:139:11: error: ‘::wint_t’ has not been declared using ::wint_t; ^~~~~~ /usr/include/c++/7/cwchar:141:11: error: ‘::btowc’ has not been declared using ::btowc; ^~~~~
標準ライブラリ(iostream)のヘッダをincludeする箇所がnamespace内にあることが原因です。標準ライブラリ内には、識別子がグローバル名前空間に置かれることを仮定したコードがあります。
これを回避するため、#include
の箇所はグローバル名前空間にします。
namespace YANEURAOU_GOUGI_NAMESPACE { // ソースファイルの先頭で名前空間を開始 } // includeの直前で名前空間を閉じる #include <iostream> namespace YANEURAOU_GOUGI_NAMESPACE { // includeの直後で名前空間を開始 /* 実装 */ } // ソースファイルの末尾で名前空間を閉じる
手作業でこの書き換えを行うことは困難なため、#include
を含む行を検出して名前空間の開始・終了を挿入するソースファイル書き換えツールを実装しました。cppファイルからインクルードされるヘッダファイル内も、やねうら王の実装部分はnamespaceに入れる必要があるため、cppファイル、hppファイル両方にこの処理を施します。
グローバル名前空間を仮定したコードを修正
やねうら王の内部でも、グローバル名前空間を仮定したコードがいくつかありビルドエラーとなるため、修正します。例えば以下の箇所です。
std::string to_usi_string() const { return ::to_usi_string(move); } ~~^
自動化は困難なため、内容を目視確認して一か所ずつ適切な修正を行います。
std::string to_usi_string() const { return YANEURAOU_GOUGI_NAMESPACE::to_usi_string(move); }
そのほか以下のような手直しを行いました。
- 定跡作成コードでエラーが多発したため
config.h
を書き換えて定跡作成コードをビルド対象から外す - Objective-C++コードでObjective-Cのクラス定義を行っている箇所はnamespaceに入れられないため手動書き換え
- 標準ライブラリのテンプレートの特殊化箇所をグローバル名前空間に置く
各種対処を盛り込んだ自動書き換えコードは以下の通りです。
https://github.com/select766/YaneuraOuGougiiOSSPM/blob/main/modify_cpp_into_namespace.py
エントリポイントの実装
メインスレッド(GUIスレッド)からやねうら王・ふかうら王を起動する箇所は以下のようになっています。グローバル名前空間のyaneuraou_ios_main
関数を呼び出すことで、やねうら王・ふかうら王それぞれのエントリポイントとなる関数を新しいスレッドで起動します。
namespace YANEURAOU_GOUGI_NAMESPACE { void yaneuraou_ios_thread_main() { // --- 全体的な初期化 int argc = 1; char *argv[] = {"yaneuraou"}; CommandLine::init(argc,argv); USI::init(Options); Bitboards::init(); Position::init(); Search::init(); } } // DEEP側のビルド時にエントリポイント関数をビルドする #if defined(YANEURAOU_ENGINE_DEEP) extern "C" int yaneuraou_ios_main(const char* deep_server_ip, int deep_server_port, const char* deep_modelc_url, int deep_coreml_compute_units, const char* nnue_server_ip, int nnue_server_port, const char* nnue_file_path) { // エンジンごとの設定をグローバル変数経由で設定 yaneuraou_gougi_deep::modelc_url_cache = deep_modelc_url; yaneuraou_gougi_deep::coreml_compute_units_cache = deep_coreml_compute_units; yaneuraou_gougi_nnue::nnue_file_path = nnue_file_path; // エンジンを新しいスレッドで起動 std::thread thread_deep(yaneuraou_gougi_deep::yaneuraou_ios_thread_main); thread_deep.detach(); std::thread thread_nnue(yaneuraou_gougi_nnue::yaneuraou_ios_thread_main); thread_nnue.detach(); return 0; } #endif
.aファイルの結合
iPadアプリのXcodeプロジェクトに、C++で記述されたライブラリを含める手段として、以前のやねうら王単独でのビルドの際と同様にSPMパッケージ形式を用います。
SPMパッケージには、C++ソースをビルドしたオブジェクトファイル(*.o
)をarコマンドで結合した.a
ファイル(tarやzipに相当する機能をもつファイル)が必要です。やねうら王のビルドで得られる.a
ファイルとふかうら王のビルドで得られる.a
ファイルを結合して1つにします。その方法は、一度ar x
コマンドで.a
ファイルを.o
ファイルに展開し、すべての.o
ファイルをar rcs
コマンドで結合します。
ビルド全体の流れは以下のファイルからたどれます。
https://github.com/select766/YaneuraOuGougiiOSSPM/blob/main/build/build.sh
以上のステップを経ることで、やねうら王・ふかうら王を1つのプロセス内で共存させるビルドができます。標準入出力の分離について、次回の記事で示します。