diff --git a/go/src/client/main.go b/go/src/client/main.go index 8aca974ce..f8bccf7df 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": "9", + "version": "10", } } diff --git a/lc0/meson.build b/lc0/meson.build index e6ffd58c2..8ed74d2da 100644 --- a/lc0/meson.build +++ b/lc0/meson.build @@ -42,9 +42,12 @@ cuda_gen = generator(nvcc, ) files = [ + 'src/analyzer/analyzer.cc', + 'src/analyzer/table.cc', 'src/chess/bitboard.cc', 'src/chess/board.cc', 'src/chess/position.cc', + 'src/chess/uciloop.cc', 'src/mcts/node.cc', 'src/mcts/search.cc', 'src/neural/cache.cc', @@ -55,16 +58,16 @@ files = [ 'src/neural/network_mux.cc', 'src/neural/network_random.cc', 'src/neural/network_tf.cc', - 'src/utils/transpose.cc', 'src/utils/commandline.cc', 'src/utils/optionsdict.cc', + 'src/utils/optionsparser.cc', 'src/utils/random.cc', + 'src/utils/string.cc', + 'src/utils/transpose.cc', 'src/engine.cc', - 'src/optionsparser.cc', 'src/selfplay/game.cc', 'src/selfplay/tournament.cc', 'src/selfplay/loop.cc', - 'src/uciloop.cc', cuda_gen.process(cuda_files) ] diff --git a/lc0/src/analyzer/analyzer.cc b/lc0/src/analyzer/analyzer.cc new file mode 100644 index 000000000..5fb5f420b --- /dev/null +++ b/lc0/src/analyzer/analyzer.cc @@ -0,0 +1,264 @@ +/* + 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 "analyzer/analyzer.h" +#include "analyzer/table.h" +#include "mcts/search.h" +#include "neural/factory.h" +#include "neural/loader.h" +#include "utils/optionsparser.h" +#include "utils/string.h" + +#include +#include +#include +#include +#include +#include + +namespace lczero { +namespace { +const char* kTsvReportStr = "Filename of the tab-separated report file"; +const char* kTxtReportStr = "Filename of the text report file"; +const char* kMovesStr = "Moves in UCI format, space separated"; +const char* kMovesToAnalyzeStr = "Number of (last) moves to analyze"; +const char* kNodesStr = "(comma separated) How many nodes to calculate"; +const char* kTrainExamplesStr = + "How many examples of training data to generate"; +const char* kWeightsStr = "Network weights file path"; +const char* kNnBackendStr = "NN backend to use"; +const char* kNnBackendOptionsStr = "NN backend parameters"; + +const char* kAutoDiscover = ""; +} // namespace + +Analyzer::Analyzer() { + options_parser_.AddContext("play"); + options_parser_.AddContext("training"); + play_options_ = &options_parser_.GetOptionsDict("play"); + play_options_ = &options_parser_.GetOptionsDict("training"); + + options_parser_.Add(kTsvReportStr, "tsv-report"); + options_parser_.Add(kTxtReportStr, "txt-report"); + options_parser_.Add(kMovesStr, "moves"); + options_parser_.Add(kMovesToAnalyzeStr, 1, 999, "num-moves") = 4; + options_parser_.Add(kNodesStr, "nodes-list") = + "10,50,100,200,400,600,800,1200,1600,5000,10000"; + options_parser_.Add(kTrainExamplesStr, 1, 999, + "training-examples") = 10; + options_parser_.Add(kWeightsStr, "weights", 'w') = + kAutoDiscover; + + // TODO(mooskagh) Move all that to network factory. + const auto backends = NetworkFactory::Get()->GetBackendsList(); + options_parser_.Add(kNnBackendStr, backends, "backend") = + backends.empty() ? "" : backends[0]; + options_parser_.Add(kNnBackendOptionsStr, "backend-opts"); + + Search::PopulateUciParams(&options_parser_); + + // Overriding default options of search. + auto defaults = options_parser_.GetMutableDefaultsOptions(); + defaults->Set(Search::kMiniBatchSizeStr, 1); // Minibatch = 1 + defaults->Set(Search::kSmartPruningStr, false); // No smart pruning + defaults->Set(Search::kVerboseStatsStr, true); // Verbose stats +} + +void Analyzer::GatherStats(Table3d* table, const Node* root_node, + std::string& col, bool flip) { + uint64_t total_n = 0; + float factor = play_options_->Get(Search::kCpuctStr) * + std::sqrt(std::max(root_node->GetN(), 1u)); + const float parent_q = + -root_node->GetQ(0) - play_options_->Get(Search::kFpuReductionStr); + + for (Node* node : root_node->Children()) { + const auto n = node->GetNStarted(); + total_n += n; + const auto u = factor * node->GetU(); + const auto q = node->GetQ(parent_q); + const auto move = node->GetMove(flip).as_string(); + table->Add3dVal(col, move, "N", std::to_string(n)); + table->Add3dVal(col, move, "U", std::to_string(u)); + table->Add3dVal(col, move, "Q", std::to_string(q)); + table->Add3dVal(col, move, "U+Q", std::to_string(u + q)); + } + + for (Node* node : root_node->Children()) { + auto n = node->GetNStarted(); + table->Add3dVal(col, node->GetMove(flip).as_string(), "N%", + std::to_string(static_cast(n) / total_n)); + } +} + +void Analyzer::RunOnePosition(const std::vector& moves) { + NNCache cache(200000); + NodeTree tree; + tree.ResetToPosition(ChessBoard::kStartingFen, moves); + + auto nodeses = ParseIntList(play_options_->Get(kNodesStr)); + std::sort(nodeses.begin(), nodeses.end()); + + Table3d table; + std::vector cols; + + // Run search in increasing number of nodes. + for (int nodes : nodeses) { + WriteToLog("Nodes: " + std::to_string(nodes)); + SearchLimits limits; + limits.visits = nodes; + + Search search(tree, network_.get(), + std::bind(&Analyzer::OnBestMove, this, std::placeholders::_1), + std::bind(&Analyzer::OnInfo, this, std::placeholders::_1), + limits, *play_options_, &cache); + + search.RunBlocking(1); + + auto col = std::to_string(nodes); + cols.push_back(col); + GatherStats(&table, tree.GetCurrentHead(), col, tree.IsBlackToMove()); + table.AddColVal(col, "bestmove", search.GetBestMove().first.as_string()); + } + + // Fetch MCTS-agnostic per-move stats P and V. + std::vector nodes; + for (Node* iter : tree.GetCurrentHead()->Children()) { + nodes.emplace_back(iter); + } + std::sort(nodes.begin(), nodes.end(), [](const Node* a, const Node* b) { + return a->GetNStarted() > b->GetNStarted(); + }); + std::vector rows; + for (const Node* node : nodes) { + auto move = node->GetMove(tree.IsBlackToMove()).as_string(); + rows.push_back(move); + table.AddRowVal(move, "P", std::to_string(node->GetP())); + table.AddRowVal(move, "V", std::to_string(node->GetV())); + } + + // Dump table to log. + auto lines = table.RenderTable(cols, rows, {"N", "N%", "U", "Q", "U+Q"}, + {"P", "V"}, {"bestmove"}); + for (const auto& line : lines) WriteToTsvLog(line); +} + +void Analyzer::Run() { + if (!options_parser_.ProcessAllFlags()) return; + + // Open log files. + if (!play_options_->Get(kTxtReportStr).empty()) { + log_.open(play_options_->Get(kTxtReportStr).c_str()); + } + if (!play_options_->Get(kTsvReportStr).empty()) { + tsvlog_.open(play_options_->Get(kTsvReportStr).c_str()); + } + + // Load network + InitializeNetwork(); + + // Parse moves. + std::vector moves_str = + StrSplitAtWhitespace(play_options_->Get(kMovesStr)); + std::vector moves; + for (const auto& move : moves_str) moves.emplace_back(move); + + for (int i = 0; i < play_options_->Get(kMovesToAnalyzeStr); ++i) { + // Write position to logs. + std::string initial_pos; + if (moves_str.empty()) { + WriteToLog("Position: startpos"); + WriteToTsvLog({"Startpos."}); + } else { + WriteToLog("Position: moves " + StrJoin(moves_str)); + WriteToTsvLog({"Moves " + StrJoin(moves_str)}); + } + + // Run Mcts at different depths. + RunOnePosition(moves); + + // Run training several times. + + if (moves.empty()) break; + moves.pop_back(); + moves_str.pop_back(); + WriteToTsvLog({}); + } + + // DumpFlags(); +} + +void Analyzer::InitializeNetwork() { + std::string network_path = play_options_->Get(kWeightsStr); + std::string backend = play_options_->Get(kNnBackendStr); + std::string backend_options = + play_options_->Get(kNnBackendOptionsStr); + + std::string net_path = network_path; + if (net_path == kAutoDiscover) { + net_path = DiscoveryWeightsFile(); + } + Weights weights = LoadWeightsFromFile(net_path); + + OptionsDict network_options = OptionsDict::FromString( + backend_options, &options_parser_.GetOptionsDict()); + + network_ = NetworkFactory::Get()->Create(backend, weights, network_options); +} + +void Analyzer::WriteToLog(const std::string& line) const { + std::cout << line << std::endl; + if (log_) log_ << line << std::endl; +} + +void Analyzer::WriteToTsvLog(const std::vector& line) const { + if (!tsvlog_) return; + bool first = true; + for (const auto& chunk : line) { + if (first) { + first = false; + } else { + tsvlog_ << "\t"; + } + tsvlog_ << chunk; + } + tsvlog_ << std::endl; +} + +void Analyzer::OnBestMove(const BestMoveInfo& move) const { + WriteToLog("BestMove: " + move.bestmove.as_string()); +} + +void Analyzer::OnInfo(const ThinkingInfo& info) const { + std::string res = "Info"; + if (info.depth >= 0) res += " depth " + std::to_string(info.depth); + 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); + if (info.nps >= 0) res += " nps " + std::to_string(info.nps); + + if (!info.pv.empty()) { + res += " pv"; + for (const auto& move : info.pv) res += " " + move.as_string(); + } + if (!info.comment.empty()) res += " string " + info.comment; + WriteToLog(res); +} + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/analyzer/analyzer.h b/lc0/src/analyzer/analyzer.h new file mode 100644 index 000000000..64c004622 --- /dev/null +++ b/lc0/src/analyzer/analyzer.h @@ -0,0 +1,56 @@ +/* + 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 "analyzer/table.h" +#include "chess/board.h" +#include "chess/callbacks.h" +#include "mcts/node.h" +#include "neural/network.h" +#include "utils/optionsparser.h" + +namespace lczero { + +class Analyzer { + public: + Analyzer(); + void Run(); + + private: + void RunOnePosition(const std::vector& position); + + void WriteToLog(const std::string& line) const; + void WriteToTsvLog(const std::vector& line) const; + + void InitializeNetwork(); + void OnBestMove(const BestMoveInfo& move) const; + void OnInfo(const ThinkingInfo& info) const; + void GatherStats(Table3d* table, const Node* root_node, std::string& col, + bool flip); + + std::unique_ptr network_; + OptionsParser options_parser_; + const OptionsDict* play_options_; + // const OptionsDict* training_options_; + mutable std::ofstream log_; + mutable std::ofstream tsvlog_; +}; + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/analyzer/table.cc b/lc0/src/analyzer/table.cc new file mode 100644 index 000000000..7e7eff4ea --- /dev/null +++ b/lc0/src/analyzer/table.cc @@ -0,0 +1,109 @@ +/* + 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 "analyzer/table.h" +#include + +namespace lczero { + +void Table3d::Add3dVal(const std::string& col, const std::string& row, + const std::string& metric, const std::string& val) { + data_3d_[col][row][metric] = val; +} + +void Table3d::AddRowVal(const std::string& row, const std::string& metric, + const std::string& val) { + data_row_[row][metric] = val; +} + +void Table3d::AddColVal(const std::string& col, const std::string& metric, + const std::string& val) { + data_col_[col][metric] = val; +} + +namespace { +std::string BuildDimensionsStr(int cols, int rows, int attrs, int rattrs, + int cattrs) { + std::ostringstream oss; + oss << "(##" << cols << 'x' << rows << 'x' << attrs << 'x' << rattrs << 'x' + << cattrs << ")"; + return oss.str(); +} + +} // namespace + +std::vector> Table3d::RenderTable( + const std::vector& cols, const std::vector& rows, + const std::vector& attrs, + const std::vector& row_attrs, + const std::vector& col_attrs) { + std::vector> lines; + const bool has_row_attrs = !row_attrs.empty(); + + std::vector line; + + // Header. + // Topleft cell contains format for appscript to know the table dimensions. + line.push_back(BuildDimensionsStr(cols.size(), rows.size(), attrs.size(), + row_attrs.size(), col_attrs.size())); + for (int i = 0; i < (has_row_attrs ? 3 : 1); ++i) line.emplace_back(); + for (const auto& col : cols) line.emplace_back(col); + lines.emplace_back(std::move(line)); + + // Col-level values. + for (const auto& attr : col_attrs) { + line.clear(); + for (int i = 0; i < (has_row_attrs ? 3 : 1); ++i) line.emplace_back(); + line.push_back(attr + ":"); + for (const auto& col : cols) line.emplace_back(data_col_[col][attr]); + lines.emplace_back(std::move(line)); + } + + // Main table. + for (const auto& row : rows) { + const int stripes = std::max(attrs.size(), row_attrs.size()); + for (int i = 0; i < stripes; ++i) { + line.clear(); + if (i == 0) { + line.push_back(row); + } else { + line.emplace_back(); + } + if (has_row_attrs) { + if (i < row_attrs.size()) { + line.push_back(row_attrs[i] + ":"); + line.push_back(data_row_[row][row_attrs[i]]); + } else { + line.emplace_back(); + line.emplace_back(); + } + } + if (i < attrs.size()) { + auto attr = attrs[i]; + line.emplace_back(attr + ":"); + for (const auto& col : cols) + line.emplace_back(data_3d_[col][row][attr]); + } + lines.emplace_back(std::move(line)); + } + } + + return lines; +} + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/analyzer/table.h b/lc0/src/analyzer/table.h new file mode 100644 index 000000000..eb40a1183 --- /dev/null +++ b/lc0/src/analyzer/table.h @@ -0,0 +1,55 @@ +/* + 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 + +namespace lczero { + +class Table3d { + public: + void Add3dVal(const std::string& col, const std::string& row, + const std::string& metric, const std::string& val); + + void AddRowVal(const std::string& row, const std::string& metric, + const std::string& val); + + void AddColVal(const std::string& col, const std::string& metric, + const std::string& val); + + std::vector> RenderTable( + const std::vector& cols, + const std::vector& rows, + const std::vector& attrs, + const std::vector& row_attrs = {}, + const std::vector& col_attrs = {}); + + private: + using AttrToVal = std::map; + using RowAttrToVal = std::map; + using ColRowAttrToVal = std::map; + + ColRowAttrToVal data_3d_; + RowAttrToVal data_row_; + RowAttrToVal data_col_; +}; + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/chess/board.cc b/lc0/src/chess/board.cc index ed184ba65..4d8d30568 100644 --- a/lc0/src/chess/board.cc +++ b/lc0/src/chess/board.cc @@ -52,6 +52,8 @@ void ChessBoard::Mirror() { } namespace { +static const BitBoard kPawnMask = 0x00FFFFFFFFFFFF00ULL; + static const std::pair kKingMoves[] = { {-1, -1}, {-1, 0}, {-1, 1}, {0, -1}, {0, 1}, {1, -1}, {1, 0}, {1, 1}}; @@ -171,6 +173,8 @@ static const Move::Promotion kPromotions[] = { } // namespace +BitBoard ChessBoard::pawns() const { return pawns_ * kPawnMask; } + MoveList ChessBoard::GeneratePseudolegalMoves() const { MoveList result; for (auto source : our_pieces_) { @@ -685,7 +689,7 @@ void ChessBoard::SetFromFen(const std::string& fen, int* no_capture_ply, pawns_.set((square.row() == 2) ? 0 : 7, square.col()); } - if (who_to_move == "b") { + if (who_to_move == "b" || who_to_move == "B") { Mirror(); } if (no_capture_ply) *no_capture_ply = no_capture_halfmoves; diff --git a/lc0/src/chess/board.h b/lc0/src/chess/board.h index 2d7562ad4..3a37b4ab2 100644 --- a/lc0/src/chess/board.h +++ b/lc0/src/chess/board.h @@ -115,7 +115,7 @@ class ChessBoard { BitBoard ours() const { return our_pieces_; } BitBoard theirs() const { return their_pieces_; } - BitBoard pawns() const { return pawns_ * kPawnMask; } + BitBoard pawns() const; BitBoard bishops() const { return bishops_ - rooks_; } BitBoard rooks() const { return rooks_ - bishops_; } BitBoard queens() const { return rooks_ * bishops_; } @@ -142,8 +142,6 @@ class ChessBoard { bool operator!=(const ChessBoard& other) const { return !operator==(other); } private: - static constexpr BitBoard kPawnMask = 0x00FFFFFFFFFFFF00ULL; - // All white pieces. BitBoard our_pieces_; // All black pieces. diff --git a/lc0/src/mcts/callbacks.h b/lc0/src/chess/callbacks.h similarity index 100% rename from lc0/src/mcts/callbacks.h rename to lc0/src/chess/callbacks.h diff --git a/lc0/src/chess/position.cc b/lc0/src/chess/position.cc index eb80c930c..a90496cb0 100644 --- a/lc0/src/chess/position.cc +++ b/lc0/src/chess/position.cc @@ -38,9 +38,7 @@ Position::Position(const ChessBoard& board, int no_capture_ply, int game_ply) } uint64_t Position::Hash() const { - // return board.Hash(); - return HashCat({us_board_.Hash(), static_cast(no_capture_ply_), - static_cast(repetitions_)}); + return HashCat({us_board_.Hash(), static_cast(repetitions_)}); } bool Position::CanCastle(Castling castling) const { @@ -86,7 +84,10 @@ void PositionHistory::Reset(const ChessBoard& board, int no_capture_ply, } void PositionHistory::Append(Move m) { - positions_.emplace_back(Last(), m); + // TODO(mooskagh) That should be emplace_back(Last(), m), but MSVS STL + // has a bug in implementation of emplace_back, when + // reallocation happens. (it also reallocates Last()) + positions_.push_back(Position(Last(), m)); positions_.back().SetRepetitions(ComputeLastMoveRepetitions()); } @@ -105,4 +106,14 @@ int PositionHistory::ComputeLastMoveRepetitions() const { return 0; } +uint64_t PositionHistory::HashLast(int positions) const { + uint64_t hash = positions; + for (auto iter = positions_.rbegin(), end = positions_.rend(); iter != end; + ++iter) { + if (!positions--) break; + hash = HashCat(hash, iter->Hash()); + } + return HashCat(hash, Last().GetNoCapturePly()); +} + } // namespace lczero \ No newline at end of file diff --git a/lc0/src/chess/position.h b/lc0/src/chess/position.h index d59492518..f83130ba6 100644 --- a/lc0/src/chess/position.h +++ b/lc0/src/chess/position.h @@ -111,6 +111,9 @@ class PositionHistory { // Returns whether next move is history should be black's. bool IsBlackToMove() const { return Last().IsBlackToMove(); } + // Builds a hash from last X positions. + uint64_t HashLast(int positions) const; + private: int ComputeLastMoveRepetitions() const; diff --git a/lc0/src/uciloop.cc b/lc0/src/chess/uciloop.cc similarity index 97% rename from lc0/src/uciloop.cc rename to lc0/src/chess/uciloop.cc index 74d0049ee..d35ece762 100644 --- a/lc0/src/uciloop.cc +++ b/lc0/src/chess/uciloop.cc @@ -27,6 +27,7 @@ #include #include #include "utils/exception.h" +#include "utils/string.h" namespace lczero { @@ -121,12 +122,8 @@ bool UciLoop::DispatchCommand( if (ContainsKey(params, "fen") == ContainsKey(params, "startpos")) { throw Exception("Position requires either fen or startpos"); } - std::vector moves; - { - std::istringstream iss(GetOrEmpty(params, "moves")); - std::string move; - while (iss >> move) moves.push_back(move); - } + std::vector moves = + StrSplitAtWhitespace(GetOrEmpty(params, "moves")); CmdPosition(GetOrEmpty(params, "fen"), moves); } else if (command == "go") { GoParams go_params; diff --git a/lc0/src/uciloop.h b/lc0/src/chess/uciloop.h similarity index 98% rename from lc0/src/uciloop.h rename to lc0/src/chess/uciloop.h index 434751f97..23967ebb7 100644 --- a/lc0/src/uciloop.h +++ b/lc0/src/chess/uciloop.h @@ -22,7 +22,7 @@ #include #include #include -#include "mcts/callbacks.h" +#include "chess/callbacks.h" #include "utils/exception.h" namespace lczero { diff --git a/lc0/src/engine.cc b/lc0/src/engine.cc index a94b00ee9..9bf517048 100644 --- a/lc0/src/engine.cc +++ b/lc0/src/engine.cc @@ -26,10 +26,13 @@ namespace lczero { namespace { +// TODO(mooskagh) Move threads parameter handling to search. const int kDefaultThreads = 2; const char* kThreadsOption = "Number of worker threads"; const char* kDebugLogStr = "Do debug logging into file"; +// TODO(mooskagh) Move weights/backend/backend-opts parameter handling to +// network factory. const char* kWeightsStr = "Network weights file path"; const char* kNnBackendStr = "NN backend to use"; const char* kNnBackendOptionsStr = "NN backend parameters"; diff --git a/lc0/src/engine.h b/lc0/src/engine.h index 998d8060d..6eb2237d3 100644 --- a/lc0/src/engine.h +++ b/lc0/src/engine.h @@ -18,12 +18,12 @@ #pragma once +#include "chess/uciloop.h" #include "mcts/search.h" #include "neural/cache.h" #include "neural/network.h" -#include "optionsparser.h" -#include "uciloop.h" #include "utils/mutex.h" +#include "utils/optionsparser.h" // CUDNN eval // comment/disable this to enable tensor flow path diff --git a/lc0/src/main.cc b/lc0/src/main.cc index 294ad6dd3..9e56ba2ba 100644 --- a/lc0/src/main.cc +++ b/lc0/src/main.cc @@ -17,6 +17,7 @@ */ #include +#include "analyzer/analyzer.h" #include "engine.h" #include "selfplay/loop.h" #include "utils/commandline.h" @@ -28,13 +29,17 @@ int main(int argc, const char** argv) { using namespace lczero; CommandLine::Init(argc, argv); CommandLine::RegisterMode("uci", "(default) Act as UCI engine"); - CommandLine::RegisterMode("selfplay", "Play games with itself"); + CommandLine::RegisterMode("debug", "Generate debug data for a position"); if (CommandLine::ConsumeCommand("selfplay")) { // Selfplay mode. SelfPlayLoop loop; loop.RunLoop(); + } else if (CommandLine::ConsumeCommand("debug")) { + // Runs analyzer mode. + Analyzer analyzer; + analyzer.Run(); } else { // Consuming optional "uci" mode. CommandLine::ConsumeCommand("uci"); diff --git a/lc0/src/mcts/node.h b/lc0/src/mcts/node.h index 919d97741..0429a35cc 100644 --- a/lc0/src/mcts/node.h +++ b/lc0/src/mcts/node.h @@ -21,8 +21,8 @@ #include #include #include "chess/board.h" +#include "chess/callbacks.h" #include "chess/position.h" -#include "mcts/callbacks.h" #include "neural/writer.h" #include "utils/mutex.h" diff --git a/lc0/src/mcts/search.cc b/lc0/src/mcts/search.cc index 0299ec40d..17c6b9428 100644 --- a/lc0/src/mcts/search.cc +++ b/lc0/src/mcts/search.cc @@ -33,18 +33,20 @@ namespace lczero { -namespace { -const char* kMiniBatchSizeStr = "Minibatch size for NN inference"; -const char* kMiniPrefetchBatchStr = "Max prefetch nodes, per NN call"; -const char* kCpuctStr = "Cpuct MCTS option"; -const char* kTemperatureStr = "Initial temperature"; -const char* kTempDecayStr = "Per move temperature decay"; -const char* kNoiseStr = "Add Dirichlet noise at root node"; -const char* kVerboseStatsStr = "Display verbose move stats"; -const char* kSmartPruningStr = "Enable smart pruning"; -const char* kVirtualLossBugStr = "Virtual loss bug"; -const char* kFpuReductionStr = "First Play Urgency Inflation"; +const char* Search::kMiniBatchSizeStr = "Minibatch size for NN inference"; +const char* Search::kMiniPrefetchBatchStr = "Max prefetch nodes, per NN call"; +const char* Search::kCpuctStr = "Cpuct MCTS option"; +const char* Search::kTemperatureStr = "Initial temperature"; +const char* Search::kTempDecayStr = "Per move temperature decay"; +const char* Search::kNoiseStr = "Add Dirichlet noise at root node"; +const char* Search::kVerboseStatsStr = "Display verbose move stats"; +const char* Search::kSmartPruningStr = "Enable smart pruning"; +const char* Search::kVirtualLossBugStr = "Virtual loss bug"; +const char* Search::kFpuReductionStr = "First Play Urgency Reduction"; +const char* Search::kCacheHistoryLengthStr = + "Length of history to include in cache"; +namespace { const int kSmartPruningToleranceNodes = 100; const int kSmartPruningToleranceMs = 200; } // namespace @@ -61,7 +63,9 @@ void Search::PopulateUciParams(OptionsParser* options) { options->Add(kVirtualLossBugStr, -100, 100, "virtual-loss-bug") = 0.0f; options->Add(kFpuReductionStr, -100, 100, "fpu-reduction") = - -0.2f; + 0.2f; + options->Add(kCacheHistoryLengthStr, 1, 8, + "cache-history-length") = 8; } Search::Search(const NodeTree& tree, Network* network, @@ -86,13 +90,14 @@ Search::Search(const NodeTree& tree, Network* network, kVerboseStats(options.Get(kVerboseStatsStr)), kSmartPruning(options.Get(kSmartPruningStr)), kVirtualLossBug(options.Get(kVirtualLossBugStr)), - kFpuReduction(options.Get(kFpuReductionStr)) {} + kFpuReduction(options.Get(kFpuReductionStr)), + kCacheHistoryLength(options.Get(kCacheHistoryLengthStr)) {} // Returns whether node was already in cache. bool Search::AddNodeToCompute(Node* node, CachingComputation* computation, const PositionHistory& history, bool add_if_cached) { - auto hash = history.Last().Hash(); + auto hash = history.HashLast(kCacheHistoryLength); // If already in cache, no need to do anything. if (add_if_cached) { if (computation->AddInputByHash(hash)) return true; diff --git a/lc0/src/mcts/search.h b/lc0/src/mcts/search.h index 6d66839f8..f1bf45ca1 100644 --- a/lc0/src/mcts/search.h +++ b/lc0/src/mcts/search.h @@ -21,14 +21,14 @@ #include #include #include -#include "mcts/callbacks.h" +#include "chess/callbacks.h" +#include "chess/uciloop.h" #include "mcts/node.h" #include "neural/cache.h" #include "neural/network.h" -#include "optionsparser.h" -#include "uciloop.h" #include "utils/mutex.h" #include "utils/optionsdict.h" +#include "utils/optionsparser.h" namespace lczero { @@ -70,6 +70,19 @@ class Search { // Returns best move, from the point of view of white player. And also ponder. std::pair GetBestMove() const; + // Strings for UCI params. So that others can override defaults. + static const char* kMiniBatchSizeStr; + static const char* kMiniPrefetchBatchStr; + static const char* kCpuctStr; + static const char* kTemperatureStr; + static const char* kTempDecayStr; + static const char* kNoiseStr; + static const char* kVerboseStatsStr; + static const char* kSmartPruningStr; + static const char* kVirtualLossBugStr; + static const char* kFpuReductionStr; + static const char* kCacheHistoryLengthStr; + private: // Can run several copies of it in separate threads. void Worker(); @@ -138,6 +151,7 @@ class Search { const bool kSmartPruning; const float kVirtualLossBug; const float kFpuReduction; + const bool kCacheHistoryLength; }; } // namespace lczero \ No newline at end of file diff --git a/lc0/src/selfplay/game.cc b/lc0/src/selfplay/game.cc index a48e68e9d..fdcfd3335 100644 --- a/lc0/src/selfplay/game.cc +++ b/lc0/src/selfplay/game.cc @@ -63,8 +63,8 @@ void SelfPlayGame::Play(int white_threads, int black_threads) { if (abort_) break; // Append training data. - training_data_.push_back(tree_[0]->GetCurrentHead()->GetV3TrainingData( - GameResult::UNDECIDED, tree_[0]->GetPositionHistory())); + training_data_.push_back(tree_[idx]->GetCurrentHead()->GetV3TrainingData( + GameResult::UNDECIDED, tree_[idx]->GetPositionHistory())); // Add best move to the tree. Move move = search_->GetBestMove().first; diff --git a/lc0/src/selfplay/game.h b/lc0/src/selfplay/game.h index e1ee2053d..d8257d207 100644 --- a/lc0/src/selfplay/game.h +++ b/lc0/src/selfplay/game.h @@ -19,11 +19,11 @@ #pragma once #include "chess/position.h" +#include "chess/uciloop.h" #include "mcts/search.h" #include "neural/cache.h" #include "neural/network.h" -#include "optionsparser.h" -#include "uciloop.h" +#include "utils/optionsparser.h" namespace lczero { diff --git a/lc0/src/selfplay/loop.h b/lc0/src/selfplay/loop.h index 13c977d91..0a1a22559 100644 --- a/lc0/src/selfplay/loop.h +++ b/lc0/src/selfplay/loop.h @@ -19,9 +19,9 @@ #pragma once #include -#include "optionsparser.h" +#include "chess/uciloop.h" #include "selfplay/tournament.h" -#include "uciloop.h" +#include "utils/optionsparser.h" namespace lczero { diff --git a/lc0/src/selfplay/tournament.cc b/lc0/src/selfplay/tournament.cc index 9b67ceb7a..31c8cc730 100644 --- a/lc0/src/selfplay/tournament.cc +++ b/lc0/src/selfplay/tournament.cc @@ -20,8 +20,8 @@ #include "mcts/search.h" #include "neural/factory.h" #include "neural/loader.h" -#include "optionsparser.h" #include "selfplay/game.h" +#include "utils/optionsparser.h" #include "utils/random.h" namespace lczero { diff --git a/lc0/src/selfplay/tournament.h b/lc0/src/selfplay/tournament.h index 0877c4c00..3e6b1893d 100644 --- a/lc0/src/selfplay/tournament.h +++ b/lc0/src/selfplay/tournament.h @@ -19,10 +19,10 @@ #pragma once #include -#include "optionsparser.h" #include "selfplay/game.h" #include "utils/mutex.h" #include "utils/optionsdict.h" +#include "utils/optionsparser.h" namespace lczero { diff --git a/lc0/src/utils/hashcat.h b/lc0/src/utils/hashcat.h index fdbac5f35..cd11e8471 100644 --- a/lc0/src/utils/hashcat.h +++ b/lc0/src/utils/hashcat.h @@ -28,12 +28,16 @@ inline uint64_t Hash(uint64_t val) { 0x7acec0050bf82f43ULL * ((val >> 31) + 0xd571b3a92b1b2755ULL); } -// Combines 64-bit hashes into one. +// Appends value to a hash. +inline uint64_t HashCat(uint64_t hash, uint64_t x) { + hash ^= 0x299799adf0d95defULL + Hash(x) + (hash << 6) + (hash >> 2); + return hash; +} + +// Combines 64-bit values into concatenated hash. 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); - } + for (uint64_t x : args) hash = HashCat(hash, x); return hash; } diff --git a/lc0/src/utils/optionsdict.h b/lc0/src/utils/optionsdict.h index 472987c18..b2a40ca0c 100644 --- a/lc0/src/utils/optionsdict.h +++ b/lc0/src/utils/optionsdict.h @@ -117,7 +117,7 @@ T OptionsDict::GetOrDefault(const std::string& key, template void OptionsDict::Set(const std::string& key, const T& value) { - TypeDict::dict_.emplace(key, value); + TypeDict::dict_[key] = value; } template diff --git a/lc0/src/optionsparser.cc b/lc0/src/utils/optionsparser.cc similarity index 98% rename from lc0/src/optionsparser.cc rename to lc0/src/utils/optionsparser.cc index 30c556632..063e73ed2 100644 --- a/lc0/src/optionsparser.cc +++ b/lc0/src/utils/optionsparser.cc @@ -356,12 +356,14 @@ bool FloatOption::ProcessShortFlagWithValue(char flag, const std::string& value, std::string FloatOption::GetHelp(const OptionsDict& dict) const { std::string long_flag = GetLongFlag(); if (!long_flag.empty()) { - long_flag += "=" + std::to_string(min_) + ".." + std::to_string(max_); + std::ostringstream oss; + oss << std::setprecision(3) << min_ << ".." << max_; + long_flag += "=" + oss.str(); } - return FormatFlag(GetShortFlag(), long_flag, GetName(), - std::to_string(GetVal(dict)) + - " min: " + std::to_string(min_) + - " max: " + std::to_string(max_)); + std::ostringstream oss; + oss << std::setprecision(3) << GetVal(dict) << " min: " << min_ + << " max: " << max_; + return FormatFlag(GetShortFlag(), long_flag, GetName(), oss.str()); } std::string FloatOption::GetOptionString(const OptionsDict& dict) const { diff --git a/lc0/src/optionsparser.h b/lc0/src/utils/optionsparser.h similarity index 98% rename from lc0/src/optionsparser.h rename to lc0/src/utils/optionsparser.h index abc903dbe..9c38803a7 100644 --- a/lc0/src/optionsparser.h +++ b/lc0/src/utils/optionsparser.h @@ -96,6 +96,8 @@ class OptionsParser { const OptionsDict& GetOptionsDict(const std::string& context = {}); // Gets the dictionary for given context which caller can modify. OptionsDict* GetMutableOptions(const std::string& context = {}); + // Gets the mutable list of default options. + OptionsDict* GetMutableDefaultsOptions() { return &defaults_; } // Adds a subdictionary for a given context. void AddContext(const std::string&); diff --git a/lc0/src/utils/string.cc b/lc0/src/utils/string.cc new file mode 100644 index 000000000..a13344855 --- /dev/null +++ b/lc0/src/utils/string.cc @@ -0,0 +1,64 @@ +/* + 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/string.h" + +#include +#include + +namespace lczero { + +std::string StrJoin(const std::vector& strings, + const std::string& delim) { + std::string res; + for (const auto& str : strings) { + if (!res.empty()) res += delim; + res += str; + } + return res; +} + +std::vector StrSplitAtWhitespace(const std::string& str) { + std::vector result; + std::istringstream iss(str); + std::string tmp; + while (iss >> tmp) result.emplace_back(std::move(tmp)); + return result; +} + +std::vector StrSplit(const std::string& str, + const std::string& delim) { + std::vector result; + for (std::string::size_type pos = 0, next = 0; pos != std::string::npos; + pos = next) { + next = str.find(delim, pos); + result.push_back(str.substr(pos, next)); + if (next != std::string::npos) next += delim.size(); + } + return result; +} + +std::vector ParseIntList(const std::string& str) { + std::vector result; + for (const auto& x : StrSplit(str, ",")) { + result.push_back(std::stoi(x)); + } + return result; +} + +} // namespace lczero \ No newline at end of file diff --git a/lc0/src/utils/string.h b/lc0/src/utils/string.h new file mode 100644 index 000000000..d95a262a6 --- /dev/null +++ b/lc0/src/utils/string.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 . +*/ + +#pragma once + +#include +#include + +namespace lczero { + +// Joins strings using @delim as delimeter. +std::string StrJoin(const std::vector& strings, + const std::string& delim = " "); + +// Splits strings at whitespace. +std::vector StrSplitAtWhitespace(const std::string& str); + +// Split string by delimeter. +std::vector StrSplit(const std::string& str, + const std::string& delim); + +// Parses comma-separated list of integers. +std::vector ParseIntList(const std::string& str); + +} // namespace lczero diff --git a/src/Network.cpp b/src/Network.cpp index 34e01857c..9d8d3f03a 100644 --- a/src/Network.cpp +++ b/src/Network.cpp @@ -1054,6 +1054,9 @@ Network::Netresult Network::get_scored_moves(const BoardHistory& pos, DebugRawDa } Network::Netresult Network::get_scored_moves_internal(const BoardHistory& pos, NNPlanes& planes, DebugRawData* debug_data) { + // NNPlanes is sized to support either version, so this assert uses + // MAX_INPUT_CHANNELS. The rest of the code uses get_input_channels() + // to match the actual number of bits expected by each network. assert(MAX_INPUT_CHANNELS == planes.bit.size()+3); constexpr int width = 8; constexpr int height = 8; @@ -1066,8 +1069,8 @@ Network::Netresult Network::get_scored_moves_internal(const BoardHistory& pos, N std::vector winrate_data(Network::NUM_VALUE_CHANNELS); std::vector winrate_out(1); // Data layout is input_data[(c * height + h) * width + w] - input_data.reserve(MAX_INPUT_CHANNELS * width * height); - for (int c = 0; c < MAX_INPUT_CHANNELS - 3; ++c) { + input_data.reserve(get_input_channels() * width * height); + for (int c = 0; c < get_input_channels() - 3; ++c) { for (int i = 0; i < 64; ++i) { input_data.emplace_back(net_t(planes.bit[c][i])); } @@ -1083,7 +1086,7 @@ Network::Netresult Network::get_scored_moves_internal(const BoardHistory& pos, N for (int i = 0; i < 64; ++i) { input_data.emplace_back(net_t(m_format_version == 1 ? 0.0 : 1.0)); } - assert(input_data.size() == MAX_INPUT_CHANNELS * width * height); + assert(input_data.size() == get_input_channels() * width * height); #ifdef USE_OPENCL opencl.forward(input_data, policy_data, value_data); #elif defined(USE_BLAS) && !defined(USE_OPENCL) diff --git a/src/config.h b/src/config.h index 387232ec2..b2973d07e 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.9" +#define PROGRAM_VERSION "v0.10" // OpenBLAS limitation #if defined(USE_BLAS) && defined(USE_OPENBLAS)