diff --git a/libs/lczero-common b/libs/lczero-common index 55e1b382ef..fafda0f59c 160000 --- a/libs/lczero-common +++ b/libs/lczero-common @@ -1 +1 @@ -Subproject commit 55e1b382efadd57903e37f2a2e29caef3ea85799 +Subproject commit fafda0f59c8511b5d933ef758c1e4b10a62da1e0 diff --git a/meson.build b/meson.build index 27d6c6cb63..5b91275fa6 100644 --- a/meson.build +++ b/meson.build @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with Leela Chess. If not, see . -project('lc0', 'cpp', +project('lc0', ['c', 'cpp'], default_options : ['cpp_std=c++17', 'b_ndebug=if-release', 'warning_level=3', 'b_lto=true', 'b_vscrt=mt'], meson_version: '>=0.55') @@ -691,6 +691,7 @@ endif if get_option('lc0') files += common_files + deps += subproject('gaviotatb').get_variable('gaviotatb_dep') executable('lc0', 'src/main.cc', files, include_directories: includes, dependencies: deps, install: true) endif diff --git a/src/benchmark/benchmark.cc b/src/benchmark/benchmark.cc index d24638a002..fadd57ae42 100644 --- a/src/benchmark/benchmark.cc +++ b/src/benchmark/benchmark.cc @@ -101,13 +101,16 @@ void Benchmark::Run() { tree.ResetToPosition(position, {}); const auto start = std::chrono::steady_clock::now(); + std::unique_ptr gaviotaEnabled_; + gaviotaEnabled_ = std::make_unique(false); + auto search = std::make_unique( tree, network.get(), std::make_unique( std::bind(&Benchmark::OnBestMove, this, std::placeholders::_1), std::bind(&Benchmark::OnInfo, this, std::placeholders::_1)), MoveList(), start, std::move(stopper), false, false, option_dict, - &cache, nullptr); + &cache, nullptr, &gaviotaEnabled_); search->StartThreads(option_dict.Get(kThreadsOptionId)); search->Wait(); const auto end = std::chrono::steady_clock::now(); diff --git a/src/engine.cc b/src/engine.cc index e4157657c6..faf764d5fd 100644 --- a/src/engine.cc +++ b/src/engine.cc @@ -51,6 +51,12 @@ const OptionId kSyzygyTablebaseId{ "List of Syzygy tablebase directories, list entries separated by system " "separator (\";\" for Windows, \":\" for Linux).", 's'}; +const OptionId kGaviotaTablebaseId{"gaviotatb-paths", "GaviotaPath", + "List of Gaviota tablebase directories. If both Syzygy and Gaviota are " + "provided, Gaviota will take precedence when only 5 pieces remain. " + "Note that if this parameter is set it is assumed that all Gaviota " + "tables (3, 4 and 5-men) are available, but this is not checked, " + "so using this parameter without all of these is not supported."}; const OptionId kPonderId{"", "Ponder", "This option is ignored. Here to please chess GUIs."}; const OptionId kUciChess960{ @@ -116,6 +122,7 @@ void EngineController::PopulateOptions(OptionsParser* options) { options->UnhideOption(SearchParams::kMultiPvId); } options->Add(kSyzygyTablebaseId); + options->Add(kGaviotaTablebaseId); // Add "Ponder" option to signal to GUIs that we support pondering. // This option is currently not used by lc0 in any way. options->Add(kPonderId) = true; @@ -139,6 +146,13 @@ void EngineController::ResetMoveTimer() { move_start_time_ = std::chrono::steady_clock::now(); } +// Needed for Gaviota +#ifdef _WIN32 +#define SEP_CHAR ';' +#else +#define SEP_CHAR ':' +#endif + // Updates values from Uci options. void EngineController::UpdateFromUciOptions() { SharedLock lock(busy_mutex_); @@ -158,6 +172,29 @@ void EngineController::UpdateFromUciOptions() { tb_paths_.clear(); } + // Init Gaviota, if a path is given + auto dtmPaths = options_.Get(kGaviotaTablebaseId); + if (dtmPaths.size() != 0) { + std::stringstream path_string_stream(dtmPaths); + std::string path; + auto paths = tbpaths_init(); + while (std::getline(path_string_stream, path, SEP_CHAR)) { + paths = tbpaths_add(paths, path.c_str()); + } + tb_init(0, tb_CP4, paths); + tbcache_init(64 * 1024 * 1024, 64); + if (tb_availability() != 63) { + std::cerr << "UNEXPECTED gaviota availability" << std::endl; + gaviotaEnabled_ = std::make_unique(false); + return; + } else { + gaviotaEnabled_ = std::make_unique(true); + std::cerr << "Found Gaviota TBs" << std::endl; + } + } else { + gaviotaEnabled_ = std::make_unique(false); + } + // Network. const auto network_configuration = NetworkFactory::BackendConfiguration(options_); @@ -394,7 +431,7 @@ void EngineController::Go(const GoParams& params) { *tree_, network_.get(), std::move(responder), StringsToMovelist(params.searchmoves, tree_->HeadPosition().GetBoard()), *move_start_time_, std::move(stopper), params.infinite, params.ponder, - options_, &cache_, syzygy_tb_.get()); + options_, &cache_, syzygy_tb_.get(), &gaviotaEnabled_); LOGFILE << "Timer started at " << FormatTime(SteadyClockToSystemClock(*move_start_time_)); diff --git a/src/engine.h b/src/engine.h index 9743679a43..334bc1684e 100644 --- a/src/engine.h +++ b/src/engine.h @@ -96,6 +96,7 @@ class EngineController { std::unique_ptr search_; std::unique_ptr tree_; std::unique_ptr syzygy_tb_; + std::unique_ptr gaviotaEnabled_ = nullptr; std::unique_ptr network_; NNCache cache_; diff --git a/src/mcts/search.cc b/src/mcts/search.cc index 71eb543c10..b94aa5efab 100644 --- a/src/mcts/search.cc +++ b/src/mcts/search.cc @@ -50,27 +50,215 @@ namespace { // Maximum delay between outputting "uci info" when nothing interesting happens. const int kUciInfoMinimumFrequencyMs = 5000; +void gaviota_tb_probe_hard(const Position& pos, unsigned int& info, + unsigned int& dtm) { + unsigned int wsq[17]; + unsigned int bsq[17]; + unsigned char wpc[17]; + unsigned char bpc[17]; + + auto stm = pos.IsBlackToMove() ? tb_BLACK_TO_MOVE : tb_WHITE_TO_MOVE; + auto& board = pos.IsBlackToMove() ? pos.GetThemBoard() : pos.GetBoard(); + auto epsq = tb_NOSQUARE; + for (auto sq : board.en_passant()) { + // Our internal representation stores en_passant 2 rows away + // from the actual sq. + if (sq.row() == 0) { + epsq = (TB_squares)(sq.as_int() + 16); + } else { + epsq = (TB_squares)(sq.as_int() - 16); + } + } + int idx = 0; + for (auto sq : (board.ours() & board.kings())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_KING; + idx++; + } + for (auto sq : (board.ours() & board.knights())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_KNIGHT; + idx++; + } + for (auto sq : (board.ours() & board.queens())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_QUEEN; + idx++; + } + for (auto sq : (board.ours() & board.rooks())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_ROOK; + idx++; + } + for (auto sq : (board.ours() & board.bishops())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_BISHOP; + idx++; + } + for (auto sq : (board.ours() & board.pawns())) { + wsq[idx] = (TB_squares)sq.as_int(); + wpc[idx] = tb_PAWN; + idx++; + } + wsq[idx] = tb_NOSQUARE; + wpc[idx] = tb_NOPIECE; + + idx = 0; + for (auto sq : (board.theirs() & board.kings())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_KING; + idx++; + } + for (auto sq : (board.theirs() & board.knights())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_KNIGHT; + idx++; + } + for (auto sq : (board.theirs() & board.queens())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_QUEEN; + idx++; + } + for (auto sq : (board.theirs() & board.rooks())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_ROOK; + idx++; + } + for (auto sq : (board.theirs() & board.bishops())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_BISHOP; + idx++; + } + for (auto sq : (board.theirs() & board.pawns())) { + bsq[idx] = (TB_squares)sq.as_int(); + bpc[idx] = tb_PAWN; + idx++; + } + bsq[idx] = tb_NOSQUARE; + bpc[idx] = tb_NOPIECE; + + tb_probe_hard(stm, epsq, tb_NOCASTLE, wsq, bsq, wpc, bpc, &info, &dtm); +} + +bool root_probe_gaviota(const Position& pos, std::vector* safe_moves) { + // if the position is winning the strategy is trivial: shortest mate for the winning side, longest mate for the losing side. + // if the position is draw, all non-losing moves are equal. + + // Generate the list of legal moves. + auto root_moves = pos.GetBoard().GenerateLegalMoves(); + + // Create a vector to store dtm information in. + std::vector dtms (root_moves.size()); + // And a vector for info information. + std::vector infos (root_moves.size()); + unsigned int minimum_dtm = 1000; + unsigned int maximum_dtm = 0; + unsigned int target_dtm = 0; + int dtm_idx = 0; + bool at_least_there_is_a_draw = false; + + // for all legal moves identify minimum and maximum dtm, if any. + for (auto& move : root_moves) { + Position next_pos = Position(pos, move); + unsigned int info; + unsigned int dtm; + gaviota_tb_probe_hard(next_pos, info, dtm); + // LOGFILE << "DTM for move: " << move.as_string() << " is " << dtm << " and info is " << info << "\n"; + dtms[dtm_idx] = dtm; + infos[dtm_idx] = info; + dtm_idx++; + // info == 0 means draw. + // info == 1 and info == 2 implies a decisive move + + // if some moves have info == 1 and others have info == 2, then the info == 1 moves appears to be + // moves that lose in a otherwise won position. + + // when info == 2 && dtm is odd then root is losing + // when info == 2 && dtm is even then root is winning does the same hold when info == 1? + + if (info == 2 || info == 1){ + if(dtm % 2 == 0 || dtm == 0){ + // root is winning, minimise + if(dtm < minimum_dtm) minimum_dtm = dtm; + } else { + // it root is losing, maximise + if(dtm > maximum_dtm) maximum_dtm = dtm; + } + } else { + // Set the draw flag if not already set + if (!at_least_there_is_a_draw){ + at_least_there_is_a_draw = true; + } + } + } + + // Set a target DTM if the game is not drawn, as implied by + // minium_dtm or maximum_dtm are changed from there default values + if (minimum_dtm != 1000) { + target_dtm = minimum_dtm; + // LOGFILE << "Winning, opting for lowest dtm = " << target_dtm; + } else { + // Only care about how to lose when actually losing. + if (!at_least_there_is_a_draw) { + target_dtm = maximum_dtm; + // LOGFILE << "Losing, opting for highest dtm = " << target_dtm; + } + } + + if (minimum_dtm != 1000 || !at_least_there_is_a_draw) { + dtm_idx = 0; + for (auto& move : root_moves) { + if (dtms[dtm_idx] == target_dtm && /* stalemate is also dtm 0, make sure to pick a proper mate */ infos[dtm_idx] != 0) { + safe_moves->push_back(move); + } + dtm_idx++; + } + } else { + // LOGFILE << "Drawing is optimal exclude losing moves"; + // Draw is the optimal outcome, but keep only drawing moves (info == 0 means draw). + dtm_idx = 0; + for (auto& move : root_moves) { + if (infos[dtm_idx] == 0) { + safe_moves->push_back(move); + } + dtm_idx++; + } + } + return true; +} + MoveList MakeRootMoveFilter(const MoveList& searchmoves, SyzygyTablebase* syzygy_tb, const PositionHistory& history, bool fast_play, - std::atomic* tb_hits, bool* dtz_success) { + std::atomic* tb_hits, bool* dtz_success, + std::unique_ptr* gaviotaEnabled) { assert(tb_hits); assert(dtz_success); // Search moves overrides tablebase. if (!searchmoves.empty()) return searchmoves; const auto& board = history.Last().GetBoard(); MoveList root_moves; - if (!syzygy_tb || !board.castlings().no_legal_castle() || - (board.ours() | board.theirs()).count() > syzygy_tb->max_cardinality()) { - return root_moves; - } - if (syzygy_tb->root_probe( + + // Select TB to use. + // If gaviota is available and at most 5 pieces left, then use gaviota, else use syzygy. + + if (gaviotaEnabled && (board.ours() | board.theirs()).count() <= 5 && + root_probe_gaviota(history.Last(), &root_moves)){ + tb_hits->fetch_add(1, std::memory_order_acq_rel); + } else { + // Try syzygy instead + if (!syzygy_tb || !board.castlings().no_legal_castle() || + (board.ours() | board.theirs()).count() > syzygy_tb->max_cardinality()) { + return root_moves; + } + if (syzygy_tb->root_probe( history.Last(), fast_play || history.DidRepeatSinceLastZeroingMove(), false, &root_moves)) { - *dtz_success = true; - tb_hits->fetch_add(1, std::memory_order_acq_rel); - } else if (syzygy_tb->root_probe_wdl(history.Last(), &root_moves)) { - tb_hits->fetch_add(1, std::memory_order_acq_rel); + *dtz_success = true; + tb_hits->fetch_add(1, std::memory_order_acq_rel); + } else if (syzygy_tb->root_probe_wdl(history.Last(), &root_moves)) { + tb_hits->fetch_add(1, std::memory_order_acq_rel); + } } return root_moves; } @@ -155,7 +343,7 @@ Search::Search(const NodeTree& tree, Network* network, std::chrono::steady_clock::time_point start_time, std::unique_ptr stopper, bool infinite, bool ponder, const OptionsDict& options, NNCache* cache, - SyzygyTablebase* syzygy_tb) + SyzygyTablebase* syzygy_tb, std::unique_ptr* gaviotaEnabled) : ok_to_respond_bestmove_(!infinite && !ponder), stopper_(std::move(stopper)), root_node_(tree.GetCurrentHead()), @@ -169,7 +357,7 @@ Search::Search(const NodeTree& tree, Network* network, initial_visits_(root_node_->GetN()), root_move_filter_(MakeRootMoveFilter( searchmoves_, syzygy_tb_, played_history_, - params_.GetSyzygyFastPlay(), &tb_hits_, &root_is_in_dtz_)), + params_.GetSyzygyFastPlay(), &tb_hits_, &root_is_in_dtz_, gaviotaEnabled)), uci_responder_(std::move(uci_responder)) { if (params_.GetMaxConcurrentSearchers() != 0) { pending_searchers_.store(params_.GetMaxConcurrentSearchers(), diff --git a/src/mcts/search.h b/src/mcts/search.h index c2ff2aa116..8786ba43a2 100644 --- a/src/mcts/search.h +++ b/src/mcts/search.h @@ -42,6 +42,7 @@ #include "neural/cache.h" #include "neural/network.h" #include "syzygy/syzygy.h" +#include "gtb-probe.h" #include "utils/logging.h" #include "utils/mutex.h" @@ -55,7 +56,7 @@ class Search { std::chrono::steady_clock::time_point start_time, std::unique_ptr stopper, bool infinite, bool ponder, const OptionsDict& options, NNCache* cache, - SyzygyTablebase* syzygy_tb); + SyzygyTablebase* syzygy_tb, std::unique_ptr* gaviotaEnabled); ~Search(); @@ -167,6 +168,12 @@ class Search { // Fixed positions which happened before the search. const PositionHistory& played_history_; + // Probes Gaviota tables to determine which moves are on the optimal play path. + // Thread safe. + // Returns false if the position is not in the tablebase. + // Safe moves are added to the safe_moves output paramater. + bool root_probe_gaviota(const Position& pos, std::vector* safe_moves); + Network* const network_; const SearchParams params_; const MoveList searchmoves_; diff --git a/src/selfplay/game.cc b/src/selfplay/game.cc index 68554c2fc3..3b6325861b 100644 --- a/src/selfplay/game.cc +++ b/src/selfplay/game.cc @@ -128,7 +128,9 @@ SelfPlayGame::SelfPlayGame(PlayerOptions white, PlayerOptions black, } void SelfPlayGame::Play(int white_threads, int black_threads, bool training, - SyzygyTablebase* syzygy_tb, bool enable_resign) { + SyzygyTablebase* syzygy_tb, + std::unique_ptr* gaviotaEnabled, + bool enable_resign) { bool blacks_move = tree_[0]->IsBlackToMove(); // If we are training, verify that input formats are consistent. @@ -182,7 +184,8 @@ void SelfPlayGame::Play(int white_threads, int black_threads, bool training, *tree_[idx], options_[idx].network, std::move(responder), /* searchmoves */ MoveList(), std::chrono::steady_clock::now(), std::move(stoppers), /* infinite */ false, /* ponder */ false, - *options_[idx].uci_options, options_[idx].cache, syzygy_tb); + *options_[idx].uci_options, options_[idx].cache, syzygy_tb, + /* gaviota */ gaviotaEnabled); } // Do search. diff --git a/src/selfplay/game.h b/src/selfplay/game.h index 918c328ce1..a1435df98c 100644 --- a/src/selfplay/game.h +++ b/src/selfplay/game.h @@ -80,7 +80,8 @@ class SelfPlayGame { // Starts the game and blocks until the game is finished. void Play(int white_threads, int black_threads, bool training, - SyzygyTablebase* syzygy_tb, bool enable_resign = true); + SyzygyTablebase* syzygy_tb, std::unique_ptr* gaviotaEnabled, + bool enable_resign = true); // Aborts the game currently played, doesn't matter if it's synchronous or // not. void Abort(); diff --git a/src/selfplay/tournament.cc b/src/selfplay/tournament.cc index 993e449a23..50b2963e11 100644 --- a/src/selfplay/tournament.cc +++ b/src/selfplay/tournament.cc @@ -96,6 +96,12 @@ const OptionId kSyzygyTablebaseId{ "List of Syzygy tablebase directories, list entries separated by system " "separator (\";\" for Windows, \":\" for Linux).", 's'}; +const OptionId kGaviotaTablebaseId{"gaviotatb-paths", "GaviotaPath", + "List of Gaviota tablebase directories. If both Syzygy and Gaviota are " + "provided, Gaviota will take precedence when only 5 pieces remain. " + "Note that if this parameter is set it is assumed that all Gaviota " + "tables (3, 4 and 5-men) are available, but this is not checked, " + "so using this parameter without all of these is not supported."}; } // namespace @@ -136,6 +142,7 @@ void SelfPlayTournament::PopulateOptions(OptionsParser* options) { options->Add(kOpeningsModeId, openings_modes) = "sequential"; options->Add(kSyzygyTablebaseId); + options->Add(kGaviotaTablebaseId); SelfPlayGame::PopulateUciParams(options); auto defaults = options->GetMutableDefaultsOptions(); @@ -157,6 +164,13 @@ void SelfPlayTournament::PopulateOptions(OptionsParser* options) { defaults->Set(SearchParams::kTaskWorkersPerSearchWorkerId, 0); } +// Needed for Gaviota +#ifdef _WIN32 +#define SEP_CHAR ';' +#else +#define SEP_CHAR ':' +#endif + SelfPlayTournament::SelfPlayTournament( const OptionsDict& options, CallbackUciResponder::BestMoveCallback best_move_info, @@ -270,6 +284,29 @@ SelfPlayTournament::SelfPlayTournament( syzygy_tb_ = nullptr; } } + + // Init Gaviota, if a path is given. + auto dtmPaths = options.Get(kGaviotaTablebaseId); + if (dtmPaths.size() != 0 && !gaviotaEnabled_) { + std::stringstream path_string_stream(dtmPaths); + std::string path; + auto paths = tbpaths_init(); + while (std::getline(path_string_stream, path, SEP_CHAR)) { + paths = tbpaths_add(paths, path.c_str()); + } + tb_init(0, tb_CP4, paths); + tbcache_init(64 * 1024 * 1024, 64); + if (tb_availability() != 63) { + std::cerr << "UNEXPECTED gaviota availability" << std::endl; + gaviotaEnabled_ = std::make_unique(false); + // return; + } else { + gaviotaEnabled_ = std::make_unique(true); + std::cerr << "Found Gaviota TBs" << std::endl; + } + } else { + gaviotaEnabled_ = std::make_unique(false); + } } void SelfPlayTournament::PlayOneGame(int game_number) { @@ -394,7 +431,7 @@ void SelfPlayTournament::PlayOneGame(int game_number) { auto player1_threads = player_options_[0][color_idx[0]].Get(kThreadsId); auto player2_threads = player_options_[1][color_idx[1]].Get(kThreadsId); game.Play(player1_threads, player2_threads, kTraining, syzygy_tb, - enable_resign); + &gaviotaEnabled_, enable_resign); // If game was aborted, it's still undecided. if (game.GetGameResult() != GameResult::UNDECIDED) { diff --git a/src/selfplay/tournament.h b/src/selfplay/tournament.h index d695570a2e..0da3163385 100644 --- a/src/selfplay/tournament.h +++ b/src/selfplay/tournament.h @@ -79,6 +79,7 @@ class SelfPlayTournament { // Whether first game will be black for player1. bool first_game_black_ GUARDED_BY(mutex_) = false; std::unique_ptr syzygy_tb_ GUARDED_BY(mutex_); + std::unique_ptr gaviotaEnabled_ GUARDED_BY(mutex_) = nullptr; std::vector discard_pile_ GUARDED_BY(mutex_); // Number of games which already started. int games_count_ GUARDED_BY(mutex_) = 0;