1
0
Fork 0
mirror of https://github.com/sockspls/badfish synced 2025-04-29 16:23:09 +00:00

Refactor global variables

This aims to remove some of the annoying global structure which Stockfish has.

Overall there is no major elo regression to be expected.

Non regression SMP STC (paused, early version):
https://tests.stockfishchess.org/tests/view/65983d7979aa8af82b9608f1
LLR: 0.23 (-2.94,2.94) <-1.75,0.25>
Total: 76232 W: 19035 L: 19096 D: 38101
Ptnml(0-2): 92, 8735, 20515, 8690, 84

Non regression STC (early version):
https://tests.stockfishchess.org/tests/view/6595b3a479aa8af82b95da7f
LLR: 2.93 (-2.94,2.94) <-1.75,0.25>
Total: 185344 W: 47027 L: 46972 D: 91345
Ptnml(0-2): 571, 21285, 48943, 21264, 609

Non regression SMP STC:
https://tests.stockfishchess.org/tests/view/65a0715c79aa8af82b96b7e4
LLR: 2.94 (-2.94,2.94) <-1.75,0.25>
Total: 142936 W: 35761 L: 35662 D: 71513
Ptnml(0-2): 209, 16400, 38135, 16531, 193

These global structures/variables add hidden dependencies and allow data
to be mutable from where it shouldn't it be (i.e. options). They also
prevent Stockfish from internal selfplay, which would be a nice thing to
be able to do, i.e. instantiate two Stockfish instances and let them
play against each other. It will also allow us to make Stockfish a
library, which can be easier used on other platforms.

For consistency with the old search code, `thisThread` has been kept,
even though it is not strictly necessary anymore. This the first major
refactor of this kind (in recent time), and future changes are required,
to achieve the previously described goals. This includes cleaning up the
dependencies, transforming the network to be self contained and coming
up with a plan to deal with proper tablebase memory management (see
comments for more information on this).

The removal of these global structures has been discussed in parts with
Vondele and Sopel.

closes https://github.com/official-stockfish/Stockfish/pull/4968

No functional change
This commit is contained in:
Disservin 2024-01-08 19:48:46 +01:00
parent 6deb88728f
commit a107910951
27 changed files with 1200 additions and 1001 deletions

View file

@ -63,7 +63,7 @@ HEADERS = benchmark.h bitboard.h evaluate.h misc.h movegen.h movepick.h \
nnue/layers/sqr_clipped_relu.h nnue/nnue_accumulator.h nnue/nnue_architecture.h \
nnue/nnue_common.h nnue/nnue_feature_transformer.h position.h \
search.h syzygy/tbprobe.h thread.h thread_win32_osx.h timeman.h \
tt.h tune.h types.h uci.h
tt.h tune.h types.h uci.h ucioption.h
OBJS = $(notdir $(SRCS:.cpp=.o))

View file

