diff --git a/.gitignore b/.gitignore index 08dd12258..8df484d17 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,5 @@ training/tf/models/ *.exe *.dll /go/src/client/settings.json +*.vspx +*.psess diff --git a/CMakeLists.txt b/CMakeLists.txt index fdba80112..bc4b4954d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -29,7 +29,9 @@ set (Boost_USE_MULTITHREADED ON) find_package (Boost 1.54.0 REQUIRED program_options filesystem) find_package (Threads REQUIRED) find_package (ZLIB REQUIRED) -find_package (OpenCL REQUIRED) +if (NOT FEATURE_USE_CPU_ONLY) + find_package (OpenCL REQUIRED) +endif() # We need OpenBLAS for now, because we make some specific # calls. Ideally we'd use OpenBLAS is possible and fall back to @@ -84,7 +86,9 @@ set (SrcPath "${CMAKE_CURRENT_SOURCE_DIR}/src") include_directories (${IncludePath}) include_directories (SYSTEM ${Boost_INCLUDE_DIRS}) -include_directories (SYSTEM ${OpenCL_INCLUDE_DIRS}) +if (NOT FEATURE_USE_CPU_ONLY) + include_directories (SYSTEM ${OpenCL_INCLUDE_DIRS}) +endif() include_directories (SYSTEM ${ZLIB_INCLUDE_DIRS}) if ((UNIX AND NOT APPLE) OR WIN32) @@ -106,7 +110,9 @@ add_executable (lczero $ ${lczero_MAIN}) target_link_libraries (lczero ${Boost_LIBRARIES}) target_link_libraries (lczero ${BLAS_LIBRARIES}) -target_link_libraries (lczero ${OpenCL_LIBRARIES}) +if (NOT FEATURE_USE_CPU_ONLY) + target_link_libraries (lczero ${OpenCL_LIBRARIES}) +endif() target_link_libraries (lczero ${ZLIB_LIBRARIES}) target_link_libraries (lczero ${CMAKE_THREAD_LIBS_INIT}) install (TARGETS lczero DESTINATION bin) @@ -117,6 +123,8 @@ add_executable (tests ${tests_SRC} $) target_link_libraries (tests ${Boost_LIBRARIES}) target_link_libraries (tests ${BLAS_LIBRARIES}) -target_link_libraries (tests ${OpenCL_LIBRARIES}) +if (NOT FEATURE_USE_CPU_ONLY) + target_link_libraries (tests ${OpenCL_LIBRARIES}) +endif() target_link_libraries (tests ${ZLIB_LIBRARIES}) target_link_libraries (tests gtest_main ${CMAKE_THREAD_LIBS_INIT}) diff --git a/go/src/client/http/http.go b/go/src/client/http/http.go index 63357ca90..693373775 100644 --- a/go/src/client/http/http.go +++ b/go/src/client/http/http.go @@ -2,7 +2,6 @@ package client import ( "bytes" - "compress/gzip" "encoding/json" "errors" "fmt" @@ -117,12 +116,6 @@ func DownloadNetwork(httpClient *http.Client, hostname string, networkPath strin return err } - // Copy while decompressing - zr, err := gzip.NewReader(r.Body) - if err != nil { - log.Fatal(err) - } - - _, err = io.Copy(out, zr) + _, err = io.Copy(out, r.Body) return err } diff --git a/go/src/client/main.go b/go/src/client/main.go index 38e4d8d1b..d51c6c908 100644 --- a/go/src/client/main.go +++ b/go/src/client/main.go @@ -78,7 +78,7 @@ func getExtraParams() map[string]string { return map[string]string{ "user": *USER, "password": *PASSWORD, - "version": "6", + "version": "7", } } diff --git a/go/src/server/main.go b/go/src/server/main.go index 6f4620eed..abfeb8c73 100644 --- a/go/src/server/main.go +++ b/go/src/server/main.go @@ -225,7 +225,7 @@ func uploadNetwork(c *gin.Context) { } // TODO(gary): Make this more generic - upload to s3 for now - cmd := exec.Command("aws", "s3", "cp", network.Path, "s3://lczero/" + network.Path) + cmd := exec.Command("aws", "s3", "cp", network.Path, "s3://lczero/networks/") err = cmd.Run() if err != nil { log.Println(err.Error()) diff --git a/lc0/build.sh b/lc0/build.sh index db6edf037..de0d65973 100755 --- a/lc0/build.sh +++ b/lc0/build.sh @@ -1,7 +1,8 @@ #!/usr/bin/bash rm -fr build -CC=clang CXX=clang++ meson build --buildtype release -# CC=clang CXX=clang++ meson build --buildtype debugoptimized +CC=clang CXX=clang++ meson build --buildtype release # -Db_ndebug=true +# CC=clang CXX=clang++ meson build --buildtype debugoptimized -Db_asneeded=false +# CC=clang CXX=clang++ meson build --buildtype debug cd build ninja diff --git a/lc0/meson.build b/lc0/meson.build index 2edc81a89..c5e6eead9 100644 --- a/lc0/meson.build +++ b/lc0/meson.build @@ -1,7 +1,7 @@ -project('lc0', 'cpp', - default_options : ['c_std=c17', 'cpp_std=c++17']) +project('lc0', 'cpp') + # default_options : ['cpp_std=c++17']) -# add_global_arguments('-Wno-macro-redefined', language : 'cpp') +add_global_arguments('-std=c++17', language : 'cpp') cc = meson.get_compiler('cpp') # Installed from https://github.com/FloopCZ/tensorflow_cc @@ -33,6 +33,8 @@ files = [ 'src/chess/board.cc', 'src/neural/loader.cc', 'src/neural/network_tf.cc', + 'src/neural/network_random.cc', + 'src/neural/cache.cc', 'src/mcts/search.cc', 'src/mcts/node.cc', 'src/engine.cc', @@ -62,3 +64,8 @@ test('Network', executable('network_test', 'src/neural/network_test.cc', files, include_directories: includes, dependencies: test_deps )) + +test('HashCat', + executable('hashcat_test', 'src/utils/hashcat_test.cc', + files, include_directories: includes, dependencies: test_deps +)) diff --git a/lc0/src/chess/bitboard.cc b/lc0/src/chess/bitboard.cc index c9c020cf8..d91323e41 100644 --- a/lc0/src/chess/bitboard.cc +++ b/lc0/src/chess/bitboard.cc @@ -256,15 +256,7 @@ const Move kIdxToMove[] = { "e7f8r", "e7f8b", "f7e8q", "f7e8r", "f7e8b", "f7f8q", "f7f8r", "f7f8b", "f7g8q", "f7g8r", "f7g8b", "g7f8q", "g7f8r", "g7f8b", "g7g8q", "g7g8r", "g7g8b", "g7h8q", "g7h8r", "g7h8b", "h7g8q", "h7g8r", "h7g8b", "h7h8q", - "h7h8r", "h7h8b", "a2a1q", "a2a1r", "a2a1b", "a2b1q", "a2b1r", "a2b1b", - "b2a1q", "b2a1r", "b2a1b", "b2b1q", "b2b1r", "b2b1b", "b2c1q", "b2c1r", - "b2c1b", "c2b1q", "c2b1r", "c2b1b", "c2c1q", "c2c1r", "c2c1b", "c2d1q", - "c2d1r", "c2d1b", "d2c1q", "d2c1r", "d2c1b", "d2d1q", "d2d1r", "d2d1b", - "d2e1q", "d2e1r", "d2e1b", "e2d1q", "e2d1r", "e2d1b", "e2e1q", "e2e1r", - "e2e1b", "e2f1q", "e2f1r", "e2f1b", "f2e1q", "f2e1r", "f2e1b", "f2f1q", - "f2f1r", "f2f1b", "f2g1q", "f2g1r", "f2g1b", "g2f1q", "g2f1r", "g2f1b", - "g2g1q", "g2g1r", "g2g1b", "g2h1q", "g2h1r", "g2h1b", "h2g1q", "h2g1r", - "h2g1b", "h2h1q", "h2h1r", "h2h1b"}; + "h7h8r", "h7h8b"}; std::vector BuildMoveIndices() { std::vector res(4 * 64 * 64); diff --git a/lc0/src/chess/bitboard.h b/lc0/src/chess/bitboard.h index d1dc8cda6..6bc573e08 100644 --- a/lc0/src/chess/bitboard.h +++ b/lc0/src/chess/bitboard.h @@ -36,7 +36,7 @@ class BoardSquare { // From row(bottom to top), and col(left to right), 0-based. constexpr BoardSquare(int row, int col) : BoardSquare(row * 8 + col) {} // From Square name, e.g e4. Only lowercase. - constexpr BoardSquare(const std::string& str, bool black = false) + BoardSquare(const std::string& str, bool black = false) : BoardSquare(black ? '8' - str[1] : str[1] - '1', str[0] - 'a') {} constexpr std::uint8_t as_int() const { return square_; } void set(int row, int col) { square_ = row * 8 + col; } @@ -198,11 +198,13 @@ class Move { BoardSquare from() const { return from_; } BoardSquare to() const { return to_; } Promotion promotion() const { return promotion_; } + bool IsCastling() const { return castling_; } + void SetCastling() { castling_ = true; } // 0 .. 16384, knight promotion and no promotion is the same. uint16_t as_packed_int() const; - // 0 .. 1923, to use in neural networks. + // 0 .. 1857, to use in neural networks. uint16_t as_nn_index() const; bool operator==(const Move& other) const { @@ -219,7 +221,11 @@ class Move { } std::string as_string() const { - std::string res = from_.as_string() + to_.as_string(); + BoardSquare to = to_; + if (castling_) { + to = BoardSquare(to.row(), (to.col() == 7) ? 6 : 2); + } + std::string res = from_.as_string() + to.as_string(); switch (promotion_) { case Promotion::None: return res; @@ -238,6 +244,7 @@ class Move { BoardSquare from_; BoardSquare to_; Promotion promotion_ = Promotion::None; + bool castling_ = false; }; using MoveList = std::vector; diff --git a/lc0/src/chess/board.cc b/lc0/src/chess/board.cc index d1eb1e741..79fdfad14 100644 --- a/lc0/src/chess/board.cc +++ b/lc0/src/chess/board.cc @@ -24,6 +24,10 @@ #include #include "utils/exception.h" +#ifdef _MSC_VER +#include +#endif + namespace lczero { using std::string; @@ -199,7 +203,8 @@ MoveList ChessBoard::GeneratePseudovalidMoves() const { } } if (can_castle) { - result.emplace_back(source, BoardSquare(0, 6)); + result.emplace_back(source, BoardSquare(0, 7)); + result.back().SetCastling(); } } if (castlings_.we_can_000()) { @@ -219,7 +224,8 @@ MoveList ChessBoard::GeneratePseudovalidMoves() const { } } if (can_castle) { - result.emplace_back(source, BoardSquare(0, 2)); + result.emplace_back(source, BoardSquare(0, 0)); + result.back().SetCastling(); } } continue; @@ -303,7 +309,7 @@ MoveList ChessBoard::GeneratePseudovalidMoves() const { // Ordinary capture. result.emplace_back(source, destination); } - } else if (dst_row == 5 and pawns_.get(7, dst_col)) { + } else if (dst_row == 5 && pawns_.get(7, dst_col)) { // En passant. result.emplace_back(source, destination); } @@ -330,9 +336,9 @@ bool ChessBoard::ApplyMove(Move move) { const auto to_row = to.row(); const auto to_col = to.col(); - // Move in our pieces. + // Remove our piece from old location, but not put to destination + // (for the case of castling). our_pieces_.reset(from); - our_pieces_.set(to); // Remove captured piece bool reset_50_moves = their_pieces_.get(to); @@ -364,24 +370,33 @@ bool ChessBoard::ApplyMove(Move move) { if (from == our_king_) { castlings_.reset_we_can_00(); castlings_.reset_we_can_000(); - our_king_ = to; // Castling - if (to_col - from_col == 2) { + if (to_col - from_col > 1) { // 0-0 our_pieces_.reset(7); rooks_.reset(7); our_pieces_.set(5); rooks_.set(5); - } else if (from_col - to_col == 2) { + our_king_ = BoardSquare(0, 6); /* g8 */ + our_pieces_.set(our_king_); + } else if (from_col - to_col > 1) { // 0-0-0 our_pieces_.reset(0); rooks_.reset(0); our_pieces_.set(3); rooks_.set(3); + our_king_ = BoardSquare(0, 2); /* c8 */ + our_pieces_.set(our_king_); + } else { + our_king_ = to; + our_pieces_.set(to); } return reset_50_moves; } + // Now destination square for our piece is known. + our_pieces_.set(to); + // Promotion if (move.promotion() != Move::Promotion::None) { switch (move.promotion()) { @@ -418,7 +433,7 @@ bool ChessBoard::ApplyMove(Move move) { pawns_.reset(from); // Set en passant flag. - if (to_row - from_row == 2 and pawns_.get(to)) { + if (to_row - from_row == 2 && pawns_.get(to)) { pawns_.set(0, to_col); } return reset_50_moves; @@ -600,8 +615,13 @@ bool ChessBoard::HasMatingMaterial() const { return true; } +#ifdef _MSC_VER + int our = _mm_popcnt_u64(our_pieces_.as_int()); + int their = _mm_popcnt_u64(their_pieces_.as_int()); +#else int our = __builtin_popcountll(our_pieces_.as_int()); int their = __builtin_popcountll(their_pieces_.as_int()); +#endif if (our > 2 || their > 2) { return true; } @@ -661,7 +681,8 @@ string ChessBoard::DebugString() const { } if (i == 0) { result += " " + castlings_.as_string(); - result += flipped_ ? " (from black's eyes)" : "(from white's eyes)"; + result += flipped_ ? " (from black's eyes)" : " (from white's eyes)"; + result += " Hash: " + std::to_string(Hash()); } result += '\n'; } diff --git a/lc0/src/chess/board.h b/lc0/src/chess/board.h index 4da0f9903..48f45a6bc 100644 --- a/lc0/src/chess/board.h +++ b/lc0/src/chess/board.h @@ -20,6 +20,7 @@ #include #include "chess/bitboard.h" +#include "utils/hashcat.h" namespace lczero { @@ -59,6 +60,13 @@ class ChessBoard { // Returns a list of valid moves and board positions after the move is made. std::vector GenerateValidMoves() const; + uint64_t Hash() const { + return HashCat({our_pieces_.as_int(), their_pieces_.as_int(), + rooks_.as_int(), bishops_.as_int(), pawns_.as_int(), + our_king_.as_int(), their_king_.as_int(), + castlings_.as_int(), flipped_}); + } + class Castlings { public: void set_we_can_00() { data_ |= 1; } @@ -88,6 +96,8 @@ class ChessBoard { return result; } + uint8_t as_int() const { return data_; } + bool operator==(const Castlings& other) const { return data_ == other.data_; } diff --git a/lc0/src/chess/board_test.cc b/lc0/src/chess/board_test.cc index a6ca031a8..9b6666329 100644 --- a/lc0/src/chess/board_test.cc +++ b/lc0/src/chess/board_test.cc @@ -74,7 +74,6 @@ int Perft(const ChessBoard& board, int max_depth, bool dump = false, int count = Perft(new_board, max_depth, dump, depth + 1); if (dump && depth == 0) { Move m = move; - if (depth == 0) m.Mirror(); std::cerr << m.as_string() << ' ' << count << '\n' << new_board.DebugString(); } @@ -100,7 +99,7 @@ TEST(ChessBoard, MoveGenStartingPos) { TEST(ChessBoard, MoveGenKiwipete) { ChessBoard board; board.SetFromFen( - "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq -"); + "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - 1 1"); EXPECT_EQ(Perft(board, 1), 48); EXPECT_EQ(Perft(board, 2), 2039); @@ -111,7 +110,7 @@ TEST(ChessBoard, MoveGenKiwipete) { TEST(ChessBoard, MoveGenPosition3) { ChessBoard board; - board.SetFromFen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -"); + board.SetFromFen("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 1 1"); EXPECT_EQ(Perft(board, 1), 14); EXPECT_EQ(Perft(board, 2), 191); diff --git a/lc0/src/engine.cc b/lc0/src/engine.cc index a106daa0e..71f79d4c3 100644 --- a/lc0/src/engine.cc +++ b/lc0/src/engine.cc @@ -16,11 +16,13 @@ along with Leela Chess. If not, see . */ +#include #include #include "engine.h" #include "mcts/search.h" #include "neural/loader.h" +#include "neural/network_random.h" #include "neural/network_tf.h" namespace lczero { @@ -29,6 +31,25 @@ const int kDefaultThreads = 2; const char* kThreadsOption = "Number of worker threads"; const char* kAutoDiscover = ""; + +SearchLimits PopulateSearchLimits(int ply, bool is_black, + const GoParams& params) { + SearchLimits limits; + limits.nodes = params.nodes; + limits.time_ms = params.movetime; + int64_t time = (is_black ? params.btime : params.wtime); + if (params.infinite || time < 0) return limits; + int increment = std::max(int64_t(0), is_black ? params.binc : params.winc); + + // During first few moves policy network is mostly fine, so don't search deep. + if (ply < 4 && (limits.nodes < 0 || limits.nodes > 400)) limits.nodes = 400; + + int movestogo = params.movestogo < 0 ? 50 : params.movestogo; + limits.time_ms = (time + (increment * (movestogo - 1))) * 0.95 / movestogo; + + return limits; +} + } // namespace EngineController::EngineController(BestMoveInfo::Callback best_move_callback, @@ -45,6 +66,10 @@ void EngineController::GetUciOptions(UciOptions* options) { options->Add(std::make_unique(kThreadsOption, kDefaultThreads, 1, 128, std::function{}, "threads", 't')); + options->Add(std::make_unique( + "NNCache size", 200000, 0, 999999999, + std::bind(&EngineController::SetCacheSize, this, _1))); + Search::PopulateUciParams(options); } @@ -60,8 +85,11 @@ void EngineController::SetNetworkPath(const std::string& path) { Weights weights = LoadWeightsFromFile(net_path); // TODO Make backend selection. network_ = MakeTensorflowNetwork(weights); + // network_ = MakeRandomNetwork(); } +void EngineController::SetCacheSize(int size) { cache_.SetCapacity(size); } + void EngineController::NewGame() { SharedLock lock(busy_mutex_); search_.reset(); @@ -85,13 +113,13 @@ void EngineController::MakeMove(Move move) { new_head = node_pool_->GetNode(); current_head_->child = new_head; new_head->parent = current_head_; - new_head->board_flipped = current_head_->board; - const bool capture = new_head->board_flipped.ApplyMove(move); - new_head->board = new_head->board_flipped; + new_head->board = current_head_->board; + const bool capture = new_head->board.ApplyMove(move); new_head->board.Mirror(); new_head->ply_count = current_head_->ply_count + 1; new_head->no_capture_ply = capture ? 0 : current_head_->no_capture_ply + 1; new_head->repetitions = ComputeRepetitions(new_head); + new_head->move = move; } current_head_ = new_head; } @@ -117,8 +145,6 @@ void EngineController::SetPosition(const std::string& fen, if (!gamebegin_node_) { gamebegin_node_ = node_pool_->GetNode(); gamebegin_node_->board = starting_board; - gamebegin_node_->board_flipped = starting_board; - gamebegin_node_->board_flipped.Mirror(); gamebegin_node_->no_capture_ply = no_capture_ply; gamebegin_node_->ply_count = full_moves * 2 - (starting_board.flipped() ? 1 : 2); @@ -136,15 +162,12 @@ void EngineController::Go(const GoParams& params) { SetPosition(ChessBoard::kStartingFen, {}); } - SearchLimits limits; - limits.nodes = params.nodes; - limits.time_ms = - (current_head_->board.flipped() ? params.btime : params.wtime); - if (limits.time_ms >= 0) limits.time_ms /= 20; + auto limits = PopulateSearchLimits(current_head_->ply_count, + current_head_->board.flipped(), params); - search_ = std::make_unique(current_head_, node_pool_.get(), - network_.get(), best_move_callback_, - info_callback_, limits, uci_options_); + search_ = std::make_unique( + current_head_, node_pool_.get(), network_.get(), best_move_callback_, + info_callback_, limits, uci_options_, &cache_); search_->StartThreads(uci_options_ ? uci_options_->GetIntValue(kThreadsOption) : kDefaultThreads); diff --git a/lc0/src/engine.h b/lc0/src/engine.h index f67ef3c63..f0c16b3a8 100644 --- a/lc0/src/engine.h +++ b/lc0/src/engine.h @@ -20,6 +20,7 @@ #include #include "mcts/search.h" +#include "neural/cache.h" #include "neural/network.h" #include "uciloop.h" #include "ucioptions.h" @@ -67,6 +68,7 @@ class EngineController { // Must not block. void Stop(); void SetNetworkPath(const std::string& path); + void SetCacheSize(int size); private: void MakeMove(Move move); @@ -77,6 +79,7 @@ class EngineController { BestMoveInfo::Callback best_move_callback_; UciInfo::Callback info_callback_; + NNCache cache_; std::unique_ptr network_; // Locked means that there is some work to wait before responding readyok. rp_shared_mutex busy_mutex_; diff --git a/lc0/src/mcts/node.cc b/lc0/src/mcts/node.cc index 58ed3f9e4..b2f6bb401 100644 --- a/lc0/src/mcts/node.cc +++ b/lc0/src/mcts/node.cc @@ -18,8 +18,10 @@ #include "mcts/node.h" +#include #include #include +#include "utils/hashcat.h" namespace lczero { @@ -39,30 +41,21 @@ Node* NodePool::GetNode() { return result; } -void NodePool::AllocateNewBatch() { - allocations_.emplace_back(std::make_unique(kAllocationSize)); - for (int i = 0; i < kAllocationSize; ++i) { - pool_.push_back(allocations_.back().get() + i); - } +void NodePool::ReleaseNode(Node* node) { + std::lock_guard lock(mutex_); + pool_.push_back(node); } -void NodePool::ReleaseNode(Node* node) { pool_.push_back(node); } - void NodePool::ReleaseChildren(Node* node) { + std::lock_guard lock(mutex_); for (Node* iter = node->child; iter; iter = iter->sibling) { ReleaseSubtree(iter); } node->child = nullptr; } -void NodePool::ReleaseSubtree(Node* node) { - for (Node* iter = node->child; iter; iter = iter->sibling) { - ReleaseSubtree(iter); - ReleaseNode(iter); - } -} - void NodePool::ReleaseAllChildrenExceptOne(Node* root, Node* subtree) { + std::lock_guard lock(mutex_); Node* child = nullptr; for (Node* iter = root->child; iter; iter = iter->sibling) { if (iter == subtree) { @@ -82,6 +75,27 @@ uint64_t NodePool::GetAllocatedNodeCount() const { return kAllocationSize * allocations_.size() - pool_.size(); } +// Mutex must be hold. +void NodePool::ReleaseSubtree(Node* node) { + for (Node* iter = node->child; iter; iter = iter->sibling) { + ReleaseSubtree(iter); + pool_.push_back(iter); + } +} + +// Mutex must be hold. +void NodePool::AllocateNewBatch() { + allocations_.emplace_back(std::make_unique(kAllocationSize)); + for (int i = 0; i < kAllocationSize; ++i) { + pool_.push_back(allocations_.back().get() + i); + } +} + +uint64_t Node::BoardHash() const { + return board.Hash(); + // return HashCat({board.Hash(), no_capture_ply, repetitions}); +} + std::string Node::DebugString() const { std::ostringstream oss; oss << "Move: " << move.as_string() << "\n" diff --git a/lc0/src/mcts/node.h b/lc0/src/mcts/node.h index c2dbd1e1e..bb29e871b 100644 --- a/lc0/src/mcts/node.h +++ b/lc0/src/mcts/node.h @@ -31,9 +31,6 @@ struct Node { Move move; // The board from the point of view of the player to move. ChessBoard board; - // The board from the point of view of the opponent. Used to fill historical - // planes. - ChessBoard board_flipped; // How many half-moves without capture or pawn move was there. std::uint8_t no_capture_ply; // How many repetitions this position had before. For new positions it's 0. @@ -47,6 +44,8 @@ struct Node { uint32_t n_in_flight; // How many completed visits this node had. uint32_t n; + // Q value fetched from neural network. + float v; // Average value (from value head of neural network) of all visited nodes in // subtree. Terminal nodes (which lead to checkmate or draw) may be visited // several times, those are counted several times. q = w / n @@ -72,6 +71,7 @@ struct Node { // Pointer to a next sibling. nullptr if there are no further siblings. Node* sibling; + uint64_t BoardHash() const; std::string DebugString() const; }; @@ -88,13 +88,13 @@ class NodePool { void ReleaseAllChildrenExceptOne(Node* root, Node* subtree); // Releases all children, but doesn't release the node isself. void ReleaseChildren(Node*); - // Release all children of the node and the node itself. - void ReleaseSubtree(Node*); // Returns total number of nodes allocated. uint64_t GetAllocatedNodeCount() const; private: + // Release all children of the node and the node itself. + void ReleaseSubtree(Node*); void AllocateNewBatch(); mutable std::mutex mutex_; diff --git a/lc0/src/mcts/search.cc b/lc0/src/mcts/search.cc index e02bb805d..205057c1a 100644 --- a/lc0/src/mcts/search.cc +++ b/lc0/src/mcts/search.cc @@ -17,56 +17,55 @@ */ #include "mcts/search.h" -#include "mcts/node.h" +#include #include + +#include "mcts/node.h" +#include "neural/cache.h" #include "neural/network_tf.h" namespace lczero { namespace { -const int kDefaultMiniBatchSize = 32; +const int kDefaultMiniBatchSize = 16; const char* kMiniBatchSizeOption = "Minibatch size for NN inference"; -const int kDefaultCpuct = 170; -const char* kCpuctOption = "Cpuct MCTS option (x100)"; - -const bool kDefaultPopulateMoves = false; -const char* kPopulateMovesOption = "(oldbug) Populate movecount plane"; +const int kDefaultPrefetchBatchSize = 64; +const char* kMiniPrefetchBatchOption = "Max prefetch nodes, per NN call"; -const bool kDefaultFlipHistory = true; -const char* kFlipHistoryOption = "(oldbug) Flip opponents history"; +const bool kDefaultAggresiveCaching = false; +const char* kAggresiveCachingOption = "Try hard to find what to cache"; -const bool kDefaultFlipMove = true; -const char* kFlipMoveOption = "(oldbug) Flip black's moves"; +const int kDefaultCpuct = 170; +const char* kCpuctOption = "Cpuct MCTS option (x100)"; } // namespace void Search::PopulateUciParams(UciOptions* options) { options->Add(std::make_unique(kMiniBatchSizeOption, - kDefaultMiniBatchSize, 1, 128, + kDefaultMiniBatchSize, 1, 1024, std::function{})); - options->Add(std::make_unique(kCpuctOption, kDefaultCpuct, 0, - 9999, std::function{})); + options->Add(std::make_unique(kMiniPrefetchBatchOption, + kDefaultPrefetchBatchSize, 0, 1024, + std::function{})); - options->Add(std::make_unique(kPopulateMovesOption, - kDefaultPopulateMoves, + options->Add(std::make_unique(kAggresiveCachingOption, + kDefaultAggresiveCaching, std::function{})); - options->Add(std::make_unique( - kFlipHistoryOption, kDefaultFlipHistory, std::function{})); - - options->Add(std::make_unique(kFlipMoveOption, kDefaultFlipMove, - std::function{})); + options->Add(std::make_unique(kCpuctOption, kDefaultCpuct, 0, + 9999, std::function{})); } Search::Search(Node* root_node, NodePool* node_pool, const Network* network, BestMoveInfo::Callback best_move_callback, UciInfo::Callback info_callback, const SearchLimits& limits, - UciOptions* uci_options) + UciOptions* uci_options, NNCache* cache) : root_node_(root_node), node_pool_(node_pool), + cache_(cache), network_(network), limits_(limits), start_time_(std::chrono::steady_clock::now()), @@ -75,16 +74,120 @@ Search::Search(Node* root_node, NodePool* node_pool, const Network* network, kMiniBatchSize(uci_options ? uci_options->GetIntValue(kMiniBatchSizeOption) : kDefaultMiniBatchSize), + kMiniPrefetchBatch( + uci_options ? uci_options->GetIntValue(kMiniPrefetchBatchOption) + : kDefaultPrefetchBatchSize), + kAggresiveCaching(uci_options + ? uci_options->GetBoolValue(kAggresiveCachingOption) + : kDefaultAggresiveCaching), kCpuct((uci_options ? uci_options->GetIntValue(kCpuctOption) : kDefaultCpuct) / - 100.0f), - kPopulateMoves(uci_options - ? uci_options->GetBoolValue(kPopulateMovesOption) - : kDefaultPopulateMoves), - kFlipHistory(uci_options ? uci_options->GetBoolValue(kFlipHistoryOption) - : kDefaultFlipHistory), - kFlipMove(uci_options ? uci_options->GetBoolValue(kFlipMoveOption) - : kDefaultFlipMove) {} + 100.0f) {} + +// Returns whether node was already in cache. +bool Search::AddNodeToCompute(Node* node, CachingComputation* computation, + bool add_if_cached) { + auto hash = node->BoardHash(); + // If already in cache, no need to do anything. + if (add_if_cached) { + if (computation->AddInputByHash(hash)) return true; + } else { + if (cache_->ContainsKey(hash)) return true; + } + auto planes = EncodeNode(node); + + std::vector moves; + + if (node->child) { + // Valid moves are known, using them. + for (Node* iter = node->child; iter; iter = iter->sibling) { + moves.emplace_back(iter->move.as_nn_index()); + } + } else { + // Cache pseudovalid moves. A bit of a waste, but faster. + const auto& pseudovalid_moves = node->board.GeneratePseudovalidMoves(); + moves.reserve(pseudovalid_moves.size()); + for (const Move& m : pseudovalid_moves) { + moves.emplace_back(m.as_nn_index()); + } + } + + computation->AddInput(hash, std::move(planes), std::move(moves)); + return false; +} + +namespace { +inline float ScoreNodeU(Node* node) { + return node->p / (1 + node->n + node->n_in_flight); +} + +inline float ScoreNodeQ(Node* node) { + return (node->n ? node->q : -node->parent->q); +} +} // namespace + +// Prefetches up to @budget nodes into cache. Returns number of nodes +// prefetched. +int Search::PrefetchIntoCache(Node* node, int budget, + CachingComputation* computation) { + if (budget <= 0) return 0; + + // We are in a leaf, which is not yet being processed. + if (node->n + node->n_in_flight == 0) { + if (AddNodeToCompute(node, computation, false)) { + return kAggresiveCaching ? 0 : 1; + } + return 1; + } + + // If it's a node in progress of expansion or is terminal, not prefetching. + if (!node->child) return 0; + + // Populate all subnodes and their scores. + typedef std::pair ScoredNode; + std::vector scores; + float factor = kCpuct * std::sqrt(node->n + 1); + for (Node* iter = node->child; iter; iter = iter->sibling) { + scores.emplace_back(factor * ScoreNodeU(iter) + ScoreNodeQ(iter), iter); + } + + int first_unsorted_index = 0; + int total_budget_spent = 0; + int budget_to_spend = budget; // Initializing for the case there's only + // on child. + for (int i = 0; i < scores.size(); ++i) { + if (budget <= 0) break; + + // Sort next chunk of a vector. 3 of a time. Most of the times it's fine. + if (first_unsorted_index != scores.size() && + i + 2 >= first_unsorted_index) { + const int new_unsorted_index = std::min( + static_cast(scores.size()), + budget < 2 ? first_unsorted_index + 2 : first_unsorted_index + 3); + std::partial_sort(scores.begin() + first_unsorted_index, + scores.begin() + new_unsorted_index, scores.end()); + first_unsorted_index = new_unsorted_index; + } + + Node* n = scores[i].second; + // Last node gets the same budget as prev-to-last node. + if (i != scores.size() - 1) { + const float next_score = scores[i + 1].first; + const float q = ScoreNodeQ(n); + if (next_score > q) { + budget_to_spend = std::min( + budget, + int(n->p * factor / (next_score - q) - n->n - n->n_in_flight) + 1); + } else { + budget_to_spend = budget; + } + } + const int budget_spent = PrefetchIntoCache(n, budget_to_spend, computation); + budget -= budget_spent; + total_budget_spent += budget_spent; + } + return total_budget_spent; +} void Search::Worker() { std::vector nodes_to_process; @@ -94,10 +197,12 @@ void Search::Worker() { do { int new_nodes = 0; nodes_to_process.clear(); - auto computation = network_->NewComputation(); + auto computation = CachingComputation(network_->NewComputation(), cache_); // Gather nodes to process in the current batch. for (int i = 0; i < kMiniBatchSize; ++i) { + // If there's something to do without touching slow neural net, do it. + if (i > 0 && computation.GetCacheMisses() == 0) break; Node* node = PickNodeToExtend(root_node_); // If we hit the node that is already processed (by our batch or in // another thread) stop gathering and process smaller batch. @@ -114,26 +219,34 @@ void Search::Worker() { // If node turned out to be a terminal one, no need to send to NN for // evaluation. if (!node->is_terminal) { - auto planes = EncodeNode(node); - computation->AddInput(std::move(planes)); + AddNodeToCompute(node, &computation); } } + // If there are requests to NN, but the batch is not full, try to prefetch + // nodes which are likely useful in future. + if (computation.GetCacheMisses() > 0 && + computation.GetCacheMisses() < kMiniPrefetchBatch) { + std::shared_lock lock{nodes_mutex_}; + PrefetchIntoCache(root_node_, + kMiniPrefetchBatch - computation.GetCacheMisses(), + &computation); + } + // Evaluate nodes through NN. - if (computation->GetBatchSize() != 0) { - computation->ComputeBlocking(); + if (computation.GetBatchSize() != 0) { + computation.ComputeBlocking(); int idx_in_computation = 0; for (Node* node : nodes_to_process) { if (node->is_terminal) continue; // Populate Q value. - node->q = -computation->GetQVal(idx_in_computation); + node->v = -computation.GetQVal(idx_in_computation); // Populate P values. float total = 0.0; for (Node* n = node->child; n; n = n->sibling) { - Move m = n->move; - if (kFlipMove && node->board.flipped()) m.Mirror(); - float p = computation->GetPVal(idx_in_computation, m.as_nn_index()); + float p = + computation.GetPVal(idx_in_computation, n->move.as_nn_index()); total += p; n->p = p; } @@ -152,7 +265,7 @@ void Search::Worker() { std::unique_lock lock{nodes_mutex_}; total_nodes_ += new_nodes; for (Node* node : nodes_to_process) { - float v = node->q; + float v = node->v; // Maximum depth the node is explored. uint16_t depth = 0; // If the node is terminal, mark it as fully explored to an infinite @@ -228,9 +341,10 @@ void Search::SendUciInfo() { uci_info_.seldepth = root_node_->max_depth; uci_info_.time = GetTimeSinceStart(); uci_info_.nodes = total_nodes_; + uci_info_.hashfull = cache_->GetSize() * 1000LL / cache_->GetCapacity(); uci_info_.nps = uci_info_.time ? (uci_info_.nodes * 1000 / uci_info_.time) : 0; - uci_info_.score = -91 * log(2 / (best_move_node_->q + 1) - 1); + uci_info_.score = -191 * log(2 / (best_move_node_->q * 0.99 + 1) - 1); uci_info_.pv.clear(); for (Node* iter = best_move_node_; iter; iter = GetBestChild(iter)) { @@ -270,7 +384,9 @@ void Search::MaybeTriggerStop() { if (stop_ && !responded_bestmove_) { responded_bestmove_ = true; SendUciInfo(); - best_move_callback_(GetBestMove()); + auto best_move = GetBestMove(); + best_move_callback_({best_move.first, best_move.second}); + best_move_node_ = nullptr; } } @@ -286,30 +402,30 @@ void Search::ExtendNode(Node* node) { node->is_terminal = true; if (board.IsUnderCheck()) { // Checkmate. - node->q = 1.0f; + node->v = 1.0f; } else { // Stalemate. - node->q = 0.0f; + node->v = 0.0f; } return; } if (!board.HasMatingMaterial()) { node->is_terminal = true; - node->q = 0.0f; + node->v = 0.0f; return; } if (node->no_capture_ply >= 100) { node->is_terminal = true; - node->q = 0.0f; + node->v = 0.0f; return; } node->repetitions = ComputeRepetitions(node); if (node->repetitions >= 2) { node->is_terminal = true; - node->q = 0.0f; + node->v = 0.0f; return; } @@ -326,7 +442,6 @@ void Search::ExtendNode(Node* node) { } new_node->move = move.move; - new_node->board_flipped = move.board; new_node->board = move.board; new_node->board.Mirror(); new_node->no_capture_ply = @@ -362,10 +477,9 @@ Node* Search::PickNodeToExtend(Node* node) { float factor = kCpuct * std::sqrt(node->n + 1); float best = -100.0f; for (Node* iter = node->child; iter; iter = iter->sibling) { - const float u = factor * iter->p / (1 + iter->n + iter->n_in_flight); - const float v = u + iter->q; - if (v > best) { - best = v; + const float score = factor * ScoreNodeU(iter) + ScoreNodeQ(iter); + if (score > best) { + best = score; node = iter; } } @@ -374,7 +488,8 @@ Node* Search::PickNodeToExtend(Node* node) { InputPlanes Search::EncodeNode(const Node* node) { const int kMoveHistory = 8; - const int kAuxPlaneBase = 14 * kMoveHistory; + const int planesPerBoard = 13; + const int kAuxPlaneBase = planesPerBoard * kMoveHistory; InputPlanes result(kAuxPlaneBase + 8); @@ -383,11 +498,10 @@ InputPlanes Search::EncodeNode(const Node* node) { for (int i = 0; i < kMoveHistory; ++i, flip = !flip) { if (!node) break; - ChessBoard board = flip ? node->board_flipped : node->board; + ChessBoard board = node->board; + if (flip) board.Mirror(); - if (kFlipHistory && i % 2 == 1) board.Mirror(); - - const int base = i * 14; + const int base = i * planesPerBoard; if (i == 0) { if (board.castlings().we_can_000()) result[kAuxPlaneBase + 0].SetAll(); if (board.castlings().we_can_00()) result[kAuxPlaneBase + 1].SetAll(); @@ -395,7 +509,6 @@ InputPlanes Search::EncodeNode(const Node* node) { if (board.castlings().they_can_00()) result[kAuxPlaneBase + 3].SetAll(); if (we_are_black) result[kAuxPlaneBase + 4].SetAll(); result[kAuxPlaneBase + 5].Fill(node->no_capture_ply); - if (kPopulateMoves) result[kAuxPlaneBase + 6].Fill(node->ply_count % 256); } result[base + 0].mask = (board.ours() * board.pawns()).as_int(); @@ -414,7 +527,6 @@ InputPlanes Search::EncodeNode(const Node* node) { const int repetitions = node->repetitions; if (repetitions >= 1) result[base + 12].SetAll(); - if (repetitions >= 2) result[base + 13].SetAll(); node = node->parent; } @@ -422,12 +534,18 @@ InputPlanes Search::EncodeNode(const Node* node) { return result; } -Move Search::GetBestMove() const { +std::pair Search::GetBestMove() const { std::shared_lock lock(nodes_mutex_); Node* best_node = GetBestChild(root_node_); Move move = best_node->move; if (!best_node->board.flipped()) move.Mirror(); - return move; + + Move ponder_move; + if (best_node->child) { + ponder_move = GetBestChild(best_node)->move; + if (best_node->board.flipped()) ponder_move.Mirror(); + } + return {move, ponder_move}; } void Search::StartThreads(int how_many) { diff --git a/lc0/src/mcts/search.h b/lc0/src/mcts/search.h index bad491872..ad5b354aa 100644 --- a/lc0/src/mcts/search.h +++ b/lc0/src/mcts/search.h @@ -22,6 +22,7 @@ #include #include #include "mcts/node.h" +#include "neural/cache.h" #include "neural/network.h" #include "uciloop.h" #include "ucioptions.h" @@ -38,7 +39,7 @@ class Search { Search(Node* root_node, NodePool* node_pool, const Network* network, BestMoveInfo::Callback best_move_callback, UciInfo::Callback info_callback, const SearchLimits& limits, - UciOptions* uci_options); + UciOptions* uci_options, NNCache* cache); ~Search(); @@ -56,8 +57,8 @@ class Search { // Aborts the search, and blocks until all worker thread finish. void AbortAndWait(); - // Returns best move, from the point of view of white player. - Move GetBestMove() const; + // Returns best move, from the point of view of white player. And also ponder. + std::pair GetBestMove() const; private: // Can run several copies of it in separate threads. @@ -66,6 +67,10 @@ class Search { uint64_t GetTimeSinceStart() const; void MaybeTriggerStop(); void MaybeOutputInfo(); + bool AddNodeToCompute(Node* node, CachingComputation* computation, + bool add_if_cached = true); + int PrefetchIntoCache(Node* node, int budget, + CachingComputation* computation); void SendUciInfo(); // Requires nodes_mutex_ to be held. @@ -80,6 +85,7 @@ class Search { Node* root_node_; NodePool* node_pool_; + NNCache* cache_; mutable std::shared_mutex nodes_mutex_; const Network* network_; @@ -95,10 +101,9 @@ class Search { // External parameters. const int kMiniBatchSize; + const int kMiniPrefetchBatch; + const bool kAggresiveCaching; const float kCpuct; - const bool kPopulateMoves; - const bool kFlipHistory; - const bool kFlipMove; }; } // namespace lczero \ No newline at end of file diff --git a/lc0/src/neural/cache.cc b/lc0/src/neural/cache.cc new file mode 100644 index 000000000..9caecb05d --- /dev/null +++ b/lc0/src/neural/cache.cc @@ -0,0 +1,99 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ +#include "neural/cache.h" +#include +#include + +namespace lczero { +CachingComputation::CachingComputation( + std::unique_ptr parent, NNCache* cache) + : parent_(std::move(parent)), cache_(cache) {} + +int CachingComputation::GetCacheMisses() const { + return parent_->GetBatchSize(); +} + +int CachingComputation::GetBatchSize() const { return batch_.size(); } + +bool CachingComputation::AddInputByHash(uint64_t hash) { + NNCacheLock lock(cache_, hash); + if (!lock) return false; + batch_.emplace_back(); + batch_.back().lock = std::move(lock); + batch_.back().hash = hash; + return true; +} + +void CachingComputation::AddInput( + uint64_t hash, InputPlanes&& input, + std::vector&& probabilities_to_cache) { + if (AddInputByHash(hash)) return; + batch_.emplace_back(); + batch_.back().hash = hash; + batch_.back().idx_in_parent = parent_->GetBatchSize(); + batch_.back().probabilities_to_cache = probabilities_to_cache; + parent_->AddInput(std::move(input)); +} + +void CachingComputation::PopLastInputHit() { + assert(!batch_.empty()); + assert(batch_.back().idx_in_parent == -1); + batch_.pop_back(); +} + +void CachingComputation::ComputeBlocking() { + if (parent_->GetBatchSize() == 0) return; + parent_->ComputeBlocking(); + + // Fill cache with data from NN. + for (const auto& item : batch_) { + if (item.idx_in_parent == -1) continue; + auto req = std::make_unique(); + req->q = parent_->GetQVal(item.idx_in_parent); + for (auto x : item.probabilities_to_cache) { + req->p.emplace_back(x, parent_->GetPVal(item.idx_in_parent, x)); + } + cache_->Insert(item.hash, std::move(req)); + } +} + +float CachingComputation::GetQVal(int sample) const { + const auto& item = batch_[sample]; + if (item.idx_in_parent >= 0) return parent_->GetQVal(item.idx_in_parent); + return item.lock->q; +} + +float CachingComputation::GetPVal(int sample, int move_id) const { + auto& item = batch_[sample]; + if (item.idx_in_parent >= 0) + return parent_->GetPVal(item.idx_in_parent, move_id); + const auto& moves = item.lock->p; + + int total_count = 0; + while (total_count < moves.size()) { + // Optimization: usually moves are stored in the same order as queried. + const auto& move = moves[item.last_idx++]; + if (move.first == move_id) return move.second; + if (item.last_idx == moves.size()) item.last_idx = 0; + ++total_count; + } + assert(false); // Move not found. + return 0; +} + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/neural/cache.h b/lc0/src/neural/cache.h new file mode 100644 index 000000000..68f7821af --- /dev/null +++ b/lc0/src/neural/cache.h @@ -0,0 +1,79 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ +#pragma once + +#include "neural/network.h" +#include "utils/cache.h" + +namespace lczero { + +struct CachedNNRequest { + typedef std::pair IdxAndProb; + float q; + std::vector p; +}; + +typedef LruCache NNCache; +typedef LruCacheLock NNCacheLock; + +// Wraps around NetworkComputation and caches result. +// While it mostly repeats NetworkComputation interface, it's not derived +// from it, as AddInput() needs hash and index of probabilities to store. +class CachingComputation { + public: + CachingComputation(std::unique_ptr parent, + NNCache* cache); + + // How many inputs are not found in cache and will be forwarded to a wrapped + // computation. + int GetCacheMisses() const; + // Total number of timea AddInput/AddInputByHash were (successfully) called. + int GetBatchSize() const; + // Adds input by hash only. If that hash is not in cache, returns false + // and does nothing. Otherwise adds. + bool AddInputByHash(uint64_t hash); + // Adds a sample to the batch. + // @hash is a hash to store/lookup it in the cache. + // @probabilities_to_cache is which indices of policy head to store. + void AddInput(uint64_t hash, InputPlanes&& input, + std::vector&& probabilities_to_cache); + // Undos last AddInput. If it was a cache miss, the it's actually not removed + // from parent's batch. + void PopLastInputHit(); + // Do the computation. + void ComputeBlocking(); + // Returns Q value of @sample. + float GetQVal(int sample) const; + // Returns P value @move_id of @sample. + float GetPVal(int sample, int move_id) const; + + private: + struct WorkItem { + uint64_t hash; + NNCacheLock lock; + int idx_in_parent = -1; + std::vector probabilities_to_cache; + mutable int last_idx = 0; + }; + + std::unique_ptr parent_; + NNCache* cache_; + std::vector batch_; +}; + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/neural/loader.cc b/lc0/src/neural/loader.cc index 65c243129..cb70fa6ca 100644 --- a/lc0/src/neural/loader.cc +++ b/lc0/src/neural/loader.cc @@ -70,7 +70,7 @@ Weights LoadWeightsFromFile(const std::string& filename) { if (vecs.size() <= 19) throw Exception("Weithts file " + filename + " should have at least 19 lines"); - if (vecs[0][0] != 1) throw Exception("Weights version 1 expected"); + if (vecs[0][0] != 2) throw Exception("Weights version 2 expected"); Weights result; // Populating backwards. @@ -112,11 +112,11 @@ std::string DiscoveryWeightsFile(const std::string& binary_name) { } std::vector> candidates; - for (const auto& file : recursive_directory_iterator( - path, directory_options::skip_permission_denied)) { + for (const auto& file : recursive_directory_iterator(path)) { if (!is_regular_file(file.path())) continue; if (file_size(file.path()) < kMinFileSize) continue; - candidates.emplace_back(last_write_time(file.path()), file.path()); + candidates.emplace_back(last_write_time(file.path()), + file.path().generic_u8string()); } std::sort(candidates.rbegin(), candidates.rend()); @@ -125,7 +125,7 @@ std::string DiscoveryWeightsFile(const std::string& binary_name) { std::ifstream file(candidate.second.c_str()); int val = 0; file >> val; - if (!file.fail() && val == 1) { + if (!file.fail() && val == 2) { std::cerr << "Found network file: " << candidate.second << std::endl; return candidate.second; } diff --git a/lc0/src/neural/network.h b/lc0/src/neural/network.h index 0c7a21705..4969aff70 100644 --- a/lc0/src/neural/network.h +++ b/lc0/src/neural/network.h @@ -23,7 +23,7 @@ namespace lczero { -const int kInputPlanes = 120; +const int kInputPlanes = 112; struct Weights { using Vec = std::vector; diff --git a/lc0/src/neural/network_random.cc b/lc0/src/neural/network_random.cc new file mode 100644 index 000000000..5b43a2c63 --- /dev/null +++ b/lc0/src/neural/network_random.cc @@ -0,0 +1,62 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ +#include "neural/network_random.h" + +#include +#include "utils/hashcat.h" + +namespace lczero { + +class RandomNetworkComputation : public NetworkComputation { + public: + RandomNetworkComputation() {} + void AddInput(InputPlanes&& input) override { + std::uint64_t hash = 0; + for (const auto& plane : input) { + hash = HashCat({hash, plane.mask}); + } + inputs_.push_back(hash); + } + void ComputeBlocking() override { return; } + + int GetBatchSize() const override { return inputs_.size(); } + float GetQVal(int sample) const override { + return (int(inputs_[sample] % 200000) - 100000) / 100000.0; + } + float GetPVal(int sample, int move_id) const override { + return (HashCat({inputs_[sample], static_cast(move_id)}) % + 10000) / + 10000.0; + } + + private: + std::vector inputs_; +}; + +class RandomNetwork : public Network { + public: + std::unique_ptr NewComputation() const override { + return std::make_unique(); + } +}; + +std::unique_ptr MakeRandomNetwork() { + return std::make_unique(); +} + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/neural/network_random.h b/lc0/src/neural/network_random.h new file mode 100644 index 000000000..323ed2550 --- /dev/null +++ b/lc0/src/neural/network_random.h @@ -0,0 +1,28 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ + +#pragma once + +#include "neural/network.h" + +namespace lczero { + +// Creates a network which just returns random values (for plain nps testing). +std::unique_ptr MakeRandomNetwork(); + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/neural/network_test.cc b/lc0/src/neural/network_test.cc index 0aa53fc82..dc5545943 100644 --- a/lc0/src/neural/network_test.cc +++ b/lc0/src/neural/network_test.cc @@ -29,8 +29,8 @@ TEST(Network, FakeData) { auto network = MakeTensorflowNetwork(weights); auto compute = network->NewComputation(); for (int j = 0; j < 4; ++j) { - InputPlanes planes(120); - for (int i = 0; i < 120; ++i) { + InputPlanes planes(kInputPlanes); + for (int i = 0; i < kInputPlanes; ++i) { planes[i].mask = 0x230709012008ull; } compute->AddInput(std::move(planes)); diff --git a/lc0/src/neural/network_tf.cc b/lc0/src/neural/network_tf.cc index 01fdcc75d..0bcd7b0e9 100644 --- a/lc0/src/neural/network_tf.cc +++ b/lc0/src/neural/network_tf.cc @@ -97,10 +97,11 @@ Output MakeResidualBlock(const Scope& scope, Input input, int channels, std::pair MakeNetwork(const Scope& scope, Input input, const Weights& weights) { - const int filters = weights.input.weights.size() / 120 / 9; + const int filters = weights.input.weights.size() / kInputPlanes / 9; // Input convolution. - auto flow = MakeConvBlock(scope, input, 3, 120, filters, weights.input); + auto flow = + MakeConvBlock(scope, input, 3, kInputPlanes, filters, weights.input); // Residual tower for (const auto& block : weights.residual) { @@ -110,8 +111,8 @@ std::pair MakeNetwork(const Scope& scope, Input input, // Policy head auto conv_pol = MakeConvBlock(scope, flow, 1, filters, 32, weights.policy); conv_pol = Reshape(scope, conv_pol, Const(scope, {-1, 32 * 8 * 8})); - auto ip_pol_w = MakeConst(scope, {32 * 8 * 8, 1924}, weights.ip_pol_w); - auto ip_pol_b = MakeConst(scope, {1924}, weights.ip_pol_b); + auto ip_pol_w = MakeConst(scope, {32 * 8 * 8, 1858}, weights.ip_pol_w); + auto ip_pol_b = MakeConst(scope, {1858}, weights.ip_pol_b); auto policy_fc = Add(scope, MatMul(scope, conv_pol, ip_pol_w), ip_pol_b); auto policy_head = Softmax(scope, policy_fc); diff --git a/lc0/src/uciloop.cc b/lc0/src/uciloop.cc index 08935297c..66bd23b73 100644 --- a/lc0/src/uciloop.cc +++ b/lc0/src/uciloop.cc @@ -48,7 +48,8 @@ void SendInfo(const UciInfo& info) { if (info.seldepth >= 0) res += " seldepth " + std::to_string(info.seldepth); if (info.time >= 0) res += " time " + std::to_string(info.time); if (info.nodes >= 0) res += " nodes " + std::to_string(info.nodes); - if (info.score) res += " score cp " + std::to_string(info.score.value()); + if (info.score) res += " score cp " + std::to_string(*info.score); + if (info.hashfull >= 0) res += " hashfull " + std::to_string(info.hashfull); if (info.nps >= 0) res += " nps " + std::to_string(info.nps); if (!info.pv.empty()) { diff --git a/lc0/src/uciloop.h b/lc0/src/uciloop.h index 5b81f8486..40f68e27b 100644 --- a/lc0/src/uciloop.h +++ b/lc0/src/uciloop.h @@ -19,7 +19,12 @@ #pragma once #include +#ifdef _MSC_VER +#include "utils/optional.h" +#else #include +using std::optional; +#endif #include #include #include "chess/bitboard.h" @@ -30,7 +35,8 @@ namespace lczero { void UciLoop(int argc, const char** argv); struct BestMoveInfo { - BestMoveInfo(Move bestmove) : bestmove(bestmove) {} + BestMoveInfo(Move bestmove, Move ponder = Move{}) + : bestmove(bestmove), ponder(ponder) {} Move bestmove; Move ponder; using Callback = std::function; @@ -47,8 +53,10 @@ struct UciInfo { int64_t nodes = -1; // Nodes per second. int nps = -1; + // Hash fullness * 1000 + int hashfull = -1; // Win in centipawns. - std::optional score; + optional score; // Best line found. Moves are from perspective of white player. std::vector pv; // Freeform comment. diff --git a/lc0/src/utils/cache-old.h b/lc0/src/utils/cache-old.h new file mode 100644 index 000000000..da04688ec --- /dev/null +++ b/lc0/src/utils/cache-old.h @@ -0,0 +1,189 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include +#include +#include "utils/hashcat.h" + +namespace lczero { + +// Generic LRU cache. Thread-safe. +template +class LruCache { + public: + LruCache(int capacity = 128) : capacity_(capacity) {} + + // Inserts the element under key @key with value @val. + // If the element is pinned, old value is still kept (until fully unpinned), + // but new lookups will return updated value. + // If @pinned, pins inserted element, Unpin has to be called to unpin. + // In any case, puts element to front of the queue (makes it last to evict). + V* Insert(K key, std::unique_ptr val, bool pinned = false) { + std::lock_guard lock(mutex_); + V* new_val = val.get(); + auto iter = lookup_.find(key); + if (iter != lookup_.end()) { + auto list_iter = iter->second; + MakePending(list_iter); + list_iter->pins = pinned ? 1 : 0; + list_iter->value = std::move(val); + BringToFront(list_iter); + } else { + MaybeCleanup(capacity_ - 1); + lru_.emplace_front(key, std::move(val), pinned ? 1 : 0); + lookup_[key] = lru_.begin(); + } + return new_val; + } + + // Looks up and pins the element by key. Returns nullptr if not found. + // If found, brings the element to the head of the queue (makes it last to + // evict). + V* Lookup(K key) { + std::lock_guard lock(mutex_); + auto iter = lookup_.find(key); + if (iter == lookup_.end()) return nullptr; + ++iter->second->pins; + return iter->second->value.get(); + } + + // Checks whether a key exists. Doesn't lock. Of course the next moment the + // key may be evicted. + bool ContainsKey(K key) { + std::lock_guard lock(mutex_); + auto iter = lookup_.find(key); + return iter != lookup_.end(); + } + + // Unpins the element given key and value. + void Unpin(K key, V* value) { + std::lock_guard lock(mutex_); + auto pending_iter = pending_pins_.find({key, value}); + if (pending_iter != pending_pins_.end()) { + if (--pending_iter->second.pins == 0) pending_pins_.erase(pending_iter); + return; + } + auto iter = lookup_.find(key); + --iter->second->pins; + } + + // Sets the capacity of the cache. If new capacity is less than current size + // of the cache, oldest entries are evicted. + void SetCapacity(int capacity) { + std::lock_guard lock(mutex_); + capacity_ = capacity; + MaybeCleanup(capacity); + } + + size_t GetSize() const { return lru_.size() + pending_pins_.size(); } + size_t GetCapacity() const { return capacity_; } + + private: + // Mutex should be locked when calling this function. + void MaybeCleanup(int size) { + if (size >= lookup_.size()) return; + int to_delete = lookup_.size() - size; + auto iter = std::prev(lru_.end()); + for (int i = 0; i < to_delete; ++i) { + lookup_.erase(iter->key); + MakePending(iter); + iter = lru_.erase(iter); + --iter; + } + } + + struct Item { + Item(K key, std::unique_ptr value, int pins) + : key(key), value(std::move(value)), pins(pins) {} + K key; + std::unique_ptr value; + int pins; + }; + + void MakePending(typename std::list::iterator iter) { + if (iter->pins > 0) { + K key = iter->key; + V* val = iter->value.get(); + int pins = iter->pins; + pending_pins_.emplace(std::make_pair(key, val), + Item{key, std::move(iter->value), pins}); + } + } + + void BringToFront(typename std::list::iterator iter) { + if (iter != lru_.begin()) + lru_.splice(lru_.begin(), lru_, iter, std::next(iter)); + } + // Fresh in front, stale on back. + int capacity_; + std::list lru_; + using ListIter = typename std::list::iterator; + std::unordered_map lookup_; + + struct PairHash { + std::size_t operator()(const std::pair& p) const { + return HashCat({std::hash{}(p.second), std::hash{}(p.first)}); + } + }; + std::unordered_map, Item, PairHash> pending_pins_; + std::mutex mutex_; +}; + +template +class LruCacheLock { + public: + // Looks up the value in @cache by @key and pins it if found. + LruCacheLock(LruCache* cache, K key) + : cache_(cache), key_(key), value_(cache->Lookup(key_)) {} + + // Unpins the cache entry (if holds). + ~LruCacheLock() { + if (value_) cache_->Unpin(key_, value_); + } + + // Returns whether lock holds any value. + operator bool() const { return value_; } + + // Gets the value. + V* operator->() const { return value_; } + V* operator*() const { return value_; } + + LruCacheLock() {} + LruCacheLock(LruCacheLock&& other) + : cache_(other.cache_), key_(other.key_), value_(other.value_) { + other.value_ = nullptr; + } + void operator=(LruCacheLock&& other) { + cache_ = other.cache_; + key_ = other.key_; + value_ = other.value_; + other.value_ = nullptr; + } + + private: + LruCache* cache_ = nullptr; + K key_; + V* value_ = nullptr; +}; + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/utils/cache.h b/lc0/src/utils/cache.h new file mode 100644 index 000000000..384abf905 --- /dev/null +++ b/lc0/src/utils/cache.h @@ -0,0 +1,305 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ + +#pragma once + +#include +#include +#include +#include +#include + +namespace lczero { + +// Generic LRU cache. Thread-safe. +template +class LruCache { + static const double constexpr kLoadFactor = 1.33; + + public: + LruCache(int capacity = 128) + : capacity_(capacity), hash_(capacity * kLoadFactor) { + std::memset(&hash_[0], 0, sizeof(hash_[0]) * hash_.size()); + } + + ~LruCache() { + ShrinkToCapacity(0); + assert(size_ == 0); + assert(allocated_ == 0); + } + + // Inserts the element under key @key with value @val. + // If the element is pinned, old value is still kept (until fully unpinned), + // but new lookups will return updated value. + // If @pinned, pins inserted element, Unpin has to be called to unpin. + // In any case, puts element to front of the queue (makes it last to evict). + V* Insert(K key, std::unique_ptr val, bool pinned = false) { + std::lock_guard lock(mutex_); + + auto hash = hasher_(key) % hash_.size(); + auto& hash_head = hash_[hash]; + for (Item* iter = hash_head; iter; iter = iter->next_in_hash) { + if (key == iter->key) { + EvictItem(iter); + break; + } + } + + ShrinkToCapacity(capacity_ - 1); + ++size_; + ++allocated_; + Item* new_item = new Item(key, std::move(val), pinned ? 1 : 0); + new_item->next_in_hash = hash_head; + hash_head = new_item; + InsertIntoLru(new_item); + return new_item->value.get(); + } + + // Checks whether a key exists. Doesn't lock. Of course the next moment the + // key may be evicted. + bool ContainsKey(K key) { + std::lock_guard lock(mutex_); + auto hash = hasher_(key) % hash_.size(); + for (Item* iter = hash_[hash]; iter; iter = iter->next_in_hash) { + if (key == iter->key) return true; + } + return false; + } + + // Looks up and pins the element by key. Returns nullptr if not found. + // If found, brings the element to the head of the queue (makes it last to + // evict). + V* Lookup(K key) { + std::lock_guard lock(mutex_); + + auto hash = hasher_(key) % hash_.size(); + for (Item* iter = hash_[hash]; iter; iter = iter->next_in_hash) { + if (key == iter->key) { + // BringToFront(iter); + ++iter->pins; + return iter->value.get(); + } + } + return nullptr; + } + + // Unpins the element given key and value. + void Unpin(K key, V* value) { + std::lock_guard lock(mutex_); + + // Checking evicted list first. + Item** cur = &evicted_head_; + for (Item* el = evicted_head_; el; el = el->next_in_hash) { + if (key == el->key && value == el->value.get()) { + if (--el->pins == 0) { + *cur = el->next_in_hash; + --allocated_; + delete el; + } + return; + } + cur = &el->next_in_hash; + } + + // Now lookup in actve list. + auto hash = hasher_(key) % hash_.size(); + for (Item* iter = hash_[hash]; iter; iter = iter->next_in_hash) { + if (key == iter->key && value == iter->value.get()) { + assert(iter->pins > 0); + --iter->pins; + return; + } + } + assert(false); + } + + // Sets the capacity of the cache. If new capacity is less than current size + // of the cache, oldest entries are evicted. In any case the hashtable is + // rehashed. + void SetCapacity(int capacity) { + std::lock_guard lock(mutex_); + + if (capacity_ == capacity) return; + ShrinkToCapacity(capacity); + capacity_ = capacity; + + std::vector new_hash(capacity * kLoadFactor); + std::memset(&new_hash[0], 0, sizeof(new_hash[0]) * new_hash.size()); + + if (size_ != 0) { + for (Item* head : hash_) { + for (Item* iter = head; head; head = head->next_in_hash) { + auto& new_hash_head = new_hash[hasher_(iter->key) % new_hash.size()]; + iter->next_in_hash = new_hash_head; + new_hash_head = iter; + } + } + } + hash_.swap(new_hash); + } + + size_t GetSize() const { return size_ + allocated_; } + size_t GetCapacity() const { return capacity_; } + + private: + struct Item { + Item(K key, std::unique_ptr value, int pins) + : key(key), value(std::move(value)), pins(pins) {} + K key; + std::unique_ptr value; + int pins = 0; + Item* next_in_hash = nullptr; + Item* prev_in_queue = nullptr; + Item* next_in_queue = nullptr; + + /* std::string DebugString() const { + std::ostringstream oss; + oss << "this:" << this; + oss << " key:" << key; + oss << " val:" << value.get(); + oss << " pins:" << pins; + oss << " evicted:" << evicted; + oss << " next_in_hash:" << next_in_hash; + oss << " prev_in_queue:" << prev_in_queue; + oss << " next_in_queue:" << next_in_queue; + return oss.str(); + } */ + }; + + // All private functions require mutex to be locked. + void EvictItem(Item* iter) { + --size_; + + // Remove from LRU list. + if (lru_head_ == iter) { + lru_head_ = iter->next_in_queue; + } else { + iter->prev_in_queue->next_in_queue = iter->next_in_queue; + } + if (lru_tail_ == iter) { + lru_tail_ = iter->prev_in_queue; + } else { + iter->next_in_queue->prev_in_queue = iter->prev_in_queue; + } + + // Destroy or move into evicted list dependending on whether it's pinned. + Item** cur = &hash_[hasher_(iter->key) % hash_.size()]; + for (Item* el = *cur; el; el = el->next_in_hash) { + if (el == iter) { + *cur = el->next_in_hash; + if (el->pins == 0) { + --allocated_; + delete el; + } else { + el->next_in_hash = evicted_head_; + evicted_head_ = el; + } + return; + } + cur = &el->next_in_hash; + } + + assert(false); + } + + void ShrinkToCapacity(int capacity) { + while (lru_tail_ && size_ > capacity) { + EvictItem(lru_tail_); + } + } + + void BringToFront(Item* iter) { + if (lru_head_ == iter) { + return; + } else { + iter->prev_in_queue->next_in_queue = iter->next_in_queue; + } + if (lru_tail_ == iter) { + lru_tail_ = iter->prev_in_queue; + } else { + iter->next_in_queue->prev_in_queue = iter->prev_in_queue; + } + + InsertIntoLru(iter); + } + + void InsertIntoLru(Item* iter) { + iter->next_in_queue = lru_head_; + iter->prev_in_queue = nullptr; + + if (lru_head_) { + lru_head_->prev_in_queue = iter; + } + lru_head_ = iter; + if (lru_tail_ == nullptr) { + lru_tail_ = iter; + } + } + + // Fresh in front, stale on back. + int capacity_; + int size_ = 0; + int allocated_ = 0; + Item* lru_head_ = nullptr; // Newest elements. + Item* lru_tail_ = nullptr; // Oldest elements. + Item* evicted_head_ = nullptr; // Evicted but pinned elements. + std::vector hash_; + std::hash hasher_; + + std::mutex mutex_; +}; + +// Convenience class for pinning cache items. +template +class LruCacheLock { + public: + // Looks up the value in @cache by @key and pins it if found. + LruCacheLock(LruCache* cache, K key) + : cache_(cache), key_(key), value_(cache->Lookup(key_)) {} + + // Unpins the cache entry (if holds). + ~LruCacheLock() { + if (value_) cache_->Unpin(key_, value_); + } + + // Returns whether lock holds any value. + operator bool() const { return value_; } + + // Gets the value. + V* operator->() const { return value_; } + V* operator*() const { return value_; } + + LruCacheLock() {} + LruCacheLock(LruCacheLock&& other) + : cache_(other.cache_), key_(other.key_), value_(other.value_) { + other.value_ = nullptr; + } + void operator=(LruCacheLock&& other) { + cache_ = other.cache_; + key_ = other.key_; + value_ = other.value_; + other.value_ = nullptr; + } + + private: + LruCache* cache_ = nullptr; + K key_; + V* value_ = nullptr; +}; + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/utils/hashcat.h b/lc0/src/utils/hashcat.h new file mode 100644 index 000000000..fdbac5f35 --- /dev/null +++ b/lc0/src/utils/hashcat.h @@ -0,0 +1,40 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ + +#include +#include + +#pragma once +namespace lczero { + +// Tries to scramble @val. +inline uint64_t Hash(uint64_t val) { + return 0xfad0d7f2fbb059f1ULL * (val + 0xbaad41cdcb839961ULL) + + 0x7acec0050bf82f43ULL * ((val >> 31) + 0xd571b3a92b1b2755ULL); +} + +// Combines 64-bit hashes into one. +inline uint64_t HashCat(std::initializer_list args) { + uint64_t hash = 0; + for (uint64_t x : args) { + hash ^= 0x299799adf0d95defULL + Hash(x) + (hash << 6) + (hash >> 2); + } + return hash; +} + +} // namespace lczero diff --git a/lc0/src/utils/hashcat_test.cc b/lc0/src/utils/hashcat_test.cc new file mode 100644 index 000000000..58a98293d --- /dev/null +++ b/lc0/src/utils/hashcat_test.cc @@ -0,0 +1,37 @@ +/* + This file is part of Leela Chess Zero. + Copyright (C) 2018 The LCZero Authors + + Leela Chess is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Leela Chess is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Leela Chess. If not, see . +*/ + +#include "utils/hashcat.h" +#include + +namespace lczero { + +TEST(HashCat, TestCollision) { + uint64_t hash1 = HashCat({0x8000000010500000, 0x4000080000002000, + 0x8000000000002000, 0x4000000000000000}); + uint64_t hash2 = HashCat({0x4000000010500000, 0x1000080000002000, + 0x4000000000002000, 0x1000000000000000}); + EXPECT_NE(hash1, hash2); +} + +} // namespace lczero + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +} \ No newline at end of file diff --git a/src/Makefile b/src/Makefile index d0368635c..fad0b6cd4 100644 --- a/src/Makefile +++ b/src/Makefile @@ -51,7 +51,7 @@ CPPFLAGS += -MD -MP sources = Network.cpp Training.cpp UCTSearch.cpp Utils.cpp Random.cpp Parameters.cpp \ UCTNode.cpp OpenCL.cpp Timing.cpp main.cpp Tuner.cpp \ Bitboard.cpp Movegen.cpp Position.cpp UCI.cpp pgn.cpp SMP.cpp OpenCLScheduler.cpp \ - NNCache.cpp TimeMan.cpp + NNCache.cpp TimeMan.cpp UCIOption.cpp objects = $(sources:.cpp=.o) deps = $(sources:%.cpp=%.d) diff --git a/src/Network.cpp b/src/Network.cpp index 05fb9c2f1..8a22f0bf3 100644 --- a/src/Network.cpp +++ b/src/Network.cpp @@ -263,15 +263,15 @@ std::pair Network::load_network(std::istream& wtfile) { while (std::getline(wtfile, line)) { std::vector weights; float weight; - std::istringstream iss(line); + char * fz = &line[0]; bool ok = true; - for (; iss;) { - if (iss >> weight) { - weights.emplace_back(weight); - } else if (!iss.eof()) { - ok = false; - break; + for (; *fz != '\0';) { + char * tmp = fz; + weight = strtof(fz, &fz); // if the read fails, fz is unchanged and weight is 0.0F. + if (weight == 0.0F && tmp == fz) { + ok = false; break; } + weights.emplace_back(weight); } if (!ok) { myprintf("\nFailed to parse weight file. Error on line %d.\n", @@ -575,10 +575,16 @@ int Network::lookup(Move move, Color c) { if (c == WHITE) { return new_move_lookup.at(move); } else { + Move flipped_move; + if (type_of(move) == PROMOTION) { + flipped_move = make(~from_sq(move), ~to_sq(move), promotion_type(move)); + } else { + flipped_move = make_move(~from_sq(move), ~to_sq(move)); + } // The NN plays BLACK with the board flipped vertically, // And outputs the moves as if BLACK is moving up. // Flip the policy so the moves are normal. - return new_move_lookup.at(make_move(~from_sq(move), ~to_sq(move))); + return new_move_lookup.at(flipped_move); } } } diff --git a/src/Parameters.cpp b/src/Parameters.cpp index b548dc110..7c69f5e6d 100644 --- a/src/Parameters.cpp +++ b/src/Parameters.cpp @@ -51,6 +51,7 @@ int cfg_resignpct; int cfg_noise; int cfg_randomize; int cfg_timemanage; +int cfg_slowmover; int cfg_min_resign_moves; int cfg_root_temp_decay; uint64_t cfg_rng_seed; @@ -68,6 +69,7 @@ std::string cfg_logfile; std::string cfg_supervise; FILE* cfg_logfile_handle; bool cfg_quiet; +bool cfg_go_nodes_as_visits; void Parameters::setup_default_parameters() { cfg_allow_pondering = true; @@ -94,9 +96,11 @@ void Parameters::setup_default_parameters() { cfg_noise = false; cfg_randomize = false; cfg_timemanage = true; + cfg_slowmover = 89; cfg_logfile_handle = nullptr; cfg_quiet = false; cfg_rng_seed = 0; cfg_weightsfile = "weights.txt"; + cfg_go_nodes_as_visits = true; } diff --git a/src/Parameters.h b/src/Parameters.h index 16358b59b..41b8eb8f7 100644 --- a/src/Parameters.h +++ b/src/Parameters.h @@ -34,6 +34,7 @@ extern int cfg_resignpct; extern int cfg_noise; extern int cfg_randomize; extern int cfg_timemanage; +extern int cfg_slowmover; extern int cfg_min_resign_moves; extern int cfg_root_temp_decay; extern uint64_t cfg_rng_seed; @@ -51,6 +52,7 @@ extern std::string cfg_weightsfile; extern std::string cfg_supervise; extern FILE* cfg_logfile_handle; extern bool cfg_quiet; +extern bool cfg_go_nodes_as_visits; class Parameters { public: diff --git a/src/ThreadPool.h b/src/ThreadPool.h index 70368467d..e5ce9f88c 100644 --- a/src/ThreadPool.h +++ b/src/ThreadPool.h @@ -50,6 +50,10 @@ class ThreadPool { template auto add_task(F&& f, Args&&... args) -> std::future::type>; + + std::size_t size() const { + return m_threads.size(); + } private: std::vector m_threads; std::queue> m_tasks; diff --git a/src/TimeMan.cpp b/src/TimeMan.cpp index 03d89f1a8..57625e99e 100644 --- a/src/TimeMan.cpp +++ b/src/TimeMan.cpp @@ -21,51 +21,34 @@ #include #include "TimeMan.h" +#include "Parameters.h" TimeManagement Time; // Our global time management object -namespace { +// move_importance() is a skew-logistic function based on naive statistical +// analysis of "how many games are still undecided after n half-moves". Game +// is considered "undecided" as long as neither side has >275cp advantage. +// Data was extracted from the CCRL game database with some simple filtering criteria. - enum TimeType { OptimumTime, MaxTime }; +double move_importance(int ply) { - const int MoveHorizon = 50; // Plan time management at most this many moves ahead - const double MaxRatio = 7.3; // When in trouble, we can step over reserved time with this ratio - const double StealRatio = 0.34; // However we must not steal time from remaining moves over this ratio + const double XScale = 6.85; + const double XShift = 64.5; + const double Skew = 0.171; + return pow((1 + exp((ply - XShift) / XScale)), -Skew) + DBL_MIN; // Ensure non-zero +} - // move_importance() is a skew-logistic function based on naive statistical - // analysis of "how many games are still undecided after n half-moves". Game - // is considered "undecided" as long as neither side has >275cp advantage. - // Data was extracted from the CCRL game database with some simple filtering criteria. +int remaining(int myTime, int movesToGo, int ply) { - double move_importance(int ply) { + double moveImportance = move_importance(ply) * cfg_slowmover / 100; // Slow Mover Ratio + double otherMovesImportance = 0; - const double XScale = 6.85; - const double XShift = 64.5; - const double Skew = 0.171; + for (int i = 1; i < movesToGo; ++i) + otherMovesImportance += move_importance(ply + 2 * i); - return pow((1 + exp((ply - XShift) / XScale)), -Skew) + DBL_MIN; // Ensure non-zero - } - - template - int remaining(int myTime, int movesToGo, int ply, int slowMover) { - - const double TMaxRatio = (T == OptimumTime ? 1 : MaxRatio); - const double TStealRatio = (T == OptimumTime ? 0 : StealRatio); - - double moveImportance = (move_importance(ply) * slowMover) / 100; - double otherMovesImportance = 0; - - for (int i = 1; i < movesToGo; ++i) - otherMovesImportance += move_importance(ply + 2 * i); - - double ratio1 = (TMaxRatio * moveImportance) / (TMaxRatio * moveImportance + otherMovesImportance); - double ratio2 = (moveImportance + TStealRatio * otherMovesImportance) / (moveImportance + otherMovesImportance); - - return int(myTime * std::min(ratio1, ratio2)); // Intel C++ asks for an explicit cast - } - -} // namespace + return int(myTime * moveImportance / (moveImportance + otherMovesImportance)); // Intel C++ asks for an explicit cast +} /// init() is called at the beginning of the search and calculates the allowed @@ -79,28 +62,16 @@ namespace { void TimeManagement::init(Color us, int ply) { + int minThinkingTime = std::min(20, Limits.time[us] / 5); + int moveOverhead = 30; + int MoveHorizon = Limits.movestogo ? std::min(Limits.movestogo - 1, 50) : 50; - startTime = Limits.startTime; - optimumTime = maximumTime = Limits.time[us]; - - const int MaxMTG = Limits.movestogo ? std::min(Limits.movestogo, MoveHorizon) : MoveHorizon; - - // We calculate optimum time usage for different hypothetical "moves to go"-values - // and choose the minimum of calculated search time values. Usually the greatest - // hypMTG gives the minimum values. - for (int hypMTG = 1; hypMTG <= MaxMTG; ++hypMTG) - { - // Calculate thinking time for hypothetical "moves to go"-value - int hypMyTime = Limits.time[us] - + Limits.inc[us] * (hypMTG - 1) - - 30 * (2 + std::min(hypMTG, 40)); - - hypMyTime = std::max(hypMyTime, 0); + startTime = Limits.startTime; + optimumTime = maximumTime = std::max(Limits.time[us] - 3 * moveOverhead, minThinkingTime); - int t1 = remaining(hypMyTime, hypMTG, ply, 89); - int t2 = remaining(hypMyTime, hypMTG, ply, 89); + // Calculate thinking time for hypothetical "moves to go"-value + int hypMyTime = std::max (0, Limits.time[us] + (Limits.inc[us] - moveOverhead) * MoveHorizon); - optimumTime = std::min(t1, optimumTime); - maximumTime = std::min(t2, maximumTime); - } -} \ No newline at end of file + optimumTime = std::min(minThinkingTime + remaining(hypMyTime, MoveHorizon, ply), optimumTime); + maximumTime = std::min(optimumTime * 7, maximumTime); +} diff --git a/src/UCI.cpp b/src/UCI.cpp index c9b205ba9..7a050022a 100644 --- a/src/UCI.cpp +++ b/src/UCI.cpp @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include "Movegen.h" #include "Parameters.h" @@ -69,12 +71,9 @@ namespace { bh.do_move(m); } - // setoption() is called when engine receives the "setoption" UCI command. The // function updates the UCI option ("name") to the given value ("value"). - void setoption(istringstream& is) { - string token, name, value; is >> token; // Consume "name" token @@ -87,7 +86,40 @@ namespace { while (is >> token) value += string(" ", value.empty() ? 0 : 1) + token; - myprintf_so("No such option: %s\n", name.c_str()); + if (Options.count(name)) + Options[name] = value; + else + myprintf_so("No such option: %s\n", name.c_str()); + } + + void parse_limits(istringstream& is, UCTSearch& search) { + Limits = LimitsType(); + std::string token; + + search.set_visit_limit(cfg_max_visits); + search.set_playout_limit(cfg_max_playouts); + + while (is >> token) { + if (token == "wtime") is >> Limits.time[WHITE]; + else if (token == "btime") is >> Limits.time[BLACK]; + else if (token == "winc") is >> Limits.inc[WHITE]; + else if (token == "binc") is >> Limits.inc[BLACK]; + else if (token == "movestogo") is >> Limits.movestogo; + else if (token == "depth") is >> Limits.depth; + else if (token == "nodes") { + is >> Limits.nodes; + + if (cfg_go_nodes_as_visits) { + search.set_visit_limit(static_cast(Limits.nodes)); + search.set_playout_limit(MAXINT_DIV2); + } else { + search.set_playout_limit(static_cast(Limits.nodes)); + search.set_visit_limit(MAXINT_DIV2); + } + } + else if (token == "movetime") is >> Limits.movetime; + else if (token == "infinite") Limits.infinite = 1; + }; } // called when receiving the 'perft Depth' command @@ -101,7 +133,12 @@ namespace { } void printVersion() { - myprintf_so("id name lczero " PROGRAM_VERSION "\nid author The LCZero Authors\nuciok\n"); + std::stringstream options; + options << "id name lczero " PROGRAM_VERSION "\nid author The LCZero Authors"; + options << Options << "\n"; + options << "uciok\n"; + + myprintf_so("%s", options.str().c_str()); } // Return the score from the self-play game @@ -240,45 +277,6 @@ uint64_t UCI::perft(BoardHistory& bh, Depth depth) { } -// go() is called when engine receives the "go" UCI command. The function sets -// the thinking time and other parameters from the input string, then starts -// the search. - -void gohelper(UCTSearch & search, BoardHistory &bh) { - Move move = search.think(bh.shallow_clone()); - - bh.do_move(move); - myprintf_so("bestmove %s\n", UCI::move(move).c_str()); -} - -void go(UCTSearch& search, BoardHistory& bh, istringstream& is) { - - Limits = LimitsType(); - string token; - - // TODO: See issue #287. - //if ((is >> token) && token == "infinite") Limits.infinite = 1; - //else Limits.infinite = 0; - Limits.infinite = 0; - - do { - if (token == "wtime") is >> Limits.time[WHITE]; - else if (token == "btime") is >> Limits.time[BLACK]; - else if (token == "winc") is >> Limits.inc[WHITE]; - else if (token == "binc") is >> Limits.inc[BLACK]; - else if (token == "movestogo") is >> Limits.movestogo; - else if (token == "depth") is >> Limits.depth; - else if (token == "nodes") is >> Limits.nodes; - else if (token == "movetime") is >> Limits.movetime; - } while (is >> token); - - // TODO: See issue #287. - //std::thread lol(gohelper, std::ref(search), std::ref(bh)); - //lol.detach(); - gohelper(search, bh); -} - - /// UCI::loop() waits for a command from stdin, parses it and calls the appropriate /// function. Also intercepts EOF from stdin to ensure gracefully exiting if the /// GUI dies unexpectedly. When called with some command line arguments, e.g. to @@ -290,6 +288,8 @@ void UCI::loop(const std::string& start) { BoardHistory bh; bh.set(Position::StartFEN); UCTSearch search (bh.shallow_clone());//std::make_unique(bh.shallow_clone()); + std::thread search_thread; + std::mutex bh_mutex; do { if (start.empty() && !getline(cin, cmd)) // Block here waiting for input or EOF @@ -316,66 +316,142 @@ void UCI::loop(const std::string& start) { Threads.ponder = false; // Switch to normal search */ - if (token == "uci") printVersion(); - else if (token == "setoption") setoption(is); - else if (token == "go") go(search,bh,is); - else if (token == "stop") search.please_stop(); - else if (token == "perft") uci_perft(bh, is); - else if (token == "position") position(bh, is); - else if (token == "ucinewgame") ; + std::unique_lock bh_guard(bh_mutex); //locking for all commands for safety, explicitly unlock when needed + + auto wait_search = [&bh_guard, &search, &search_thread]() { + bh_guard.unlock(); + if (Limits.infinite) search.please_stop(); + + if (search_thread.joinable()) search_thread.join(); + }; + + auto stop_and_wait_search = [&bh_guard, &search, &search_thread] { + bh_guard.unlock(); + search.please_stop(); + + if (search_thread.joinable()) search_thread.join(); + }; + + if (token == "uci") { + printVersion(); + } + else if (token == "setoption") { + wait_search(); //blocking this to be safe + + setoption(is); + } + else if (token == "go") { + wait_search(); + + parse_limits(is, search); + + search_thread = std::thread([&bh, &search, bhc = bh.shallow_clone(), &bh_mutex]() mutable { + Move move = search.think(std::move(bhc)); + std::lock_guard l(bh_mutex); //synchronizing with uci loop board history + + bh.do_move(move); + myprintf_so("bestmove %s\n", UCI::move(move).c_str()); + }); + } + else if (token == "stop") { + stop_and_wait_search(); + } + else if (token == "perft") { + stop_and_wait_search(); + + uci_perft(bh, is); + } + else if (token == "position") { + wait_search(); + + position(bh, is); + } + else if (token == "ucinewgame") { + stop_and_wait_search(); + Training::clear_training(); + } else if (token == "isready") { Network::initialize(); - myprintf_so("readyok\n"); + myprintf_so("readyok\n"); //"readyok" can be sent also when the engine is calculating } // Additional custom non-UCI commands, mainly for debugging - else if (token == "train") generate_training_games(is); - else if (token == "bench") bench(); - else if (token == "d" || token == "showboard") { - std::stringstream ss; - ss << bh.cur(); - myprintf_so("%s\n", ss.str().c_str()); - } - else if (token == "showfen") { - std::stringstream ss; - ss << bh.cur().fen(); - myprintf_so("%s\n", ss.str().c_str()); - } - else if (token == "showgame") { - std::string result; - for (const auto &p : bh.positions) { - if (result == "") { - result = " "; // first position has no move - } else { - result += UCI::move(p.get_move()) + " "; - } - } - myprintf_so("position startpos%s\n", result.c_str()); - } - else if (token == "showpgn") myprintf_so("%s\n", bh.pgn().c_str()); - else if (token == "undo") myprintf_so(bh.undo_move() ? "Undone\n" : "At first move\n"); - else if (token == "usermove" || token == "play") { - std::string ms; is >> ms; - Move m = UCI::to_move(bh.cur(), ms); - if (m == MOVE_NONE) m = bh.cur().san_to_move(ms); - if (m != MOVE_NONE) { - bh.do_move(m); - myprintf_so("usermove %s\n", UCI::move(m).c_str()); - } - else { - myprintf_so("Illegal move: %s\n", ms.c_str()); - } - } - else if (UCI::to_move(bh.cur(), token) != MOVE_NONE) { - Move m = UCI::to_move(bh.cur(), token); - bh.do_move(m); - myprintf_so("usermove %s\n", UCI::move(m).c_str()); - } - //else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl; - else if (token != "quit") { - myprintf_so("Unknown command: %s\n", cmd.c_str()); - } + else if (token == "train") { + stop_and_wait_search(); + + generate_training_games(is); + } + else if (token == "bench") { + stop_and_wait_search(); + + bench(); + } + else if (token == "d" || token == "showboard") { //bh is guarded by bh_guard + std::stringstream ss; + ss << bh.cur(); + myprintf_so("%s\n", ss.str().c_str()); + } + else if (token == "showfen") { + std::stringstream ss; + ss << bh.cur().fen(); + myprintf_so("%s\n", ss.str().c_str()); + } + else if (token == "showgame") { + std::string result; + for (const auto &p : bh.positions) { + if (result == "") { + result = " "; // first position has no move + } else { + result += UCI::move(p.get_move()) + " "; + } + } + myprintf_so("position startpos%s\n", result.c_str()); + } + else if (token == "showpgn") myprintf_so("%s\n", bh.pgn().c_str()); + else if (token == "undo") { + wait_search(); + + myprintf_so(bh.undo_move() ? "Undone\n" : "At first move\n"); + } + else if (token == "usermove" || token == "play") { + wait_search(); + + std::string ms; is >> ms; + Move m = UCI::to_move(bh.cur(), ms); + if (m == MOVE_NONE) m = bh.cur().san_to_move(ms); + if (m != MOVE_NONE) { + bh.do_move(m); + myprintf_so("usermove %s\n", UCI::move(m).c_str()); + } + else { + myprintf_so("Illegal move: %s\n", ms.c_str()); + } + } + else if (UCI::to_move(bh.cur(), token) != MOVE_NONE) { + wait_search(); + + auto m = UCI::to_move(bh.cur(), token); + + if (m != MOVE_NONE) { //double check in case search modified the board + bh.do_move(m); + + myprintf_so("usermove %s\n", UCI::move(m).c_str()); + } else { + myprintf_so("Illegal move: %s\n", token.c_str()); + } + } + //else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl; + else if (token != "quit") { + myprintf_so("Unknown command: %s\n", cmd.c_str()); + } } while (start.empty()); // Command line args are one-shot + + //cancel and wait existing search if any + search.please_stop(); + + if (search_thread.joinable()) { + search_thread.join(); + } } /// UCI::square() converts a Square to a string in algebraic notation (g1, a7, etc.) diff --git a/src/UCI.h b/src/UCI.h index a7acf312d..cc269f14b 100644 --- a/src/UCI.h +++ b/src/UCI.h @@ -27,11 +27,11 @@ #include "Types.h" class Position; +struct BoardHistory; namespace UCI { class Option; - /// Custom comparator because UCI options should be case insensitive struct CaseInsensitiveLess { bool operator() (const std::string&, const std::string&) const; @@ -42,9 +42,9 @@ typedef std::map OptionsMap; /// Option class implements an option as defined by UCI protocol class Option { - +protected: typedef void (*OnChange)(const Option&); - + public: Option(OnChange = nullptr); Option(bool v, OnChange = nullptr); @@ -56,13 +56,33 @@ class Option { operator int() const; operator std::string() const; -private: +protected: friend std::ostream& operator<<(std::ostream&, const OptionsMap&); std::string defaultValue, currentValue, type; int min, max; size_t idx; OnChange on_change; + bool advertise {true}; +}; + +class SilentOption : public Option { +public: + SilentOption(OnChange f = nullptr) : Option(f) { + advertise = false; + } + + SilentOption(bool v, OnChange f = nullptr) : Option(v, f) { + advertise = false; + } + + SilentOption(const char* v, OnChange f = nullptr) : Option(v, f) { + advertise = false; + } + + SilentOption(int v, int minv, int maxv, OnChange f = nullptr) : Option(v, minv, maxv, f) { + advertise = false; + } }; void init(OptionsMap&); diff --git a/src/UCIOption.cpp b/src/UCIOption.cpp new file mode 100644 index 000000000..129cd1bd2 --- /dev/null +++ b/src/UCIOption.cpp @@ -0,0 +1,233 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2008 Tord Romstad (Glaurung author) + Copyright (C) 2008-2015 Marco Costalba, Joona Kiiski, Tord Romstad + Copyright (C) 2015-2018 Marco Costalba, Joona Kiiski, Gary Linscott, Tord Romstad + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#include +#include +#include + +#include "Utils.h" +#include "UCI.h" +#include "Parameters.h" + +using std::string; + +using namespace Utils; + +UCI::OptionsMap Options; // Global object + +namespace UCI { + +/// 'On change' actions, triggered by an option's value change + void on_threads(const Option& o) { + int num_threads = o; + + if (num_threads > cfg_num_threads) { + for (auto i = thread_pool.size(); i < static_cast(num_threads); ++i) { + thread_pool.add_thread([](){}); + } + + cfg_num_threads = num_threads; + } else { + cfg_num_threads = num_threads; //we just decrease the number of threads used in the search loop + } + + myprintf("Using %d thread(s).\n", num_threads); + } + + void on_quiet(const Option& o) { + bool value = o; + + if (value) { + myprintf("Enabled quiet mode\n", value); + } else { + myprintf("Disabled quiet mode\n", value); + } + + cfg_quiet = value; + } + + bool set_float_cfg(float& cfg_param, const std::string& value) { + try { + cfg_param = std::strtof(value.c_str(), nullptr); + } catch (const std::logic_error& exc) { + myprintf("Could not convert to float: %s\n", value.c_str()); + + return false; + } + + return true; + } + + void on_softmaxtemp(const Option& o) { + std::string value = o; + + if (set_float_cfg(cfg_softmax_temp, value)) { + myprintf("Set cfg_softmax_temp to %.6f\n", cfg_softmax_temp); + } + } + + void on_fpureduction(const Option& o) { + std::string value = o; + + if (set_float_cfg(cfg_fpu_reduction, value)) { + myprintf("Set cfg_fpu_reduction to %.6f\n", cfg_fpu_reduction); + } + } + + void on_fpudynamiceval(const Option& o) { + bool value = o; + + cfg_fpu_dynamic_eval = value; + + if (value) { + myprintf("cfg_fpu_dynamic_eval enabled\n"); + } else { + myprintf("cfg_fpu_dynamic_eval disabled\n"); + } + } + + void on_puct(const Option& o) { + std::string value = o; + + if (set_float_cfg(cfg_puct, value)) { + myprintf("Set puct to %.6f\n", cfg_puct); + } + } + + void on_slowmover(const Option& o) { + int value = o; + + cfg_slowmover = value; + myprintf("Set cfg_slowmover to %d.\n", cfg_slowmover); + } + + void on_nodes_as_vistis(const Option& o) { + cfg_go_nodes_as_visits = o; + + if (cfg_go_nodes_as_visits) { + myprintf("Set go nodes to visits.\n"); + } else { + myprintf("Set go nodes to playouts.\n"); + } + } + +/// Our case insensitive less() function as required by UCI protocol + bool CaseInsensitiveLess::operator() (const string& s1, const string& s2) const { + + return std::lexicographical_compare(s1.begin(), s1.end(), s2.begin(), s2.end(), + [](char c1, char c2) { return tolower(c1) < tolower(c2); }); + } + + +/// init() initializes the UCI options to their hard-coded default values + + void init(OptionsMap& o) { + o["Threads"] << Option(cfg_num_threads, 1, cfg_max_threads, on_threads); + o["Quiet"] << Option(cfg_quiet, on_quiet); + o["Softmax Temp"] << SilentOption(std::to_string(cfg_softmax_temp).c_str(), on_softmaxtemp); + o["FPU Reduction"] << Option(std::to_string(cfg_fpu_reduction).c_str(), on_fpureduction); + o["FPU Dynamic Eval"] << SilentOption(cfg_fpu_dynamic_eval, on_fpudynamiceval); + o["Puct"] << Option(std::to_string(cfg_puct).c_str(), on_puct); + o["SlowMover"] << Option(cfg_slowmover, 1, std::numeric_limits::max(), on_slowmover); + o["Go Nodes Visits"] << Option(cfg_go_nodes_as_visits, on_nodes_as_vistis); + } + +/// operator<<() is used to print all the options default values in chronological +/// insertion order (the idx field) and in the format defined by the UCI protocol. + + std::ostream& operator<<(std::ostream& os, const OptionsMap& om) { + + for (size_t idx = 0; idx < om.size(); ++idx) + for (const auto& it : om) + if (it.second.idx == idx && it.second.advertise) + { + const Option& o = it.second; + + os << "\noption name " << it.first << " type " << o.type; + + if (o.type != "button") + os << " default " << o.defaultValue; + + if (o.type == "spin") + os << " min " << o.min << " max " << o.max; + + break; + } + + return os; + } + + +/// Option class constructors and conversion operators + + Option::Option(const char* v, OnChange f) : type("string"), min(0), max(0), on_change(f) + { defaultValue = currentValue = v; } + + Option::Option(bool v, OnChange f) : type("check"), min(0), max(0), on_change(f) + { defaultValue = currentValue = (v ? "true" : "false"); } + + Option::Option(OnChange f) : type("button"), min(0), max(0), on_change(f) + {} + + Option::Option(int v, int minv, int maxv, OnChange f) : type("spin"), min(minv), max(maxv), on_change(f) + { defaultValue = currentValue = std::to_string(v); } + + Option::operator int() const { + assert(type == "check" || type == "spin"); + return (type == "spin" ? stoi(currentValue) : currentValue == "true"); + } + + Option::operator std::string() const { + assert(type == "string"); + return currentValue; + } + + +/// operator<<() inits options and assigns idx in the correct printing order + + void Option::operator<<(const Option& o) { + + static size_t insert_order = 0; + + *this = o; + idx = insert_order++; + } + + +/// operator=() updates currentValue and triggers on_change() action. It's up to +/// the GUI to check for option's limits, but we could receive the new value from +/// the user by console window, so let's check the bounds anyway. + + Option& Option::operator=(const string& v) { + + assert(!type.empty()); + + if ( (type != "button" && v.empty()) + || (type == "check" && v != "true" && v != "false") + || (type == "spin" && (stoi(v) < min || stoi(v) > max))) + return *this; + + if (type != "button") + currentValue = v; + + if (on_change) + on_change(*this); + + return *this; + } + +} // namespace UCI \ No newline at end of file diff --git a/src/UCTSearch.cpp b/src/UCTSearch.cpp index 5f784bf08..011876de9 100644 --- a/src/UCTSearch.cpp +++ b/src/UCTSearch.cpp @@ -151,7 +151,8 @@ void UCTSearch::dump_stats(BoardHistory& state, UCTNode& parent) { StateInfo si; state.cur().do_move(node->get_move(), si); - pvstring += " " + get_pv(state, *node); + // Since this is just a string, set use_san=true + pvstring += " " + get_pv(state, *node, true); state.cur().undo_move(node->get_move()); myprintf_so("%s\n", pvstring.c_str()); @@ -176,15 +177,15 @@ Move UCTSearch::get_best_move() { // Check whether to randomize the best move proportional // to the (exponentiated) visit counts. - + if (cfg_randomize) { auto root_temperature = 1.0f; // If a temperature decay schedule is set, calculate root temperature from - // ply count and decay constant. Set default value for too small root temperature. + // ply count and decay constant. Set default value for too small root temperature. if (cfg_root_temp_decay > 0) { root_temperature = get_root_temperature(); myprintf("Game ply: %d, root temperature: %5.2f \n",bh_.cur().game_ply()+1, root_temperature); - } + } m_root->randomize_first_proportionally(root_temperature); } @@ -211,19 +212,19 @@ Move UCTSearch::get_best_move() { return bestmove; } -std::string UCTSearch::get_pv(BoardHistory& state, UCTNode& parent) { +std::string UCTSearch::get_pv(BoardHistory& state, UCTNode& parent, bool use_san) { if (!parent.has_children()) { return std::string(); } auto& best_child = parent.get_best_root_child(state.cur().side_to_move()); auto best_move = best_child.get_move(); - auto res = state.cur().move_to_san(best_move); + auto res = use_san ? state.cur().move_to_san(best_move) : UCI::move(best_move); StateInfo st; state.cur().do_move(best_move, st); - auto next = get_pv(state, best_child); + auto next = get_pv(state, best_child, use_san); if (!next.empty()) { res.append(" ").append(next); } @@ -239,11 +240,11 @@ void UCTSearch::dump_analysis(int64_t elapsed, bool force_output) { auto bh = bh_.shallow_clone(); Color color = bh.cur().side_to_move(); - std::string pvstring = get_pv(bh, *m_root); + // UCI requires long algebraic notation, so use_san=false + std::string pvstring = get_pv(bh, *m_root, false); float feval = m_root->get_eval(color); - float winrate = 100.0f * feval; // UCI-like output wants a depth and a cp, so convert winrate to a cp estimate. - int cp = 162 * tan(3.14 * (feval - 0.5)); + int cp = 290.680623072 * tan(3.096181612 * (feval - 0.5)); // same for nodes to depth, assume nodes = 1.8 ^ depth. int depth = log(float(m_nodes)) / log(1.8); // To report nodes, use visits. @@ -253,9 +254,9 @@ void UCTSearch::dump_analysis(int64_t elapsed, bool force_output) { // To report nps, use m_playouts to exclude nodes added by tree reuse, // which is similar to a ponder hit. The user will expect to know how // fast nodes are being added, not how big the ponder hit was. - myprintf_so("info depth %d nodes %d nps %0.f score cp %d winrate %5.2f%% time %lld pv %s\n", + myprintf_so("info depth %d nodes %d nps %0.f score cp %d time %lld pv %s\n", depth, visits, 1000.0 * m_playouts / (elapsed + 1), - cp, winrate, elapsed, pvstring.c_str()); + cp, elapsed, pvstring.c_str()); } bool UCTSearch::is_running() const { @@ -358,7 +359,7 @@ Move UCTSearch::think(BoardHistory&& new_bh) { auto start_nodes = m_root->count_nodes(); #endif - uci_stop = false; + uci_stop.store(false, std::memory_order_seq_cst); // See if the position is in our previous search tree. // If not, construct a new m_root. @@ -385,6 +386,7 @@ Move UCTSearch::think(BoardHistory&& new_bh) { Time.init(bh_.cur().side_to_move(), bh_.cur().game_ply()); m_target_time = get_search_time(); + m_max_time = Time.maximum(); m_start_time = Limits.timeStarted(); // create a sorted list of legal moves (make sure we @@ -497,16 +499,16 @@ int UCTSearch::get_search_time() { // Used to check if we've run out of time or reached out playout limit bool UCTSearch::should_halt_search() { - if (uci_stop) return true; + if (uci_stop.load(std::memory_order_seq_cst)) return true; + if (Limits.infinite) return false; auto elapsed_millis = now() - m_start_time; return m_target_time < 0 ? pv_limit_reached() - : m_target_time < elapsed_millis; + : (m_target_time < elapsed_millis || elapsed_millis > m_max_time); } // Asks the search to stop politely -void UCTSearch::please_stop() -{ - uci_stop = true; +void UCTSearch::please_stop() { + uci_stop.store(true, std::memory_order_seq_cst); } void UCTSearch::set_playout_limit(int playouts) { diff --git a/src/UCTSearch.h b/src/UCTSearch.h index 2bcaf5e70..9f9ec9137 100644 --- a/src/UCTSearch.h +++ b/src/UCTSearch.h @@ -85,7 +85,7 @@ class UCTSearch { private: void dump_stats(BoardHistory& pos, UCTNode& parent); - std::string get_pv(BoardHistory& pos, UCTNode& parent); + std::string get_pv(BoardHistory& pos, UCTNode& parent, bool use_san); void dump_analysis(int64_t elapsed, bool force_output); Move get_best_move(); float get_root_temperature(); @@ -96,6 +96,7 @@ class UCTSearch { std::atomic m_nodes{0}; std::atomic m_playouts{0}; int64_t m_target_time{0}; + int64_t m_max_time{0}; int64_t m_start_time{0}; std::atomic m_run{false}; int m_maxplayouts; diff --git a/src/config.h b/src/config.h index bdea66367..bc51701d9 100644 --- a/src/config.h +++ b/src/config.h @@ -39,7 +39,7 @@ static constexpr int SELFCHECK_PROBABILITY = 2000; static constexpr int SELFCHECK_MIN_EXPANSIONS = 2'000'000; //#define USE_TUNER -#define PROGRAM_VERSION "v0.6" +#define PROGRAM_VERSION "v0.7" // OpenBLAS limitation #if defined(USE_BLAS) && defined(USE_OPENBLAS) diff --git a/src/main.cpp b/src/main.cpp index 7fec821dc..48a21ef84 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -375,6 +375,7 @@ int main(int argc, char* argv[]) { return 0; } + UCI::init(Options); UCI::loop(uci_start); return 0;