From 48bfe86d274cb7ef42a3139da245db294ad3395d Mon Sep 17 00:00:00 2001 From: Hisayori Noda Date: Sun, 16 Jun 2019 10:33:53 +0900 Subject: [PATCH] Implemented the logic to update Eval List and Dirty Pieces. --- src/eval/nnue/evaluate_nnue.cpp | 6 +- src/evaluate.cpp | 73 +++++++++++++++++- src/evaluate.h | 35 ++++----- src/position.cpp | 131 +++++++++++++++++++++++++++++++- src/position.h | 5 ++ src/types.h | 11 ++- src/uci.cpp | 69 ++++++++++++++++- src/uci.h | 2 + src/ucioption.cpp | 11 +++ 9 files changed, 313 insertions(+), 30 deletions(-) diff --git a/src/eval/nnue/evaluate_nnue.cpp b/src/eval/nnue/evaluate_nnue.cpp index de86ebe9..6009d888 100644 --- a/src/eval/nnue/evaluate_nnue.cpp +++ b/src/eval/nnue/evaluate_nnue.cpp @@ -233,12 +233,14 @@ void prefetch_evalhash(const Key key) { void load_eval() { NNUE::Initialize(); -#if defined(EVAL_LEARN) if (!Options["SkipLoadingEval"]) -#endif { const std::string dir_name = Options["EvalDir"]; const std::string file_name = Path::Combine(dir_name, NNUE::kFileName); + //{ + // std::ofstream stream(file_name, std::ios::binary); + // NNUE::WriteParameters(stream); + //} std::ifstream stream(file_name, std::ios::binary); const bool result = NNUE::ReadParameters(stream); diff --git a/src/evaluate.cpp b/src/evaluate.cpp index d211db64..9e85e2ae 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -29,6 +29,7 @@ #include "material.h" #include "pawns.h" #include "thread.h" +#include "eval/nnue/evaluate_nnue.h" namespace Trace { @@ -864,7 +865,8 @@ namespace { /// evaluation of the position from the point of view of the side to move. Value Eval::evaluate(const Position& pos) { - return Evaluation(pos).value(); + //return Evaluation(pos).value(); + return Eval::NNUE::evaluate(pos); } @@ -907,3 +909,72 @@ std::string Eval::trace(const Position& pos) { return ss.str(); } + +namespace Eval { +ExtBonaPiece kpp_board_index[PIECE_NB] = { + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + { f_pawn, e_pawn }, + { f_knight, e_knight }, + { f_bishop, e_bishop }, + { f_rook, e_rook }, + { f_queen, e_queen }, + { f_king, e_king }, + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + + // 後手から見た場合。fとeが入れ替わる。 + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, + { e_pawn, f_pawn }, + { e_knight, f_knight }, + { e_bishop, f_bishop }, + { e_rook, f_rook }, + { e_queen, f_queen }, + { e_king, f_king }, + { BONA_PIECE_ZERO, BONA_PIECE_ZERO }, // 金の成りはない +}; + +// 内部で保持しているpieceListFw[]が正しいBonaPieceであるかを検査する。 +// 注 : デバッグ用。遅い。 +bool EvalList::is_valid(const Position& pos) +{ + for (int i = 0; i < length(); ++i) + { + BonaPiece fw = pieceListFw[i]; + // このfwが本当に存在するかをPositionクラスのほうに調べに行く。 + + if (fw == Eval::BONA_PIECE_ZERO) { + continue; + } + + // 範囲外 + if (!(0 <= fw && fw < fe_end)) + return false; + + // 盤上の駒なのでこの駒が本当に存在するか調べにいく。 + for (Piece pc = NO_PIECE; pc < PIECE_NB; ++pc) + { + auto pt = type_of(pc); + if (pt == NO_PIECE || pt == 7) // 存在しない駒 + continue; + + // 駒pcのBonaPieceの開始番号 + auto s = BonaPiece(kpp_board_index[pc].fw); + if (s <= fw && fw < s + SQUARE_NB) + { + // 見つかったのでこの駒がsqの地点にあるかを調べる。 + Square sq = (Square)(fw - s); + Piece pc2 = pos.piece_on(sq); + + if (pc2 != pc) + return false; + + goto Found; + } + } + // 何故か存在しない駒であった.. + return false; + Found:; + } + + return true; +} +} diff --git a/src/evaluate.h b/src/evaluate.h index c96a1288..e9e13e7d 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -35,6 +35,15 @@ std::string trace(const Position& pos); Value evaluate(const Position& pos); +// 評価関数ファイルを読み込む。 +// これは、"is_ready"コマンドの応答時に1度だけ呼び出される。2度呼び出すことは想定していない。 +// (ただし、EvalDir(評価関数フォルダ)が変更になったあと、isreadyが再度送られてきたら読みなおす。) +void load_eval(); + +static uint64_t calc_check_sum() { return 0; } + +static void print_softname(uint64_t check_sum) {} + // --- 評価関数で使う定数 KPP(玉と任意2駒)のPに相当するenum // (評価関数の実験のときには、BonaPieceは自由に定義したいのでここでは定義しない。) @@ -145,22 +154,10 @@ struct EvalList v = PIECE_NUMBER_NB; } - // list長が可変のときは、add()/remove()をサポートする。 - // DirtyPieceのほうから呼び出される。 - - // listにadd()する。 - void add(BonaPiece fb); - - // listからremoveする。 - void remove(BonaPiece fb); - - // 内部で保持しているpieceListFb[]が正しいBonaPieceであるかを検査する。 + // 内部で保持しているpieceListFw[]が正しいBonaPieceであるかを検査する。 // 注 : デバッグ用。遅い。 bool is_valid(const Position& pos); - -protected: - // 盤上sqにあるpiece_noの駒のBonaPieceがfb,fwであることを設定する。 inline void set_piece_on_board(PieceNumber piece_no, BonaPiece fw, BonaPiece fb, Square sq) { @@ -173,7 +170,7 @@ protected: // 駒リスト。駒番号(PieceNumber)いくつの駒がどこにあるのか(BonaPiece)を示す。FV38などで用いる。 // 駒リストの長さ - // 38固定 + // 38固定 public: int length() const { return PIECE_NUMBER_KING; } @@ -181,15 +178,15 @@ public: // また、KPPT型評価関数などは、39,40番目の要素がゼロであることを前提とした // アクセスをしている箇所があるので注意すること。 static const int MAX_LENGTH = 40; + + // 盤上の駒に対して、その駒番号(PieceNumber)を保持している配列 + // 玉がSQ_NBに移動しているとき用に+1まで保持しておくが、 + // SQ_NBの玉を移動させないので、この値を使うことはないはず。 + PieceNumber piece_no_list_board[SQUARE_NB_PLUS1]; private: BonaPiece pieceListFw[MAX_LENGTH]; BonaPiece pieceListFb[MAX_LENGTH]; - - // 盤上の駒に対して、その駒番号(PieceNumber)を保持している配列 - // 玉がSQ_NBに移動しているとき用に+1まで保持しておくが、 - // SQ_NBの玉を移動させないので、この値を使うことはないはず。 - PieceNumber piece_no_list_board[SQUARE_NB_PLUS1]; }; // 評価値の差分計算の管理用 diff --git a/src/position.cpp b/src/position.cpp index edb40499..43f986f9 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -243,6 +243,20 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th std::fill_n(&pieceList[0][0], sizeof(pieceList) / sizeof(Square), SQ_NONE); st = si; + // evalListのclear。上でmemsetでゼロクリアしたときにクリアされているが…。 + evalList.clear(); + + // PieceListを更新する上で、どの駒がどこにあるかを設定しなければならないが、 + // それぞれの駒をどこまで使ったかのカウンター + PieceNumber piece_no_count[KING] = { + PIECE_NUMBER_ZERO, + PIECE_NUMBER_PAWN, + PIECE_NUMBER_KNIGHT, + PIECE_NUMBER_BISHOP, + PIECE_NUMBER_ROOK, + PIECE_NUMBER_QUEEN + }; + ss >> std::noskipws; // 1. Piece placement @@ -256,7 +270,15 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th else if ((idx = PieceToChar.find(token)) != string::npos) { - put_piece(Piece(idx), sq); + auto pc = Piece(idx); + put_piece(pc, sq); + + PieceNumber piece_no = + (idx == W_KING) ? PIECE_NUMBER_WKING : // 先手玉 + (idx == B_KING) ? PIECE_NUMBER_BKING : // 後手玉 + piece_no_count[type_of(Piece(idx))]++; // それ以外 + evalList.put_piece(piece_no, sq, pc); // sqの升にpcの駒を配置する + ++sq; } } @@ -319,6 +341,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th set_state(st); assert(pos_is_ok()); + assert(evalList.is_valid(*this)); return *this; } @@ -739,6 +762,9 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { ++st->rule50; ++st->pliesFromNull; + st->accumulator.computed_accumulation = false; + st->accumulator.computed_score = false; + Color us = sideToMove; Color them = ~us; Square from = from_sq(m); @@ -750,6 +776,9 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(captured == NO_PIECE || color_of(captured) == (type_of(m) != CASTLING ? them : us)); assert(type_of(captured) != KING); + auto& dp = st->dirtyPiece; + dp.dirty_num = 1; + if (type_of(m) == CASTLING) { assert(pc == make_piece(us, KING)); @@ -766,6 +795,8 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { { Square capsq = to; + PieceNumber piece_no1; + // If the captured piece is a pawn, update pawn hash key, otherwise // update non-pawn material. if (type_of(captured) == PAWN) @@ -780,14 +811,22 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { assert(piece_on(to) == NO_PIECE); assert(piece_on(capsq) == make_piece(them, PAWN)); + piece_no1 = piece_no_of(capsq); + board[capsq] = NO_PIECE; // Not done by remove_piece() } + else { + piece_no1 = piece_no_of(capsq); + } st->pawnKey ^= Zobrist::psq[captured][capsq]; } - else + else { st->nonPawnMaterial[them] -= PieceValue[MG][captured]; + piece_no1 = piece_no_of(capsq); + } + // Update board and piece lists remove_piece(captured, capsq); @@ -798,6 +837,19 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { // Reset rule 50 counter st->rule50 = 0; + + dp.dirty_num = 2; // 動いた駒は2個 + + dp.pieceNo[1] = piece_no1; + dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1); + // Do not use Eval::EvalList::put_piece() because the piece is removed + // from the game, and the corresponding elements of the piece lists + // needs to be Eval::BONA_PIECE_ZERO. + evalList.set_piece_on_board(piece_no1, Eval::BONA_PIECE_ZERO, Eval::BONA_PIECE_ZERO, capsq); + // Set PIECE_NUMBER_NB to piece_no_of_board[capsq] directly because it + // will not be overritten to pc if the move type is enpassant. + evalList.piece_no_list_board[capsq] = PIECE_NUMBER_NB; + dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1); } // Update hash key @@ -819,8 +871,16 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } // Move the piece. The tricky Chess960 castling is handled earlier - if (type_of(m) != CASTLING) - move_piece(pc, from, to); + if (type_of(m) != CASTLING) { + PieceNumber piece_no0 = piece_no_of(from); + + move_piece(pc, from, to); + + dp.pieceNo[0] = piece_no0; + dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + evalList.put_piece(piece_no0, to, pc); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); + } // If the moving piece is a pawn do some special extra work if (type_of(pc) == PAWN) @@ -843,6 +903,12 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { remove_piece(pc, to); put_piece(promotion, to); + PieceNumber piece_no0 = piece_no_of(to); + dp.pieceNo[0] = piece_no0; + dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + evalList.put_piece(piece_no0, to, promotion); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); + // Update hash keys k ^= Zobrist::psq[pc][to] ^ Zobrist::psq[promotion][to]; st->pawnKey ^= Zobrist::psq[pc][to]; @@ -894,6 +960,7 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { } assert(pos_is_ok()); + assert(evalList.is_valid(*this)); } @@ -923,6 +990,9 @@ void Position::undo_move(Move m) { remove_piece(pc, to); pc = make_piece(us, PAWN); put_piece(pc, to); + + PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0]; + evalList.put_piece(piece_no0, to, pc); } if (type_of(m) == CASTLING) @@ -932,8 +1002,12 @@ void Position::undo_move(Move m) { } else { + move_piece(pc, to, from); // Put the piece back at the source square + PieceNumber piece_no0 = st->dirtyPiece.pieceNo[0]; + evalList.put_piece(piece_no0, from, pc); + if (st->capturedPiece) { Square capsq = to; @@ -950,6 +1024,11 @@ void Position::undo_move(Move m) { } put_piece(st->capturedPiece, capsq); // Restore the captured piece + + PieceNumber piece_no1 = st->dirtyPiece.pieceNo[1]; + assert(evalList.bona_piece(piece_no1).fw == Eval::BONA_PIECE_ZERO); + assert(evalList.bona_piece(piece_no1).fb == Eval::BONA_PIECE_ZERO); + evalList.put_piece(piece_no1, capsq, st->capturedPiece); } } @@ -958,6 +1037,7 @@ void Position::undo_move(Move m) { --gamePly; assert(pos_is_ok()); + assert(evalList.is_valid(*this)); } @@ -965,18 +1045,50 @@ void Position::undo_move(Move m) { /// is a bit tricky in Chess960 where from/to squares can overlap. template void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto) { + auto& dp = st->dirtyPiece; + // 差分計算のために移動した駒をStateInfoに記録しておく。 + dp.dirty_num = 2; // 動いた駒は2個 + + PieceNumber piece_no0; + PieceNumber piece_no1; + + if (Do) { + piece_no0 = piece_no_of(from); + piece_no1 = piece_no_of(to); + } bool kingSide = to > from; rfrom = to; // Castling is encoded as "king captures friendly rook" rto = relative_square(us, kingSide ? SQ_F1 : SQ_D1); to = relative_square(us, kingSide ? SQ_G1 : SQ_C1); + if (!Do) { + piece_no0 = piece_no_of(to); + piece_no1 = piece_no_of(rto); + } + // Remove both pieces first since squares could overlap in Chess960 remove_piece(make_piece(us, KING), Do ? from : to); remove_piece(make_piece(us, ROOK), Do ? rfrom : rto); board[Do ? from : to] = board[Do ? rfrom : rto] = NO_PIECE; // Since remove_piece doesn't do it for us put_piece(make_piece(us, KING), Do ? to : from); put_piece(make_piece(us, ROOK), Do ? rto : rfrom); + + if (Do) { + dp.pieceNo[0] = piece_no0; + dp.changed_piece[0].old_piece = evalList.bona_piece(piece_no0); + evalList.put_piece(piece_no0, to, make_piece(us, KING)); + dp.changed_piece[0].new_piece = evalList.bona_piece(piece_no0); + + dp.pieceNo[1] = piece_no1; + dp.changed_piece[1].old_piece = evalList.bona_piece(piece_no1); + evalList.put_piece(piece_no1, rto, make_piece(us, ROOK)); + dp.changed_piece[1].new_piece = evalList.bona_piece(piece_no1); + } + else { + evalList.put_piece(piece_no0, from, make_piece(us, KING)); + evalList.put_piece(piece_no1, rfrom, make_piece(us, ROOK)); + } } @@ -1313,3 +1425,14 @@ bool Position::pos_is_ok() const { return true; } + +PieceNumber Position::piece_no_of(Square sq) const +{ + if (piece_on(sq) == NO_PIECE) { + sync_cout << *this << sync_endl; + } + assert(piece_on(sq) != NO_PIECE); + PieceNumber n = evalList.piece_no_of_board(sq); + assert(is_ok(n)); + return n; +} diff --git a/src/position.h b/src/position.h index d26b1a63..8111c663 100644 --- a/src/position.h +++ b/src/position.h @@ -23,10 +23,12 @@ #include #include +#include #include // For std::unique_ptr #include #include "bitboard.h" +#include "misc.h" #include "types.h" #include "eval/nnue/nnue_accumulator.h" @@ -194,6 +196,9 @@ private: template void do_castling(Color us, Square from, Square& to, Square& rfrom, Square& rto); + // 盤上のsqの升にある駒のPieceNumberを返す。 + PieceNumber piece_no_of(Square sq) const; + // Data members Piece board[SQUARE_NB]; Bitboard byTypeBB[PIECE_TYPE_NB]; diff --git a/src/types.h b/src/types.h index c4458fe4..ef6cbb40 100644 --- a/src/types.h +++ b/src/types.h @@ -469,7 +469,7 @@ constexpr bool is_ok(Move m) { // -------------------- // Positionクラスで用いる、駒リスト(どの駒がどこにあるのか)を管理するときの番号。 -enum PieceNumber : int8_t +enum PieceNumber : uint8_t { PIECE_NUMBER_PAWN = 0, PIECE_NUMBER_KNIGHT = 16, @@ -483,8 +483,13 @@ enum PieceNumber : int8_t PIECE_NUMBER_NB = 32, }; -inline PieceNumber& operator++(PieceNumber& d) { return d = PieceNumber(int(d) + 1); } \ -inline PieceNumber& operator--(PieceNumber& d) { return d = PieceNumber(int(d) - 1); } +inline PieceNumber& operator++(PieceNumber& d) { return d = PieceNumber(int8_t(d) + 1); } +inline PieceNumber operator++(PieceNumber& d, int) { + PieceNumber x = d; + d = PieceNumber(int8_t(d) + 1); + return x; +} +inline PieceNumber& operator--(PieceNumber& d) { return d = PieceNumber(int8_t(d) - 1); } // PieceNumberの整合性の検査。assert用。 constexpr bool is_ok(PieceNumber pn) { return pn < PIECE_NUMBER_NB; } diff --git a/src/uci.cpp b/src/uci.cpp index 739cf343..bee5acd7 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -177,6 +177,73 @@ namespace { << "\nNodes/second : " << 1000 * nodes / elapsed << endl; } + // check sumを計算したとき、それを保存しておいてあとで次回以降、整合性のチェックを行なう。 + uint64_t eval_sum; + + // is_ready_cmd()を外部から呼び出せるようにしておく。(benchコマンドなどから呼び出したいため) + // 局面は初期化されないので注意。 + void is_ready(Position& pos, istringstream& is, StateListPtr& states) + { + // "isready"を受け取ったあと、"readyok"を返すまで5秒ごとに改行を送るように修正する。(keep alive的な処理) + // USI2.0の仕様より。 + // -"isready"のあとのtime out時間は、30秒程度とする。これを超えて、評価関数の初期化、hashテーブルの確保をしたい場合、 + // 思考エンジン側から定期的に何らかのメッセージ(改行可)を送るべきである。 + // -ShogiGUIではすでにそうなっているので、MyShogiもそれに追随する。 + // -また、やねうら王のエンジン側は、"isready"を受け取ったあと、"readyok"を返すまで5秒ごとに改行を送るように修正する。 + + auto ended = false; + auto th = std::thread([&ended] { + int count = 0; + while (!ended) + { + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + if (++count >= 50 /* 5秒 */) + { + count = 0; + sync_cout << sync_endl; // 改行を送信する。 + } + } + }); + + // 評価関数の読み込みなど時間のかかるであろう処理はこのタイミングで行なう。 + // 起動時に時間のかかる処理をしてしまうと将棋所がタイムアウト判定をして、思考エンジンとしての認識をリタイアしてしまう。 + if (!UCI::load_eval_finished) + { + // 評価関数の読み込み + Eval::load_eval(); + + // チェックサムの計算と保存(その後のメモリ破損のチェックのため) + eval_sum = Eval::calc_check_sum(); + + // ソフト名の表示 + Eval::print_softname(eval_sum); + + UCI::load_eval_finished = true; + + } + else + { + // メモリが破壊されていないかを調べるためにチェックサムを毎回調べる。 + // 時間が少しもったいない気もするが.. 0.1秒ぐらいのことなので良しとする。 + if (eval_sum != Eval::calc_check_sum()) + sync_cout << "Error! : EVAL memory is corrupted" << sync_endl; + } + + // isreadyに対してはreadyokを返すまで次のコマンドが来ないことは約束されているので + // このタイミングで各種変数の初期化もしておく。 + + TT.resize(Options["Hash"]); + Search::clear(); + Time.availableNodes = 0; + + Threads.stop = false; + + // keep aliveを送信するために生成したスレッドを終了させ、待機する。 + ended = true; + th.join(); + + sync_cout << "readyok" << sync_endl; + } } // namespace @@ -227,7 +294,7 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "go") go(pos, is, states); else if (token == "position") position(pos, is, states); else if (token == "ucinewgame") Search::clear(); - else if (token == "isready") sync_cout << "readyok" << sync_endl; + else if (token == "isready") is_ready(pos, is, states); // Additional custom non-UCI commands, mainly for debugging else if (token == "flip") pos.flip(); diff --git a/src/uci.h b/src/uci.h index 31b63e2f..4a7771ca 100644 --- a/src/uci.h +++ b/src/uci.h @@ -75,6 +75,8 @@ std::string move(Move m, bool chess960); std::string pv(const Position& pos, Depth depth, Value alpha, Value beta); Move to_move(const Position& pos, std::string& str); +// 評価関数を読み込んだかのフラグ。これはevaldirの変更にともなってfalseにする。 +extern bool load_eval_finished; // = false; } // namespace UCI extern UCI::OptionsMap Options; diff --git a/src/ucioption.cpp b/src/ucioption.cpp index 813a0890..e549c6e0 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -42,6 +42,7 @@ void on_hash_size(const Option& o) { TT.resize(o); } void on_logger(const Option& o) { start_logger(o); } void on_threads(const Option& o) { Threads.set(o); } void on_tb_path(const Option& o) { Tablebases::init(o); } +void on_eval_dir(const Option& o) { load_eval_finished = false; } /// Our case insensitive less() function as required by UCI protocol @@ -78,6 +79,14 @@ void init(OptionsMap& o) { o["SyzygyProbeDepth"] << Option(1, 1, 100); o["Syzygy50MoveRule"] << Option(true); o["SyzygyProbeLimit"] << Option(7, 0, 7); + // 評価関数フォルダ。これを変更したとき、評価関数を次のisreadyタイミングで読み直す必要がある。 + o["EvalDir"] << Option("eval", on_eval_dir); + // isreadyタイミングで評価関数を読み込まれると、新しい評価関数の変換のために + // test evalconvertコマンドを叩きたいのに、その新しい評価関数がないがために + // このコマンドの実行前に異常終了してしまう。 + // そこでこの隠しオプションでisready時の評価関数の読み込みを抑制して、 + // test evalconvertコマンドを叩く。 + o["SkipLoadingEval"] << Option(false); } @@ -186,4 +195,6 @@ Option& Option::operator=(const string& v) { return *this; } +// 評価関数を読み込んだかのフラグ。これはevaldirの変更にともなってfalseにする。 +bool load_eval_finished = false; } // namespace UCI