/* 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 . */ #include "network.h" #include #include #include #include #include #include #include #include #include "../evaluate.h" #include "../incbin/incbin.h" #include "../misc.h" #include "../position.h" #include "../types.h" #include "nnue_architecture.h" #include "nnue_common.h" #include "nnue_misc.h" namespace { // Macro to embed the default efficiently updatable neural network (NNUE) file // data in the engine binary (using incbin.h, by Dale Weiler). // This macro invocation will declare the following three variables // const unsigned char gEmbeddedNNUEData[]; // a pointer to the embedded data // const unsigned char *const gEmbeddedNNUEEnd; // a marker to the end // 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(EmbeddedNNUEBig, EvalFileDefaultNameBig); INCBIN(EmbeddedNNUESmall, EvalFileDefaultNameSmall); #else 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 struct EmbeddedNNUE { EmbeddedNNUE(const unsigned char* embeddedData, const unsigned char* embeddedEnd, const unsigned int embeddedSize) : data(embeddedData), end(embeddedEnd), size(embeddedSize) {} const unsigned char* data; const unsigned char* end; const unsigned int size; }; using namespace Stockfish::Eval::NNUE; EmbeddedNNUE get_embedded(EmbeddedNNUEType type) { if (type == EmbeddedNNUEType::BIG) return EmbeddedNNUE(gEmbeddedNNUEBigData, gEmbeddedNNUEBigEnd, gEmbeddedNNUEBigSize); else return EmbeddedNNUE(gEmbeddedNNUESmallData, gEmbeddedNNUESmallEnd, gEmbeddedNNUESmallSize); } } namespace Stockfish::Eval::NNUE { namespace Detail { // Initialize the evaluation function parameters template void initialize(AlignedPtr& pointer) { pointer.reset(reinterpret_cast(std_aligned_alloc(alignof(T), sizeof(T)))); std::memset(pointer.get(), 0, sizeof(T)); } template void initialize(LargePagePtr& pointer) { static_assert(alignof(T) <= 4096, "aligned_large_pages_alloc() may fail for such a big alignment requirement of T"); pointer.reset(reinterpret_cast(aligned_large_pages_alloc(sizeof(T)))); std::memset(pointer.get(), 0, sizeof(T)); } // Read evaluation function parameters template bool read_parameters(std::istream& stream, T& reference) { std::uint32_t header; header = read_little_endian(stream); if (!stream || header != T::get_hash_value()) return false; return reference.read_parameters(stream); } // Write evaluation function parameters template bool write_parameters(std::ostream& stream, const T& reference) { write_little_endian(stream, T::get_hash_value()); return reference.write_parameters(stream); } } // namespace Detail template void Network::load(const std::string& rootDirectory, std::string evalfilePath) { #if defined(DEFAULT_NNUE_DIRECTORY) std::vector dirs = {"", "", rootDirectory, stringify(DEFAULT_NNUE_DIRECTORY)}; #else std::vector dirs = {"", "", rootDirectory}; #endif if (evalfilePath.empty()) evalfilePath = evalFile.defaultName; for (const auto& directory : dirs) { if (evalFile.current != evalfilePath) { if (directory != "") { load_user_net(directory, evalfilePath); } if (directory == "" && evalfilePath == evalFile.defaultName) { load_internal(); } } } } template bool Network::save(const std::optional& filename) const { std::string actualFilename; std::string msg; if (filename.has_value()) actualFilename = filename.value(); else { if (evalFile.current != evalFile.defaultName) { msg = "Failed to export a net. " "A non-embedded net can only be saved if the filename is specified"; sync_cout << msg << sync_endl; return false; } actualFilename = evalFile.defaultName; } std::ofstream stream(actualFilename, std::ios_base::binary); bool saved = save(stream, evalFile.current, evalFile.netDescription); msg = saved ? "Network saved successfully to " + actualFilename : "Failed to export a net"; sync_cout << msg << sync_endl; return saved; } template Value Network::evaluate(const Position& pos, bool adjusted, int* complexity, bool psqtOnly) const { // We manually align the arrays on the stack because with gcc < 9.3 // overaligning stack variables with alignas() doesn't work correctly. constexpr uint64_t alignment = CacheLineSize; constexpr int delta = 24; #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) TransformedFeatureType transformedFeaturesUnaligned [FeatureTransformer::BufferSize + alignment / sizeof(TransformedFeatureType)]; auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); #else 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, psqtOnly); const auto positional = !psqtOnly ? (network[bucket]->propagate(transformedFeatures)) : 0; if (complexity) *complexity = !psqtOnly ? std::abs(psqt - positional) / OutputScale : 0; // Give more value to positional evaluation when adjusted flag is set if (adjusted) return static_cast(((1024 - delta) * psqt + (1024 + delta) * positional) / (1024 * OutputScale)); else return static_cast((psqt + positional) / OutputScale); } template void Network::verify(std::string evalfilePath) const { if (evalfilePath.empty()) evalfilePath = evalFile.defaultName; if (evalFile.current != evalfilePath) { std::string msg1 = "Network evaluation parameters compatible with the engine must be available."; std::string msg2 = "The network file " + evalfilePath + " 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/" + evalFile.defaultName; 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 " << evalfilePath << sync_endl; } template void Network::hint_common_access(const Position& pos, bool psqtOnl) const { featureTransformer->hint_common_access(pos, psqtOnl); } template NnueEvalTrace Network::trace_evaluate(const Position& pos) const { // We manually align the arrays on the stack because with gcc < 9.3 // overaligning stack variables with alignas() doesn't work correctly. constexpr uint64_t alignment = CacheLineSize; #if defined(ALIGNAS_ON_STACK_VARIABLES_BROKEN) TransformedFeatureType transformedFeaturesUnaligned [FeatureTransformer::BufferSize + alignment / sizeof(TransformedFeatureType)]; auto* transformedFeatures = align_ptr_up(&transformedFeaturesUnaligned[0]); #else alignas(alignment) TransformedFeatureType transformedFeatures [FeatureTransformer::BufferSize]; #endif ASSERT_ALIGNED(transformedFeatures, alignment); NnueEvalTrace t{}; t.correctBucket = (pos.count() - 1) / 4; for (IndexType bucket = 0; bucket < LayerStacks; ++bucket) { const auto materialist = featureTransformer->transform(pos, transformedFeatures, bucket, false); const auto positional = network[bucket]->propagate(transformedFeatures); t.psqt[bucket] = static_cast(materialist / OutputScale); t.positional[bucket] = static_cast(positional / OutputScale); } return t; } template void Network::load_user_net(const std::string& dir, const std::string& evalfilePath) { std::ifstream stream(dir + evalfilePath, std::ios::binary); auto description = load(stream); if (description.has_value()) { evalFile.current = evalfilePath; evalFile.netDescription = description.value(); } } template void Network::load_internal() { // 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); } }; const auto embedded = get_embedded(embeddedType); MemoryBuffer buffer(const_cast(reinterpret_cast(embedded.data)), size_t(embedded.size)); std::istream stream(&buffer); auto description = load(stream); if (description.has_value()) { evalFile.current = evalFile.defaultName; evalFile.netDescription = description.value(); } } template void Network::initialize() { Detail::initialize(featureTransformer); for (std::size_t i = 0; i < LayerStacks; ++i) Detail::initialize(network[i]); } template bool Network::save(std::ostream& stream, const std::string& name, const std::string& netDescription) const { if (name.empty() || name == "None") return false; return write_parameters(stream, netDescription); } template std::optional Network::load(std::istream& stream) { initialize(); std::string description; return read_parameters(stream, description) ? std::make_optional(description) : std::nullopt; } // Read network header template bool Network::read_header(std::istream& stream, std::uint32_t* hashValue, std::string* desc) const { std::uint32_t version, size; version = read_little_endian(stream); *hashValue = read_little_endian(stream); size = read_little_endian(stream); if (!stream || version != Version) return false; desc->resize(size); stream.read(&(*desc)[0], size); return !stream.fail(); } // Write network header template bool Network::write_header(std::ostream& stream, std::uint32_t hashValue, const std::string& desc) const { write_little_endian(stream, Version); write_little_endian(stream, hashValue); write_little_endian(stream, std::uint32_t(desc.size())); stream.write(&desc[0], desc.size()); return !stream.fail(); } template bool Network::read_parameters(std::istream& stream, std::string& netDescription) const { std::uint32_t hashValue; if (!read_header(stream, &hashValue, &netDescription)) return false; if (hashValue != Network::hash) return false; if (!Detail::read_parameters(stream, *featureTransformer)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) { if (!Detail::read_parameters(stream, *(network[i]))) return false; } return stream && stream.peek() == std::ios::traits_type::eof(); } template bool Network::write_parameters(std::ostream& stream, const std::string& netDescription) const { if (!write_header(stream, Network::hash, netDescription)) return false; if (!Detail::write_parameters(stream, *featureTransformer)) return false; for (std::size_t i = 0; i < LayerStacks; ++i) { if (!Detail::write_parameters(stream, *(network[i]))) return false; } return bool(stream); } // Explicit template instantiation template class Network< NetworkArchitecture, FeatureTransformer>; template class Network< NetworkArchitecture, FeatureTransformer>; } // namespace Stockfish::Eval::NNUE