diff --git a/src/Makefile b/src/Makefile index 1ae9cd5f..586656d3 100644 --- a/src/Makefile +++ b/src/Makefile @@ -63,6 +63,7 @@ SRCS = benchmark.cpp bitbase.cpp bitboard.cpp endgame.cpp evaluate.cpp main.cpp learn/sfen_packer.cpp \ learn/learn.cpp \ learn/gensfen.cpp \ + learn/gensfen_nonpv.cpp \ learn/opening_book.cpp \ learn/convert.cpp \ learn/transform.cpp diff --git a/src/evaluate.cpp b/src/evaluate.cpp index dd204a52..709a50ff 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -914,6 +914,8 @@ make_v: Value Eval::evaluate(const Position& pos) { + pos.this_thread()->on_eval(); + Value v; if (NNUE::useNNUE == NNUE::UseNNUEMode::Pure) { diff --git a/src/learn/gensfen_nonpv.cpp b/src/learn/gensfen_nonpv.cpp new file mode 100644 index 00000000..a5c667b5 --- /dev/null +++ b/src/learn/gensfen_nonpv.cpp @@ -0,0 +1,474 @@ +#include "gensfen_nonpv.h" + +#include "sfen_writer.h" +#include "packed_sfen.h" +#include "opening_book.h" + +#include "misc.h" +#include "position.h" +#include "thread.h" +#include "tt.h" +#include "uci.h" + +#include "extra/nnue_data_binpack_format.h" + +#include "nnue/evaluate_nnue.h" +#include "nnue/evaluate_nnue_learner.h" + +#include "syzygy/tbprobe.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +namespace Learner +{ + // Class to generate sfen with multiple threads + struct GensfenNonPv + { + struct Params + { + // The depth for search on the fens gathered during exploration + int search_depth = 3; + + // the min/max number of nodes to use for exploration per ply + int exploration_min_nodes = 5000; + int exploration_max_nodes = 15000; + + // The pct of positions explored that are saved for rescoring + float exploration_save_rate = 0.01; + + // Upper limit of evaluation value of generated situation + int eval_limit = 4000; + + // the upper limit on evaluation during exploration selfplay + int exploration_eval_limit = 4000; + + int exploration_max_ply = 200; + + int exploration_min_pieces = 8; + + std::string output_file_name = "generated_gensfen_nonpv"; + + SfenOutputType sfen_format = SfenOutputType::Binpack; + + std::string seed; + + int num_threads; + + std::string book; + + void enforce_constraints() + { + // Limit the maximum to a one-stop score. (Otherwise you might not end the loop) + eval_limit = std::min(eval_limit, (int)mate_in(2)); + exploration_eval_limit = std::min(eval_limit, (int)mate_in(2)); + exploration_min_nodes = std::max(100, exploration_min_nodes); + exploration_max_nodes = std::max(exploration_min_nodes, exploration_max_nodes); + + num_threads = Options["Threads"]; + } + }; + + static constexpr uint64_t REPORT_DOT_EVERY = 5000; + static constexpr uint64_t REPORT_STATS_EVERY = 200000; + static_assert(REPORT_STATS_EVERY % REPORT_DOT_EVERY == 0); + + GensfenNonPv( + const Params& prm + ) : + params(prm), + prng(prm.seed), + sfen_writer(prm.output_file_name, prm.num_threads, std::numeric_limits::max(), prm.sfen_format) + { + if (!prm.book.empty()) + { + opening_book = open_opening_book(prm.book, prng); + if (opening_book == nullptr) + { + std::cout << "WARNING: Failed to open opening book " << prm.book << ". Falling back to startpos.\n"; + } + } + + // Output seed to veryfy by the user if it's not identical by chance. + std::cout << prng << std::endl; + } + + void generate(uint64_t limit); + + private: + Params params; + + PRNG prng; + + std::mutex stats_mutex; + TimePoint last_stats_report_time; + + // sfen exporter + SfenWriter sfen_writer; + + SynchronizedRegionLogger::Region out; + + std::unique_ptr opening_book; + + static void set_gensfen_search_limits(); + + void generate_worker( + Thread& th, + std::atomic& counter, + uint64_t limit); + + bool commit_psv( + Thread& th, + PSVector& sfens, + std::atomic& counter, + uint64_t limit); + + PSVector do_exploration( + Thread& th, + int count); + + void report(uint64_t done, uint64_t new_done); + + void maybe_report(uint64_t done); + }; + + void GensfenNonPv::set_gensfen_search_limits() + { + // About Search::Limits + // Be careful because this member variable is global and affects other threads. + auto& limits = Search::Limits; + + // Make the search equivalent to the "go infinite" command. (Because it is troublesome if time management is done) + limits.infinite = true; + + // Since PV is an obstacle when displayed, erase it. + limits.silent = true; + + // If you use this, it will be compared with the accumulated nodes of each thread. Therefore, do not use it. + limits.nodes = 0; + + // depth is also processed by the one passed as an argument of Learner::search(). + limits.depth = 0; + } + + void GensfenNonPv::generate(uint64_t limit) + { + last_stats_report_time = 0; + + set_gensfen_search_limits(); + + std::atomic counter{0}; + Threads.execute_with_workers([&counter, limit, this](Thread& th) { + generate_worker(th, counter, limit); + }); + Threads.wait_for_workers_finished(); + + sfen_writer.flush(); + + if (limit % REPORT_STATS_EVERY != 0) + { + report(limit, limit % REPORT_STATS_EVERY); + } + + std::cout << std::endl; + } + + PSVector GensfenNonPv::do_exploration( + Thread& th, + int count) + { + constexpr int max_depth = 30; + + PSVector psv; + + std::vector> states( + max_depth + MAX_PLY /* == search_depth_min + α */); + + th.set_eval_callback([this, &psv](Position& pos) { + if ((double)prng.rand() / std::numeric_limits::max() < params.exploration_save_rate) + { + psv.emplace_back(); + pos.sfen_pack(psv.back().sfen); + } + }); + + auto& pos = th.rootPos; + StateInfo si; + + for (int i = 0; i < count; ++i) + { + if (opening_book != nullptr) + { + auto& fen = opening_book->next_fen(); + pos.set(fen, false, &si, &th); + } + else + { + pos.set(StartFEN, false, &si, &th); + } + + for(int ply = 0; ply < params.exploration_max_ply; ++ply) + { + auto nodes = prng.rand(params.exploration_max_nodes - params.exploration_min_nodes + 1) + params.exploration_min_nodes; + + auto [search_value, search_pv] = Search::search(pos, max_depth, 1, nodes); + + if (search_pv.empty()) + { + break; + } + + if (std::abs(search_value) > params.exploration_eval_limit) + { + break; + } + + pos.do_move(search_pv[0], states[ply]); + + if (popcount(pos.pieces()) < params.exploration_min_pieces) + { + break; + } + } + } + + th.clear_eval_callback(); + + return psv; + } + + void GensfenNonPv::generate_worker( + Thread& th, + std::atomic& counter, + uint64_t limit) + { + constexpr int exploration_batch_size = 1; + + StateInfo si; + + PSVector psv; + + // end flag + bool quit = false; + + // repeat until the specified number of times + while (!quit) + { + // It is necessary to set a dependent thread for Position. + // When parallelizing, Threads (since this is a vector, + // Do the same for up to Threads[0]...Threads[thread_num-1]. + auto& pos = th.rootPos; + + auto packed_sfens = do_exploration(th, exploration_batch_size); + psv.clear(); + + for (auto& ps : packed_sfens) + { + pos.set_from_packed_sfen(ps.sfen, &si, &th); + pos.state()->rule50 = 0; + auto [search_value, search_pv] = Search::search(pos, params.search_depth, 1); + + if (search_pv.empty()) + { + continue; + } + + if (std::abs(search_value) > params.eval_limit) + { + continue; + } + + auto& new_ps = psv.emplace_back(); + pos.sfen_pack(new_ps.sfen); + new_ps.score = search_value; + new_ps.move = search_pv[0]; + new_ps.gamePly = 1; + new_ps.game_result = 0; + new_ps.padding = 0; + } + + quit = commit_psv(th, psv, counter, limit); + } + } + + // Write out the phases loaded in sfens to a file. + // result: win/loss in the next phase after the final phase in sfens + // 1 when winning. -1 when losing. Pass 0 for a draw. + // Return value: true if the specified number of + // sfens has already been reached and the process ends. + bool GensfenNonPv::commit_psv( + Thread& th, + PSVector& sfens, + std::atomic& counter, + uint64_t limit) + { + // Write sfens in move order to make potential compression easier + for (auto& sfen : sfens) + { + // Return true if there is already enough data generated. + const auto iter = counter.fetch_add(1); + if (iter >= limit) + return true; + + // because `iter` was done, now we do one more + maybe_report(iter + 1); + + // Write out one sfen. + sfen_writer.write(th.thread_idx(), sfen); + } + + return false; + } + + void GensfenNonPv::report(uint64_t done, uint64_t new_done) + { + const auto now_time = now(); + const TimePoint elapsed = now_time - last_stats_report_time + 1; + + out + << endl + << done << " sfens, " + << new_done * 1000 / elapsed << " sfens/second, " + << "at " << now_string() << sync_endl; + + last_stats_report_time = now_time; + + out = sync_region_cout.new_region(); + } + + void GensfenNonPv::maybe_report(uint64_t done) + { + if (done % REPORT_DOT_EVERY == 0) + { + std::lock_guard lock(stats_mutex); + + if (last_stats_report_time == 0) + { + last_stats_report_time = now(); + out = sync_region_cout.new_region(); + } + + if (done != 0) + { + out << '.'; + + if (done % REPORT_STATS_EVERY == 0) + { + report(done, REPORT_STATS_EVERY); + } + } + } + } + + // Command to generate a game record + void gensfen_nonpv(istringstream& is) + { + // Number of generated game records default = 8 billion phases (Ponanza specification) + GensfenNonPv::Params params; + + uint64_t count = 1'000'000; + + // Add a random number to the end of the file name. + std::string sfen_format = "binpack"; + + string token; + while (true) + { + token = ""; + is >> token; + if (token == "") + break; + + if (token == "depth") + is >> params.search_depth; + else if (token == "count") + is >> count; + else if (token == "output_file") + is >> params.output_file_name; + else if (token == "exploration_eval_limit") + is >> params.exploration_eval_limit; + else if (token == "eval_limit") + is >> params.eval_limit; + else if (token == "exploration_min_nodes") + is >> params.exploration_min_nodes; + else if (token == "exploration_max_nodes") + is >> params.exploration_max_nodes; + else if (token == "exploration_min_pieces") + is >> params.exploration_min_pieces; + else if (token == "exploration_save_rate") + is >> params.exploration_save_rate; + else if (token == "book") + is >> params.book; + else if (token == "sfen_format") + is >> sfen_format; + else if (token == "seed") + is >> params.seed; + else if (token == "set_recommended_uci_options") + { + UCI::setoption("Contempt", "0"); + UCI::setoption("Skill Level", "20"); + UCI::setoption("UCI_Chess960", "false"); + UCI::setoption("UCI_AnalyseMode", "false"); + UCI::setoption("UCI_LimitStrength", "false"); + UCI::setoption("PruneAtShallowDepth", "false"); + UCI::setoption("EnableTranspositionTable", "true"); + } + else + cout << "ERROR: Ignoring unknown option " << token << endl; + } + + if (!sfen_format.empty()) + { + if (sfen_format == "bin") + params.sfen_format = SfenOutputType::Bin; + else if (sfen_format == "binpack") + params.sfen_format = SfenOutputType::Binpack; + else + cout << "WARNING: Unknown sfen format `" << sfen_format << "`. Using bin\n"; + } + + params.enforce_constraints(); + + std::cout << "INFO: Executing gensfen_nonpv command\n"; + + std::cout << "INFO: Parameters:\n"; + std::cout + << " - search_depth = " << params.search_depth << endl + << " - output_file = " << params.output_file_name << endl + << " - exploration_eval_limit = " << params.exploration_eval_limit << endl + << " - eval_limit = " << params.eval_limit << endl + << " - exploration_min_nodes = " << params.exploration_min_nodes << endl + << " - exploration_max_nodes = " << params.exploration_max_nodes << endl + << " - exploration_min_pieces = " << params.exploration_min_pieces << endl + << " - exploration_save_rate = " << params.exploration_save_rate << endl + << " - book = " << params.book << endl + << " - sfen_format = " << sfen_format << endl + << " - seed = " << params.seed << endl + << " - count = " << count << endl; + + // Show if the training data generator uses NNUE. + Eval::NNUE::verify_eval_file_loaded(); + + Threads.main()->ponder = false; + + GensfenNonPv gensfen(params); + gensfen.generate(count); + + std::cout << "INFO: gensfen_nonpv finished." << endl; + } +} diff --git a/src/learn/gensfen_nonpv.h b/src/learn/gensfen_nonpv.h new file mode 100644 index 00000000..38ccaa60 --- /dev/null +++ b/src/learn/gensfen_nonpv.h @@ -0,0 +1,12 @@ +#ifndef _GENSFEN_NONPV_H_ +#define _GENSFEN_NONPV_H_ + +#include + +namespace Learner { + + // Automatic generation of teacher position + void gensfen_nonpv(std::istringstream& is); +} + +#endif \ No newline at end of file diff --git a/src/thread.h b/src/thread.h index 83ba2f33..6eb38136 100644 --- a/src/thread.h +++ b/src/thread.h @@ -55,6 +55,7 @@ class Thread { size_t idx; bool exit = false, searching = true; // Set before starting std::thread std::function worker; + std::function on_eval_callback; NativeThread stdThread; public: @@ -75,6 +76,13 @@ public: void wait_for_worker_finished(); size_t thread_idx() const { return idx; } + template + void set_eval_callback(FuncT&& f) { on_eval_callback = std::forward(f); } + + void clear_eval_callback() { on_eval_callback = nullptr; } + + void on_eval() { if (on_eval_callback) on_eval_callback(rootPos); } + Pawns::Table pawnsTable; Material::Table materialTable; size_t pvIdx, pvLast; diff --git a/src/uci.cpp b/src/uci.cpp index 8e64da6b..55fccea7 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -36,6 +36,7 @@ #include "uci.h" #include "learn/gensfen.h" +#include "learn/gensfen_nonpv.h" #include "learn/learn.h" #include "learn/convert.h" #include "learn/transform.h" @@ -341,6 +342,7 @@ void UCI::loop(int argc, char* argv[]) { else if (token == "compiler") sync_cout << compiler_info() << sync_endl; else if (token == "gensfen") Learner::gensfen(is); + else if (token == "gensfen_nonpv") Learner::gensfen_nonpv(is); else if (token == "learn") Learner::learn(is); else if (token == "convert") Learner::convert(is); else if (token == "convert_bin") Learner::convert_bin(is);