Skip to content

Commit

Permalink
Also stats of opponent's moves into verbose-move-stats (#480)
Browse files Browse the repository at this point in the history
* Refactor verbose move stats. Doesn't yet include opponent's move stats.

* Refactor best move handling.

* Output stats for possible opponent's moves into log.
  • Loading branch information
mooskagh authored Nov 3, 2018
1 parent 76ec079 commit 7f1dfb8
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 69 deletions.
2 changes: 2 additions & 0 deletions src/mcts/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ Edge* Node::GetEdgeToNode(const Node* node) const {
return &edges_[node->index_];
}

Edge* Node::GetOwnEdge() const { return GetParent()->GetEdgeToNode(this); }

std::string Node::DebugString() const {
std::ostringstream oss;
oss << " Term:" << is_terminal_ << " This:" << this << " Parent:" << parent_
Expand Down
7 changes: 6 additions & 1 deletion src/mcts/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ class Node {
// For a child node, returns corresponding edge.
Edge* GetEdgeToNode(const Node* node) const;

// Returns edge to the own node.
Edge* GetOwnEdge() const;

// Debug information about the node.
std::string DebugString() const;

Expand Down Expand Up @@ -309,7 +312,9 @@ class EdgeAndNode {

// Edge related getters.
float GetP() const { return edge_->GetP(); }
Move GetMove(bool flip = false) const { return edge_->GetMove(flip); }
Move GetMove(bool flip = false) const {
return edge_ ? edge_->GetMove(flip) : Move();
}

// Returns U = numerator * p / N.
// Passed numerator is expected to be equal to (cpuct * sqrt(N[parent])).
Expand Down
142 changes: 84 additions & 58 deletions src/mcts/search.cc
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ void ApplyDirichletNoise(Node* node, float eps, double alpha) {
} // namespace

void Search::SendUciInfo() REQUIRES(nodes_mutex_) {
if (!best_move_edge_) return;
if (!current_best_edge_) return;

auto edges = GetBestChildrenNoTemperature(root_node_, params_.GetMultiPv());
auto score_type = params_.GetScoreType();
Expand Down Expand Up @@ -143,7 +143,7 @@ void Search::SendUciInfo() REQUIRES(nodes_mutex_) {
}

if (!uci_infos.empty()) last_outputted_uci_info_ = uci_infos.front();
if (!edges.empty()) last_outputted_best_move_edge_ = best_move_edge_.edge();
if (!edges.empty()) last_outputted_info_edge_ = current_best_edge_.edge();

info_callback_(uci_infos);
}
Expand All @@ -153,8 +153,8 @@ void Search::SendUciInfo() REQUIRES(nodes_mutex_) {
void Search::MaybeOutputInfo() {
SharedMutex::Lock lock(nodes_mutex_);
Mutex::Lock counters_lock(counters_mutex_);
if (!responded_bestmove_ && best_move_edge_ &&
(best_move_edge_.edge() != last_outputted_best_move_edge_ ||
if (!bestmove_is_sent_ && current_best_edge_ &&
(current_best_edge_.edge() != last_outputted_info_edge_ ||
last_outputted_uci_info_.depth !=
static_cast<int>(cum_depth_ /
(total_playouts_ ? total_playouts_ : 1)) ||
Expand Down Expand Up @@ -184,16 +184,16 @@ int64_t Search::GetTimeToDeadline() const {
.count();
}

void Search::SendMovesStats() const {
std::vector<std::string> Search::GetVerboseStats(Node* node,
bool is_black_to_move) const {
const float parent_q =
-root_node_->GetQ() -
params_.GetFpuReduction() * std::sqrt(root_node_->GetVisitedPolicy());
-node->GetQ() -
params_.GetFpuReduction() * std::sqrt(node->GetVisitedPolicy());
const float U_coeff =
params_.GetCpuct() *
std::sqrt(std::max(root_node_->GetChildrenVisits(), 1u));
params_.GetCpuct() * std::sqrt(std::max(node->GetChildrenVisits(), 1u));

std::vector<EdgeAndNode> edges;
for (const auto& edge : root_node_->Edges()) edges.push_back(edge);
for (const auto& edge : node->Edges()) edges.push_back(edge);

std::sort(edges.begin(), edges.end(),
[&parent_q, &U_coeff](EdgeAndNode a, EdgeAndNode b) {
Expand All @@ -203,11 +203,8 @@ void Search::SendMovesStats() const {
b.GetQ(parent_q) + b.GetU(U_coeff));
});

const bool is_black_to_move = played_history_.IsBlackToMove();
std::vector<ThinkingInfo> infos;
std::vector<std::string> infos;
for (const auto& edge : edges) {
infos.emplace_back();
ThinkingInfo& info = infos.back();
std::ostringstream oss;
oss << std::fixed;

Expand Down Expand Up @@ -236,7 +233,7 @@ void Search::SendMovesStats() const {
if (edge.IsTerminal()) {
v = edge.node()->GetQ();
} else {
NNCacheLock nneval = GetCachedFirstPlyResult(edge);
NNCacheLock nneval = GetCachedNNEval(edge.node());
if (nneval) v = -nneval->q;
}
if (v) {
Expand All @@ -247,21 +244,50 @@ void Search::SendMovesStats() const {
oss << ") ";

if (edge.IsTerminal()) oss << "(T) ";
infos.emplace_back(oss.str());
}
return infos;
}

info.comment = oss.str();
void Search::SendMovesStats() const REQUIRES(counters_mutex_) {
const bool is_black_to_move = played_history_.IsBlackToMove();
auto move_stats = GetVerboseStats(root_node_, is_black_to_move);

if (params_.GetVerboseStats()) {
std::vector<ThinkingInfo> infos;
std::transform(move_stats.begin(), move_stats.end(),
std::back_inserter(infos), [](const std::string& line) {
ThinkingInfo info;
info.comment = line;
return info;
});
info_callback_(infos);
} else {
LOGFILE << "=== Move stats:";
for (const auto& line : move_stats) LOGFILE << line;
}
if (final_bestmove_.HasNode()) {
LOGFILE
<< "--- Opponent moves after: "
<< final_bestmove_.GetMove(played_history_.IsBlackToMove()).as_string();
for (const auto& line :
GetVerboseStats(final_bestmove_.node(), !is_black_to_move)) {
LOGFILE << line;
}
}
info_callback_(infos);
}

NNCacheLock Search::GetCachedFirstPlyResult(EdgeAndNode edge) const {
if (!edge.HasNode()) return {};
assert(edge.node()->GetParent() == root_node_);
// It would be relatively straightforward to generalize this to fetch NN
// results for an abitrary move.
optional<float> retval;
PositionHistory history(played_history_); // Is it worth it to move this
// initialization to SendMoveStats, reducing n memcpys to 1? Probably not.
history.Append(edge.GetMove());
NNCacheLock Search::GetCachedNNEval(Node* node) const {
if (!node) return {};

std::vector<Move> moves;
for (; node != root_node_; node = node->GetParent()) {
moves.push_back(node->GetOwnEdge()->GetMove());
}
PositionHistory history(played_history_);
for (auto iter = moves.rbegin(), end = moves.rend(); iter != end; ++iter) {
history.Append(*iter);
}
auto hash = history.HashLast(params_.GetCacheHistoryLength() + 1);
NNCacheLock nneval(cache_, hash);
return nneval;
Expand All @@ -271,14 +297,14 @@ void Search::MaybeTriggerStop() {
SharedMutex::Lock nodes_lock(nodes_mutex_);
Mutex::Lock lock(counters_mutex_);
// Already responded bestmove, nothing to do here.
if (responded_bestmove_) return;
if (bestmove_is_sent_) return;
// Don't stop when the root node is not yet expanded.
if (total_playouts_ == 0) return;

// If not yet stopped, try to stop for different reasons.
if (!stop_.load(std::memory_order_acquire)) {
// If smart pruning tells to stop (best move found), stop.
if (found_best_move_) {
if (only_one_possible_move_left_) {
FireStopInternal();
LOGFILE << "Stopped search: Only one move candidate left.";
}
Expand Down Expand Up @@ -310,13 +336,15 @@ void Search::MaybeTriggerStop() {
}
// If we are the first to see that stop is needed.
if (stop_.load(std::memory_order_acquire) && ok_to_respond_bestmove_ &&
!responded_bestmove_) {
!bestmove_is_sent_) {
SendUciInfo();
if (params_.GetVerboseStats()) SendMovesStats();
best_move_ = GetBestMoveInternal();
best_move_callback_({best_move_.first, best_move_.second});
responded_bestmove_ = true;
best_move_edge_ = EdgeAndNode();
EnsureBestMoveKnown();
SendMovesStats();
best_move_callback_(
{final_bestmove_.GetMove(played_history_.IsBlackToMove()),
final_pondermove_.GetMove(!played_history_.IsBlackToMove())});
bestmove_is_sent_ = true;
current_best_edge_ = EdgeAndNode();
}
}

Expand Down Expand Up @@ -380,10 +408,12 @@ float Search::GetBestEval() const {
return best_edge.GetQ(parent_q);
}

std::pair<Move, Move> Search::GetBestMove() const {
SharedMutex::SharedLock lock(nodes_mutex_);
std::pair<Move, Move> Search::GetBestMove() {
SharedMutex::Lock lock(nodes_mutex_);
Mutex::Lock counters_lock(counters_mutex_);
return GetBestMoveInternal();
EnsureBestMoveKnown();
return {final_bestmove_.GetMove(played_history_.IsBlackToMove()),
final_pondermove_.GetMove(!played_history_.IsBlackToMove())};
}

std::int64_t Search::GetTotalPlayouts() const {
Expand All @@ -408,11 +438,11 @@ bool Search::PopulateRootMoveLimit(MoveList* root_moves) const {
syzygy_tb_->root_probe_wdl(played_history_.Last(), root_moves);
}

// Returns the best move, maybe with temperature (according to the settings).
std::pair<Move, Move> Search::GetBestMoveInternal() const
REQUIRES_SHARED(nodes_mutex_) REQUIRES_SHARED(counters_mutex_) {
if (responded_bestmove_) return best_move_;
if (!root_node_->HasChildren()) return {};
// Computes the best move, maybe with temperature (according to the settings).
void Search::EnsureBestMoveKnown() REQUIRES(nodes_mutex_)
REQUIRES(counters_mutex_) {
if (bestmove_is_sent_) return;
if (!root_node_->HasChildren()) return;

float temperature = params_.GetTemperature();
if (temperature && params_.GetTempDecayMoves()) {
Expand All @@ -425,17 +455,13 @@ std::pair<Move, Move> Search::GetBestMoveInternal() const
}
}

auto best_node = temperature
? GetBestChildWithTemperature(root_node_, temperature)
: GetBestChildNoTemperature(root_node_);
final_bestmove_ = temperature
? GetBestChildWithTemperature(root_node_, temperature)
: GetBestChildNoTemperature(root_node_);

Move ponder_move; // Default is "null move" which means "don't display
// anything".
if (best_node.HasNode() && best_node.node()->HasChildren()) {
ponder_move = GetBestChildNoTemperature(best_node.node())
.GetMove(!played_history_.IsBlackToMove());
if (final_bestmove_.HasNode() && final_bestmove_.node()->HasChildren()) {
final_pondermove_ = GetBestChildNoTemperature(final_bestmove_.node());
}
return {best_node.GetMove(played_history_.IsBlackToMove()), ponder_move};
}

// Returns @count children with most visits.
Expand Down Expand Up @@ -570,7 +596,7 @@ void Search::WatchdogThread() {
Mutex::Lock lock(counters_mutex_);
// Only exit when bestmove is responded. It may happen that search threads
// already all exited, and we need at least one thread that can do that.
if (responded_bestmove_) break;
if (bestmove_is_sent_) break;

auto remaining_time = limits_.search_deadline
? std::chrono::milliseconds(GetTimeToDeadline())
Expand Down Expand Up @@ -604,7 +630,7 @@ void Search::Stop() {

void Search::Abort() {
Mutex::Lock lock(counters_mutex_);
responded_bestmove_ = true;
bestmove_is_sent_ = true;
FireStopInternal();
LOGFILE << "Aborting search, if it is still active.";
}
Expand Down Expand Up @@ -764,7 +790,7 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend(
SharedMutex::Lock lock(search_->nodes_mutex_);

// Fetch the current best root node visits for possible smart pruning.
int64_t best_node_n = search_->best_move_edge_.GetN();
int64_t best_node_n = search_->current_best_edge_.GetN();

// True on first iteration, false as we dive deeper.
bool is_root_node = true;
Expand Down Expand Up @@ -816,7 +842,7 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend(
// best_move_node_ could have changed since best_node_n was retrieved.
// To ensure we have at least one node to expand, always include
// current best node.
if (child != search_->best_move_edge_ &&
if (child != search_->current_best_edge_ &&
search_->remaining_playouts_ < best_node_n - child.GetN()) {
continue;
}
Expand Down Expand Up @@ -854,7 +880,7 @@ SearchWorker::NodeToProcess SearchWorker::PickNodeToExtend(
// If there is only one move theoretically possible within remaining time,
// output it.
Mutex::Lock counters_lock(search_->counters_mutex_);
search_->found_best_move_ = true;
search_->only_one_possible_move_left_ = true;
}
is_root_node = false;
}
Expand Down Expand Up @@ -1150,8 +1176,8 @@ void SearchWorker::DoBackupUpdateSingleNode(
// Update the stats.
// Best move.
if (n->GetParent() == search_->root_node_ &&
search_->best_move_edge_.GetN() <= n->GetN()) {
search_->best_move_edge_ =
search_->current_best_edge_.GetN() <= n->GetN()) {
search_->current_best_edge_ =
search_->GetBestChildNoTemperature(search_->root_node_);
}
}
Expand Down
25 changes: 15 additions & 10 deletions src/mcts/search.h
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ class Search {

// Returns best move, from the point of view of white player. And also ponder.
// May or may not use temperature, according to the settings.
std::pair<Move, Move> GetBestMove() const;
std::pair<Move, Move> GetBestMove();
// Returns the evaluation of the best move, WITHOUT temperature. This differs
// from the above function; with temperature enabled, these two functions may
// return results from different possible moves.
Expand All @@ -96,8 +96,8 @@ class Search {
const SearchParams& GetParams() const { return params_; }

private:
// Returns the best move, maybe with temperature (according to the settings).
std::pair<Move, Move> GetBestMoveInternal() const;
// Computes the best move, maybe with temperature (according to the settings).
void EnsureBestMoveKnown();

// Returns a child with most visits, with or without temperature.
// NoTemperature is safe to use on non-extended nodes, while WithTemperature
Expand Down Expand Up @@ -126,8 +126,12 @@ class Search {
// Returns true if the population came from tablebase.
bool PopulateRootMoveLimit(MoveList* root_moves) const;

// We only need first ply for debug output, but could be easily generalized.
NNCacheLock GetCachedFirstPlyResult(EdgeAndNode) const;
// Returns verbose information about given node, as vector of strings.
std::vector<std::string> GetVerboseStats(Node* node,
bool is_black_to_move) const;

// Returns NN eval for a given node from cache, if that node is cached.
NNCacheLock GetCachedNNEval(Node* node) const;

mutable Mutex counters_mutex_ ACQUIRED_AFTER(nodes_mutex_);
// Tells all threads to stop.
Expand All @@ -140,12 +144,13 @@ class Search {
bool ok_to_respond_bestmove_ GUARDED_BY(counters_mutex_) = true;
// There is already one thread that responded bestmove, other threads
// should not do that.
bool responded_bestmove_ GUARDED_BY(counters_mutex_) = false;
bool bestmove_is_sent_ GUARDED_BY(counters_mutex_) = false;
// Becomes true when smart pruning decides that no better move can be found.
bool found_best_move_ GUARDED_BY(counters_mutex_) = false;
bool only_one_possible_move_left_ GUARDED_BY(counters_mutex_) = false;
// Stored so that in the case of non-zero temperature GetBestMove() returns
// consistent results.
std::pair<Move, Move> best_move_ GUARDED_BY(counters_mutex_);
EdgeAndNode final_bestmove_ GUARDED_BY(counters_mutex_);
EdgeAndNode final_pondermove_ GUARDED_BY(counters_mutex_);

Mutex threads_mutex_;
std::vector<std::thread> threads_ GUARDED_BY(threads_mutex_);
Expand All @@ -163,8 +168,8 @@ class Search {
optional<std::chrono::steady_clock::time_point> nps_start_time_;

mutable SharedMutex nodes_mutex_;
EdgeAndNode best_move_edge_ GUARDED_BY(nodes_mutex_);
Edge* last_outputted_best_move_edge_ GUARDED_BY(nodes_mutex_) = nullptr;
EdgeAndNode current_best_edge_ GUARDED_BY(nodes_mutex_);
Edge* last_outputted_info_edge_ GUARDED_BY(nodes_mutex_) = nullptr;
ThinkingInfo last_outputted_uci_info_ GUARDED_BY(nodes_mutex_);
int64_t total_playouts_ GUARDED_BY(nodes_mutex_) = 0;
int64_t remaining_playouts_ GUARDED_BY(nodes_mutex_) =
Expand Down

0 comments on commit 7f1dfb8

Please sign in to comment.