@ -25,6 +25,7 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <sstream>
#include <unordered_map>
#include <vector>
@ -34,9 +35,10 @@
#include "nnue/evaluate_nnue.h"
#include "nnue/nnue_architecture.h"
#include "position.h"
#include "thread.h"
#include "search.h"
#include "types.h"
#include "uci.h"
#include "ucioption.h"
// Macro to embed the default efficiently updatable neural network (NNUE) file
// data in the engine binary (using incbin.h, by Dale Weiler).
@ -62,10 +64,6 @@ namespace Stockfish {
namespace Eval {
std::unordered_map<NNUE::NetSize, EvalFile> EvalFiles = {
{NNUE::Big, {"EvalFile", EvalFileDefaultNameBig, "None"}},
{NNUE::Small, {"EvalFileSmall", EvalFileDefaultNameSmall, "None"}}};
// Tries to load a NNUE network at startup time, or when the engine
// receives a UCI command "setoption name EvalFile value nn-[a-z0-9]{12}.nnue"
@ -74,38 +72,45 @@ std::unordered_map<NNUE::NetSize, EvalFile> EvalFiles = {
// network may be embedded in the binary), in the active working directory and
// in the engine directory. Distro packagers may define the DEFAULT_NNUE_DIRECTORY
// variable to have the engine search in a special directory in their distro.
void NNUE::init() {
NNUE::EvalFiles NNUE::load_networks(const std::string& rootDirectory,
const OptionsMap& options,
NNUE::EvalFiles evalFiles) {
for (auto& [netSize, evalFile] : EvalFiles)
for (auto& [netSize, evalFile] : evalFiles)
{
// Replace with
// Options[evalFile.option_name]
// options[evalFile.optionName]
// once fishtest supports the uci option EvalFileSmall
std::string user_eval_file =
netSize == Small ? evalFile.default_name : Options[evalFile.option_name];
netSize == Small ? evalFile.defaultName : options[evalFile.optionName];
if (user_eval_file.empty())
user_eval_file = evalFile.default_name;
user_eval_file = evalFile.defaultName;
#if defined(DEFAULT_NNUE_DIRECTORY)
std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory,
std::vector<std::string> dirs = {"<internal>", "", rootDirectory,
stringify(DEFAULT_NNUE_DIRECTORY)};
#else
std::vector<std::string> dirs = {"<internal>", "", CommandLine::binaryDirectory};
std::vector<std::string> dirs = {"<internal>", "", rootDirectory};
#endif
for (const std::string& directory : dirs)
{
if (evalFile.selected_name != user_eval_file)
if (evalFile.current != user_eval_file)
{
if (directory != "<internal>")
{
std::ifstream stream(directory + user_eval_file, std::ios::binary);
if (NNUE::load_eval(user_eval_file, stream, netSize))
evalFile.selected_name = user_eval_file;
auto description = NNUE::load_eval(stream, netSize);
if (description.has_value())
{
evalFile.current = user_eval_file;
evalFile.netDescription = description.value();
}
}
if (directory == "<internal>" && user_eval_file == evalFile.default_name)
if (directory == "<internal>" && user_eval_file == evalFile.defaultName)
{
// C++ way to prepare a buffer for a memory stream
class MemoryBuffer: public std::basic_streambuf<char> {
@ -124,28 +129,36 @@ void NNUE::init() {
(void) gEmbeddedNNUESmallEnd;
std::istream stream(&buffer);
if (NNUE::load_eval(user_eval_file, stream, netSize))
evalFile.selected_name = user_eval_file;
auto description = NNUE::load_eval(stream, netSize);
if (description.has_value())
{
evalFile.current = user_eval_file;
evalFile.netDescription = description.value();
}
}
}
}
}
return evalFiles;
}
// Verifies that the last net used was loaded successfully
void NNUE::verify() {
void NNUE::verify(const OptionsMap& options,
const std::unordered_map<Eval::NNUE::NetSize, EvalFile>& evalFiles) {
for (const auto& [netSize, evalFile] : EvalFiles)
for (const auto& [netSize, evalFile] : evalFiles)
{
// Replace with
// Options[evalFile.option_name]
// options[evalFile.optionName]
// once fishtest supports the uci option EvalFileSmall
std::string user_eval_file =
netSize == Small ? evalFile.default_name : Options[evalFile.option_name];
netSize == Small ? evalFile.defaultName : options[evalFile.optionName];
if (user_eval_file.empty())
user_eval_file = evalFile.default_name;
user_eval_file = evalFile.defaultName;
if (evalFile.selected_name != user_eval_file)
if (evalFile.current != user_eval_file)
{
std::string msg1 =
"Network evaluation parameters compatible with the engine must be available.";
@ -155,7 +168,7 @@ void NNUE::verify() {
"including the directory name, to the network file.";
std::string msg4 = "The default net can be downloaded from: "
"https://tests.stockfishchess.org/api/nn/"
+ evalFile.default_name;
+ evalFile.defaultName;
std::string msg5 = "The engine will be terminated now.";
sync_cout << "info string ERROR: " << msg1 << sync_endl;
@ -183,7 +196,7 @@ int Eval::simple_eval(const Position& pos, Color c) {
// Evaluate is the evaluator for the outer world. It returns a static evaluation
// of the position from the point of view of the side to move.
Value Eval::evaluate(const Position& pos) {
Value Eval::evaluate(const Position& pos, const Search::Worker& workerThread) {
assert(!pos.checkers());
@ -204,7 +217,7 @@ Value Eval::evaluate(const Position& pos) {
Value nnue = smallNet ? NNUE::evaluate<NNUE::Small>(pos, true, &nnueComplexity)
: NNUE::evaluate<NNUE::Big>(pos, true, &nnueComplexity);
int optimism = pos.this_thread()->optimism[stm];
int optimism = workerThread.optimism[stm];
// Blend optimism and eval with nnue complexity and material imbalance
optimism += optimism * (nnueComplexity + std::abs(simpleEval - nnue)) / 512;
@ -227,16 +240,16 @@ Value Eval::evaluate(const Position& pos) {
// a string (suitable for outputting to stdout) that contains the detailed
// descriptions and values of each evaluation term. Useful for debugging.
// Trace scores are from white's point of view
std::string Eval::trace(Position& pos) {
std::string Eval::trace(Position& pos, Search::Worker& workerThread) {
if (pos.checkers())
return "Final evaluation: none (in check)";
// Reset any global variable used in eval
pos.this_thread()->bestValue = VALUE_ZERO;
pos.this_thread()->rootSimpleEval = VALUE_ZERO;
pos.this_thread()->optimism[WHITE] = VALUE_ZERO;
pos.this_thread()->optimism[BLACK] = VALUE_ZERO;
workerThread.iterBestValue = VALUE_ZERO;
workerThread.rootSimpleEval = VALUE_ZERO;
workerThread.optimism[WHITE] = VALUE_ZERO;
workerThread.optimism[BLACK] = VALUE_ZERO;
std::stringstream ss;
ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2);
@ -249,7 +262,7 @@ std::string Eval::trace(Position& pos) {
v = pos.side_to_move() == WHITE ? v : -v;
ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n";
v = evaluate(pos);
v = evaluate(pos, workerThread);
v = pos.side_to_move() == WHITE ? v : -v;
ss << "Final evaluation " << 0.01 * UCI::to_cp(v) << " (white side)";
ss << " [with scaled NNUE, ...]";

View file

@ -27,13 +27,18 @@
namespace Stockfish {
class Position;
class OptionsMap;
namespace Search {
class Worker;
}
namespace Eval {
std::string trace(Position& pos);
std::string trace(Position& pos, Search::Worker& workerThread);
int simple_eval(const Position& pos, Color c);
Value evaluate(const Position& pos);
Value evaluate(const Position& pos, const Search::Worker& workerThread);
// The default net name MUST follow the format nn-[SHA256 first 12 digits].nnue
// for the build process (profile-build and fishtest) to work. Do not change the
@ -41,23 +46,28 @@ Value evaluate(const Position& pos);
#define EvalFileDefaultNameBig "nn-baff1edbea57.nnue"
#define EvalFileDefaultNameSmall "nn-baff1ede1f90.nnue"
struct EvalFile {
// UCI option name
std::string optionName;
// Default net name, will use one of the macros above
std::string defaultName;
// Selected net name, either via uci option or default
std::string current;
// Net description extracted from the net file
std::string netDescription;
};
namespace NNUE {
enum NetSize : int;
void init();
void verify();
using EvalFiles = std::unordered_map<Eval::NNUE::NetSize, EvalFile>;
EvalFiles load_networks(const std::string&, const OptionsMap&, EvalFiles);
void verify(const OptionsMap&, const EvalFiles&);
} // namespace NNUE
struct EvalFile {
std::string option_name;
std::string default_name;
std::string selected_name;
};
extern std::unordered_map<NNUE::NetSize, EvalFile> EvalFiles;
} // namespace Eval
} // namespace Stockfish

View file

@ -16,15 +16,13 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include <cstddef>
#include <iostream>
#include <unordered_map>
#include "bitboard.h"
#include "evaluate.h"
#include "misc.h"
#include "position.h"
#include "search.h"
#include "thread.h"
#include "tune.h"
#include "types.h"
#include "uci.h"
@ -35,17 +33,16 @@ int main(int argc, char* argv[]) {
std::cout << engine_info() << std::endl;
CommandLine::init(argc, argv);
UCI::init(Options);
Tune::init();
Bitboards::init();
Position::init();
Threads.set(size_t(Options["Threads"]));
Search::clear(); // After threads are up
Eval::NNUE::init();
UCI::loop(argc, argv);
UCI uci(argc, argv);
Tune::init(uci.options);
uci.evalFiles = Eval::NNUE::load_networks(uci.workingDirectory(), uci.options, uci.evalFiles);
uci.loop();
Threads.set(0);
return 0;
}

View file

@ -721,17 +721,13 @@ void bindThisThread(size_t idx) {
#define GETCWD getcwd
#endif
namespace CommandLine {
std::string argv0; // path+name of the executable binary, as given by argv[0]
std::string binaryDirectory; // path of the executable directory
std::string workingDirectory; // path of the working directory
void init([[maybe_unused]] int argc, char* argv[]) {
CommandLine::CommandLine(int _argc, char** _argv) :
argc(_argc),
argv(_argv) {
std::string pathSeparator;
// Extract the path+name of the executable binary
argv0 = argv[0];
std::string argv0 = argv[0];
#ifdef _WIN32
pathSeparator = "\\";
@ -766,7 +762,4 @@ void init([[maybe_unused]] int argc, char* argv[]) {
binaryDirectory.replace(0, 1, workingDirectory);
}
} // namespace CommandLine
} // namespace Stockfish

View file

@ -176,12 +176,17 @@ namespace WinProcGroup {
void bindThisThread(size_t idx);
}
namespace CommandLine {
void init(int argc, char* argv[]);
extern std::string binaryDirectory; // path of the executable directory
extern std::string workingDirectory; // path of the working directory
}
struct CommandLine {
public:
CommandLine(int, char**);
int argc;
char** argv;
std::string binaryDirectory; // path of the executable directory
std::string workingDirectory; // path of the working directory
};
} // namespace Stockfish

View file

@ -26,8 +26,10 @@
#include <fstream>
#include <iomanip>
#include <iostream>
#include <optional>
#include <sstream>
#include <string_view>
#include <type_traits>
#include <unordered_map>
#include "../evaluate.h"
@ -51,8 +53,6 @@ AlignedPtr<Network<TransformedFeatureDimensionsBig, L2Big, L3Big>> network
AlignedPtr<Network<TransformedFeatureDimensionsSmall, L2Small, L3Small>> networkSmall[LayerStacks];
// Evaluation function file names
std::string fileName[2];
std::string netDescription[2];
namespace Detail {
@ -136,10 +136,10 @@ static bool write_header(std::ostream& stream, std::uint32_t hashValue, const st
}
// Read network parameters
static bool read_parameters(std::istream& stream, NetSize netSize) {
static bool read_parameters(std::istream& stream, NetSize netSize, std::string& netDescription) {
std::uint32_t hashValue;
if (!read_header(stream, &hashValue, &netDescription[netSize]))
if (!read_header(stream, &hashValue, &netDescription))
return false;
if (hashValue != HashValue[netSize])
return false;
@ -158,9 +158,10 @@ static bool read_parameters(std::istream& stream, NetSize netSize) {
}
// Write network parameters
static bool write_parameters(std::ostream& stream, NetSize netSize) {
static bool
write_parameters(std::ostream& stream, NetSize netSize, const std::string& netDescription) {
if (!write_header(stream, HashValue[netSize], netDescription[netSize]))
if (!write_header(stream, HashValue[netSize], netDescription))
return false;
if (netSize == Big && !Detail::write_parameters(stream, *featureTransformerBig))
return false;
@ -424,24 +425,30 @@ std::string trace(Position& pos) {
// Load eval, from a file stream or a memory stream
bool load_eval(const std::string name, std::istream& stream, NetSize netSize) {
std::optional<std::string> load_eval(std::istream& stream, NetSize netSize) {
initialize(netSize);
fileName[netSize] = name;
return read_parameters(stream, netSize);
std::string netDescription;
return read_parameters(stream, netSize, netDescription) ? std::make_optional(netDescription)
: std::nullopt;
}
// Save eval, to a file stream or a memory stream
bool save_eval(std::ostream& stream, NetSize netSize) {
bool save_eval(std::ostream& stream,
NetSize netSize,
const std::string& name,
const std::string& netDescription) {
if (fileName[netSize].empty())
if (name.empty() || name == "None")
return false;
return write_parameters(stream, netSize);
return write_parameters(stream, netSize, netDescription);
}
// Save eval, to a file given by its name
bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
bool save_eval(const std::optional<std::string>& filename,
NetSize netSize,
const std::unordered_map<Eval::NNUE::NetSize, Eval::EvalFile>& evalFiles) {
std::string actualFilename;
std::string msg;
@ -450,7 +457,7 @@ bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
actualFilename = filename.value();
else
{
if (EvalFiles.at(netSize).selected_name
if (evalFiles.at(netSize).current
!= (netSize == Small ? EvalFileDefaultNameSmall : EvalFileDefaultNameBig))
{
msg = "Failed to export a net. "
@ -463,7 +470,8 @@ bool save_eval(const std::optional<std::string>& filename, NetSize netSize) {
}
std::ofstream stream(actualFilename, std::ios_base::binary);
bool saved = save_eval(stream, netSize);
bool saved = save_eval(stream, netSize, evalFiles.at(netSize).current,
evalFiles.at(netSize).netDescription);
msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net";

View file

@ -26,14 +26,20 @@
#include <memory>
#include <optional>
#include <string>
#include <unordered_map>
#include "../misc.h"
#include "../types.h"
#include "nnue_architecture.h"
#include "nnue_feature_transformer.h"
#include "../types.h"
namespace Stockfish {
class Position;
namespace Eval {
struct EvalFile;
}
}
namespace Stockfish::Eval::NNUE {
@ -73,9 +79,14 @@ template<NetSize Net_Size>
Value evaluate(const Position& pos, bool adjusted = false, int* complexity = nullptr);
void hint_common_parent_position(const Position& pos);
bool load_eval(const std::string name, std::istream& stream, NetSize netSize);
bool save_eval(std::ostream& stream, NetSize netSize);
bool save_eval(const std::optional<std::string>& filename, NetSize netSize);
std::optional<std::string> load_eval(std::istream& stream, NetSize netSize);
bool save_eval(std::ostream& stream,
NetSize netSize,
const std::string& name,
const std::string& netDescription);
bool save_eval(const std::optional<std::string>& filename,
NetSize netSize,
const std::unordered_map<Eval::NNUE::NetSize, Eval::EvalFile>&);
} // namespace Stockfish::Eval::NNUE

View file

@ -19,7 +19,6 @@
#include "position.h"
#include <algorithm>
#include <atomic>
#include <cassert>
#include <cctype>
#include <cstddef>
@ -36,7 +35,6 @@
#include "movegen.h"
#include "nnue/nnue_common.h"
#include "syzygy/tbprobe.h"
#include "thread.h"
#include "tt.h"
#include "uci.h"
@ -87,7 +85,7 @@ std::ostream& operator<<(std::ostream& os, const Position& pos) {
ASSERT_ALIGNED(&st, Eval::NNUE::CacheLineSize);
Position p;
p.set(pos.fen(), pos.is_chess960(), &st, pos.this_thread());
p.set(pos.fen(), pos.is_chess960(), &st);
Tablebases::ProbeState s1, s2;
Tablebases::WDLScore wdl = Tablebases::probe_wdl(p, &s1);
int dtz = Tablebases::probe_dtz(p, &s2);
@ -160,7 +158,7 @@ void Position::init() {
// Initializes the position object with the given FEN string.
// This function is not very robust - make sure that input FENs are correct,
// this is assumed to be the responsibility of the GUI.
Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Thread* th) {
Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si) {
/*
A FEN string defines a particular position using only the ASCII character set.
@ -286,8 +284,7 @@ Position& Position::set(const string& fenStr, bool isChess960, StateInfo* si, Th
// handle also common incorrect FEN with fullmove = 0.
gamePly = std::max(2 * (gamePly - 1), 0) + (sideToMove == BLACK);
chess960 = isChess960;
thisThread = th;
chess960 = isChess960;
set_state();
assert(pos_is_ok());
@ -388,7 +385,7 @@ Position& Position::set(const string& code, Color c, StateInfo* si) {
string fenStr = "8/" + sides[0] + char(8 - sides[0].length() + '0') + "/8/8/8/8/" + sides[1]
+ char(8 - sides[1].length() + '0') + "/8 w - - 0 10";
return set(fenStr, false, si, nullptr);
return set(fenStr, false, si);
}
@ -667,7 +664,6 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) {
assert(m.is_ok());
assert(&newSt != st);
thisThread->nodes.fetch_add(1, std::memory_order_relaxed);
Key k = st->key ^ Zobrist::side;
// Copy some fields of the old state to our new StateInfo object except the
@ -959,7 +955,7 @@ void Position::do_castling(Color us, Square from, Square& to, Square& rfrom, Squ
// Used to do a "null move": it flips
// the side to move without executing any move on the board.
void Position::do_null_move(StateInfo& newSt) {
void Position::do_null_move(StateInfo& newSt, TranspositionTable& tt) {
assert(!checkers());
assert(&newSt != st);
@ -982,7 +978,7 @@ void Position::do_null_move(StateInfo& newSt) {
st->key ^= Zobrist::side;
++st->rule50;
prefetch(TT.first_entry(key()));
prefetch(tt.first_entry(key()));
st->pliesFromNull = 0;
@ -1235,7 +1231,7 @@ void Position::flip() {
std::getline(ss, token); // Half and full moves
f += token;
set(f, is_chess960(), st, this_thread());
set(f, is_chess960(), st);
assert(pos_is_ok());
}

View file

@ -32,6 +32,8 @@
namespace Stockfish {
class TranspositionTable;
// StateInfo struct stores information needed to restore a Position object to
// its previous state when we retract a move. Whenever a move is made on the
// board (by calling Position::do_move), a StateInfo object must be passed.
@ -75,8 +77,6 @@ using StateListPtr = std::unique_ptr<std::deque<StateInfo>>;
// pieces, side to move, hash keys, castling info, etc. Important methods are
// do_move() and undo_move(), used by the search to update node info when
// traversing the search tree.
class Thread;
class Position {
public:
static void init();
@ -86,7 +86,7 @@ class Position {
Position& operator=(const Position&) = delete;
// FEN string input/output
Position& set(const std::string& fenStr, bool isChess960, StateInfo* si, Thread* th);
Position& set(const std::string& fenStr, bool isChess960, StateInfo* si);
Position& set(const std::string& code, Color c, StateInfo* si);
std::string fen() const;
@ -139,7 +139,7 @@ class Position {
void do_move(Move m, StateInfo& newSt);
void do_move(Move m, StateInfo& newSt, bool givesCheck);
void undo_move(Move m);
void do_null_move(StateInfo& newSt);
void do_null_move(StateInfo& newSt, TranspositionTable& tt);
void undo_null_move();
// Static Exchange Evaluation
@ -152,16 +152,15 @@ class Position {
Key pawn_key() const;
// Other properties of the position
Color side_to_move() const;
int game_ply() const;
bool is_chess960() const;
Thread* this_thread() const;
bool is_draw(int ply) const;
bool has_game_cycle(int ply) const;
bool has_repeated() const;
int rule50_count() const;
Value non_pawn_material(Color c) const;
Value non_pawn_material() const;
Color side_to_move() const;
int game_ply() const;
bool is_chess960() const;
bool is_draw(int ply) const;
bool has_game_cycle(int ply) const;
bool has_repeated() const;
int rule50_count() const;
Value non_pawn_material(Color c) const;
Value non_pawn_material() const;
// Position consistency check, for debugging
bool pos_is_ok() const;
@ -194,7 +193,6 @@ class Position {
int castlingRightsMask[SQUARE_NB];
Square castlingRookSquare[CASTLING_RIGHT_NB];
Bitboard castlingPath[CASTLING_RIGHT_NB];
Thread* thisThread;
StateInfo* st;
int gamePly;
Color sideToMove;
@ -328,8 +326,6 @@ inline bool Position::capture_stage(Move m) const {
inline Piece Position::captured_piece() const { return st->capturedPiece; }
inline Thread* Position::this_thread() const { return thisThread; }
inline void Position::put_piece(Piece pc, Square s) {
board[s] = pc;

File diff suppressed because it is too large Load diff

View file

@ -19,19 +19,37 @@
#ifndef SEARCH_H_INCLUDED
#define SEARCH_H_INCLUDED
#include <atomic>
#include <cassert>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <vector>
#include "misc.h"
#include "movepick.h"
#include "position.h"
#include "timeman.h"
#include "types.h"
namespace Stockfish {
class Position;
// Different node types, used as a template parameter
enum NodeType {
NonPV,
PV,
Root
};
class TranspositionTable;
class ThreadPool;
class OptionsMap;
class UCI;
namespace Search {
// Called at startup to initialize various lookup tables, after program startup
void init(int);
// Stack struct keeps track of the information we need to remember from nodes
// shallower and deeper in the tree during the search. Each search thread has
@ -61,7 +79,7 @@ struct RootMove {
explicit RootMove(Move m) :
pv(1, m) {}
bool extract_ponder_from_tt(Position& pos);
bool extract_ponder_from_tt(const TranspositionTable& tt, Position& pos);
bool operator==(const Move& m) const { return pv[0] == m; }
// Sort in descending order
bool operator<(const RootMove& m) const {
@ -85,7 +103,6 @@ using RootMoves = std::vector<RootMove>;
// LimitsType struct stores information sent by GUI about available time to
// search the current move, maximum depth/time, or if we are in analysis mode.
struct LimitsType {
// Init explicitly due to broken value-initialization of non POD in MSVC
@ -103,10 +120,136 @@ struct LimitsType {
int64_t nodes;
};
extern LimitsType Limits;
void init();
void clear();
// The UCI stores the uci options, thread pool, and transposition table.
// This struct is used to easily forward data to the Search::Worker class.
struct SharedState {
SharedState(const OptionsMap& o, ThreadPool& tp, TranspositionTable& t) :
options(o),
threads(tp),
tt(t) {}
const OptionsMap& options;
ThreadPool& threads;
TranspositionTable& tt;
};
class Worker;
// Null Object Pattern, implement a common interface
// for the SearchManagers. A Null Object will be given to
// non-mainthread workers.
class ISearchManager {
public:
virtual ~ISearchManager() {}
virtual void check_time(Search::Worker&) = 0;
};
// SearchManager manages the search from the main thread. It is responsible for
// keeping track of the time, and storing data strictly related to the main thread.
class SearchManager: public ISearchManager {
public:
void check_time(Search::Worker& worker) override;
Stockfish::TimeManagement tm;
int callsCnt;
std::atomic_bool ponder;
double previousTimeReduction;
Value bestPreviousScore;
Value bestPreviousAverageScore;
Value iterValue[4];
bool stopOnPonderhit;
size_t id;
};
class NullSearchManager: public ISearchManager {
public:
void check_time(Search::Worker&) override {}
};
// Search::Worker is the class that does the actual search.
// It is instantiated once per thread, and it is responsible for keeping track
// of the search history, and storing data required for the search.
class Worker {
public:
Worker(SharedState&, std::unique_ptr<ISearchManager>, size_t);
// Reset histories, usually before a new game
void clear();
// Called when the program receives the UCI 'go'
// command. It searches from the root position and outputs the "bestmove".
void start_searching();
bool is_mainthread() const { return thread_idx == 0; }
// Public because evaluate uses this
Value iterBestValue, optimism[COLOR_NB];
Value rootSimpleEval;
// Public because they need to be updatable by the stats
CounterMoveHistory counterMoves;
ButterflyHistory mainHistory;
CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2];
PawnHistory pawnHistory;
CorrectionHistory correctionHistory;
private:
void iterative_deepening();
// Main search function for both PV and non-PV nodes
template<NodeType nodeType>
Value search(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth, bool cutNode);
// Quiescence search function, which is called by the main search
template<NodeType nodeType>
Value qsearch(Position& pos, Stack* ss, Value alpha, Value beta, Depth depth = 0);
Depth reduction(bool i, Depth d, int mn, int delta) {
int reductionScale = reductions[d] * reductions[mn];
return (reductionScale + 1346 - int(delta) * 896 / int(rootDelta)) / 1024
+ (!i && reductionScale > 880);
}
// Get a pointer to the search manager, only allowed to be called by the
// main thread.
SearchManager* main_manager() const {
assert(thread_idx == 0);
return static_cast<SearchManager*>(manager.get());
}
LimitsType limits;
size_t pvIdx, pvLast;
std::atomic<uint64_t> nodes, tbHits, bestMoveChanges;
int selDepth, nmpMinPly;
Position rootPos;
StateInfo rootState;
RootMoves rootMoves;
Depth rootDepth, completedDepth;
Value rootDelta;
size_t thread_idx;
// Reductions lookup table initialized at startup
int reductions[MAX_MOVES]; // [depth or moveNumber]
// The main thread has a SearchManager, the others have a NullSearchManager
std::unique_ptr<ISearchManager> manager;
const OptionsMap& options;
ThreadPool& threads;
TranspositionTable& tt;
friend class Stockfish::ThreadPool;
friend class Stockfish::UCI;
friend class SearchManager;
};
} // namespace Search

View file

@ -42,7 +42,6 @@
#include "../position.h"
#include "../search.h"
#include "../types.h"
#include "../uci.h"
#ifndef _WIN32
#include <fcntl.h>
@ -1574,7 +1573,7 @@ int Tablebases::probe_dtz(Position& pos, ProbeState* result) {
// Use the DTZ tables to rank root moves.
//
// A return value false indicates that not all probes were successful.
bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50) {
ProbeState result = OK;
StateInfo st;
@ -1585,7 +1584,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// Check whether a position was repeated since the last zeroing move.
bool rep = pos.has_repeated();
int dtz, bound = Options["Syzygy50MoveRule"] ? (MAX_DTZ - 100) : 1;
int dtz, bound = rule50 ? (MAX_DTZ - 100) : 1;
// Probe and rank each move
for (auto& m : rootMoves)
@ -1647,7 +1646,7 @@ bool Tablebases::root_probe(Position& pos, Search::RootMoves& rootMoves) {
// This is a fallback for the case that some or all DTZ tables are missing.
//
// A return value false indicates that not all probes were successful.
bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) {
bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50) {
static const int WDL_to_rank[] = {-MAX_DTZ, -MAX_DTZ + 101, 0, MAX_DTZ - 101, MAX_DTZ};
@ -1655,7 +1654,6 @@ bool Tablebases::root_probe_wdl(Position& pos, Search::RootMoves& rootMoves) {
StateInfo st;
WDLScore wdl;
bool rule50 = Options["Syzygy50MoveRule"];
// Probe and rank each move
for (auto& m : rootMoves)

View file

@ -25,6 +25,7 @@
namespace Stockfish {
class Position;
class OptionsMap;
}
namespace Stockfish::Tablebases {
@ -47,12 +48,13 @@ enum ProbeState {
extern int MaxCardinality;
void init(const std::string& paths);
WDLScore probe_wdl(Position& pos, ProbeState* result);
int probe_dtz(Position& pos, ProbeState* result);
bool root_probe(Position& pos, Search::RootMoves& rootMoves);
bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves);
void rank_root_moves(Position& pos, Search::RootMoves& rootMoves);
bool root_probe(Position& pos, Search::RootMoves& rootMoves, bool rule50);
bool root_probe_wdl(Position& pos, Search::RootMoves& rootMoves, bool rule50);
void rank_root_moves(const OptionsMap& options, Position& pos, Search::RootMoves& rootMoves);
} // namespace Stockfish::Tablebases

View file

@ -23,9 +23,8 @@
#include <cmath>
#include <cstdlib>
#include <deque>
#include <initializer_list>
#include <unordered_map>
#include <memory>
#include <unordered_map>
#include <utility>
#include "evaluate.h"
@ -33,18 +32,21 @@
#include "movegen.h"
#include "search.h"
#include "syzygy/tbprobe.h"
#include "timeman.h"
#include "tt.h"
#include "uci.h"
#include "types.h"
#include "ucioption.h"
namespace Stockfish {
ThreadPool Threads; // Global object
// Constructor launches the thread and waits until it goes to sleep
// in idle_loop(). Note that 'searching' and 'exit' should be already set.
Thread::Thread(size_t n) :
Thread::Thread(Search::SharedState& sharedState,
std::unique_ptr<Search::ISearchManager> sm,
size_t n) :
worker(std::make_unique<Search::Worker>(sharedState, std::move(sm), n)),
idx(n),
nthreads(sharedState.options["Threads"]),
stdThread(&Thread::idle_loop, this) {
wait_for_search_finished();
@ -62,24 +64,6 @@ Thread::~Thread() {
stdThread.join();
}
// Reset histories, usually before a new game
void Thread::clear() {
counterMoves.fill(Move::none());
mainHistory.fill(0);
captureHistory.fill(0);
pawnHistory.fill(0);
correctionHistory.fill(0);
for (bool inCheck : {false, true})
for (StatsType c : {NoCaptures, Captures})
for (auto& to : continuationHistory[inCheck][c])
for (auto& h : to)
h->fill(-71);
}
// Wakes up the thread that will start the search
void Thread::start_searching() {
mutex.lock();
@ -108,7 +92,7 @@ void Thread::idle_loop() {
// some Windows NUMA hardware, for instance in fishtest. To make it simple,
// just check if running threads are below a threshold, in this case, all this
// NUMA machinery is not needed.
if (Options["Threads"] > 8)
if (nthreads > 8)
WinProcGroup::bindThisThread(idx);
while (true)
@ -123,36 +107,41 @@ void Thread::idle_loop() {
lk.unlock();
search();
worker->start_searching();
}
}
// Creates/destroys threads to match the requested number.
// Created and launched threads will immediately go to sleep in idle_loop.
// Upon resizing, threads are recreated to allow for binding if necessary.
void ThreadPool::set(size_t requested) {
void ThreadPool::set(Search::SharedState sharedState) {
if (threads.size() > 0) // destroy any existing thread(s)
{
main()->wait_for_search_finished();
main_thread()->wait_for_search_finished();
while (threads.size() > 0)
delete threads.back(), threads.pop_back();
}
const size_t requested = sharedState.options["Threads"];
if (requested > 0) // create new thread(s)
{
threads.push_back(new MainThread(0));
threads.push_back(new Thread(
sharedState, std::unique_ptr<Search::ISearchManager>(new Search::SearchManager()), 0));
while (threads.size() < requested)
threads.push_back(new Thread(threads.size()));
threads.push_back(new Thread(
sharedState, std::unique_ptr<Search::ISearchManager>(new Search::NullSearchManager()),
threads.size()));
clear();
// Reallocate the hash with the new threadpool size
TT.resize(size_t(Options["Hash"]));
main_thread()->wait_for_search_finished();
// Init thread number dependent search params.
Search::init();
// Reallocate the hash with the new threadpool size
sharedState.tt.resize(sharedState.options["Hash"], requested);
}
}
@ -161,28 +150,31 @@ void ThreadPool::set(size_t requested) {
void ThreadPool::clear() {
for (Thread* th : threads)
th->clear();
th->worker->clear();
main()->callsCnt = 0;
main()->bestPreviousScore = VALUE_INFINITE;
main()->bestPreviousAverageScore = VALUE_INFINITE;
main()->previousTimeReduction = 1.0;
main_manager()->callsCnt = 0;
main_manager()->bestPreviousScore = VALUE_INFINITE;
main_manager()->bestPreviousAverageScore = VALUE_INFINITE;
main_manager()->previousTimeReduction = 1.0;
main_manager()->tm.clear();
}
// Wakes up main thread waiting in idle_loop() and
// returns immediately. Main thread will wake up other threads and start the search.
void ThreadPool::start_thinking(Position& pos,
StateListPtr& states,
const Search::LimitsType& limits,
bool ponderMode) {
void ThreadPool::start_thinking(const OptionsMap& options,
Position& pos,
StateListPtr& states,
Search::LimitsType limits,
bool ponderMode) {
main()->wait_for_search_finished();
main_thread()->wait_for_search_finished();
main_manager()->stopOnPonderhit = stop = false;
main_manager()->ponder = ponderMode;
increaseDepth = true;
main()->stopOnPonderhit = stop = false;
increaseDepth = true;
main()->ponder = ponderMode;
Search::Limits = limits;
Search::RootMoves rootMoves;
for (const auto& m : MoveList<LEGAL>(pos))
@ -191,7 +183,7 @@ void ThreadPool::start_thinking(Position& pos,
rootMoves.emplace_back(m);
if (!rootMoves.empty())
Tablebases::rank_root_moves(pos, rootMoves);
Tablebases::rank_root_moves(options, pos, rootMoves);
// After ownership transfer 'states' becomes empty, so if we stop the search
// and call 'go' again without setting a new position states.get() == nullptr.
@ -207,15 +199,17 @@ void ThreadPool::start_thinking(Position& pos,
// since they are read-only.
for (Thread* th : threads)
{
th->nodes = th->tbHits = th->nmpMinPly = th->bestMoveChanges = 0;
th->rootDepth = th->completedDepth = 0;
th->rootMoves = rootMoves;
th->rootPos.set(pos.fen(), pos.is_chess960(), &th->rootState, th);
th->rootState = setupStates->back();
th->rootSimpleEval = Eval::simple_eval(pos, pos.side_to_move());
th->worker->limits = limits;
th->worker->nodes = th->worker->tbHits = th->worker->nmpMinPly =
th->worker->bestMoveChanges = 0;
th->worker->rootDepth = th->worker->completedDepth = 0;
th->worker->rootMoves = rootMoves;
th->worker->rootPos.set(pos.fen(), pos.is_chess960(), &th->worker->rootState);
th->worker->rootState = setupStates->back();
th->worker->rootSimpleEval = Eval::simple_eval(pos, pos.side_to_move());
}
main()->start_searching();
main_thread()->start_searching();
}
Thread* ThreadPool::get_best_thread() const {
@ -226,30 +220,32 @@ Thread* ThreadPool::get_best_thread() const {
// Find the minimum score of all threads
for (Thread* th : threads)
minScore = std::min(minScore, th->rootMoves[0].score);
minScore = std::min(minScore, th->worker->rootMoves[0].score);
// Vote according to score and depth, and select the best thread
auto thread_value = [minScore](Thread* th) {
return (th->rootMoves[0].score - minScore + 14) * int(th->completedDepth);
return (th->worker->rootMoves[0].score - minScore + 14) * int(th->worker->completedDepth);
};
for (Thread* th : threads)
votes[th->rootMoves[0].pv[0]] += thread_value(th);
votes[th->worker->rootMoves[0].pv[0]] += thread_value(th);
for (Thread* th : threads)
if (std::abs(bestThread->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY)
if (std::abs(bestThread->worker->rootMoves[0].score) >= VALUE_TB_WIN_IN_MAX_PLY)
{
// Make sure we pick the shortest mate / TB conversion or stave off mate the longest
if (th->rootMoves[0].score > bestThread->rootMoves[0].score)
if (th->worker->rootMoves[0].score > bestThread->worker->rootMoves[0].score)
bestThread = th;
}
else if (th->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY
|| (th->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY
&& (votes[th->rootMoves[0].pv[0]] > votes[bestThread->rootMoves[0].pv[0]]
|| (votes[th->rootMoves[0].pv[0]] == votes[bestThread->rootMoves[0].pv[0]]
&& thread_value(th) * int(th->rootMoves[0].pv.size() > 2)
else if (th->worker->rootMoves[0].score >= VALUE_TB_WIN_IN_MAX_PLY
|| (th->worker->rootMoves[0].score > VALUE_TB_LOSS_IN_MAX_PLY
&& (votes[th->worker->rootMoves[0].pv[0]]
> votes[bestThread->worker->rootMoves[0].pv[0]]
|| (votes[th->worker->rootMoves[0].pv[0]]
== votes[bestThread->worker->rootMoves[0].pv[0]]
&& thread_value(th) * int(th->worker->rootMoves[0].pv.size() > 2)
> thread_value(bestThread)
* int(bestThread->rootMoves[0].pv.size() > 2)))))
* int(bestThread->worker->rootMoves[0].pv.size() > 2)))))
bestThread = th;
return bestThread;
@ -257,7 +253,7 @@ Thread* ThreadPool::get_best_thread() const {
// Start non-main threads
// Will be invoked by main thread after it has started searching
void ThreadPool::start_searching() {
for (Thread* th : threads)

View file

@ -23,91 +23,76 @@
#include <condition_variable>
#include <cstddef>
#include <cstdint>
#include <memory>
#include <mutex>
#include <vector>
#include "movepick.h"
#include "position.h"
#include "search.h"
#include "thread_win32_osx.h"
#include "types.h"
namespace Stockfish {
// Thread class keeps together all the thread-related stuff.
class Thread {
class OptionsMap;
using Value = int;
// Abstraction of a thread. It contains a pointer to the worker and a native thread.
// After construction, the native thread is started with idle_loop()
// waiting for a signal to start searching.
// When the signal is received, the thread starts searching and when
// the search is finished, it goes back to idle_loop() waiting for a new signal.
class Thread {
public:
Thread(Search::SharedState&, std::unique_ptr<Search::ISearchManager>, size_t);
virtual ~Thread();
void idle_loop();
void start_searching();
void wait_for_search_finished();
size_t id() const { return idx; }
std::unique_ptr<Search::Worker> worker;
private:
std::mutex mutex;
std::condition_variable cv;
size_t idx;
size_t idx, nthreads;
bool exit = false, searching = true; // Set before starting std::thread
NativeThread stdThread;
public:
explicit Thread(size_t);
virtual ~Thread();
virtual void search();
void clear();
void idle_loop();
void start_searching();
void wait_for_search_finished();
size_t id() const { return idx; }
size_t pvIdx, pvLast;
std::atomic<uint64_t> nodes, tbHits, bestMoveChanges;
int selDepth, nmpMinPly;
Value bestValue;
int optimism[COLOR_NB];
Position rootPos;
StateInfo rootState;
Search::RootMoves rootMoves;
Depth rootDepth, completedDepth;
int rootDelta;
Value rootSimpleEval;
CounterMoveHistory counterMoves;
ButterflyHistory mainHistory;
CapturePieceToHistory captureHistory;
ContinuationHistory continuationHistory[2][2];
PawnHistory pawnHistory;
CorrectionHistory correctionHistory;
};
// MainThread is a derived class specific for main thread
struct MainThread: public Thread {
using Thread::Thread;
void search() override;
void check_time();
double previousTimeReduction;
Value bestPreviousScore;
Value bestPreviousAverageScore;
Value iterValue[4];
int callsCnt;
bool stopOnPonderhit;
std::atomic_bool ponder;
};
// ThreadPool struct handles all the threads-related stuff like init, starting,
// parking and, most importantly, launching a thread. All the access to threads
// is done through this class.
struct ThreadPool {
class ThreadPool {
void start_thinking(Position&, StateListPtr&, const Search::LimitsType&, bool = false);
public:
~ThreadPool() {
// destroy any existing thread(s)
if (threads.size() > 0)
{
main_thread()->wait_for_search_finished();
while (threads.size() > 0)
delete threads.back(), threads.pop_back();
}
}
void
start_thinking(const OptionsMap&, Position&, StateListPtr&, Search::LimitsType, bool = false);
void clear();
void set(size_t);
void set(Search::SharedState);
MainThread* main() const { return static_cast<MainThread*>(threads.front()); }
uint64_t nodes_searched() const { return accumulate(&Thread::nodes); }
uint64_t tb_hits() const { return accumulate(&Thread::tbHits); }
Thread* get_best_thread() const;
void start_searching();
void wait_for_search_finished() const;
Search::SearchManager* main_manager() const {
return static_cast<Search::SearchManager*>(main_thread()->worker.get()->manager.get());
};
Thread* main_thread() const { return threads.front(); }
uint64_t nodes_searched() const { return accumulate(&Search::Worker::nodes); }
uint64_t tb_hits() const { return accumulate(&Search::Worker::tbHits); }
Thread* get_best_thread() const;
void start_searching();
void wait_for_search_finished() const;
std::atomic_bool stop, increaseDepth;
@ -122,17 +107,15 @@ struct ThreadPool {
StateListPtr setupStates;
std::vector<Thread*> threads;
uint64_t accumulate(std::atomic<uint64_t> Thread::*member) const {
uint64_t accumulate(std::atomic<uint64_t> Search::Worker::*member) const {
uint64_t sum = 0;
for (Thread* th : threads)
sum += (th->*member).load(std::memory_order_relaxed);
sum += (th->worker.get()->*member).load(std::memory_order_relaxed);
return sum;
}
};
extern ThreadPool Threads;
} // namespace Stockfish
#endif // #ifndef THREAD_H_INCLUDED

View file

@ -30,31 +30,35 @@
#if defined(__APPLE__) || defined(__MINGW32__) || defined(__MINGW64__) || defined(USE_PTHREADS)
#include <pthread.h>
#include <functional>
namespace Stockfish {
static const size_t TH_STACK_SIZE = 8 * 1024 * 1024;
template<class T, class P = std::pair<T*, void (T::*)()>>
void* start_routine(void* ptr) {
P* p = reinterpret_cast<P*>(ptr);
(p->first->*(p->second))(); // Call member function pointer
delete p;
// free function to be passed to pthread_create()
inline void* start_routine(void* ptr) {
auto func = reinterpret_cast<std::function<void()>*>(ptr);
(*func)(); // Call the function
delete func;
return nullptr;
}
class NativeThread {
pthread_t thread;
static constexpr size_t TH_STACK_SIZE = 8 * 1024 * 1024;
public:
template<class T, class P = std::pair<T*, void (T::*)()>>
explicit NativeThread(void (T::*fun)(), T* obj) {
template<class Function, class... Args>
explicit NativeThread(Function&& fun, Args&&... args) {
auto func = new std::function<void()>(
std::bind(std::forward<Function>(fun), std::forward<Args>(args)...));
pthread_attr_t attr_storage, *attr = &attr_storage;
pthread_attr_init(attr);
pthread_attr_setstacksize(attr, TH_STACK_SIZE);
pthread_create(&thread, attr, start_routine<T>, new P(obj, fun));
pthread_create(&thread, attr, start_routine, func);
}
void join() { pthread_join(thread, nullptr); }
};

View file

@ -19,30 +19,47 @@
#include "timeman.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstdint>
#include "search.h"
#include "uci.h"
#include "ucioption.h"
namespace Stockfish {
TimeManagement Time; // Our global time management object
TimePoint TimeManagement::optimum() const { return optimumTime; }
TimePoint TimeManagement::maximum() const { return maximumTime; }
TimePoint TimeManagement::elapsed(size_t nodes) const {
return useNodesTime ? TimePoint(nodes) : now() - startTime;
}
void TimeManagement::clear() {
availableNodes = 0; // When in 'nodes as time' mode
}
void TimeManagement::advance_nodes_time(std::int64_t nodes) {
assert(useNodesTime);
availableNodes += nodes;
}
// Called at the beginning of the search and calculates
// the bounds of time allowed for the current game ply. We currently support:
// 1) x basetime (+ z increment)
// 2) x moves in y seconds (+ z increment)
void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) {
void TimeManagement::init(Search::LimitsType& limits,
Color us,
int ply,
const OptionsMap& options) {
// If we have no time, no need to initialize TM, except for the start time,
// which is used by movetime.
startTime = limits.startTime;
if (limits.time[us] == 0)
return;
TimePoint moveOverhead = TimePoint(Options["Move Overhead"]);
TimePoint npmsec = TimePoint(Options["nodestime"]);
TimePoint moveOverhead = TimePoint(options["Move Overhead"]);
TimePoint npmsec = TimePoint(options["nodestime"]);
// optScale is a percentage of available time to use for the current move.
// maxScale is a multiplier applied to optimumTime.
@ -54,6 +71,8 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) {
// must be much lower than the real engine speed.
if (npmsec)
{
useNodesTime = true;
if (!availableNodes) // Only once at game start
availableNodes = npmsec * limits.time[us]; // Time is in msec
@ -100,7 +119,7 @@ void TimeManagement::init(Search::LimitsType& limits, Color us, int ply) {
maximumTime =
TimePoint(std::min(0.84 * limits.time[us] - moveOverhead, maxScale * optimumTime)) - 10;
if (Options["Ponder"])
if (options["Ponder"])
optimumTime += optimumTime / 4;
}

View file

@ -19,35 +19,41 @@
#ifndef TIMEMAN_H_INCLUDED
#define TIMEMAN_H_INCLUDED
#include <cstddef>
#include <cstdint>
#include "misc.h"
#include "search.h"
#include "thread.h"
#include "types.h"
namespace Stockfish {
class OptionsMap;
namespace Search {
struct LimitsType;
}
// The TimeManagement class computes the optimal time to think depending on
// the maximum available time, the game move number, and other parameters.
class TimeManagement {
public:
void init(Search::LimitsType& limits, Color us, int ply);
TimePoint optimum() const { return optimumTime; }
TimePoint maximum() const { return maximumTime; }
TimePoint elapsed() const {
return Search::Limits.npmsec ? TimePoint(Threads.nodes_searched()) : now() - startTime;
}
void init(Search::LimitsType& limits, Color us, int ply, const OptionsMap& options);
int64_t availableNodes; // When in 'nodes as time' mode
TimePoint optimum() const;
TimePoint maximum() const;
TimePoint elapsed(std::size_t nodes) const;
void clear();
void advance_nodes_time(std::int64_t nodes);
private:
TimePoint startTime;
TimePoint optimumTime;
TimePoint maximumTime;
};
extern TimeManagement Time;
std::int64_t availableNodes = 0; // When in 'nodes as time' mode
bool useNodesTime = false; // True if we are in 'nodes as time' mode
};
} // namespace Stockfish

View file

@ -26,16 +26,13 @@
#include <vector>
#include "misc.h"
#include "thread.h"
#include "uci.h"
namespace Stockfish {
TranspositionTable TT; // Our global transposition table
// Populates the TTEntry with a new node's data, possibly
// overwriting an old position. The update is not atomic and can be racy.
void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev) {
void TTEntry::save(
Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8) {
// Preserve any existing move for the same position
if (m || uint16_t(k) != key16)
@ -49,7 +46,7 @@ void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev)
key16 = uint16_t(k);
depth8 = uint8_t(d - DEPTH_OFFSET);
genBound8 = uint8_t(TT.generation8 | uint8_t(pv) << 2 | b);
genBound8 = uint8_t(generation8 | uint8_t(pv) << 2 | b);
value16 = int16_t(v);
eval16 = int16_t(ev);
}
@ -59,10 +56,7 @@ void TTEntry::save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev)
// Sets the size of the transposition table,
// measured in megabytes. Transposition table consists of a power of 2 number
// of clusters and each cluster consists of ClusterSize number of TTEntry.
void TranspositionTable::resize(size_t mbSize) {
Threads.main()->wait_for_search_finished();
void TranspositionTable::resize(size_t mbSize, int threadCount) {
aligned_large_pages_free(table);
clusterCount = mbSize * 1024 * 1024 / sizeof(Cluster);
@ -74,28 +68,25 @@ void TranspositionTable::resize(size_t mbSize) {
exit(EXIT_FAILURE);
}
clear();
clear(threadCount);
}
// Initializes the entire transposition table to zero,
// in a multi-threaded way.
void TranspositionTable::clear() {
void TranspositionTable::clear(size_t threadCount) {
std::vector<std::thread> threads;
for (size_t idx = 0; idx < size_t(Options["Threads"]); ++idx)
for (size_t idx = 0; idx < size_t(threadCount); ++idx)
{
threads.emplace_back([this, idx]() {
threads.emplace_back([this, idx, threadCount]() {
// Thread binding gives faster search on systems with a first-touch policy
if (Options["Threads"] > 8)
if (threadCount > 8)
WinProcGroup::bindThisThread(idx);
// Each thread will zero its part of the hash table
const size_t stride = size_t(clusterCount / Options["Threads"]),
start = size_t(stride * idx),
len =
idx != size_t(Options["Threads"]) - 1 ? stride : clusterCount - start;
const size_t stride = size_t(clusterCount / threadCount), start = size_t(stride * idx),
len = idx != size_t(threadCount) - 1 ? stride : clusterCount - start;
std::memset(&table[start], 0, len * sizeof(Cluster));
});

View file

@ -45,7 +45,7 @@ struct TTEntry {
Depth depth() const { return Depth(depth8 + DEPTH_OFFSET); }
bool is_pv() const { return bool(genBound8 & 0x4); }
Bound bound() const { return Bound(genBound8 & 0x3); }
void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev);
void save(Key k, Value v, bool pv, Bound b, Depth d, Move m, Value ev, uint8_t generation8);
private:
friend class TranspositionTable;
@ -88,23 +88,23 @@ class TranspositionTable {
void new_search() { generation8 += GENERATION_DELTA; } // Lower bits are used for other things
TTEntry* probe(const Key key, bool& found) const;
int hashfull() const;
void resize(size_t mbSize);
void clear();
void resize(size_t mbSize, int threadCount);
void clear(size_t threadCount);
TTEntry* first_entry(const Key key) const {
return &table[mul_hi64(key, clusterCount)].entry[0];
}
uint8_t generation() const { return generation8; }
private:
friend struct TTEntry;
size_t clusterCount;
Cluster* table;
uint8_t generation8; // Size must be not bigger than TTEntry::genBound8
Cluster* table = nullptr;
uint8_t generation8 = 0; // Size must be not bigger than TTEntry::genBound8
};
extern TranspositionTable TT;
} // namespace Stockfish
#endif // #ifndef TT_H_INCLUDED

View file

@ -24,14 +24,15 @@
#include <sstream>
#include <string>
#include "uci.h"
#include "ucioption.h"
using std::string;
namespace Stockfish {
bool Tune::update_on_last;
const UCI::Option* LastOption = nullptr;
const Option* LastOption = nullptr;
OptionsMap* Tune::options;
static std::map<std::string, int> TuneResults;
string Tune::next(string& names, bool pop) {
@ -53,13 +54,13 @@ string Tune::next(string& names, bool pop) {
return name;
}
static void on_tune(const UCI::Option& o) {
static void on_tune(const Option& o) {
if (!Tune::update_on_last || LastOption == &o)
Tune::read_options();
}
static void make_option(const string& n, int v, const SetRange& r) {
static void make_option(OptionsMap* options, const string& n, int v, const SetRange& r) {
// Do not generate option when there is nothing to tune (ie. min = max)
if (r(v).first == r(v).second)
@ -68,8 +69,8 @@ static void make_option(const string& n, int v, const SetRange& r) {
if (TuneResults.count(n))
v = TuneResults[n];
Options[n] << UCI::Option(v, r(v).first, r(v).second, on_tune);
LastOption = &Options[n];
(*options)[n] << Option(v, r(v).first, r(v).second, on_tune);
LastOption = &((*options)[n]);
// Print formatted parameters, ready to be copy-pasted in Fishtest
std::cout << n << "," << v << "," << r(v).first << "," << r(v).second << ","
@ -79,13 +80,13 @@ static void make_option(const string& n, int v, const SetRange& r) {
template<>
void Tune::Entry<int>::init_option() {
make_option(name, value, range);
make_option(options, name, value, range);
}
template<>
void Tune::Entry<int>::read_option() {
if (Options.count(name))
value = int(Options[name]);
if (options->count(name))
value = int((*options)[name]);
}
// Instead of a variable here we have a PostUpdate function: just call it

View file

@ -28,6 +28,8 @@
namespace Stockfish {
class OptionsMap;
using Range = std::pair<int, int>; // Option's min-max values
using RangeFun = Range(int);
@ -151,7 +153,8 @@ class Tune {
return instance().add(SetDefaultRange, names.substr(1, names.size() - 2),
args...); // Remove trailing parenthesis
}
static void init() {
static void init(OptionsMap& o) {
options = &o;
for (auto& e : instance().list)
e->init_option();
read_options();
@ -160,7 +163,9 @@ class Tune {
for (auto& e : instance().list)
e->read_option();
}
static bool update_on_last;
static bool update_on_last;
static OptionsMap* options;
};
// Some macro magic :-) we define a dummy int variable that the compiler initializes calling Tune::add()

View file

@ -22,111 +22,156 @@
#include <cassert>
#include <cctype>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <deque>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <vector>
#include "benchmark.h"
#include "evaluate.h"
#include "misc.h"
#include "movegen.h"
#include "nnue/evaluate_nnue.h"
#include "nnue/nnue_architecture.h"
#include "position.h"
#include "search.h"
#include "thread.h"
#include "syzygy/tbprobe.h"
#include "types.h"
#include "ucioption.h"
namespace Stockfish {
namespace {
constexpr auto StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
constexpr int NormalizeToPawnValue = 328;
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
// FEN string for the initial position in standard chess
const char* StartFEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
UCI::UCI(int argc, char** argv) :
cli(argc, argv) {
evalFiles = {{Eval::NNUE::Big, {"EvalFile", EvalFileDefaultNameBig, "None", ""}},
{Eval::NNUE::Small, {"EvalFileSmall", EvalFileDefaultNameSmall, "None", ""}}};
// Called when the engine receives the "position" UCI command.
// It sets up the position that is described in the given FEN string ("fen") or
// the initial position ("startpos") and then makes the moves given in the following
// move list ("moves").
void position(Position& pos, std::istringstream& is, StateListPtr& states) {
options["Debug Log File"] << Option("", [](const Option& o) { start_logger(o); });
Move m;
std::string token, fen;
options["Threads"] << Option(1, 1, 1024, [this](const Option&) {
threads.set({options, threads, tt});
});
is >> token;
options["Hash"] << Option(16, 1, MaxHashMB, [this](const Option& o) {
threads.main_thread()->wait_for_search_finished();
tt.resize(o, options["Threads"]);
});
if (token == "startpos")
{
fen = StartFEN;
is >> token; // Consume the "moves" token, if any
}
else if (token == "fen")
while (is >> token && token != "moves")
fen += token + " ";
else
return;
options["Clear Hash"] << Option(true, [this](const Option&) { search_clear(); });
options["Ponder"] << Option(false);
options["MultiPV"] << Option(1, 1, 500);
options["Skill Level"] << Option(20, 0, 20);
options["Move Overhead"] << Option(10, 0, 5000);
options["nodestime"] << Option(0, 0, 10000);
options["UCI_Chess960"] << Option(false);
options["UCI_LimitStrength"] << Option(false);
options["UCI_Elo"] << Option(1320, 1320, 3190);
options["UCI_ShowWDL"] << Option(false);
options["SyzygyPath"] << Option("<empty>", [](const Option& o) { Tablebases::init(o); });
options["SyzygyProbeDepth"] << Option(1, 1, 100);
options["Syzygy50MoveRule"] << Option(true);
options["SyzygyProbeLimit"] << Option(7, 0, 7);
options["EvalFile"] << Option(EvalFileDefaultNameBig, [this](const Option&) {
evalFiles = Eval::NNUE::load_networks(cli.binaryDirectory, options, evalFiles);
});
states = StateListPtr(new std::deque<StateInfo>(1)); // Drop the old state and create a new one
pos.set(fen, Options["UCI_Chess960"], &states->back(), Threads.main());
threads.set({options, threads, tt});
// Parse the move list, if any
while (is >> token && (m = UCI::to_move(pos, token)) != Move::none())
{
states->emplace_back();
pos.do_move(m, states->back());
}
search_clear(); // After threads are up
}
// Prints the evaluation of the current position,
// consistent with the UCI options set so far.
void trace_eval(Position& pos) {
void UCI::loop() {
Position pos;
std::string token, cmd;
StateListPtr states(new std::deque<StateInfo>(1));
Position p;
p.set(pos.fen(), Options["UCI_Chess960"], &states->back(), Threads.main());
Eval::NNUE::verify();
pos.set(StartFEN, false, &states->back());
sync_cout << "\n" << Eval::trace(p) << sync_endl;
for (int i = 1; i < cli.argc; ++i)
cmd += std::string(cli.argv[i]) + " ";
do
{
if (cli.argc == 1
&& !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication
cmd = "quit";
std::istringstream is(cmd);
token.clear(); // Avoid a stale if getline() returns nothing or a blank line
is >> std::skipws >> token;
if (token == "quit" || token == "stop")
threads.stop = true;
// The GUI sends 'ponderhit' to tell that the user has played the expected move.
// So, 'ponderhit' is sent if pondering was done on the same move that the user
// has played. The search should continue, but should also switch from pondering
// to the normal search.
else if (token == "ponderhit")
threads.main_manager()->ponder = false; // Switch to the normal search
else if (token == "uci")
sync_cout << "id name " << engine_info(true) << "\n"
<< options << "\nuciok" << sync_endl;
else if (token == "setoption")
setoption(is);
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;
// Add custom non-UCI commands, mainly for debugging purposes.
// These commands must not be used during a search!
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")
trace_eval(pos);
else if (token == "compiler")
sync_cout << compiler_info() << sync_endl;
else if (token == "export_net")
{
std::optional<std::string> filename;
std::string f;
if (is >> std::skipws >> f)
filename = f;
Eval::NNUE::save_eval(filename, Eval::NNUE::Big, evalFiles);
}
else if (token == "--help" || token == "help" || token == "--license" || token == "license")
sync_cout
<< "\nStockfish is a powerful chess engine for playing and analyzing."
"\nIt is released as free software licensed under the GNU GPLv3 License."
"\nStockfish is normally used with a graphical user interface (GUI) and implements"
"\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc."
"\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme"
"\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n"
<< sync_endl;
else if (!token.empty() && token[0] != '#')
sync_cout << "Unknown command: '" << cmd << "'. Type help for more information."
<< sync_endl;
} while (token != "quit" && cli.argc == 1); // The command-line arguments are one-shot
}
void UCI::go(Position& pos, std::istringstream& is, StateListPtr& states) {
// Called when the engine receives the "setoption" UCI command.
// The function updates the UCI option ("name") to the given value ("value").
void setoption(std::istringstream& is) {
Threads.main()->wait_for_search_finished();
std::string token, name, value;
is >> token; // Consume the "name" token
// Read the option name (can contain spaces)
while (is >> token && token != "value")
name += (name.empty() ? "" : " ") + token;
// Read the option value (can contain spaces)
while (is >> token)
value += (value.empty() ? "" : " ") + token;
if (Options.count(name))
Options[name] = value;
else
sync_cout << "No such option: " << name << sync_endl;
}
// Called when the engine receives the "go" UCI command. The function sets the
// thinking time and other parameters from the input string then stars with a search
void go(Position& pos, std::istringstream& is, StateListPtr& states) {
Search::LimitsType limits;
std::string token;
@ -137,7 +182,7 @@ void go(Position& pos, std::istringstream& is, StateListPtr& states) {
while (is >> token)
if (token == "searchmoves") // Needs to be the last command on the line
while (is >> token)
limits.searchmoves.push_back(UCI::to_move(pos, token));
limits.searchmoves.push_back(to_move(pos, token));
else if (token == "wtime")
is >> limits.time[WHITE];
@ -164,16 +209,12 @@ void go(Position& pos, std::istringstream& is, StateListPtr& states) {
else if (token == "ponder")
ponderMode = true;
Threads.start_thinking(pos, states, limits, ponderMode);
Eval::NNUE::verify(options, evalFiles);
threads.start_thinking(options, pos, states, limits, ponderMode);
}
// Called when the engine receives the "bench" command.
// First, a list of UCI commands is set up according to the bench
// parameters, then it is run one by one, printing a summary at the end.
void bench(Position& pos, std::istream& args, StateListPtr& states) {
void UCI::bench(Position& pos, std::istream& args, StateListPtr& states) {
std::string token;
uint64_t num, nodes = 0, cnt = 1;
@ -196,8 +237,8 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) {
if (token == "go")
{
go(pos, is, states);
Threads.main()->wait_for_search_finished();
nodes += Threads.nodes_searched();
threads.main_thread()->wait_for_search_finished();
nodes += threads.nodes_searched();
}
else
trace_eval(pos);
@ -208,9 +249,9 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) {
position(pos, is, states);
else if (token == "ucinewgame")
{
Search::clear();
search_clear(); // Search::clear() may take a while
elapsed = now();
} // Search::clear() may take a while
}
}
elapsed = now() - elapsed + 1; // Ensure positivity to avoid a 'divide by zero'
@ -222,6 +263,160 @@ void bench(Position& pos, std::istream& args, StateListPtr& states) {
<< "\nNodes/second : " << 1000 * nodes / elapsed << std::endl;
}
void UCI::trace_eval(Position& pos) {
StateListPtr states(new std::deque<StateInfo>(1));
Position p;
p.set(pos.fen(), options["UCI_Chess960"], &states->back());
Eval::NNUE::verify(options, evalFiles);
sync_cout << "\n" << Eval::trace(p, *threads.main_thread()->worker.get()) << sync_endl;
}
void UCI::search_clear() {
threads.main_thread()->wait_for_search_finished();
tt.clear(options["Threads"]);
threads.clear();
Tablebases::init(options["SyzygyPath"]); // Free mapped files
}
void UCI::setoption(std::istringstream& is) {
threads.main_thread()->wait_for_search_finished();
options.setoption(is);
}
void UCI::position(Position& pos, std::istringstream& is, StateListPtr& states) {
Move m;
std::string token, fen;
is >> token;
if (token == "startpos")
{
fen = StartFEN;
is >> token; // Consume the "moves" token, if any
}
else if (token == "fen")
while (is >> token && token != "moves")
fen += token + " ";
else
return;
states = StateListPtr(new std::deque<StateInfo>(1)); // Drop the old state and create a new one
pos.set(fen, options["UCI_Chess960"], &states->back());
// Parse the move list, if any
while (is >> token && (m = to_move(pos, token)) != Move::none())
{
states->emplace_back();
pos.do_move(m, states->back());
}
}
int UCI::to_cp(Value v) { return 100 * v / NormalizeToPawnValue; }
std::string UCI::value(Value v) {
assert(-VALUE_INFINITE < v && v < VALUE_INFINITE);
std::stringstream ss;
if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY)
ss << "cp " << to_cp(v);
else if (std::abs(v) <= VALUE_TB)
{
const int ply = VALUE_TB - std::abs(v); // recompute ss->ply
ss << "cp " << (v > 0 ? 20000 - ply : -20000 + ply);
}
else
ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2;
return ss.str();
}
std::string UCI::square(Square s) {
return std::string{char('a' + file_of(s)), char('1' + rank_of(s))};
}
std::string UCI::move(Move m, bool chess960) {
if (m == Move::none())
return "(none)";
if (m == Move::null())
return "0000";
Square from = m.from_sq();
Square to = m.to_sq();
if (m.type_of() == CASTLING && !chess960)
to = make_square(to > from ? FILE_G : FILE_C, rank_of(from));
std::string move = square(from) + square(to);
if (m.type_of() == PROMOTION)
move += " pnbrqk"[m.promotion_type()];
return move;
}
std::string UCI::pv(const Search::Worker& workerThread,
TimePoint elapsed,
uint64_t nodesSearched,
uint64_t tb_hits,
int hashfull,
bool rootInTB) {
std::stringstream ss;
TimePoint time = elapsed + 1;
const auto& rootMoves = workerThread.rootMoves;
const auto& depth = workerThread.completedDepth;
const auto& pos = workerThread.rootPos;
size_t pvIdx = workerThread.pvIdx;
size_t multiPV = std::min(size_t(workerThread.options["MultiPV"]), rootMoves.size());
uint64_t tbHits = tb_hits + (rootInTB ? rootMoves.size() : 0);
for (size_t i = 0; i < multiPV; ++i)
{
bool updated = rootMoves[i].score != -VALUE_INFINITE;
if (depth == 1 && !updated && i > 0)
continue;
Depth d = updated ? depth : std::max(1, depth - 1);
Value v = updated ? rootMoves[i].uciScore : rootMoves[i].previousScore;
if (v == -VALUE_INFINITE)
v = VALUE_ZERO;
bool tb = rootInTB && std::abs(v) <= VALUE_TB;
v = tb ? rootMoves[i].tbScore : v;
if (ss.rdbuf()->in_avail()) // Not at first line
ss << "\n";
ss << "info"
<< " depth " << d << " seldepth " << rootMoves[i].selDepth << " multipv " << i + 1
<< " score " << value(v);
if (workerThread.options["UCI_ShowWDL"])
ss << wdl(v, pos.game_ply());
if (i == pvIdx && !tb && updated) // tablebase- and previous-scores are exact
ss << (rootMoves[i].scoreLowerbound
? " lowerbound"
: (rootMoves[i].scoreUpperbound ? " upperbound" : ""));
ss << " nodes " << nodesSearched << " nps " << nodesSearched * 1000 / time << " hashfull "
<< hashfull << " tbhits " << tbHits << " time " << time << " pv";
for (Move m : rootMoves[i].pv)
ss << " " << move(m, pos.is_chess960());
}
return ss.str();
}
namespace {
// The win rate model returns the probability of winning (in per mille units) given an
// eval and a game ply. It fits the LTC fishtest statistics rather accurately.
int win_rate_model(Value v, int ply) {
@ -236,7 +431,7 @@ int win_rate_model(Value v, int ply) {
constexpr double bs[] = {-2.29434733, 13.27689788, -14.26828904, 63.45318330};
// Enforce that NormalizeToPawnValue corresponds to a 50% win rate at ply 64
static_assert(UCI::NormalizeToPawnValue == int(as[0] + as[1] + as[2] + as[3]));
static_assert(NormalizeToPawnValue == int(as[0] + as[1] + as[2] + as[3]));
double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3];
double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3];
@ -247,132 +442,9 @@ int win_rate_model(Value v, int ply) {
// Return the win rate in per mille units, rounded to the nearest integer
return int(0.5 + 1000 / (1 + std::exp((a - x) / b)));
}
} // namespace
// Waits for a command from the stdin, parses it, and then calls the appropriate
// function. It also intercepts an end-of-file (EOF) indication from the stdin to ensure a
// graceful exit if the GUI dies unexpectedly. When called with some command-line arguments,
// like running 'bench', the function returns immediately after the command is executed.
// In addition to the UCI ones, some additional debug commands are also supported.
void UCI::loop(int argc, char* argv[]) {
Position pos;
std::string token, cmd;
StateListPtr states(new std::deque<StateInfo>(1));
pos.set(StartFEN, false, &states->back(), Threads.main());
for (int i = 1; i < argc; ++i)
cmd += std::string(argv[i]) + " ";
do
{
if (argc == 1
&& !getline(std::cin, cmd)) // Wait for an input or an end-of-file (EOF) indication
cmd = "quit";
std::istringstream is(cmd);
token.clear(); // Avoid a stale if getline() returns nothing or a blank line
is >> std::skipws >> token;
if (token == "quit" || token == "stop")
Threads.stop = true;
// The GUI sends 'ponderhit' to tell that the user has played the expected move.
// So, 'ponderhit' is sent if pondering was done on the same move that the user
// has played. The search should continue, but should also switch from pondering
// to the normal search.
else if (token == "ponderhit")
Threads.main()->ponder = false; // Switch to the normal search
else if (token == "uci")
sync_cout << "id name " << engine_info(true) << "\n"
<< Options << "\nuciok" << sync_endl;
else if (token == "setoption")
setoption(is);
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;
// Add custom non-UCI commands, mainly for debugging purposes.
// These commands must not be used during a search!
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")
trace_eval(pos);
else if (token == "compiler")
sync_cout << compiler_info() << sync_endl;
else if (token == "export_net")
{
std::optional<std::string> filename;
std::string f;
if (is >> std::skipws >> f)
filename = f;
Eval::NNUE::save_eval(filename, Eval::NNUE::Big);
}
else if (token == "--help" || token == "help" || token == "--license" || token == "license")
sync_cout
<< "\nStockfish is a powerful chess engine for playing and analyzing."
"\nIt is released as free software licensed under the GNU GPLv3 License."
"\nStockfish is normally used with a graphical user interface (GUI) and implements"
"\nthe Universal Chess Interface (UCI) protocol to communicate with a GUI, an API, etc."
"\nFor any further information, visit https://github.com/official-stockfish/Stockfish#readme"
"\nor read the corresponding README.md and Copying.txt files distributed along with this program.\n"
<< sync_endl;
else if (!token.empty() && token[0] != '#')
sync_cout << "Unknown command: '" << cmd << "'. Type help for more information."
<< sync_endl;
} while (token != "quit" && argc == 1); // The command-line arguments are one-shot
}
// Turns a Value to an integer centipawn number,
// without treatment of mate and similar special scores.
int UCI::to_cp(Value v) { return 100 * v / UCI::NormalizeToPawnValue; }
// Converts a Value to a string by adhering to the UCI protocol specification:
//
// cp <x> The score from the engine's point of view in centipawns.
// mate <y> Mate in 'y' moves (not plies). If the engine is getting mated,
// uses negative values for 'y'.
std::string UCI::value(Value v) {
assert(-VALUE_INFINITE < v && v < VALUE_INFINITE);
std::stringstream ss;
if (std::abs(v) < VALUE_TB_WIN_IN_MAX_PLY)
ss << "cp " << UCI::to_cp(v);
else if (std::abs(v) <= VALUE_TB)
{
const int ply = VALUE_TB - std::abs(v); // recompute ss->ply
ss << "cp " << (v > 0 ? 20000 - ply : -20000 + ply);
}
else
ss << "mate " << (v > 0 ? VALUE_MATE - v + 1 : -VALUE_MATE - v) / 2;
return ss.str();
}
// Reports the win-draw-loss (WDL) statistics given an evaluation
// and a game ply based on the data gathered for fishtest LTC games.
std::string UCI::wdl(Value v, int ply) {
std::stringstream ss;
int wdl_w = win_rate_model(v, ply);
@ -383,49 +455,12 @@ std::string UCI::wdl(Value v, int ply) {
return ss.str();
}
// Converts a Square to a string in algebraic notation (g1, a7, etc.)
std::string UCI::square(Square s) {
return std::string{char('a' + file_of(s)), char('1' + rank_of(s))};
}
// Converts a Move to a string in coordinate notation (g1f3, a7a8q).
// The only special case is castling where the e1g1 notation is printed in
// standard chess mode and in e1h1 notation it is printed in Chess960 mode.
// Internally, all castling moves are always encoded as 'king captures rook'.
std::string UCI::move(Move m, bool chess960) {
if (m == Move::none())
return "(none)";
if (m == Move::null())
return "0000";
Square from = m.from_sq();
Square to = m.to_sq();
if (m.type_of() == CASTLING && !chess960)
to = make_square(to > from ? FILE_G : FILE_C, rank_of(from));
std::string move = UCI::square(from) + UCI::square(to);
if (m.type_of() == PROMOTION)
move += " pnbrqk"[m.promotion_type()];
return move;
}
// Converts a string representing a move in coordinate notation
// (g1f3, a7a8q) to the corresponding legal Move, if any.
Move UCI::to_move(const Position& pos, std::string& str) {
if (str.length() == 5)
str[4] = char(tolower(str[4])); // The promotion piece character must be lowercased
for (const auto& m : MoveList<LEGAL>(pos))
if (str == UCI::move(m, pos.is_chess960()))
if (str == move(m, pos.is_chess960()))
return m;
return Move::none();

105
src/uci.h
View file

@ -19,77 +19,70 @@
#ifndef UCI_H_INCLUDED
#define UCI_H_INCLUDED
#include <cstddef>
#include <iosfwd>
#include <map>
#include <cstdint>
#include <iostream>
#include <string>
#include <unordered_map>
#include "types.h"
#include "evaluate.h"
#include "misc.h"
#include "position.h"
#include "thread.h"
#include "tt.h"
#include "ucioption.h"
namespace Stockfish {
class Position;
namespace Eval::NNUE {
enum NetSize : int;
}
namespace UCI {
namespace Search {
class Worker;
}
// Normalizes the internal value as reported by evaluate or search
// to the UCI centipawn result used in output. This value is derived from
// the win_rate_model() such that Stockfish outputs an advantage of
// "100 centipawns" for a position if the engine has a 50% probability to win
// from this position in self-play at fishtest LTC time control.
const int NormalizeToPawnValue = 328;
class Option;
// Define a custom comparator, because the UCI options should be case-insensitive
struct CaseInsensitiveLess {
bool operator()(const std::string&, const std::string&) const;
};
// The options container is defined as a std::map
using OptionsMap = std::map<std::string, Option, CaseInsensitiveLess>;
// The Option class implements each option as specified by the UCI protocol
class Option {
using OnChange = void (*)(const Option&);
class Move;
enum Square : int;
using Value = int;
class UCI {
public:
Option(OnChange = nullptr);
Option(bool v, OnChange = nullptr);
Option(const char* v, OnChange = nullptr);
Option(double v, int minv, int maxv, OnChange = nullptr);
Option(const char* v, const char* cur, OnChange = nullptr);
UCI(int argc, char** argv);
Option& operator=(const std::string&);
void operator<<(const Option&);
operator int() const;
operator std::string() const;
bool operator==(const char*) const;
void loop();
static int to_cp(Value v);
static std::string value(Value v);
static std::string square(Square s);
static std::string move(Move m, bool chess960);
static std::string pv(const Search::Worker& workerThread,
TimePoint elapsed,
uint64_t nodesSearched,
uint64_t tb_hits,
int hashfull,
bool rootInTB);
static std::string wdl(Value v, int ply);
static Move to_move(const Position& pos, std::string& str);
const std::string& workingDirectory() const { return cli.workingDirectory; }
OptionsMap options;
std::unordered_map<Eval::NNUE::NetSize, Eval::EvalFile> evalFiles;
private:
friend std::ostream& operator<<(std::ostream&, const OptionsMap&);
TranspositionTable tt;
ThreadPool threads;
CommandLine cli;
std::string defaultValue, currentValue, type;
int min, max;
size_t idx;
OnChange on_change;
void go(Position& pos, std::istringstream& is, StateListPtr& states);
void bench(Position& pos, std::istream& args, StateListPtr& states);
void position(Position& pos, std::istringstream& is, StateListPtr& states);
void trace_eval(Position& pos);
void search_clear();
void setoption(std::istringstream& is);
};
void init(OptionsMap&);
void loop(int argc, char* argv[]);
int to_cp(Value v);
std::string value(Value v);
std::string square(Square s);
std::string move(Move m, bool chess960);
std::string pv(const Position& pos, Depth depth);
std::string wdl(Value v, int ply);
Move to_move(const Position& pos, std::string& str);
} // namespace UCI
extern UCI::OptionsMap Options;
} // namespace Stockfish
#endif // #ifndef UCI_H_INCLUDED

View file

@ -16,104 +16,53 @@
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "ucioption.h"
#include <algorithm>
#include <cassert>
#include <cctype>
#include <cstddef>
#include <iosfwd>
#include <istream>
#include <map>
#include <ostream>
#include <iostream>
#include <sstream>
#include <string>
#include <utility>
#include "evaluate.h"
#include "misc.h"
#include "search.h"
#include "syzygy/tbprobe.h"
#include "thread.h"
#include "tt.h"
#include "types.h"
#include "uci.h"
using std::string;
namespace Stockfish {
UCI::OptionsMap Options; // Global object
bool CaseInsensitiveLess::operator()(const std::string& s1, const std::string& s2) const {
namespace UCI {
// 'On change' actions, triggered by an option's value change
static void on_clear_hash(const Option&) { Search::clear(); }
static void on_hash_size(const Option& o) { TT.resize(size_t(o)); }
static void on_logger(const Option& o) { start_logger(o); }
static void on_threads(const Option& o) { Threads.set(size_t(o)); }
static void on_tb_path(const Option& o) { Tablebases::init(o); }
static void on_eval_file(const Option&) { Eval::NNUE::init(); }
// 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); });
return std::lexicographical_compare(
s1.begin(), s1.end(), s2.begin(), s2.end(),
[](char c1, char c2) { return std::tolower(c1) < std::tolower(c2); });
}
void OptionsMap::setoption(std::istringstream& is) {
std::string token, name, value;
// Initializes the UCI options to their hard-coded default values
void init(OptionsMap& o) {
is >> token; // Consume the "name" token
constexpr int MaxHashMB = Is64Bit ? 33554432 : 2048;
// Read the option name (can contain spaces)
while (is >> token && token != "value")
name += (name.empty() ? "" : " ") + token;
o["Debug Log File"] << Option("", on_logger);
o["Threads"] << Option(1, 1, 1024, on_threads);
o["Hash"] << Option(16, 1, MaxHashMB, on_hash_size);
o["Clear Hash"] << Option(on_clear_hash);
o["Ponder"] << Option(false);
o["MultiPV"] << Option(1, 1, MAX_MOVES);
o["Skill Level"] << Option(20, 0, 20);
o["Move Overhead"] << Option(10, 0, 5000);
o["nodestime"] << Option(0, 0, 10000);
o["UCI_Chess960"] << Option(false);
o["UCI_LimitStrength"] << Option(false);
o["UCI_Elo"] << Option(1320, 1320, 3190);
o["UCI_ShowWDL"] << Option(false);
o["SyzygyPath"] << Option("<empty>", on_tb_path);
o["SyzygyProbeDepth"] << Option(1, 1, 100);
o["Syzygy50MoveRule"] << Option(true);
o["SyzygyProbeLimit"] << Option(7, 0, 7);
o["EvalFile"] << Option(EvalFileDefaultNameBig, on_eval_file);
// Enable this after fishtest workers support EvalFileSmall
// o["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, on_eval_file);
// Read the option value (can contain spaces)
while (is >> token)
value += (value.empty() ? "" : " ") + token;
if (options_map.count(name))
options_map[name] = value;
else
sync_cout << "No such option: " << name << sync_endl;
}
// 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)
{
const Option& o = it.second;
os << "\noption name " << it.first << " type " << o.type;
if (o.type == "string" || o.type == "check" || o.type == "combo")
os << " default " << o.defaultValue;
if (o.type == "spin")
os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max "
<< o.max;
break;
}
return os;
Option OptionsMap::operator[](const std::string& name) const {
auto it = options_map.find(name);
return it != options_map.end() ? it->second : Option();
}
Option& OptionsMap::operator[](const std::string& name) { return options_map[name]; }
// Option class constructors and conversion operators
std::size_t OptionsMap::count(const std::string& name) const { return options_map.count(name); }
Option::Option(const char* v, OnChange f) :
type("string"),
@ -184,19 +133,19 @@ void Option::operator<<(const Option& o) {
// 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) {
Option& Option::operator=(const std::string& v) {
assert(!type.empty());
if ((type != "button" && type != "string" && v.empty())
|| (type == "check" && v != "true" && v != "false")
|| (type == "spin" && (stof(v) < min || stof(v) > max)))
|| (type == "spin" && (std::stof(v) < min || std::stof(v) > max)))
return *this;
if (type == "combo")
{
OptionsMap comboMap; // To have case insensitive compare
string token;
std::string token;
std::istringstream ss(defaultValue);
while (ss >> token)
comboMap[token] << Option();
@ -213,6 +162,24 @@ Option& Option::operator=(const string& v) {
return *this;
}
} // namespace UCI
std::ostream& operator<<(std::ostream& os, const OptionsMap& om) {
for (size_t idx = 0; idx < om.options_map.size(); ++idx)
for (const auto& it : om.options_map)
if (it.second.idx == idx)
{
const Option& o = it.second;
os << "\noption name " << it.first << " type " << o.type;
} // namespace Stockfish
if (o.type == "string" || o.type == "check" || o.type == "combo")
os << " default " << o.defaultValue;
if (o.type == "spin")
os << " default " << int(stof(o.defaultValue)) << " min " << o.min << " max "
<< o.max;
break;
}
return os;
}
}

81
src/ucioption.h Normal file
View file

@ -0,0 +1,81 @@
/*
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file)
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 <http://www.gnu.org/licenses/>.
*/
#ifndef UCIOPTION_H_INCLUDED
#define UCIOPTION_H_INCLUDED
#include <cstddef>
#include <functional>
#include <iosfwd>
#include <map>
#include <string>
namespace Stockfish {
// Define a custom comparator, because the UCI options should be case-insensitive
struct CaseInsensitiveLess {
bool operator()(const std::string&, const std::string&) const;
};
class Option;
class OptionsMap {
public:
void setoption(std::istringstream&);
friend std::ostream& operator<<(std::ostream&, const OptionsMap&);
Option operator[](const std::string&) const;
Option& operator[](const std::string&);
std::size_t count(const std::string&) const;
private:
// The options container is defined as a std::map
using OptionsStore = std::map<std::string, Option, CaseInsensitiveLess>;
OptionsStore options_map;
};
// The Option class implements each option as specified by the UCI protocol
class Option {
public:
using OnChange = std::function<void(const Option&)>;
Option(OnChange = nullptr);
Option(bool v, OnChange = nullptr);
Option(const char* v, OnChange = nullptr);
Option(double v, int minv, int maxv, OnChange = nullptr);
Option(const char* v, const char* cur, OnChange = nullptr);
Option& operator=(const std::string&);
void operator<<(const Option&);
operator int() const;
operator std::string() const;
bool operator==(const char*) const;
friend std::ostream& operator<<(std::ostream&, const OptionsMap&);
private:
std::string defaultValue, currentValue, type;
int min, max;
size_t idx;
OnChange on_change;
};
}
#endif // #ifndef UCIOPTION_H_INCLUDED