From 29c166a0728c753b4984c18bad96fd35a8f6bffe Mon Sep 17 00:00:00 2001 From: Omri Mor Date: Thu, 7 Dec 2017 17:33:28 -0600 Subject: [PATCH] MPI/Cluster implementation for Stockfish MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Based on Peter Ă–sterlund's "Lazy Cluster" algorithm, but with some simplifications. To compile, point COMPCXX to the MPI C++ compiler wrapper (mpicxx). --- src/Makefile | 14 ++- src/cluster.cpp | 197 +++++++++++++++++++++++++++++++++++++++++ src/cluster.h | 94 ++++++++++++++++++++ src/main.cpp | 6 +- src/search.cpp | 79 ++++++++++------- src/syzygy/tbprobe.cpp | 4 +- src/thread.h | 8 ++ src/tt.h | 9 ++ src/uci.cpp | 18 ++-- 9 files changed, 388 insertions(+), 41 deletions(-) create mode 100644 src/cluster.cpp create mode 100644 src/cluster.h diff --git a/src/Makefile b/src/Makefile index 8b9b16e4..879d7a1e 100644 --- a/src/Makefile +++ b/src/Makefile @@ -36,7 +36,7 @@ BINDIR = $(PREFIX)/bin PGOBENCH = ./$(EXE) bench ### Object files -OBJS = benchmark.o bitbase.o bitboard.o endgame.o evaluate.o main.o \ +OBJS = benchmark.o bitbase.o bitboard.o cluster.o endgame.o evaluate.o main.o \ material.o misc.o movegen.o movepick.o pawns.o position.o psqt.o \ search.o thread.o timeman.o tt.o uci.o ucioption.o syzygy/tbprobe.o @@ -64,6 +64,7 @@ endif # popcnt = yes/no --- -DUSE_POPCNT --- Use popcnt asm-instruction # sse = yes/no --- -msse --- Use Intel Streaming SIMD Extensions # pext = yes/no --- -DUSE_PEXT --- Use pext x86_64 asm-instruction +# mpi = yes/no --- -DUSE_MPI --- Use Message Passing Interface # # Note that Makefile is space sensitive, so when adding new architectures # or modifying existing flags, you have to make sure there are no extra spaces @@ -78,6 +79,7 @@ prefetch = no popcnt = no sse = no pext = no +mpi = no ### 2.2 Architecture specific @@ -354,6 +356,15 @@ ifeq ($(OS), Android) LDFLAGS += -fPIE -pie endif +### 3.10 MPI +ifeq ($(CXX),$(filter $(CXX),mpicxx mpic++ mpiCC)) + mpi = yes +endif + +ifeq ($(mpi),yes) + CXXFLAGS += -DUSE_MPI +endif + ### ========================================================================== ### Section 4. Public targets @@ -472,6 +483,7 @@ config-sanity: @echo "popcnt: '$(popcnt)'" @echo "sse: '$(sse)'" @echo "pext: '$(pext)'" + @echo "mpi: '$(mpi)'" @echo "" @echo "Flags:" @echo "CXX: $(CXX)" diff --git a/src/cluster.cpp b/src/cluster.cpp new file mode 100644 index 00000000..fd5c9daf --- /dev/null +++ b/src/cluster.cpp @@ -0,0 +1,197 @@ +/* + 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 . +*/ + +#ifdef USE_MPI + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "cluster.h" +#include "thread.h" +#include "tt.h" + +namespace Cluster { + +static int world_rank = MPI_PROC_NULL; +static int world_size = 0; + +static MPI_Comm InputComm = MPI_COMM_NULL; +static MPI_Comm TTComm = MPI_COMM_NULL; +static MPI_Comm MoveComm = MPI_COMM_NULL; + +static MPI_Datatype TTEntryDatatype = MPI_DATATYPE_NULL; +static std::vector TTBuff; + +static MPI_Op BestMoveOp = MPI_OP_NULL; +static MPI_Datatype MIDatatype = MPI_DATATYPE_NULL; + +static void BestMove(void* in, void* inout, int* len, MPI_Datatype* datatype) { + if (*datatype != MIDatatype) + MPI_Abort(MPI_COMM_WORLD, EXIT_FAILURE); + MoveInfo* l = static_cast(in); + MoveInfo* r = static_cast(inout); + for (int i=0; i < *len; ++i) + { + if ( l[i].depth > r[i].depth + && (l[i].score >= r[i].score || l[i].score >= VALUE_MATE_IN_MAX_PLY)) + r[i] = l[i]; + } +} + +void init() { + int thread_support; + constexpr std::array TTblocklens = {1, 1, 1, 1, 1, 1}; + const std::array TTdisps = {offsetof(TTEntry, key16), + offsetof(TTEntry, move16), + offsetof(TTEntry, value16), + offsetof(TTEntry, eval16), + offsetof(TTEntry, genBound8), + offsetof(TTEntry, depth8)}; + const std::array TTtypes = {MPI_UINT16_T, + MPI_UINT16_T, + MPI_INT16_T, + MPI_INT16_T, + MPI_UINT8_T, + MPI_INT8_T}; + const std::array MIdisps = {offsetof(MoveInfo, depth), + offsetof(MoveInfo, score), + offsetof(MoveInfo, rank)}; + + MPI_Init_thread(nullptr, nullptr, MPI_THREAD_MULTIPLE, &thread_support); + if (thread_support < MPI_THREAD_MULTIPLE) + { + std::cerr << "Stockfish requires support for MPI_THREAD_MULTIPLE." + << std::endl; + std::exit(EXIT_FAILURE); + } + + MPI_Comm_rank(MPI_COMM_WORLD, &world_rank); + MPI_Comm_size(MPI_COMM_WORLD, &world_size); + + TTBuff.resize(TTSendBufferSize * world_size); + + MPI_Type_create_struct(6, TTblocklens.data(), TTdisps.data(), TTtypes.data(), + &TTEntryDatatype); + MPI_Type_commit(&TTEntryDatatype); + + MPI_Type_create_hindexed_block(3, 1, MIdisps.data(), MPI_INT, &MIDatatype); + MPI_Type_commit(&MIDatatype); + MPI_Op_create(BestMove, true, &BestMoveOp); + + MPI_Comm_dup(MPI_COMM_WORLD, &InputComm); + MPI_Comm_dup(MPI_COMM_WORLD, &TTComm); + MPI_Comm_dup(MPI_COMM_WORLD, &MoveComm); +} + +void finalize() { + MPI_Finalize(); +} + +bool getline(std::istream& input, std::string& str) { + int size; + std::vector vec; + bool state; + + if (is_root()) + { + state = static_cast(std::getline(input, str)); + vec.assign(str.begin(), str.end()); + size = vec.size(); + } + MPI_Bcast(&size, 1, MPI_UNSIGNED_LONG, 0, InputComm); + if (!is_root()) + vec.resize(size); + MPI_Bcast(vec.data(), size, MPI_CHAR, 0, InputComm); + if (!is_root()) + str.assign(vec.begin(), vec.end()); + MPI_Bcast(&state, 1, MPI_CXX_BOOL, 0, InputComm); + return state; +} + +int size() { + return world_size; +} + +int rank() { + return world_rank; +} + +void save(Thread* thread, TTEntry* tte, + Key k, Value v, Bound b, Depth d, Move m, Value ev, uint8_t g) { + tte->save(k, v, b, d, m, ev, g); + // Try to add to thread's send buffer + { + std::lock_guard lk(thread->ttBuffer.mutex); + thread->ttBuffer.buffer.replace(*tte); + } + + // Communicate on main search thread + if (thread == Threads.main()) { + static MPI_Request req = MPI_REQUEST_NULL; + static TTSendBuffer send_buff = {}; + int flag; + bool found; + TTEntry* replace_tte; + + // Test communication status + MPI_Test(&req, &flag, MPI_STATUS_IGNORE); + + // Current communication is complete + if (flag) { + // Save all recieved entries + for (auto&& e : TTBuff) { + replace_tte = TT.probe(e.key(), found); + replace_tte->save(e.key(), e.value(), e.bound(), e.depth(), + e.move(), e.eval(), e.gen()); + } + + // Reset send buffer + send_buff = {}; + + // Build up new send buffer: best 16 found across all threads + for (auto&& th : Threads) { + std::lock_guard lk(th->ttBuffer.mutex); + for (auto&& e : th->ttBuffer.buffer) + send_buff.replace(e); + // Reset thread's send buffer + th->ttBuffer.buffer = {}; + } + + // Start next communication + MPI_Iallgather(send_buff.data(), send_buff.size(), TTEntryDatatype, + TTBuff.data(), TTSendBufferSize, TTEntryDatatype, + TTComm, &req); + } + } +} + +void reduce_moves(MoveInfo& mi) { + MPI_Allreduce(MPI_IN_PLACE, &mi, 1, MIDatatype, BestMoveOp, MoveComm); +} + +} + +#endif // USE_MPI diff --git a/src/cluster.h b/src/cluster.h new file mode 100644 index 00000000..bbd06875 --- /dev/null +++ b/src/cluster.h @@ -0,0 +1,94 @@ +/* + 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-2017 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 . +*/ + +#ifndef CLUSTER_H_INCLUDED +#define CLUSTER_H_INCLUDED + +#include +#include +#include +#include + +#include "tt.h" + +class Thread; + +namespace Cluster { + +struct MoveInfo { + int depth; + int score; + int rank; +}; + +#ifdef USE_MPI +constexpr std::size_t TTSendBufferSize = 16; +template class TTSendBuffer : public std::array { + struct Compare { + inline bool operator()(const TTEntry& lhs, const TTEntry& rhs) { + return lhs.depth() > rhs.depth(); + } + }; + Compare compare; + +public: + bool replace(const TTEntry& value) { + if (compare(value, this->front())) { + std::pop_heap(this->begin(), this->end(), compare); + this->back() = value; + std::push_heap(this->begin(), this->end(), compare); + return true; + } + return false; + } +}; + +void init(); +void finalize(); +bool getline(std::istream& input, std::string& str); +int size(); +int rank(); +inline bool is_root() { return rank() == 0; } +void save(Thread* thread, TTEntry* tte, + Key k, Value v, Bound b, Depth d, Move m, Value ev); +void reduce_moves(MoveInfo& mi); + +#else + +inline void init() { } +inline void finalize() { } +inline bool getline(std::istream& input, std::string& str) { + return static_cast(std::getline(input, str)); +} +constexpr int size() { return 1; } +constexpr int rank() { return 0; } +constexpr bool is_root() { return true; } +inline void save(Thread* thread, TTEntry* tte, + Key k, Value v, Bound b, Depth d, Move m, Value ev) { + (void)thread; + tte->save(k, v, b, d, m, ev); +} +inline void reduce_moves(MoveInfo&) { } + +#endif /* USE_MPI */ + +} + +#endif // #ifndef CLUSTER_H_INCLUDED diff --git a/src/main.cpp b/src/main.cpp index a093b5bf..1624a91f 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -27,6 +27,7 @@ #include "tt.h" #include "uci.h" #include "syzygy/tbprobe.h" +#include "cluster.h" namespace PSQT { void init(); @@ -34,7 +35,9 @@ namespace PSQT { int main(int argc, char* argv[]) { - std::cout << engine_info() << std::endl; + Cluster::init(); + if (Cluster::is_root()) + std::cout << engine_info() << std::endl; UCI::init(Options); PSQT::init(); @@ -49,5 +52,6 @@ int main(int argc, char* argv[]) { UCI::loop(argc, argv); Threads.set(0); + Cluster::finalize(); return 0; } diff --git a/src/search.cpp b/src/search.cpp index 4a541c5b..b636e7b0 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -25,6 +25,7 @@ #include #include +#include "cluster.h" #include "evaluate.h" #include "misc.h" #include "movegen.h" @@ -143,7 +144,7 @@ namespace { nodes += cnt; pos.undo_move(m); } - if (Root) + if (Root && Cluster::is_root()) sync_cout << UCI::move(m, pos.is_chess960()) << ": " << cnt << sync_endl; } return nodes; @@ -199,7 +200,8 @@ void MainThread::search() { if (Limits.perft) { nodes = perft(rootPos, Limits.perft * ONE_PLY); - sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl; + if (Cluster::is_root()) + sync_cout << "\nNodes searched: " << nodes << "\n" << sync_endl; return; } @@ -210,9 +212,10 @@ void MainThread::search() { if (rootMoves.empty()) { rootMoves.emplace_back(MOVE_NONE); - sync_cout << "info depth 0 score " - << UCI::value(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW) - << sync_endl; + if (Cluster::is_root()) + sync_cout << "info depth 0 score " + << UCI::value(rootPos.checkers() ? -VALUE_MATE : VALUE_DRAW) + << sync_endl; } else { @@ -282,18 +285,25 @@ void MainThread::search() { } } - previousScore = bestThread->rootMoves[0].score; + Cluster::MoveInfo mi{bestThread->completedDepth, + bestThread->rootMoves[0].score, + Cluster::rank()}; + Cluster::reduce_moves(mi); + + previousScore = static_cast(mi.score); // Send again PV info if we have a new best thread - if (bestThread != this) - sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth, -VALUE_INFINITE, VALUE_INFINITE) << sync_endl; + if (mi.rank == Cluster::rank()) { + if (bestThread != this) + sync_cout << UCI::pv(bestThread->rootPos, bestThread->completedDepth, -VALUE_INFINITE, VALUE_INFINITE) << sync_endl; - sync_cout << "bestmove " << UCI::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); + sync_cout << "bestmove " << UCI::move(bestThread->rootMoves[0].pv[0], rootPos.is_chess960()); - if (bestThread->rootMoves[0].pv.size() > 1 || bestThread->rootMoves[0].extract_ponder_from_tt(rootPos)) - std::cout << " ponder " << UCI::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960()); + if (bestThread->rootMoves[0].pv.size() > 1 || bestThread->rootMoves[0].extract_ponder_from_tt(rootPos)) + std::cout << " ponder " << UCI::move(bestThread->rootMoves[0].pv[1], rootPos.is_chess960()); - std::cout << sync_endl; + std::cout << sync_endl; + } } @@ -358,9 +368,9 @@ void Thread::search() { && !(Limits.depth && mainThread && rootDepth / ONE_PLY > Limits.depth)) { // Distribute search depths across the helper threads - if (idx > 0) + if (idx + Cluster::rank() > 0) { - int i = (idx - 1) % 20; + int i = (idx + Cluster::rank() - 1) % 20; if (((rootDepth / ONE_PLY + SkipPhase[i]) / SkipSize[i]) % 2) continue; // Retry with an incremented rootDepth } @@ -431,7 +441,8 @@ void Thread::search() { // When failing high/low give some update (without cluttering // the UI) before a re-search. - if ( mainThread + if ( Cluster::is_root() + && mainThread && multiPV == 1 && (bestValue <= alpha || bestValue >= beta) && Time.elapsed() > 3000) @@ -468,7 +479,7 @@ void Thread::search() { // Sort the PV lines searched so far and update the GUI std::stable_sort(rootMoves.begin() + pvFirst, rootMoves.begin() + pvIdx + 1); - if ( mainThread + if ( Cluster::is_root() && mainThread && (Threads.stop || pvIdx + 1 == multiPV || Time.elapsed() > 3000)) sync_cout << UCI::pv(rootPos, rootDepth, alpha, beta) << sync_endl; } @@ -709,9 +720,10 @@ namespace { if ( b == BOUND_EXACT || (b == BOUND_LOWER ? value >= beta : value <= alpha)) { - tte->save(posKey, value_to_tt(value, ss->ply), b, - std::min(DEPTH_MAX - ONE_PLY, depth + 6 * ONE_PLY), - MOVE_NONE, VALUE_NONE); + Cluster::save(thisThread, tte, + posKey, value_to_tt(value, ss->ply), b, + std::min(DEPTH_MAX - ONE_PLY, depth + 6 * ONE_PLY), + MOVE_NONE, VALUE_NONE); return value; } @@ -760,7 +772,9 @@ namespace { else ss->staticEval = eval = pureStaticEval = -(ss-1)->staticEval + 2 * Eval::Tempo; - tte->save(posKey, VALUE_NONE, BOUND_NONE, DEPTH_NONE, MOVE_NONE, pureStaticEval); + Cluster::save(thisThread, tte, + posKey, VALUE_NONE, BOUND_NONE, DEPTH_NONE, MOVE_NONE, + pureStaticEval); } // Step 7. Razoring (~2 Elo) @@ -912,7 +926,7 @@ moves_loop: // When in check, search starts from here ss->moveCount = ++moveCount; - if (rootNode && thisThread == Threads.main() && Time.elapsed() > 3000) + if (rootNode && Cluster::is_root() && thisThread == Threads.main() && Time.elapsed() > 3000) sync_cout << "info depth " << depth / ONE_PLY << " currmove " << UCI::move(move, pos.is_chess960()) << " currmovenumber " << moveCount + thisThread->pvIdx << sync_endl; @@ -1209,10 +1223,11 @@ moves_loop: // When in check, search starts from here bestValue = std::min(bestValue, maxValue); if (!excludedMove) - tte->save(posKey, value_to_tt(bestValue, ss->ply), - bestValue >= beta ? BOUND_LOWER : - PvNode && bestMove ? BOUND_EXACT : BOUND_UPPER, - depth, bestMove, pureStaticEval); + Cluster::save(thisThread, tte, + posKey, value_to_tt(bestValue, ss->ply), + bestValue >= beta ? BOUND_LOWER : + PvNode && bestMove ? BOUND_EXACT : BOUND_UPPER, + depth, bestMove, pureStaticEval); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); @@ -1310,8 +1325,9 @@ moves_loop: // When in check, search starts from here if (bestValue >= beta) { if (!ttHit) - tte->save(posKey, value_to_tt(bestValue, ss->ply), BOUND_LOWER, - DEPTH_NONE, MOVE_NONE, ss->staticEval); + Cluster::save(thisThread, tte, + posKey, value_to_tt(bestValue, ss->ply), BOUND_LOWER, + DEPTH_NONE, MOVE_NONE, ss->staticEval); return bestValue; } @@ -1421,10 +1437,11 @@ moves_loop: // When in check, search starts from here if (inCheck && bestValue == -VALUE_INFINITE) return mated_in(ss->ply); // Plies to mate from the root - tte->save(posKey, value_to_tt(bestValue, ss->ply), - bestValue >= beta ? BOUND_LOWER : - PvNode && bestValue > oldAlpha ? BOUND_EXACT : BOUND_UPPER, - ttDepth, bestMove, ss->staticEval); + Cluster::save(thisThread, tte, + posKey, value_to_tt(bestValue, ss->ply), + bestValue >= beta ? BOUND_LOWER : + PvNode && bestValue > oldAlpha ? BOUND_EXACT : BOUND_UPPER, + ttDepth, bestMove, ss->staticEval); assert(bestValue > -VALUE_INFINITE && bestValue < VALUE_INFINITE); diff --git a/src/syzygy/tbprobe.cpp b/src/syzygy/tbprobe.cpp index 57c7d872..311c2204 100644 --- a/src/syzygy/tbprobe.cpp +++ b/src/syzygy/tbprobe.cpp @@ -29,6 +29,7 @@ #include #include "../bitboard.h" +#include "../cluster.h" #include "../movegen.h" #include "../position.h" #include "../search.h" @@ -1373,7 +1374,8 @@ void Tablebases::init(const std::string& paths) { } } - sync_cout << "info string Found " << TBTables.size() << " tablebases" << sync_endl; + if (Cluster::is_root()) + sync_cout << "info string Found " << TBTables.size() << " tablebases" << sync_endl; } // Probe the WDL table for a particular position. diff --git a/src/thread.h b/src/thread.h index e377e992..200f0df8 100644 --- a/src/thread.h +++ b/src/thread.h @@ -27,6 +27,7 @@ #include #include +#include "cluster.h" #include "material.h" #include "movepick.h" #include "pawns.h" @@ -73,6 +74,13 @@ public: CapturePieceToHistory captureHistory; ContinuationHistory continuationHistory; Score contempt; + +#ifdef USE_MPI + struct { + Mutex mutex; + Cluster::TTSendBuffer buffer = {}; + } ttBuffer; +#endif }; diff --git a/src/tt.h b/src/tt.h index 2cf82f58..f231c2ea 100644 --- a/src/tt.h +++ b/src/tt.h @@ -24,6 +24,11 @@ #include "misc.h" #include "types.h" +namespace Cluster { + void init(); +} +//void Cluster::init(); + /// TTEntry struct is the 10 bytes transposition table entry, defined as below: /// /// key 16 bit @@ -36,6 +41,7 @@ struct TTEntry { + Key key() const { return (Key )(key16) << 48; } Move move() const { return (Move )move16; } Value value() const { return (Value)value16; } Value eval() const { return (Value)eval16; } @@ -45,6 +51,7 @@ struct TTEntry { private: friend class TranspositionTable; + friend void Cluster::init(); uint16_t key16; uint16_t move16; @@ -64,6 +71,8 @@ private: class TranspositionTable { + friend void Cluster::init(); + static constexpr int CacheLineSize = 64; static constexpr int ClusterSize = 3; diff --git a/src/uci.cpp b/src/uci.cpp index 36d359c6..00b3d10d 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -24,6 +24,7 @@ #include #include "evaluate.h" +#include "cluster.h" #include "movegen.h" #include "position.h" #include "search.h" @@ -97,7 +98,7 @@ namespace { if (Options.count(name)) Options[name] = value; - else + else if (Cluster::is_root()) sync_cout << "No such option: " << name << sync_endl; } @@ -199,7 +200,7 @@ void UCI::loop(int argc, char* argv[]) { cmd += std::string(argv[i]) + " "; do { - if (argc == 1 && !getline(cin, cmd)) // Block here waiting for input or EOF + if (argc == 1 && !Cluster::getline(cin, cmd)) // Block here waiting for input or EOF cmd = "quit"; istringstream is(cmd); @@ -220,7 +221,7 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "ponderhit") Threads.ponder = false; // Switch to normal search - else if (token == "uci") + else if (token == "uci" && Cluster::is_root()) sync_cout << "id name " << engine_info(true) << "\n" << Options << "\nuciok" << sync_endl; @@ -229,14 +230,17 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "go") go(pos, is, states); else if (token == "position") position(pos, is, states); else if (token == "ucinewgame") Search::clear(); - else if (token == "isready") sync_cout << "readyok" << sync_endl; + else if (token == "isready" && Cluster::is_root()) + sync_cout << "readyok" << sync_endl; // Additional custom non-UCI commands, mainly for debugging else if (token == "flip") pos.flip(); else if (token == "bench") bench(pos, is, states); - else if (token == "d") sync_cout << pos << sync_endl; - else if (token == "eval") sync_cout << Eval::trace(pos) << sync_endl; - else + else if (token == "d" && Cluster::is_root()) + sync_cout << pos << sync_endl; + else if (token == "eval" && Cluster::is_root()) + sync_cout << Eval::trace(pos) << sync_endl; + else if (Cluster::is_root()) sync_cout << "Unknown command: " << cmd << sync_endl; } while (token != "quit" && argc == 1); // Command line args are one-shot