/* 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 "../evaluate.h" #include "../incbin/incbin.h" #include "../memory.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 { // 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 Network::Network(const Network& other) : evalFile(other.evalFile), embeddedType(other.embeddedType) { if (other.featureTransformer) featureTransformer = make_unique_large_page(*other.featureTransformer); network = make_unique_aligned(LayerStacks); if (!other.network) return; for (std::size_t i = 0; i < LayerStacks; ++i) network[i] = other.network[i]; } template Network& Network::operator=(const Network& other) { evalFile = other.evalFile; embeddedType = other.embeddedType; if (other.featureTransformer) featureTransformer = make_unique_large_page(*other.featureTransformer); network = make_unique_aligned(LayerStacks); if (!other.network) return *this; for (std::size_t i = 0; i < LayerStacks; ++i) network[i] = other.network[i]; return *this; } 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 NetworkOutput Network::evaluate(const Position& pos, AccumulatorCaches::Cache* cache) 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); const int bucket = (pos.count() - 1) / 4; const auto psqt = featureTransformer->transform(pos, cache, transformedFeatures, bucket); const auto positional = network[bucket].propagate(transformedFeatures); return {static_cast(psqt / OutputScale), static_cast(positional / OutputScale)}; } template void Network::verify(std::string evalfilePath, const std::function& f) const { if (evalfilePath.empty()) evalfilePath = evalFile.defaultName; if (evalFile.current != evalfilePath) { if (f) { 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."; std::string msg = "ERROR: " + msg1 + '\n' + "ERROR: " + msg2 + '\n' + "ERROR: " + msg3 + '\n' + "ERROR: " + msg4 + '\n' + "ERROR: " + msg5 + '\n'; f(msg); } exit(EXIT_FAILURE); } if (f) { size_t size = sizeof(*featureTransformer) + sizeof(Arch) * LayerStacks; f("info string NNUE evaluation using " + evalfilePath + " (" + std::to_string(size / (1024 * 1024)) + "MiB, (" + std::to_string(featureTransformer->InputDimensions) + ", " + std::to_string(network[0].TransformedFeatureDimensions) + ", " + std::to_string(network[0].FC_0_OUTPUTS) + ", " + std::to_string(network[0].FC_1_OUTPUTS) + ", 1))"); } } template void Network::hint_common_access( const Position& pos, AccumulatorCaches::Cache* cache) const { featureTransformer->hint_common_access(pos, cache); } template NnueEvalTrace Network::trace_evaluate(const Position& pos, AccumulatorCaches::Cache* cache) 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, cache, transformedFeatures, bucket); 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() { featureTransformer = make_unique_large_page(); network = make_unique_aligned(LayerStacks); } 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