From 28a4655d2dc99b6bc11bbf4df2aed3d64c9ab511 Mon Sep 17 00:00:00 2001 From: mstembera Date: Sat, 2 Dec 2023 17:50:32 -0800 Subject: [PATCH] Dual net NNUE w/ L1=256 small net by @linrock bench: 1380121 --- src/Makefile | 57 +++++++++- src/evaluate.cpp | 167 +++++++++++++++------------- src/evaluate.h | 6 +- src/nnue/evaluate_nnue.cpp | 127 +++++++++++++-------- src/nnue/evaluate_nnue.h | 19 ++-- src/nnue/nnue_accumulator.h | 4 +- src/nnue/nnue_architecture.h | 33 ++++-- src/nnue/nnue_feature_transformer.h | 108 ++++++++++-------- src/position.cpp | 14 ++- src/position.h | 24 +++- src/thread.cpp | 2 +- src/uci.cpp | 2 +- src/ucioption.cpp | 3 +- 13 files changed, 359 insertions(+), 207 deletions(-) diff --git a/src/Makefile b/src/Makefile index 59ea7bfe7b5..44814aaea69 100644 --- a/src/Makefile +++ b/src/Makefile @@ -791,6 +791,7 @@ help: @echo "profile-build > standard build with profile-guided optimization" @echo "build > skip profile-guided optimization" @echo "net > Download the default nnue net" + @echo "net2 > Download the smaller nnue net" @echo "strip > Strip executable" @echo "install > Install executable" @echo "clean > Clean up" @@ -857,13 +858,13 @@ endif clang-profile-use clang-profile-make FORCE \ format analyze -analyze: net config-sanity objclean +analyze: net net2 config-sanity objclean $(MAKE) -k ARCH=$(ARCH) COMP=$(COMP) $(OBJS) -build: net config-sanity +build: net net2 config-sanity $(MAKE) ARCH=$(ARCH) COMP=$(COMP) all -profile-build: net config-sanity objclean profileclean +profile-build: net net2 config-sanity objclean profileclean @echo "" @echo "Step 1/4. Building instrumented executable ..." $(MAKE) ARCH=$(ARCH) COMP=$(COMP) $(profile_make) @@ -907,7 +908,7 @@ profileclean: # set up shell variables for the net stuff netvariables: - $(eval nnuenet := $(shell grep EvalFileDefaultName evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) + $(eval nnuenet := $(shell grep EvalFileDefaultNameBig evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) $(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet)) $(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet)) $(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi)) @@ -951,6 +952,52 @@ net: netvariables fi; \ fi; \ +netvariables2: + $(eval nnuenet := $(shell grep EvalFileDefaultNameSmall evaluate.h | grep define | sed 's/.*\(nn-[a-z0-9]\{12\}.nnue\).*/\1/')) + $(eval nnuedownloadurl1 := https://tests.stockfishchess.org/api/nn/$(nnuenet)) + $(eval nnuedownloadurl2 := https://github.com/official-stockfish/networks/raw/master/$(nnuenet)) + $(eval curl_or_wget := $(shell if hash curl 2>/dev/null; then echo "curl -skL"; elif hash wget 2>/dev/null; then echo "wget -qO-"; fi)) + $(eval shasum_command := $(shell if hash shasum 2>/dev/null; then echo "shasum -a 256 "; elif hash sha256sum 2>/dev/null; then echo "sha256sum "; fi)) + +# evaluation network (nnue) +net2: netvariables2 + @echo "Default net: $(nnuenet)" + @if [ "x$(curl_or_wget)" = "x" ]; then \ + echo "Neither curl nor wget is installed. Install one of these tools unless the net has been downloaded manually"; \ + fi + @if [ "x$(shasum_command)" = "x" ]; then \ + echo "shasum / sha256sum not found, skipping net validation"; \ + elif test -f "$(nnuenet)"; then \ + if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ + echo "Removing invalid network"; rm -f $(nnuenet); \ + fi; \ + fi; + @for nnuedownloadurl in "$(nnuedownloadurl1)" "$(nnuedownloadurl2)"; do \ + if test -f "$(nnuenet)"; then \ + echo "$(nnuenet) available : OK"; break; \ + else \ + if [ "x$(curl_or_wget)" != "x" ]; then \ + echo "Downloading $${nnuedownloadurl}"; $(curl_or_wget) $${nnuedownloadurl} > $(nnuenet);\ + else \ + echo "No net found and download not possible"; exit 1;\ + fi; \ + fi; \ + if [ "x$(shasum_command)" != "x" ]; then \ + if [ "$(nnuenet)" != "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ + echo "Removing failed download"; rm -f $(nnuenet); \ + fi; \ + fi; \ + done + @if ! test -f "$(nnuenet)"; then \ + echo "Failed to download $(nnuenet)."; \ + fi; + @if [ "x$(shasum_command)" != "x" ]; then \ + if [ "$(nnuenet)" = "nn-"`$(shasum_command) $(nnuenet) | cut -c1-12`".nnue" ]; then \ + echo "Network validated"; break; \ + fi; \ + fi; \ + + format: $(CLANG-FORMAT) -i $(SRCS) $(HEADERS) -style=file @@ -1073,6 +1120,6 @@ icx-profile-use: .depend: $(SRCS) -@$(CXX) $(DEPENDFLAGS) -MM $(SRCS) > $@ 2> /dev/null -ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net objclean profileclean config-sanity)) +ifeq (, $(filter $(MAKECMDGOALS), help strip install clean net net2 objclean profileclean config-sanity)) -include .depend endif diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 9c39d4c07fb..007275ee676 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -43,11 +43,15 @@ // const unsigned int gEmbeddedNNUESize; // the size of the embedded file // Note that this does not work in Microsoft Visual Studio. #if !defined(_MSC_VER) && !defined(NNUE_EMBEDDING_OFF) -INCBIN(EmbeddedNNUE, EvalFileDefaultName); +INCBIN(EmbeddedNNUEBig, EvalFileDefaultNameBig); +INCBIN(EmbeddedNNUESmall, EvalFileDefaultNameSmall); #else -const unsigned char gEmbeddedNNUEData[1] = {0x0}; -const unsigned char* const gEmbeddedNNUEEnd = &gEmbeddedNNUEData[1]; -const unsigned int gEmbeddedNNUESize = 1; +const unsigned char gEmbeddedNNUEBigData[1] = {0x0}; +const unsigned char* const gEmbeddedNNUEBigEnd = &gEmbeddedNNUEBigData[1]; +const unsigned int gEmbeddedNNUEBigSize = 1; +const unsigned char gEmbeddedNNUESmallData[1] = {0x0}; +const unsigned char* const gEmbeddedNNUESmallEnd = &gEmbeddedNNUESmallData[1]; +const unsigned int gEmbeddedNNUESmallSize = 1; #endif @@ -55,7 +59,9 @@ namespace Stockfish { namespace Eval { -std::string currentEvalFileName = "None"; +std::string currentEvalFileName[2] = {"None", "None"}; +const std::string EvFiles[2] = {"EvalFileBig", "EvalFileSmall"}; +const std::string EvFileNames[2] = {EvalFileDefaultNameBig, EvalFileDefaultNameSmall}; // 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" @@ -66,9 +72,11 @@ std::string currentEvalFileName = "None"; // variable to have the engine search in a special directory in their distro. void NNUE::init() { - std::string eval_file = std::string(Options["EvalFile"]); - if (eval_file.empty()) - eval_file = EvalFileDefaultName; + for (bool small : {false, true}) + { + std::string eval_file = std::string(Options[EvFiles[small]]); + if (eval_file.empty()) + eval_file = EvFileNames[small]; #if defined(DEFAULT_NNUE_DIRECTORY) std::vector dirs = {"", "", CommandLine::binaryDirectory, @@ -77,82 +85,79 @@ void NNUE::init() { std::vector dirs = {"", "", CommandLine::binaryDirectory}; #endif - for (const std::string& directory : dirs) - if (currentEvalFileName != eval_file) + for (const std::string& directory : dirs) { - if (directory != "") + if (currentEvalFileName[small] != eval_file) { - std::ifstream stream(directory + eval_file, std::ios::binary); - if (NNUE::load_eval(eval_file, stream)) - currentEvalFileName = eval_file; - } - - if (directory == "" && eval_file == EvalFileDefaultName) - { - // C++ way to prepare a buffer for a memory stream - class MemoryBuffer: public std::basic_streambuf { - public: - MemoryBuffer(char* p, size_t n) { - setg(p, p, p + n); - setp(p, p + n); - } - }; - - MemoryBuffer buffer( - const_cast(reinterpret_cast(gEmbeddedNNUEData)), - size_t(gEmbeddedNNUESize)); - (void) gEmbeddedNNUEEnd; // Silence warning on unused variable - - std::istream stream(&buffer); - if (NNUE::load_eval(eval_file, stream)) - currentEvalFileName = eval_file; + if (directory != "") + { + std::ifstream stream(directory + eval_file, std::ios::binary); + if (NNUE::load_eval(eval_file, stream, small)) + currentEvalFileName[small] = eval_file; + } + + if (directory == "" && eval_file == EvFileNames[small]) + { + // C++ way to prepare a buffer for a memory stream + class MemoryBuffer: public std::basic_streambuf { + public: + MemoryBuffer(char* p, size_t n) { + setg(p, p, p + n); + setp(p, p + n); + } + }; + + MemoryBuffer buffer( + const_cast(reinterpret_cast( + small ? gEmbeddedNNUESmallData : gEmbeddedNNUEBigData)), + size_t(small ? gEmbeddedNNUESmallSize : gEmbeddedNNUEBigSize)); + (void) gEmbeddedNNUEBigEnd; // Silence warning on unused variable + (void) gEmbeddedNNUESmallEnd; + + std::istream stream(&buffer); + if (NNUE::load_eval(eval_file, stream, small)) + currentEvalFileName[small] = eval_file; + } } } + } } // Verifies that the last net used was loaded successfully void NNUE::verify() { - std::string eval_file = std::string(Options["EvalFile"]); - if (eval_file.empty()) - eval_file = EvalFileDefaultName; - - if (currentEvalFileName != eval_file) + for (bool small : {false, true}) { + std::string eval_file = std::string(Options[EvFiles[small]]); + if (eval_file.empty()) + eval_file = EvFileNames[small]; - std::string msg1 = - "Network evaluation parameters compatible with the engine must be available."; - std::string msg2 = "The network file " + eval_file + " was not loaded successfully."; - std::string msg3 = "The UCI option EvalFile might need to specify the full path, " - "including the directory name, to the network file."; - std::string msg4 = "The default net can be downloaded from: " - "https://tests.stockfishchess.org/api/nn/" - + std::string(EvalFileDefaultName); - std::string msg5 = "The engine will be terminated now."; - - sync_cout << "info string ERROR: " << msg1 << sync_endl; - sync_cout << "info string ERROR: " << msg2 << sync_endl; - sync_cout << "info string ERROR: " << msg3 << sync_endl; - sync_cout << "info string ERROR: " << msg4 << sync_endl; - sync_cout << "info string ERROR: " << msg5 << sync_endl; - - exit(EXIT_FAILURE); - } + if (currentEvalFileName[small] != eval_file) + { + std::string msg1 = + "Network evaluation parameters compatible with the engine must be available."; + std::string msg2 = "The network file " + eval_file + " was not loaded successfully."; + std::string msg3 = "The UCI option EvalFile might need to specify the full path, " + "including the directory name, to the network file."; + std::string msg4 = "The default net can be downloaded from: " + "https://tests.stockfishchess.org/api/nn/" + + std::string(EvFileNames[small]); + std::string msg5 = "The engine will be terminated now."; + + sync_cout << "info string ERROR: " << msg1 << sync_endl; + sync_cout << "info string ERROR: " << msg2 << sync_endl; + sync_cout << "info string ERROR: " << msg3 << sync_endl; + sync_cout << "info string ERROR: " << msg4 << sync_endl; + sync_cout << "info string ERROR: " << msg5 << sync_endl; + + exit(EXIT_FAILURE); + } - sync_cout << "info string NNUE evaluation using " << eval_file << sync_endl; -} + sync_cout << "info string NNUE evaluation using " << eval_file << sync_endl; + } } - - -// Returns a static, purely materialistic evaluation of the position from -// the point of view of the given color. It can be divided by PawnValue to get -// an approximation of the material advantage on the board in terms of pawns. -Value Eval::simple_eval(const Position& pos, Color c) { - return PawnValue * (pos.count(c) - pos.count(~c)) - + (pos.non_pawn_material(c) - pos.non_pawn_material(~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) { @@ -162,18 +167,28 @@ Value Eval::evaluate(const Position& pos) { Value v; Color stm = pos.side_to_move(); int shuffling = pos.rule50_count(); - int simpleEval = simple_eval(pos, stm) + (int(pos.key() & 7) - 3); + int simpleEval = pos.simple_eval() + (int(pos.key() & 7) - 3); + + int lazyThreshold = RookValue + KnightValue + 16 * shuffling * shuffling + + abs(pos.this_thread()->bestValue) + + abs(pos.this_thread()->rootSimpleEval); - bool lazy = abs(simpleEval) >= RookValue + KnightValue + 16 * shuffling * shuffling - + abs(pos.this_thread()->bestValue) - + abs(pos.this_thread()->rootSimpleEval); + bool lazy = abs(simpleEval) > lazyThreshold * 105 / 100; if (lazy) v = Value(simpleEval); else { - int nnueComplexity; - Value nnue = NNUE::evaluate(pos, true, &nnueComplexity); + int accBias = pos.state()->accumulatorBig.computed[0] + + pos.state()->accumulatorBig.computed[1] + - pos.state()->accumulatorSmall.computed[0] + - pos.state()->accumulatorSmall.computed[1]; + + int nnueComplexity; + bool smallNet = abs(simpleEval) > lazyThreshold * (90 + accBias) / 100; + + Value nnue = smallNet ? NNUE::evaluate(pos, true, &nnueComplexity) + : NNUE::evaluate(pos, true, &nnueComplexity); Value optimism = pos.this_thread()->optimism[stm]; @@ -216,7 +231,7 @@ std::string Eval::trace(Position& pos) { ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15); Value v; - v = NNUE::evaluate(pos, false); + v = NNUE::evaluate(pos, false); v = pos.side_to_move() == WHITE ? v : -v; ss << "NNUE evaluation " << 0.01 * UCI::to_cp(v) << " (white side)\n"; diff --git a/src/evaluate.h b/src/evaluate.h index 2ab477eced2..6eafdd539f9 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -31,15 +31,15 @@ namespace Eval { std::string trace(Position& pos); -Value simple_eval(const Position& pos, Color c); Value evaluate(const Position& pos); -extern std::string currentEvalFileName; +extern std::string currentEvalFileName[2]; // 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 // name of the macro, as it is used in the Makefile. -#define EvalFileDefaultName "nn-0000000000a0.nnue" +#define EvalFileDefaultNameBig "nn-0000000000a0.nnue" +#define EvalFileDefaultNameSmall "nn-ecb35f70ff2a.nnue" namespace NNUE { diff --git a/src/nnue/evaluate_nnue.cpp b/src/nnue/evaluate_nnue.cpp index ef6b7e91a60..26ce4bdcde2 100644 --- a/src/nnue/evaluate_nnue.cpp +++ b/src/nnue/evaluate_nnue.cpp @@ -40,14 +40,16 @@ namespace Stockfish::Eval::NNUE { // Input feature converter -LargePagePtr featureTransformer; +LargePagePtr> featureTransformerBig; +LargePagePtr> featureTransformerSmall; // Evaluation function -AlignedPtr network[LayerStacks]; +AlignedPtr> networkBig[LayerStacks]; +AlignedPtr> networkSmall[LayerStacks]; -// Evaluation function file name -std::string fileName; -std::string netDescription; +// Evaluation function file names +std::string fileName[2]; +std::string netDescription[2]; namespace Detail { @@ -91,11 +93,20 @@ bool write_parameters(std::ostream& stream, const T& reference) { // Initialize the evaluation function parameters -static void initialize() { +static void initialize(bool small) { - Detail::initialize(featureTransformer); - for (std::size_t i = 0; i < LayerStacks; ++i) - Detail::initialize(network[i]); + if (small) + { + Detail::initialize(featureTransformerSmall); + for (std::size_t i = 0; i < LayerStacks; ++i) + Detail::initialize(networkSmall[i]); + } + else + { + Detail::initialize(featureTransformerBig); + for (std::size_t i = 0; i < LayerStacks; ++i) + Detail::initialize(networkBig[i]); + } } // Read network header @@ -122,39 +133,57 @@ static bool write_header(std::ostream& stream, std::uint32_t hashValue, const st } // Read network parameters -static bool read_parameters(std::istream& stream) { +static bool read_parameters(std::istream& stream, bool small) { std::uint32_t hashValue; - if (!read_header(stream, &hashValue, &netDescription)) + if (!read_header(stream, &hashValue, &netDescription[small])) + return false; + if (hashValue != HashValue[small]) return false; - if (hashValue != HashValue) + if (!small && !Detail::read_parameters(stream, *featureTransformerBig)) return false; - if (!Detail::read_parameters(stream, *featureTransformer)) + if ( small && !Detail::read_parameters(stream, *featureTransformerSmall)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) - if (!Detail::read_parameters(stream, *(network[i]))) + { + if (!small && !Detail::read_parameters(stream, *(networkBig[i]))) + return false; + if ( small && !Detail::read_parameters(stream, *(networkSmall[i]))) return false; + } return stream && stream.peek() == std::ios::traits_type::eof(); } // Write network parameters -static bool write_parameters(std::ostream& stream) { +static bool write_parameters(std::ostream& stream, bool small) { - if (!write_header(stream, HashValue, netDescription)) + if (!write_header(stream, HashValue[small], netDescription[small])) + return false; + if (!small && !Detail::write_parameters(stream, *featureTransformerBig)) return false; - if (!Detail::write_parameters(stream, *featureTransformer)) + if (small && !Detail::write_parameters(stream, *featureTransformerSmall)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) - if (!Detail::write_parameters(stream, *(network[i]))) + { + if (!small && !Detail::write_parameters(stream, *(networkBig[i]))) return false; + if (small && !Detail::write_parameters(stream, *(networkSmall[i]))) + return false; + } return bool(stream); } void hint_common_parent_position(const Position& pos) { - featureTransformer->hint_common_access(pos); + + int simpleEval = pos.simple_eval(); + if (abs(simpleEval) < 2500) + featureTransformerBig->hint_common_access(pos); + else + featureTransformerSmall->hint_common_access(pos); } // Evaluation function. Perform differential calculation. +template Value evaluate(const Position& pos, bool adjusted, int* complexity) { // We manually align the arrays on the stack because with gcc < 9.3 @@ -165,19 +194,24 @@ Value evaluate(const Position& pos, bool adjusted, int* complexity) { #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) TransformedFeatureType - transformedFeaturesUnaligned[FeatureTransformer::BufferSize - + alignment / sizeof(TransformedFeatureType)]; + transformedFeaturesUnaligned[ + FeatureTransformer::BufferSize + + alignment / sizeof(TransformedFeatureType)]; auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); #else - alignas(alignment) TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + + alignas(alignment) TransformedFeatureType transformedFeatures[ + FeatureTransformer::BufferSize]; #endif ASSERT_ALIGNED(transformedFeatures, alignment); const int bucket = (pos.count() - 1) / 4; - const auto psqt = featureTransformer->transform(pos, transformedFeatures, bucket); - const auto positional = network[bucket]->propagate(transformedFeatures); + const auto psqt = Small ? featureTransformerSmall->transform(pos, transformedFeatures, bucket) + : featureTransformerBig->transform(pos, transformedFeatures, bucket); + const auto positional = Small ? networkSmall[bucket]->propagate(transformedFeatures) + : networkBig[bucket]->propagate(transformedFeatures); if (complexity) *complexity = abs(psqt - positional) / OutputScale; @@ -190,6 +224,9 @@ Value evaluate(const Position& pos, bool adjusted, int* complexity) { return static_cast((psqt + positional) / OutputScale); } +template Value evaluate(const Position& pos, bool adjusted, int* complexity); +template Value evaluate(const Position& pos, bool adjusted, int* complexity); + struct NnueEvalTrace { static_assert(LayerStacks == PSQTBuckets); @@ -206,12 +243,12 @@ static NnueEvalTrace trace_evaluate(const Position& pos) { #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) TransformedFeatureType - transformedFeaturesUnaligned[FeatureTransformer::BufferSize + transformedFeaturesUnaligned[FeatureTransformer::BufferSize + alignment / sizeof(TransformedFeatureType)]; auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); #else - alignas(alignment) TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; + alignas(alignment) TransformedFeatureType transformedFeatures[FeatureTransformer::BufferSize]; #endif ASSERT_ALIGNED(transformedFeatures, alignment); @@ -220,8 +257,8 @@ static NnueEvalTrace trace_evaluate(const Position& pos) { t.correctBucket = (pos.count() - 1) / 4; for (IndexType bucket = 0; bucket < LayerStacks; ++bucket) { - const auto materialist = featureTransformer->transform(pos, transformedFeatures, bucket); - const auto positional = network[bucket]->propagate(transformedFeatures); + const auto materialist = featureTransformerBig->transform(pos, transformedFeatures, bucket); + const auto positional = networkBig[bucket]->propagate(transformedFeatures); t.psqt[bucket] = static_cast(materialist / OutputScale); t.positional[bucket] = static_cast(positional / OutputScale); @@ -310,7 +347,7 @@ std::string trace(Position& pos) { // We estimate the value of each piece by doing a differential evaluation from // the current base eval, simulating the removal of the piece from its square. - Value base = evaluate(pos); + Value base = evaluate(pos); base = pos.side_to_move() == WHITE ? base : -base; for (File f = FILE_A; f <= FILE_H; ++f) @@ -325,16 +362,16 @@ std::string trace(Position& pos) { auto st = pos.state(); pos.remove_piece(sq); - st->accumulator.computed[WHITE] = false; - st->accumulator.computed[BLACK] = false; + st->accumulatorBig.computed[WHITE] = false; + st->accumulatorBig.computed[BLACK] = false; - Value eval = evaluate(pos); + Value eval = evaluate(pos); eval = pos.side_to_move() == WHITE ? eval : -eval; v = base - eval; pos.put_piece(pc, sq); - st->accumulator.computed[WHITE] = false; - st->accumulator.computed[BLACK] = false; + st->accumulatorBig.computed[WHITE] = false; + st->accumulatorBig.computed[BLACK] = false; } writeSquare(f, r, pc, v); @@ -379,24 +416,24 @@ std::string trace(Position& pos) { // Load eval, from a file stream or a memory stream -bool load_eval(std::string name, std::istream& stream) { +bool load_eval(const std::string name, std::istream& stream, bool small) { - initialize(); - fileName = name; - return read_parameters(stream); + initialize(small); + fileName[small] = name; + return read_parameters(stream, small); } // Save eval, to a file stream or a memory stream -bool save_eval(std::ostream& stream) { +bool save_eval(std::ostream& stream, bool small) { - if (fileName.empty()) + if (fileName[small].empty()) return false; - return write_parameters(stream); + return write_parameters(stream, small); } // Save eval, to a file given by its name -bool save_eval(const std::optional& filename) { +bool save_eval(const std::optional& filename, bool small) { std::string actualFilename; std::string msg; @@ -405,7 +442,7 @@ bool save_eval(const std::optional& filename) { actualFilename = filename.value(); else { - if (currentEvalFileName != EvalFileDefaultName) + if (currentEvalFileName[small] != (small ? EvalFileDefaultNameSmall : EvalFileDefaultNameBig)) { msg = "Failed to export a net. " "A non-embedded net can only be saved if the filename is specified"; @@ -413,11 +450,11 @@ bool save_eval(const std::optional& filename) { sync_cout << msg << sync_endl; return false; } - actualFilename = EvalFileDefaultName; + actualFilename = (small ? EvalFileDefaultNameSmall : EvalFileDefaultNameBig); } std::ofstream stream(actualFilename, std::ios_base::binary); - bool saved = save_eval(stream); + bool saved = save_eval(stream, small); msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net"; diff --git a/src/nnue/evaluate_nnue.h b/src/nnue/evaluate_nnue.h index 6edc212f4d7..e5367283e14 100644 --- a/src/nnue/evaluate_nnue.h +++ b/src/nnue/evaluate_nnue.h @@ -39,9 +39,11 @@ enum Value : int; namespace Stockfish::Eval::NNUE { // Hash value of evaluation function structure -constexpr std::uint32_t HashValue = - FeatureTransformer::get_hash_value() ^ Network::get_hash_value(); - +constexpr std::uint32_t HashValue[2] = + { FeatureTransformer::get_hash_value() + ^ Network::get_hash_value(), + FeatureTransformer::get_hash_value() + ^ Network::get_hash_value() }; // Deleter for automating release of memory area template @@ -67,12 +69,13 @@ template using LargePagePtr = std::unique_ptr>; std::string trace(Position& pos); -Value evaluate(const Position& pos, bool adjusted = false, int* complexity = nullptr); -void hint_common_parent_position(const Position& pos); +template +Value evaluate(const Position& pos, bool adjusted = false, int* complexity = nullptr); +void hint_common_parent_position(const Position& pos); -bool load_eval(std::string name, std::istream& stream); -bool save_eval(std::ostream& stream); -bool save_eval(const std::optional& filename); +bool load_eval(const std::string name, std::istream& stream, bool small); +bool save_eval(std::ostream& stream, bool small); +bool save_eval(const std::optional& filename, bool small); } // namespace Stockfish::Eval::NNUE diff --git a/src/nnue/nnue_accumulator.h b/src/nnue/nnue_accumulator.h index 2f1b1d35e52..6d45bd40310 100644 --- a/src/nnue/nnue_accumulator.h +++ b/src/nnue/nnue_accumulator.h @@ -29,8 +29,10 @@ namespace Stockfish::Eval::NNUE { // Class that holds the result of affine transformation of input features +template struct alignas(CacheLineSize) Accumulator { - std::int16_t accumulation[2][TransformedFeatureDimensions]; + std::int16_t accumulation[2][Small ? TransformedFeatureDimensionsSmall + : TransformedFeatureDimensionsBig]; std::int32_t psqtAccumulation[2][PSQTBuckets]; bool computed[2]; }; diff --git a/src/nnue/nnue_architecture.h b/src/nnue/nnue_architecture.h index e4c308cb267..0778c3f455e 100644 --- a/src/nnue/nnue_architecture.h +++ b/src/nnue/nnue_architecture.h @@ -38,13 +38,22 @@ namespace Stockfish::Eval::NNUE { using FeatureSet = Features::HalfKAv2_hm; // Number of input feature dimensions after conversion -constexpr IndexType TransformedFeatureDimensions = 2560; -constexpr IndexType PSQTBuckets = 8; -constexpr IndexType LayerStacks = 8; +constexpr IndexType TransformedFeatureDimensionsBig = 2560; +constexpr int L2Big = 15; +constexpr int L3Big = 32; +constexpr IndexType TransformedFeatureDimensionsSmall = 256; +constexpr int L2Small = 15; +constexpr int L3Small = 32; + +constexpr IndexType PSQTBuckets = 8; +constexpr IndexType LayerStacks = 8; + +template struct Network { - static constexpr int FC_0_OUTPUTS = 15; - static constexpr int FC_1_OUTPUTS = 32; + static constexpr IndexType TransformedFeatureDimensions = L1; + static constexpr int FC_0_OUTPUTS = L2; + static constexpr int FC_1_OUTPUTS = L3; Layers::AffineTransformSparseInput fc_0; Layers::SqrClippedReLU ac_sqr_0; @@ -84,13 +93,13 @@ struct Network { std::int32_t propagate(const TransformedFeatureType* transformedFeatures) { struct alignas(CacheLineSize) Buffer { - alignas(CacheLineSize) decltype(fc_0)::OutputBuffer fc_0_out; - alignas(CacheLineSize) decltype(ac_sqr_0)::OutputType + alignas(CacheLineSize) typename decltype(fc_0)::OutputBuffer fc_0_out; + alignas(CacheLineSize) typename decltype(ac_sqr_0)::OutputType ac_sqr_0_out[ceil_to_multiple(FC_0_OUTPUTS * 2, 32)]; - alignas(CacheLineSize) decltype(ac_0)::OutputBuffer ac_0_out; - alignas(CacheLineSize) decltype(fc_1)::OutputBuffer fc_1_out; - alignas(CacheLineSize) decltype(ac_1)::OutputBuffer ac_1_out; - alignas(CacheLineSize) decltype(fc_2)::OutputBuffer fc_2_out; + alignas(CacheLineSize) typename decltype(ac_0)::OutputBuffer ac_0_out; + alignas(CacheLineSize) typename decltype(fc_1)::OutputBuffer fc_1_out; + alignas(CacheLineSize) typename decltype(ac_1)::OutputBuffer ac_1_out; + alignas(CacheLineSize) typename decltype(fc_2)::OutputBuffer fc_2_out; Buffer() { std::memset(this, 0, sizeof(*this)); } }; @@ -108,7 +117,7 @@ struct Network { ac_sqr_0.propagate(buffer.fc_0_out, buffer.ac_sqr_0_out); ac_0.propagate(buffer.fc_0_out, buffer.ac_0_out); std::memcpy(buffer.ac_sqr_0_out + FC_0_OUTPUTS, buffer.ac_0_out, - FC_0_OUTPUTS * sizeof(decltype(ac_0)::OutputType)); + FC_0_OUTPUTS * sizeof(typename decltype(ac_0)::OutputType)); fc_1.propagate(buffer.ac_sqr_0_out, buffer.fc_1_out); ac_1.propagate(buffer.fc_1_out, buffer.ac_1_out); fc_2.propagate(buffer.ac_1_out, buffer.fc_2_out); diff --git a/src/nnue/nnue_feature_transformer.h b/src/nnue/nnue_feature_transformer.h index 2af80f07792..8154b2f0a1f 100644 --- a/src/nnue/nnue_feature_transformer.h +++ b/src/nnue/nnue_feature_transformer.h @@ -186,11 +186,6 @@ static constexpr int BestRegisterCount() { return 1; } - -static constexpr int NumRegs = - BestRegisterCount(); -static constexpr int NumPsqtRegs = - BestRegisterCount(); #if defined(__GNUC__) #pragma GCC diagnostic pop #endif @@ -198,13 +193,21 @@ static constexpr int NumPsqtRegs = // Input feature converter +template class FeatureTransformer { private: + static constexpr bool Small = TransformedFeatureDimensions == TransformedFeatureDimensionsSmall; + // Number of output dimensions for one side static constexpr IndexType HalfDimensions = TransformedFeatureDimensions; #ifdef VECTOR + static constexpr int NumRegs = + BestRegisterCount(); + static constexpr int NumPsqtRegs = + BestRegisterCount(); + static constexpr IndexType TileHeight = NumRegs * sizeof(vec_t) / 2; static constexpr IndexType PsqtTileHeight = NumPsqtRegs * sizeof(psqt_vec_t) / 4; static_assert(HalfDimensions % TileHeight == 0, "TileHeight must divide HalfDimensions"); @@ -247,14 +250,22 @@ class FeatureTransformer { return !stream.fail(); } + // Cast a pointer to a 2 dimensional array of width D + template + static constexpr T (*cast_2D(T* pt))[D] { + return (T(*)[D])pt; + } + // Convert input features std::int32_t transform(const Position& pos, OutputType* output, int bucket) const { update_accumulator(pos); update_accumulator(pos); const Color perspectives[2] = {pos.side_to_move(), ~pos.side_to_move()}; - const auto& accumulation = pos.state()->accumulator.accumulation; - const auto& psqtAccumulation = pos.state()->accumulator.psqtAccumulation; + const auto& accumulation = + cast_2D(pos.state()->template accumulation()); + const auto& psqtAccumulation = + cast_2D(pos.state()->template psqt_accumulation()); const auto psqt = (psqtAccumulation[perspectives[0]][bucket] - psqtAccumulation[perspectives[1]][bucket]) @@ -323,7 +334,7 @@ class FeatureTransformer { // of the estimated gain in terms of features to be added/subtracted. StateInfo *st = pos.state(), *next = nullptr; int gain = FeatureSet::refresh_cost(pos); - while (st->previous && !st->accumulator.computed[Perspective]) + while (st->previous && !st->template computed()[Perspective]) { // This governs when a full feature refresh is needed and how many // updates are better than just one full refresh. @@ -381,7 +392,7 @@ class FeatureTransformer { for (; i >= 0; --i) { - states_to_update[i]->accumulator.computed[Perspective] = true; + states_to_update[i]->template computed()[Perspective] = true; const StateInfo* end_state = i == 0 ? computed_st : states_to_update[i - 1]; @@ -401,10 +412,10 @@ class FeatureTransformer { { assert(states_to_update[0]); - auto accIn = - reinterpret_cast(&st->accumulator.accumulation[Perspective][0]); - auto accOut = reinterpret_cast( - &states_to_update[0]->accumulator.accumulation[Perspective][0]); + auto accIn = reinterpret_cast(&cast_2D( + st->template accumulation())[Perspective][0]); + auto accOut = reinterpret_cast(&cast_2D( + states_to_update[0]->template accumulation())[Perspective][0]); const IndexType offsetR0 = HalfDimensions * removed[0][0]; auto columnR0 = reinterpret_cast(&weights[offsetR0]); @@ -428,10 +439,10 @@ class FeatureTransformer { vec_add_16(columnR0[k], columnR1[k])); } - auto accPsqtIn = reinterpret_cast( - &st->accumulator.psqtAccumulation[Perspective][0]); - auto accPsqtOut = reinterpret_cast( - &states_to_update[0]->accumulator.psqtAccumulation[Perspective][0]); + auto accPsqtIn = reinterpret_cast(&cast_2D( + st->template psqt_accumulation())[Perspective][0]); + auto accPsqtOut = reinterpret_cast(&cast_2D( + states_to_update[0]->template psqt_accumulation())[Perspective][0]); const IndexType offsetPsqtR0 = PSQTBuckets * removed[0][0]; auto columnPsqtR0 = reinterpret_cast(&psqtWeights[offsetPsqtR0]); @@ -462,8 +473,8 @@ class FeatureTransformer { for (IndexType j = 0; j < HalfDimensions / TileHeight; ++j) { // Load accumulator - auto accTileIn = reinterpret_cast( - &st->accumulator.accumulation[Perspective][j * TileHeight]); + auto accTileIn = reinterpret_cast(&cast_2D( + st->template accumulation())[Perspective][j * TileHeight]); for (IndexType k = 0; k < NumRegs; ++k) acc[k] = vec_load(&accTileIn[k]); @@ -488,8 +499,8 @@ class FeatureTransformer { } // Store accumulator - auto accTileOut = reinterpret_cast( - &states_to_update[i]->accumulator.accumulation[Perspective][j * TileHeight]); + auto accTileOut = reinterpret_cast(&cast_2D( + states_to_update[i]->template accumulation())[Perspective][j * TileHeight]); for (IndexType k = 0; k < NumRegs; ++k) vec_store(&accTileOut[k], acc[k]); } @@ -498,8 +509,8 @@ class FeatureTransformer { for (IndexType j = 0; j < PSQTBuckets / PsqtTileHeight; ++j) { // Load accumulator - auto accTilePsqtIn = reinterpret_cast( - &st->accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]); + auto accTilePsqtIn = reinterpret_cast(&cast_2D( + st->template psqt_accumulation())[Perspective][j * PsqtTileHeight]); for (std::size_t k = 0; k < NumPsqtRegs; ++k) psqt[k] = vec_load_psqt(&accTilePsqtIn[k]); @@ -524,9 +535,8 @@ class FeatureTransformer { } // Store accumulator - auto accTilePsqtOut = reinterpret_cast( - &states_to_update[i] - ->accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]); + auto accTilePsqtOut = reinterpret_cast(&cast_2D( + states_to_update[i]->template psqt_accumulation())[Perspective][j * PsqtTileHeight]); for (std::size_t k = 0; k < NumPsqtRegs; ++k) vec_store_psqt(&accTilePsqtOut[k], psqt[k]); } @@ -535,13 +545,13 @@ class FeatureTransformer { #else for (IndexType i = 0; states_to_update[i]; ++i) { - std::memcpy(states_to_update[i]->accumulator.accumulation[Perspective], - st->accumulator.accumulation[Perspective], + std::memcpy(cast_2D(states_to_update[i]->template accumulation())[Perspective], + cast_2D(st->template accumulation())[Perspective], HalfDimensions * sizeof(BiasType)); for (std::size_t k = 0; k < PSQTBuckets; ++k) - states_to_update[i]->accumulator.psqtAccumulation[Perspective][k] = - st->accumulator.psqtAccumulation[Perspective][k]; + cast_2D(states_to_update[i]->template psqt_accumulation())[Perspective][k] = + cast_2D(st->template psqt_accumulation())[Perspective][k]; st = states_to_update[i]; @@ -551,10 +561,11 @@ class FeatureTransformer { const IndexType offset = HalfDimensions * index; for (IndexType j = 0; j < HalfDimensions; ++j) - st->accumulator.accumulation[Perspective][j] -= weights[offset + j]; + cast_2D( + st->template accumulation())[Perspective][j] -= weights[offset + j]; for (std::size_t k = 0; k < PSQTBuckets; ++k) - st->accumulator.psqtAccumulation[Perspective][k] -= + cast_2D(st->template psqt_accumulation())[Perspective][k] -= psqtWeights[index * PSQTBuckets + k]; } @@ -564,10 +575,11 @@ class FeatureTransformer { const IndexType offset = HalfDimensions * index; for (IndexType j = 0; j < HalfDimensions; ++j) - st->accumulator.accumulation[Perspective][j] += weights[offset + j]; + cast_2D( + st->template accumulation())[Perspective][j] += weights[offset + j]; for (std::size_t k = 0; k < PSQTBuckets; ++k) - st->accumulator.psqtAccumulation[Perspective][k] += + cast_2D(st->template psqt_accumulation())[Perspective][k] += psqtWeights[index * PSQTBuckets + k]; } } @@ -586,8 +598,8 @@ class FeatureTransformer { // Refresh the accumulator // Could be extracted to a separate function because it's done in 2 places, // but it's unclear if compilers would correctly handle register allocation. - auto& accumulator = pos.state()->accumulator; - accumulator.computed[Perspective] = true; + StateInfo* st = pos.state(); + st->template computed()[Perspective] = true; FeatureSet::IndexList active; FeatureSet::append_active_indices(pos, active); @@ -607,8 +619,8 @@ class FeatureTransformer { acc[k] = vec_add_16(acc[k], column[k]); } - auto accTile = - reinterpret_cast(&accumulator.accumulation[Perspective][j * TileHeight]); + auto accTile = reinterpret_cast(&cast_2D( + st->template accumulation())[Perspective][j * TileHeight]); for (unsigned k = 0; k < NumRegs; k++) vec_store(&accTile[k], acc[k]); } @@ -627,28 +639,30 @@ class FeatureTransformer { psqt[k] = vec_add_psqt_32(psqt[k], columnPsqt[k]); } - auto accTilePsqt = reinterpret_cast( - &accumulator.psqtAccumulation[Perspective][j * PsqtTileHeight]); + auto accTilePsqt = reinterpret_cast(&cast_2D( + st->template psqt_accumulation())[Perspective][j * PsqtTileHeight]); for (std::size_t k = 0; k < NumPsqtRegs; ++k) vec_store_psqt(&accTilePsqt[k], psqt[k]); } #else - std::memcpy(accumulator.accumulation[Perspective], biases, + std::memcpy(cast_2D(st->template accumulation())[Perspective], + biases, HalfDimensions * sizeof(BiasType)); for (std::size_t k = 0; k < PSQTBuckets; ++k) - accumulator.psqtAccumulation[Perspective][k] = 0; + cast_2D(st->template psqt_accumulation())[Perspective][k] = 0; for (const auto index : active) { const IndexType offset = HalfDimensions * index; for (IndexType j = 0; j < HalfDimensions; ++j) - accumulator.accumulation[Perspective][j] += weights[offset + j]; + cast_2D( + st->template accumulation())[Perspective][j] += weights[offset + j]; for (std::size_t k = 0; k < PSQTBuckets; ++k) - accumulator.psqtAccumulation[Perspective][k] += + cast_2D(st->template psqt_accumulation())[Perspective][k] += psqtWeights[index * PSQTBuckets + k]; } #endif @@ -663,12 +677,12 @@ class FeatureTransformer { // Look for a usable accumulator of an earlier position. We keep track // of the estimated gain in terms of features to be added/subtracted. // Fast early exit. - if (pos.state()->accumulator.computed[Perspective]) + if (pos.state()->template computed()[Perspective]) return; auto [oldest_st, _] = try_find_computed_accumulator(pos); - if (oldest_st->accumulator.computed[Perspective]) + if (oldest_st->template computed()[Perspective]) { // Only update current position accumulator to minimize work. StateInfo* states_to_update[2] = {pos.state(), nullptr}; @@ -685,7 +699,7 @@ class FeatureTransformer { auto [oldest_st, next] = try_find_computed_accumulator(pos); - if (oldest_st->accumulator.computed[Perspective]) + if (oldest_st->template computed()[Perspective]) { if (next == nullptr) return; diff --git a/src/position.cpp b/src/position.cpp index c45dd7b2e22..f03afb0e4f6 100644 --- a/src/position.cpp +++ b/src/position.cpp @@ -684,8 +684,10 @@ void Position::do_move(Move m, StateInfo& newSt, bool givesCheck) { ++st->pliesFromNull; // Used by NNUE - st->accumulator.computed[WHITE] = false; - st->accumulator.computed[BLACK] = false; + st->accumulatorBig.computed[WHITE] = + st->accumulatorBig.computed[BLACK] = + st->accumulatorSmall.computed[WHITE] = + st->accumulatorSmall.computed[BLACK] = false; auto& dp = st->dirtyPiece; dp.dirty_num = 1; @@ -964,15 +966,17 @@ void Position::do_null_move(StateInfo& newSt) { assert(!checkers()); assert(&newSt != st); - std::memcpy(&newSt, st, offsetof(StateInfo, accumulator)); + std::memcpy(&newSt, st, offsetof(StateInfo, accumulatorBig)); newSt.previous = st; st = &newSt; st->dirtyPiece.dirty_num = 0; st->dirtyPiece.piece[0] = NO_PIECE; // Avoid checks in UpdateAccumulator() - st->accumulator.computed[WHITE] = false; - st->accumulator.computed[BLACK] = false; + st->accumulatorBig.computed[WHITE] = + st->accumulatorBig.computed[BLACK] = + st->accumulatorSmall.computed[WHITE] = + st->accumulatorSmall.computed[BLACK] = false; if (st->epSquare != SQ_NONE) { diff --git a/src/position.h b/src/position.h index ce03c34f332..6d7aa8fde5e 100644 --- a/src/position.h +++ b/src/position.h @@ -57,8 +57,22 @@ struct StateInfo { int repetition; // Used by NNUE - Eval::NNUE::Accumulator accumulator; - DirtyPiece dirtyPiece; + Eval::NNUE::Accumulator accumulatorBig; + Eval::NNUE::Accumulator accumulatorSmall; + DirtyPiece dirtyPiece; + + template constexpr std::int16_t* accumulation() { + return Small ? (std::int16_t*)accumulatorSmall.accumulation + : (std::int16_t*)accumulatorBig.accumulation; + } + template constexpr std::int32_t* psqt_accumulation() { + return Small ? (std::int32_t*)accumulatorSmall.psqtAccumulation + : (std::int32_t*)accumulatorBig.psqtAccumulation; + } + template constexpr bool* computed() { + return Small ? accumulatorSmall.computed + : accumulatorBig.computed; + } }; @@ -160,6 +174,7 @@ class Position { int rule50_count() const; Value non_pawn_material(Color c) const; Value non_pawn_material() const; + Value simple_eval() const; // Position consistency check, for debugging bool pos_is_ok() const; @@ -305,6 +320,11 @@ inline Value Position::non_pawn_material() const { return non_pawn_material(WHITE) + non_pawn_material(BLACK); } +inline Value Position::simple_eval() const { + return PawnValue * (count(sideToMove) - count(~sideToMove)) + + (non_pawn_material(sideToMove) - non_pawn_material(~sideToMove)); +} + inline int Position::game_ply() const { return gamePly; } inline int Position::rule50_count() const { return st->rule50; } diff --git a/src/thread.cpp b/src/thread.cpp index bc884dedf01..f172199d691 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -210,7 +210,7 @@ void ThreadPool::start_thinking(Position& pos, 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->rootSimpleEval = pos.simple_eval(); } main()->start_searching(); diff --git a/src/uci.cpp b/src/uci.cpp index 95f6f349dd3..e169e78477a 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -320,7 +320,7 @@ void UCI::loop(int argc, char* argv[]) { std::string f; if (is >> std::skipws >> f) filename = f; - Eval::NNUE::save_eval(filename); + Eval::NNUE::save_eval(filename, false); } else if (token == "--help" || token == "help" || token == "--license" || token == "license") sync_cout diff --git a/src/ucioption.cpp b/src/ucioption.cpp index d0db1c76dd2..e9f48d4ee67 100644 --- a/src/ucioption.cpp +++ b/src/ucioption.cpp @@ -84,7 +84,8 @@ void init(OptionsMap& o) { o["SyzygyProbeDepth"] << Option(1, 1, 100); o["Syzygy50MoveRule"] << Option(true); o["SyzygyProbeLimit"] << Option(7, 0, 7); - o["EvalFile"] << Option(EvalFileDefaultName, on_eval_file); + o["EvalFile"] << Option(EvalFileDefaultNameBig, on_eval_file); + o["EvalFileSmall"] << Option(EvalFileDefaultNameSmall, on_eval_file); }