mirror of
https://github.com/official-stockfish/Stockfish.git
synced 2026-07-23 21:27:14 +00:00
Merge commit '69a01b88f35db2a5003d42116f573207ca5c275b' into cluster
This commit is contained in:
+15
-1
@@ -438,7 +438,7 @@ endif
|
||||
ifeq ($(COMP),gcc)
|
||||
comp=gcc
|
||||
CXX=g++
|
||||
CXXFLAGS += -pedantic -Wextra -Wshadow -Wmissing-declarations
|
||||
CXXFLAGS += -pedantic -Wextra -Wshadow -Wmissing-declarations -Wstack-usage=128000
|
||||
|
||||
ifeq ($(arch),$(filter $(arch),armv7 armv8 riscv64))
|
||||
ifeq ($(OS),Android)
|
||||
@@ -621,6 +621,19 @@ ifneq ($(comp),mingw)
|
||||
ifneq ($(KERNEL),Haiku)
|
||||
ifneq ($(COMP),ndk)
|
||||
LDFLAGS += -lpthread
|
||||
|
||||
add_lrt = yes
|
||||
ifeq ($(target_windows),yes)
|
||||
add_lrt = no
|
||||
endif
|
||||
|
||||
ifeq ($(KERNEL),Darwin)
|
||||
add_lrt = no
|
||||
endif
|
||||
|
||||
ifeq ($(add_lrt),yes)
|
||||
LDFLAGS += -lrt
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
endif
|
||||
@@ -631,6 +644,7 @@ ifeq ($(debug),no)
|
||||
CXXFLAGS += -DNDEBUG
|
||||
else
|
||||
CXXFLAGS += -g
|
||||
CXXFLAGS += -D_GLIBCXX_ASSERTIONS -D_GLIBCXX_DEBUG
|
||||
endif
|
||||
|
||||
### 3.2.2 Debugging with undefined behavior sanitizers
|
||||
|
||||
+39
-4
@@ -33,10 +33,12 @@
|
||||
#include "misc.h"
|
||||
#include "nnue/network.h"
|
||||
#include "nnue/nnue_common.h"
|
||||
#include "nnue/nnue_misc.h"
|
||||
#include "numa.h"
|
||||
#include "perft.h"
|
||||
#include "position.h"
|
||||
#include "search.h"
|
||||
#include "shm.h"
|
||||
#include "syzygy/tbprobe.h"
|
||||
#include "types.h"
|
||||
#include "uci.h"
|
||||
@@ -57,11 +59,14 @@ Engine::Engine(std::optional<std::string> path) :
|
||||
threads(),
|
||||
networks(
|
||||
numaContext,
|
||||
NN::Networks(
|
||||
NN::NetworkBig({EvalFileDefaultNameBig, "None", ""}, NN::EmbeddedNNUEType::BIG),
|
||||
NN::NetworkSmall({EvalFileDefaultNameSmall, "None", ""}, NN::EmbeddedNNUEType::SMALL))) {
|
||||
pos.set(StartFEN, false, &states->back());
|
||||
// Heap-allocate because sizeof(NN::Networks) is large
|
||||
std::make_unique<NN::Networks>(
|
||||
std::make_unique<NN::NetworkBig>(NN::EvalFile{EvalFileDefaultNameBig, "None", ""},
|
||||
NN::EmbeddedNNUEType::BIG),
|
||||
std::make_unique<NN::NetworkSmall>(NN::EvalFile{EvalFileDefaultNameSmall, "None", ""},
|
||||
NN::EmbeddedNNUEType::SMALL))) {
|
||||
|
||||
pos.set(StartFEN, false, &states->back());
|
||||
|
||||
options.add( //
|
||||
"Debug Log File", Option("", [](const Option& o) {
|
||||
@@ -257,6 +262,36 @@ void Engine::set_ponderhit(bool b) { threads.main_manager()->ponder = b; }
|
||||
void Engine::verify_networks() const {
|
||||
networks->big.verify(options["EvalFile"], onVerifyNetworks);
|
||||
networks->small.verify(options["EvalFileSmall"], onVerifyNetworks);
|
||||
|
||||
auto statuses = networks.get_status_and_errors();
|
||||
for (size_t i = 0; i < statuses.size(); ++i)
|
||||
{
|
||||
const auto [status, error] = statuses[i];
|
||||
std::string message = "Network replica " + std::to_string(i + 1) + ": ";
|
||||
if (status == SystemWideSharedConstantAllocationStatus::NoAllocation)
|
||||
{
|
||||
message += "No allocation.";
|
||||
}
|
||||
else if (status == SystemWideSharedConstantAllocationStatus::LocalMemory)
|
||||
{
|
||||
message += "Local memory.";
|
||||
}
|
||||
else if (status == SystemWideSharedConstantAllocationStatus::SharedMemory)
|
||||
{
|
||||
message += "Shared memory.";
|
||||
}
|
||||
else
|
||||
{
|
||||
message += "Unknown status.";
|
||||
}
|
||||
|
||||
if (error.has_value())
|
||||
{
|
||||
message += " " + *error;
|
||||
}
|
||||
|
||||
onVerifyNetworks(message);
|
||||
}
|
||||
}
|
||||
|
||||
void Engine::load_networks() {
|
||||
|
||||
+4
-4
@@ -115,10 +115,10 @@ class Engine {
|
||||
Position pos;
|
||||
StateListPtr states;
|
||||
|
||||
OptionsMap options;
|
||||
ThreadPool threads;
|
||||
TranspositionTable tt;
|
||||
LazyNumaReplicated<Eval::NNUE::Networks> networks;
|
||||
OptionsMap options;
|
||||
ThreadPool threads;
|
||||
TranspositionTable tt;
|
||||
LazyNumaReplicatedSystemWide<Eval::NNUE::Networks> networks;
|
||||
|
||||
Search::SearchManager::UpdateContext updateContext;
|
||||
std::function<void(std::string_view)> onVerifyNetworks;
|
||||
|
||||
+4
-4
@@ -98,8 +98,8 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) {
|
||||
if (pos.checkers())
|
||||
return "Final evaluation: none (in check)";
|
||||
|
||||
Eval::NNUE::AccumulatorStack accumulators;
|
||||
auto caches = std::make_unique<Eval::NNUE::AccumulatorCaches>(networks);
|
||||
auto accumulators = std::make_unique<Eval::NNUE::AccumulatorStack>();
|
||||
auto caches = std::make_unique<Eval::NNUE::AccumulatorCaches>(networks);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::showpoint << std::noshowpos << std::fixed << std::setprecision(2);
|
||||
@@ -107,12 +107,12 @@ std::string Eval::trace(Position& pos, const Eval::NNUE::Networks& networks) {
|
||||
|
||||
ss << std::showpoint << std::showpos << std::fixed << std::setprecision(2) << std::setw(15);
|
||||
|
||||
auto [psqt, positional] = networks.big.evaluate(pos, accumulators, &caches->big);
|
||||
auto [psqt, positional] = networks.big.evaluate(pos, *accumulators, &caches->big);
|
||||
Value v = psqt + positional;
|
||||
v = pos.side_to_move() == WHITE ? v : -v;
|
||||
ss << "NNUE evaluation " << 0.01 * UCIEngine::to_cp(v, pos) << " (white side)\n";
|
||||
|
||||
v = evaluate(networks, pos, accumulators, *caches, VALUE_ZERO);
|
||||
v = evaluate(networks, pos, *accumulators, *caches, VALUE_ZERO);
|
||||
v = pos.side_to_move() == WHITE ? v : -v;
|
||||
ss << "Final evaluation " << 0.01 * UCIEngine::to_cp(v, pos) << " (white side)";
|
||||
ss << " [with scaled NNUE, ...]";
|
||||
|
||||
+5
-5
@@ -17,18 +17,18 @@
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
|
||||
#include "bitboard.h"
|
||||
#include "misc.h"
|
||||
#include "position.h"
|
||||
#include "tune.h"
|
||||
#include "types.h"
|
||||
#include "uci.h"
|
||||
#include "tune.h"
|
||||
|
||||
using namespace Stockfish;
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
|
||||
Distributed::init();
|
||||
if (Distributed::is_root())
|
||||
std::cout << engine_info() << std::endl;
|
||||
@@ -36,11 +36,11 @@ int main(int argc, char* argv[]) {
|
||||
Bitboards::init();
|
||||
Position::init();
|
||||
|
||||
UCIEngine uci(argc, argv);
|
||||
auto uci = std::make_unique<UCIEngine>(argc, argv);
|
||||
|
||||
Tune::init(uci.engine_options());
|
||||
Tune::init(uci->engine_options());
|
||||
|
||||
uci.loop();
|
||||
uci->loop();
|
||||
|
||||
Distributed::finalize();
|
||||
|
||||
|
||||
+8
-77
@@ -55,12 +55,6 @@
|
||||
// the calls at compile time), try to load them at runtime. To do this we need
|
||||
// first to define the corresponding function pointers.
|
||||
|
||||
extern "C" {
|
||||
using OpenProcessToken_t = bool (*)(HANDLE, DWORD, PHANDLE);
|
||||
using LookupPrivilegeValueA_t = bool (*)(LPCSTR, LPCSTR, PLUID);
|
||||
using AdjustTokenPrivileges_t =
|
||||
bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
@@ -106,77 +100,14 @@ void std_aligned_free(void* ptr) {
|
||||
|
||||
static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize) {
|
||||
|
||||
#if !defined(_WIN64)
|
||||
return nullptr;
|
||||
#else
|
||||
|
||||
HANDLE hProcessToken{};
|
||||
LUID luid{};
|
||||
void* mem = nullptr;
|
||||
|
||||
const size_t largePageSize = GetLargePageMinimum();
|
||||
if (!largePageSize)
|
||||
return nullptr;
|
||||
|
||||
// Dynamically link OpenProcessToken, LookupPrivilegeValue and AdjustTokenPrivileges
|
||||
|
||||
HMODULE hAdvapi32 = GetModuleHandle(TEXT("advapi32.dll"));
|
||||
|
||||
if (!hAdvapi32)
|
||||
hAdvapi32 = LoadLibrary(TEXT("advapi32.dll"));
|
||||
|
||||
auto OpenProcessToken_f =
|
||||
OpenProcessToken_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken"));
|
||||
if (!OpenProcessToken_f)
|
||||
return nullptr;
|
||||
auto LookupPrivilegeValueA_f =
|
||||
LookupPrivilegeValueA_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA"));
|
||||
if (!LookupPrivilegeValueA_f)
|
||||
return nullptr;
|
||||
auto AdjustTokenPrivileges_f =
|
||||
AdjustTokenPrivileges_t((void (*)()) GetProcAddress(hAdvapi32, "AdjustTokenPrivileges"));
|
||||
if (!AdjustTokenPrivileges_f)
|
||||
return nullptr;
|
||||
|
||||
// We need SeLockMemoryPrivilege, so try to enable it for the process
|
||||
|
||||
if (!OpenProcessToken_f( // OpenProcessToken()
|
||||
GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken))
|
||||
return nullptr;
|
||||
|
||||
if (LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid))
|
||||
{
|
||||
TOKEN_PRIVILEGES tp{};
|
||||
TOKEN_PRIVILEGES prevTp{};
|
||||
DWORD prevTpLen = 0;
|
||||
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Luid = luid;
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
|
||||
// Try to enable SeLockMemoryPrivilege. Note that even if AdjustTokenPrivileges()
|
||||
// succeeds, we still need to query GetLastError() to ensure that the privileges
|
||||
// were actually obtained.
|
||||
|
||||
if (AdjustTokenPrivileges_f(hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp,
|
||||
&prevTpLen)
|
||||
&& GetLastError() == ERROR_SUCCESS)
|
||||
{
|
||||
// Round up size to full pages and allocate
|
||||
allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1);
|
||||
mem = VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES,
|
||||
PAGE_READWRITE);
|
||||
|
||||
// Privilege no longer needed, restore previous state
|
||||
AdjustTokenPrivileges_f(hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
CloseHandle(hProcessToken);
|
||||
|
||||
return mem;
|
||||
|
||||
#endif
|
||||
return windows_try_with_large_page_priviliges(
|
||||
[&](size_t largePageSize) {
|
||||
// Round up size to full pages and allocate
|
||||
allocSize = (allocSize + largePageSize - 1) & ~size_t(largePageSize - 1);
|
||||
return VirtualAlloc(nullptr, allocSize, MEM_RESERVE | MEM_COMMIT | MEM_LARGE_PAGES,
|
||||
PAGE_READWRITE);
|
||||
},
|
||||
[]() { return (void*) nullptr; });
|
||||
}
|
||||
|
||||
void* aligned_large_pages_alloc(size_t allocSize) {
|
||||
|
||||
@@ -29,6 +29,29 @@
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#if defined(_WIN64)
|
||||
|
||||
#if _WIN32_WINNT < 0x0601
|
||||
#undef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0601 // Force to include needed API prototypes
|
||||
#endif
|
||||
|
||||
#if !defined(NOMINMAX)
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
|
||||
#include <psapi.h>
|
||||
|
||||
extern "C" {
|
||||
using OpenProcessToken_t = bool (*)(HANDLE, DWORD, PHANDLE);
|
||||
using LookupPrivilegeValueA_t = bool (*)(LPCSTR, LPCSTR, PLUID);
|
||||
using AdjustTokenPrivileges_t =
|
||||
bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
namespace Stockfish {
|
||||
|
||||
void* std_aligned_alloc(size_t alignment, size_t size);
|
||||
@@ -211,6 +234,81 @@ T* align_ptr_up(T* ptr) {
|
||||
reinterpret_cast<char*>((ptrint + (Alignment - 1)) / Alignment * Alignment));
|
||||
}
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
template<typename FuncYesT, typename FuncNoT>
|
||||
auto windows_try_with_large_page_priviliges([[maybe_unused]] FuncYesT&& fyes, FuncNoT&& fno) {
|
||||
|
||||
#if !defined(_WIN64)
|
||||
return fno();
|
||||
#else
|
||||
|
||||
HANDLE hProcessToken{};
|
||||
LUID luid{};
|
||||
|
||||
const size_t largePageSize = GetLargePageMinimum();
|
||||
if (!largePageSize)
|
||||
return fno();
|
||||
|
||||
// Dynamically link OpenProcessToken, LookupPrivilegeValue and AdjustTokenPrivileges
|
||||
|
||||
HMODULE hAdvapi32 = GetModuleHandle(TEXT("advapi32.dll"));
|
||||
|
||||
if (!hAdvapi32)
|
||||
hAdvapi32 = LoadLibrary(TEXT("advapi32.dll"));
|
||||
|
||||
auto OpenProcessToken_f =
|
||||
OpenProcessToken_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken"));
|
||||
if (!OpenProcessToken_f)
|
||||
return fno();
|
||||
auto LookupPrivilegeValueA_f =
|
||||
LookupPrivilegeValueA_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA"));
|
||||
if (!LookupPrivilegeValueA_f)
|
||||
return fno();
|
||||
auto AdjustTokenPrivileges_f =
|
||||
AdjustTokenPrivileges_t((void (*)()) GetProcAddress(hAdvapi32, "AdjustTokenPrivileges"));
|
||||
if (!AdjustTokenPrivileges_f)
|
||||
return fno();
|
||||
|
||||
// We need SeLockMemoryPrivilege, so try to enable it for the process
|
||||
|
||||
if (!OpenProcessToken_f( // OpenProcessToken()
|
||||
GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken))
|
||||
return fno();
|
||||
|
||||
if (!LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid))
|
||||
return fno();
|
||||
|
||||
TOKEN_PRIVILEGES tp{};
|
||||
TOKEN_PRIVILEGES prevTp{};
|
||||
DWORD prevTpLen = 0;
|
||||
|
||||
tp.PrivilegeCount = 1;
|
||||
tp.Privileges[0].Luid = luid;
|
||||
tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
|
||||
|
||||
// Try to enable SeLockMemoryPrivilege. Note that even if AdjustTokenPrivileges()
|
||||
// succeeds, we still need to query GetLastError() to ensure that the privileges
|
||||
// were actually obtained.
|
||||
|
||||
if (!AdjustTokenPrivileges_f(hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp,
|
||||
&prevTpLen)
|
||||
|| GetLastError() != ERROR_SUCCESS)
|
||||
return fno();
|
||||
|
||||
auto&& ret = fyes(largePageSize);
|
||||
|
||||
// Privilege no longer needed, restore previous state
|
||||
AdjustTokenPrivileges_f(hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr);
|
||||
|
||||
CloseHandle(hProcessToken);
|
||||
|
||||
return std::forward<decltype(ret)>(ret);
|
||||
|
||||
#endif
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
|
||||
+100
-1
@@ -23,11 +23,15 @@
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <exception> // IWYU pragma: keep
|
||||
// IWYU pragma: no_include <__exception/terminate.h>
|
||||
#include <functional>
|
||||
#include <iosfwd>
|
||||
#include <optional>
|
||||
#include <cstring>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
@@ -291,6 +295,94 @@ inline uint64_t mul_hi64(uint64_t a, uint64_t b) {
|
||||
}
|
||||
|
||||
|
||||
template<typename T>
|
||||
inline void hash_combine(std::size_t& seed, const T& v) {
|
||||
std::hash<T> hasher;
|
||||
seed ^= hasher(v) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
template<>
|
||||
inline void hash_combine(std::size_t& seed, const std::size_t& v) {
|
||||
seed ^= v + 0x9e3779b9 + (seed << 6) + (seed >> 2);
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
inline std::size_t get_raw_data_hash(const T& value) {
|
||||
return std::hash<std::string_view>{}(
|
||||
std::string_view(reinterpret_cast<const char*>(&value), sizeof(value)));
|
||||
}
|
||||
|
||||
template<std::size_t Capacity>
|
||||
class FixedString {
|
||||
public:
|
||||
FixedString() :
|
||||
length_(0) {
|
||||
data_[0] = '\0';
|
||||
}
|
||||
|
||||
FixedString(const char* str) {
|
||||
size_t len = std::strlen(str);
|
||||
if (len > Capacity)
|
||||
std::terminate();
|
||||
std::memcpy(data_, str, len);
|
||||
length_ = len;
|
||||
data_[length_] = '\0';
|
||||
}
|
||||
|
||||
FixedString(const std::string& str) {
|
||||
if (str.size() > Capacity)
|
||||
std::terminate();
|
||||
std::memcpy(data_, str.data(), str.size());
|
||||
length_ = str.size();
|
||||
data_[length_] = '\0';
|
||||
}
|
||||
|
||||
std::size_t size() const { return length_; }
|
||||
std::size_t capacity() const { return Capacity; }
|
||||
|
||||
const char* c_str() const { return data_; }
|
||||
const char* data() const { return data_; }
|
||||
|
||||
char& operator[](std::size_t i) { return data_[i]; }
|
||||
|
||||
const char& operator[](std::size_t i) const { return data_[i]; }
|
||||
|
||||
FixedString& operator+=(const char* str) {
|
||||
size_t len = std::strlen(str);
|
||||
if (length_ + len > Capacity)
|
||||
std::terminate();
|
||||
std::memcpy(data_ + length_, str, len);
|
||||
length_ += len;
|
||||
data_[length_] = '\0';
|
||||
return *this;
|
||||
}
|
||||
|
||||
FixedString& operator+=(const FixedString& other) { return (*this += other.c_str()); }
|
||||
|
||||
operator std::string() const { return std::string(data_, length_); }
|
||||
|
||||
operator std::string_view() const { return std::string_view(data_, length_); }
|
||||
|
||||
template<typename T>
|
||||
bool operator==(const T& other) const noexcept {
|
||||
return (std::string_view) (*this) == other;
|
||||
}
|
||||
|
||||
template<typename T>
|
||||
bool operator!=(const T& other) const noexcept {
|
||||
return (std::string_view) (*this) != other;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
length_ = 0;
|
||||
data_[0] = '\0';
|
||||
}
|
||||
|
||||
private:
|
||||
char data_[Capacity + 1]; // +1 for null terminator
|
||||
std::size_t length_;
|
||||
};
|
||||
|
||||
struct CommandLine {
|
||||
public:
|
||||
CommandLine(int _argc, char** _argv) :
|
||||
@@ -335,4 +427,11 @@ void move_to_front(std::vector<T>& vec, Predicate pred) {
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
template<std::size_t N>
|
||||
struct std::hash<Stockfish::FixedString<N>> {
|
||||
std::size_t operator()(const Stockfish::FixedString<N>& fstr) const noexcept {
|
||||
return std::hash<std::string_view>{}((std::string_view) fstr);
|
||||
}
|
||||
};
|
||||
|
||||
#endif // #ifndef MISC_H_INCLUDED
|
||||
|
||||
@@ -181,6 +181,15 @@ class AffineTransform {
|
||||
|
||||
return !stream.fail();
|
||||
}
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, get_raw_data_hash(biases));
|
||||
hash_combine(h, get_raw_data_hash(weights));
|
||||
hash_combine(h, get_hash_value(0));
|
||||
return h;
|
||||
}
|
||||
|
||||
// Forward propagation
|
||||
void propagate(const InputType* input, OutputType* output) const {
|
||||
|
||||
|
||||
@@ -241,6 +241,15 @@ class AffineTransformSparseInput {
|
||||
|
||||
return !stream.fail();
|
||||
}
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, get_raw_data_hash(biases));
|
||||
hash_combine(h, get_raw_data_hash(weights));
|
||||
hash_combine(h, get_hash_value(0));
|
||||
return h;
|
||||
}
|
||||
|
||||
// Forward propagation
|
||||
void propagate(const InputType* input, OutputType* output) const {
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ class ClippedReLU {
|
||||
// Write network parameters
|
||||
bool write_parameters(std::ostream&) const { return true; }
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, get_hash_value(0));
|
||||
return h;
|
||||
}
|
||||
|
||||
// Forward propagation
|
||||
void propagate(const InputType* input, OutputType* output) const {
|
||||
|
||||
|
||||
@@ -58,6 +58,12 @@ class SqrClippedReLU {
|
||||
// Write network parameters
|
||||
bool write_parameters(std::ostream&) const { return true; }
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, get_hash_value(0));
|
||||
return h;
|
||||
}
|
||||
|
||||
// Forward propagation
|
||||
void propagate(const InputType* input, OutputType* output) const {
|
||||
|
||||
|
||||
+28
-54
@@ -21,7 +21,6 @@
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <type_traits>
|
||||
#include <vector>
|
||||
@@ -31,7 +30,6 @@
|
||||
|
||||
#include "../cluster.h"
|
||||
#include "../evaluate.h"
|
||||
#include "../memory.h"
|
||||
#include "../misc.h"
|
||||
#include "../position.h"
|
||||
#include "../types.h"
|
||||
@@ -102,7 +100,7 @@ bool read_parameters(std::istream& stream, T& reference) {
|
||||
|
||||
// Write evaluation function parameters
|
||||
template<typename T>
|
||||
bool write_parameters(std::ostream& stream, T& reference) {
|
||||
bool write_parameters(std::ostream& stream, const T& reference) {
|
||||
|
||||
write_little_endian<std::uint32_t>(stream, T::get_hash_value());
|
||||
return reference.write_parameters(stream);
|
||||
@@ -110,43 +108,6 @@ bool write_parameters(std::ostream& stream, T& reference) {
|
||||
|
||||
} // namespace Detail
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
Network<Arch, Transformer>::Network(const Network<Arch, Transformer>& other) :
|
||||
evalFile(other.evalFile),
|
||||
embeddedType(other.embeddedType) {
|
||||
|
||||
if (other.featureTransformer)
|
||||
featureTransformer = make_unique_large_page<Transformer>(*other.featureTransformer);
|
||||
|
||||
network = make_unique_aligned<Arch[]>(LayerStacks);
|
||||
|
||||
if (!other.network)
|
||||
return;
|
||||
|
||||
for (std::size_t i = 0; i < LayerStacks; ++i)
|
||||
network[i] = other.network[i];
|
||||
}
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
Network<Arch, Transformer>&
|
||||
Network<Arch, Transformer>::operator=(const Network<Arch, Transformer>& other) {
|
||||
evalFile = other.evalFile;
|
||||
embeddedType = other.embeddedType;
|
||||
|
||||
if (other.featureTransformer)
|
||||
featureTransformer = make_unique_large_page<Transformer>(*other.featureTransformer);
|
||||
|
||||
network = make_unique_aligned<Arch[]>(LayerStacks);
|
||||
|
||||
if (!other.network)
|
||||
return *this;
|
||||
|
||||
for (std::size_t i = 0; i < LayerStacks; ++i)
|
||||
network[i] = other.network[i];
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
void Network<Arch, Transformer>::load(const std::string& rootDirectory, std::string evalfilePath) {
|
||||
#if defined(DEFAULT_NNUE_DIRECTORY)
|
||||
@@ -161,14 +122,14 @@ void Network<Arch, Transformer>::load(const std::string& rootDirectory, std::str
|
||||
|
||||
for (const auto& directory : dirs)
|
||||
{
|
||||
if (evalFile.current != evalfilePath)
|
||||
if (std::string(evalFile.current) != evalfilePath)
|
||||
{
|
||||
if (directory != "<internal>")
|
||||
{
|
||||
load_user_net(directory, evalfilePath);
|
||||
}
|
||||
|
||||
if (directory == "<internal>" && evalfilePath == evalFile.defaultName)
|
||||
if (directory == "<internal>" && evalfilePath == std::string(evalFile.defaultName))
|
||||
{
|
||||
load_internal();
|
||||
}
|
||||
@@ -186,7 +147,7 @@ bool Network<Arch, Transformer>::save(const std::optional<std::string>& filename
|
||||
actualFilename = filename.value();
|
||||
else
|
||||
{
|
||||
if (evalFile.current != evalFile.defaultName)
|
||||
if (std::string(evalFile.current) != std::string(evalFile.defaultName))
|
||||
{
|
||||
msg = "Failed to export a net. "
|
||||
"A non-embedded net can only be saved if the filename is specified";
|
||||
@@ -223,7 +184,7 @@ Network<Arch, Transformer>::evaluate(const Position& pos
|
||||
|
||||
const int bucket = (pos.count<ALL_PIECES>() - 1) / 4;
|
||||
const auto psqt =
|
||||
featureTransformer->transform(pos, accumulatorStack, cache, transformedFeatures, bucket);
|
||||
featureTransformer.transform(pos, accumulatorStack, cache, transformedFeatures, bucket);
|
||||
const auto positional = network[bucket].propagate(transformedFeatures);
|
||||
return {static_cast<Value>(psqt / OutputScale), static_cast<Value>(positional / OutputScale)};
|
||||
}
|
||||
@@ -235,7 +196,7 @@ void Network<Arch, Transformer>::verify(std::string
|
||||
if (evalfilePath.empty())
|
||||
evalfilePath = evalFile.defaultName;
|
||||
|
||||
if (evalFile.current != evalfilePath)
|
||||
if (std::string(evalFile.current) != evalfilePath)
|
||||
{
|
||||
if (f)
|
||||
{
|
||||
@@ -246,7 +207,7 @@ void Network<Arch, Transformer>::verify(std::string
|
||||
"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(evalFile.defaultName);
|
||||
std::string msg5 = "The engine will be terminated now.";
|
||||
|
||||
std::string msg = "ERROR: " + msg1 + '\n' + "ERROR: " + msg2 + '\n' + "ERROR: " + msg3
|
||||
@@ -260,10 +221,10 @@ void Network<Arch, Transformer>::verify(std::string
|
||||
|
||||
if (f)
|
||||
{
|
||||
size_t size = sizeof(*featureTransformer) + sizeof(Arch) * LayerStacks;
|
||||
size_t size = sizeof(featureTransformer) + sizeof(Arch) * LayerStacks;
|
||||
if (Distributed::is_root())
|
||||
f("NNUE evaluation using " + evalfilePath + " (" + std::to_string(size / (1024 * 1024))
|
||||
+ "MiB, (" + std::to_string(featureTransformer->InputDimensions) + ", "
|
||||
+ "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))");
|
||||
@@ -289,7 +250,7 @@ Network<Arch, Transformer>::trace_evaluate(const Position&
|
||||
for (IndexType bucket = 0; bucket < LayerStacks; ++bucket)
|
||||
{
|
||||
const auto materialist =
|
||||
featureTransformer->transform(pos, accumulatorStack, cache, transformedFeatures, bucket);
|
||||
featureTransformer.transform(pos, accumulatorStack, cache, transformedFeatures, bucket);
|
||||
const auto positional = network[bucket].propagate(transformedFeatures);
|
||||
|
||||
t.psqt[bucket] = static_cast<Value>(materialist / OutputScale);
|
||||
@@ -343,8 +304,7 @@ void Network<Arch, Transformer>::load_internal() {
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
void Network<Arch, Transformer>::initialize() {
|
||||
featureTransformer = make_unique_large_page<Transformer>();
|
||||
network = make_unique_aligned<Arch[]>(LayerStacks);
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -368,6 +328,20 @@ std::optional<std::string> Network<Arch, Transformer>::load(std::istream& stream
|
||||
}
|
||||
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
std::size_t Network<Arch, Transformer>::get_content_hash() const {
|
||||
if (!initialized)
|
||||
return 0;
|
||||
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, featureTransformer);
|
||||
for (auto&& layerstack : network)
|
||||
hash_combine(h, layerstack);
|
||||
hash_combine(h, evalFile);
|
||||
hash_combine(h, static_cast<int>(embeddedType));
|
||||
return h;
|
||||
}
|
||||
|
||||
// Read network header
|
||||
template<typename Arch, typename Transformer>
|
||||
bool Network<Arch, Transformer>::read_header(std::istream& stream,
|
||||
@@ -401,13 +375,13 @@ bool Network<Arch, Transformer>::write_header(std::ostream& stream,
|
||||
|
||||
template<typename Arch, typename Transformer>
|
||||
bool Network<Arch, Transformer>::read_parameters(std::istream& stream,
|
||||
std::string& netDescription) const {
|
||||
std::string& netDescription) {
|
||||
std::uint32_t hashValue;
|
||||
if (!read_header(stream, &hashValue, &netDescription))
|
||||
return false;
|
||||
if (hashValue != Network::hash)
|
||||
return false;
|
||||
if (!Detail::read_parameters(stream, *featureTransformer))
|
||||
if (!Detail::read_parameters(stream, featureTransformer))
|
||||
return false;
|
||||
for (std::size_t i = 0; i < LayerStacks; ++i)
|
||||
{
|
||||
@@ -423,7 +397,7 @@ bool Network<Arch, Transformer>::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))
|
||||
if (!Detail::write_parameters(stream, featureTransformer))
|
||||
return false;
|
||||
for (std::size_t i = 0; i < LayerStacks; ++i)
|
||||
{
|
||||
|
||||
+38
-11
@@ -19,16 +19,18 @@
|
||||
#ifndef NETWORK_H_INCLUDED
|
||||
#define NETWORK_H_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <functional>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "../memory.h"
|
||||
#include "../misc.h"
|
||||
#include "../types.h"
|
||||
#include "nnue_accumulator.h"
|
||||
#include "nnue_architecture.h"
|
||||
@@ -49,6 +51,9 @@ enum class EmbeddedNNUEType {
|
||||
|
||||
using NetworkOutput = std::tuple<Value, Value>;
|
||||
|
||||
// The network must be a trivial type, i.e. the memory must be in-line.
|
||||
// This is required to allow sharing the network via shared memory, as
|
||||
// there is no way to run destructors.
|
||||
template<typename Arch, typename Transformer>
|
||||
class Network {
|
||||
static constexpr IndexType FTDimensions = Arch::TransformedFeatureDimensions;
|
||||
@@ -58,15 +63,17 @@ class Network {
|
||||
evalFile(file),
|
||||
embeddedType(type) {}
|
||||
|
||||
Network(const Network& other);
|
||||
Network(Network&& other) = default;
|
||||
Network(const Network& other) = default;
|
||||
Network(Network&& other) = default;
|
||||
|
||||
Network& operator=(const Network& other);
|
||||
Network& operator=(Network&& other) = default;
|
||||
Network& operator=(const Network& other) = default;
|
||||
Network& operator=(Network&& other) = default;
|
||||
|
||||
void load(const std::string& rootDirectory, std::string evalfilePath);
|
||||
bool save(const std::optional<std::string>& filename) const;
|
||||
|
||||
std::size_t get_content_hash() const;
|
||||
|
||||
NetworkOutput evaluate(const Position& pos,
|
||||
AccumulatorStack& accumulatorStack,
|
||||
AccumulatorCaches::Cache<FTDimensions>* cache) const;
|
||||
@@ -89,18 +96,20 @@ class Network {
|
||||
bool read_header(std::istream&, std::uint32_t*, std::string*) const;
|
||||
bool write_header(std::ostream&, std::uint32_t, const std::string&) const;
|
||||
|
||||
bool read_parameters(std::istream&, std::string&) const;
|
||||
bool read_parameters(std::istream&, std::string&);
|
||||
bool write_parameters(std::ostream&, const std::string&) const;
|
||||
|
||||
// Input feature converter
|
||||
LargePagePtr<Transformer> featureTransformer;
|
||||
Transformer featureTransformer;
|
||||
|
||||
// Evaluation function
|
||||
AlignedPtr<Arch[]> network;
|
||||
Arch network[LayerStacks];
|
||||
|
||||
EvalFile evalFile;
|
||||
EmbeddedNNUEType embeddedType;
|
||||
|
||||
bool initialized = false;
|
||||
|
||||
// Hash value of evaluation function structure
|
||||
static constexpr std::uint32_t hash = Transformer::get_hash_value() ^ Arch::get_hash_value();
|
||||
|
||||
@@ -121,9 +130,9 @@ using NetworkSmall = Network<SmallNetworkArchitecture, SmallFeatureTransformer>;
|
||||
|
||||
|
||||
struct Networks {
|
||||
Networks(NetworkBig&& nB, NetworkSmall&& nS) :
|
||||
big(std::move(nB)),
|
||||
small(std::move(nS)) {}
|
||||
Networks(std::unique_ptr<NetworkBig>&& nB, std::unique_ptr<NetworkSmall>&& nS) :
|
||||
big(std::move(*nB)),
|
||||
small(std::move(*nS)) {}
|
||||
|
||||
NetworkBig big;
|
||||
NetworkSmall small;
|
||||
@@ -132,4 +141,22 @@ struct Networks {
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
template<typename ArchT, typename FeatureTransformerT>
|
||||
struct std::hash<Stockfish::Eval::NNUE::Network<ArchT, FeatureTransformerT>> {
|
||||
std::size_t operator()(
|
||||
const Stockfish::Eval::NNUE::Network<ArchT, FeatureTransformerT>& network) const noexcept {
|
||||
return network.get_content_hash();
|
||||
}
|
||||
};
|
||||
|
||||
template<>
|
||||
struct std::hash<Stockfish::Eval::NNUE::Networks> {
|
||||
std::size_t operator()(const Stockfish::Eval::NNUE::Networks& networks) const noexcept {
|
||||
std::size_t h = 0;
|
||||
Stockfish::hash_combine(h, networks.big);
|
||||
Stockfish::hash_combine(h, networks.small);
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -87,7 +87,7 @@ struct AccumulatorCaches {
|
||||
void clear(const Network& network) {
|
||||
for (auto& entries1D : entries)
|
||||
for (auto& entry : entries1D)
|
||||
entry.clear(network.featureTransformer->biases);
|
||||
entry.clear(network.featureTransformer.biases);
|
||||
}
|
||||
|
||||
std::array<Entry, COLOR_NB>& operator[](Square sq) { return entries[sq]; }
|
||||
|
||||
@@ -97,7 +97,7 @@ struct NetworkArchitecture {
|
||||
&& fc_2.write_parameters(stream);
|
||||
}
|
||||
|
||||
std::int32_t propagate(const TransformedFeatureType* transformedFeatures) {
|
||||
std::int32_t propagate(const TransformedFeatureType* transformedFeatures) const {
|
||||
struct alignas(CacheLineSize) Buffer {
|
||||
alignas(CacheLineSize) typename decltype(fc_0)::OutputBuffer fc_0_out;
|
||||
alignas(CacheLineSize) typename decltype(ac_sqr_0)::OutputType
|
||||
@@ -136,8 +136,28 @@ struct NetworkArchitecture {
|
||||
|
||||
return outputValue;
|
||||
}
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, fc_0.get_content_hash());
|
||||
hash_combine(h, ac_sqr_0.get_content_hash());
|
||||
hash_combine(h, ac_0.get_content_hash());
|
||||
hash_combine(h, fc_1.get_content_hash());
|
||||
hash_combine(h, ac_1.get_content_hash());
|
||||
hash_combine(h, fc_2.get_content_hash());
|
||||
hash_combine(h, get_hash_value());
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Stockfish::Eval::NNUE
|
||||
|
||||
template<Stockfish::Eval::NNUE::IndexType L1, int L2, int L3>
|
||||
struct std::hash<Stockfish::Eval::NNUE::NetworkArchitecture<L1, L2, L3>> {
|
||||
std::size_t
|
||||
operator()(const Stockfish::Eval::NNUE::NetworkArchitecture<L1, L2, L3>& arch) const noexcept {
|
||||
return arch.get_content_hash();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // #ifndef NNUE_ARCHITECTURE_H_INCLUDED
|
||||
|
||||
@@ -258,8 +258,8 @@ inline void write_leb_128(std::ostream& stream, const IntType* values, std::size
|
||||
}
|
||||
};
|
||||
|
||||
auto write = [&](std::uint8_t byte) {
|
||||
buf[buf_pos++] = byte;
|
||||
auto write = [&](std::uint8_t b) {
|
||||
buf[buf_pos++] = b;
|
||||
if (buf_pos == BUF_SIZE)
|
||||
flush();
|
||||
};
|
||||
|
||||
@@ -157,20 +157,28 @@ class FeatureTransformer {
|
||||
}
|
||||
|
||||
// Write network parameters
|
||||
bool write_parameters(std::ostream& stream) {
|
||||
bool write_parameters(std::ostream& stream) const {
|
||||
std::unique_ptr<FeatureTransformer> copy = std::make_unique<FeatureTransformer>(*this);
|
||||
|
||||
unpermute_weights();
|
||||
scale_weights(false);
|
||||
copy->unpermute_weights();
|
||||
copy->scale_weights(false);
|
||||
|
||||
write_leb_128<BiasType>(stream, biases, HalfDimensions);
|
||||
write_leb_128<WeightType>(stream, weights, HalfDimensions * InputDimensions);
|
||||
write_leb_128<PSQTWeightType>(stream, psqtWeights, PSQTBuckets * InputDimensions);
|
||||
write_leb_128<BiasType>(stream, copy->biases, HalfDimensions);
|
||||
write_leb_128<WeightType>(stream, copy->weights, HalfDimensions * InputDimensions);
|
||||
write_leb_128<PSQTWeightType>(stream, copy->psqtWeights, PSQTBuckets * InputDimensions);
|
||||
|
||||
permute_weights();
|
||||
scale_weights(true);
|
||||
return !stream.fail();
|
||||
}
|
||||
|
||||
std::size_t get_content_hash() const {
|
||||
std::size_t h = 0;
|
||||
hash_combine(h, get_raw_data_hash(biases));
|
||||
hash_combine(h, get_raw_data_hash(weights));
|
||||
hash_combine(h, get_raw_data_hash(psqtWeights));
|
||||
hash_combine(h, get_hash_value());
|
||||
return h;
|
||||
}
|
||||
|
||||
// Convert input features
|
||||
std::int32_t transform(const Position& pos,
|
||||
AccumulatorStack& accumulatorStack,
|
||||
@@ -309,4 +317,14 @@ class FeatureTransformer {
|
||||
|
||||
} // namespace Stockfish::Eval::NNUE
|
||||
|
||||
|
||||
template<Stockfish::Eval::NNUE::IndexType TransformedFeatureDimensions>
|
||||
struct std::hash<Stockfish::Eval::NNUE::FeatureTransformer<TransformedFeatureDimensions>> {
|
||||
std::size_t
|
||||
operator()(const Stockfish::Eval::NNUE::FeatureTransformer<TransformedFeatureDimensions>& ft)
|
||||
const noexcept {
|
||||
return ft.get_content_hash();
|
||||
}
|
||||
};
|
||||
|
||||
#endif // #ifndef NNUE_FEATURE_TRANSFORMER_H_INCLUDED
|
||||
|
||||
@@ -120,11 +120,11 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat
|
||||
format_cp_compact(value, &board[y + 2][x + 2], pos);
|
||||
};
|
||||
|
||||
AccumulatorStack accumulators;
|
||||
auto accumulators = std::make_unique<AccumulatorStack>();
|
||||
|
||||
// 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.
|
||||
auto [psqt, positional] = networks.big.evaluate(pos, accumulators, &caches.big);
|
||||
auto [psqt, positional] = networks.big.evaluate(pos, *accumulators, &caches.big);
|
||||
Value base = psqt + positional;
|
||||
base = pos.side_to_move() == WHITE ? base : -base;
|
||||
|
||||
@@ -139,8 +139,8 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat
|
||||
{
|
||||
pos.remove_piece(sq);
|
||||
|
||||
accumulators.reset();
|
||||
std::tie(psqt, positional) = networks.big.evaluate(pos, accumulators, &caches.big);
|
||||
accumulators->reset();
|
||||
std::tie(psqt, positional) = networks.big.evaluate(pos, *accumulators, &caches.big);
|
||||
Value eval = psqt + positional;
|
||||
eval = pos.side_to_move() == WHITE ? eval : -eval;
|
||||
v = base - eval;
|
||||
@@ -156,8 +156,8 @@ trace(Position& pos, const Eval::NNUE::Networks& networks, Eval::NNUE::Accumulat
|
||||
ss << board[row] << '\n';
|
||||
ss << '\n';
|
||||
|
||||
accumulators.reset();
|
||||
auto t = networks.big.trace_evaluate(pos, accumulators, &caches.big);
|
||||
accumulators->reset();
|
||||
auto t = networks.big.trace_evaluate(pos, *accumulators, &caches.big);
|
||||
|
||||
ss << " NNUE network contributions "
|
||||
<< (pos.side_to_move() == WHITE ? "(White to move)" : "(Black to move)") << std::endl
|
||||
|
||||
+17
-4
@@ -20,8 +20,10 @@
|
||||
#define NNUE_MISC_H_INCLUDED
|
||||
|
||||
#include <cstddef>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "../misc.h"
|
||||
#include "../types.h"
|
||||
#include "nnue_architecture.h"
|
||||
|
||||
@@ -31,17 +33,17 @@ class Position;
|
||||
|
||||
namespace Eval::NNUE {
|
||||
|
||||
// EvalFile uses fixed string types because it's part of the network structure which must be trivial.
|
||||
struct EvalFile {
|
||||
// Default net name, will use one of the EvalFileDefaultName* macros defined
|
||||
// in evaluate.h
|
||||
std::string defaultName;
|
||||
FixedString<256> defaultName;
|
||||
// Selected net name, either via uci option or default
|
||||
std::string current;
|
||||
FixedString<256> current;
|
||||
// Net description extracted from the net file
|
||||
std::string netDescription;
|
||||
FixedString<256> netDescription;
|
||||
};
|
||||
|
||||
|
||||
struct NnueEvalTrace {
|
||||
static_assert(LayerStacks == PSQTBuckets);
|
||||
|
||||
@@ -58,4 +60,15 @@ std::string trace(Position& pos, const Networks& networks, AccumulatorCaches& ca
|
||||
} // namespace Stockfish::Eval::NNUE
|
||||
} // namespace Stockfish
|
||||
|
||||
template<>
|
||||
struct std::hash<Stockfish::Eval::NNUE::EvalFile> {
|
||||
std::size_t operator()(const Stockfish::Eval::NNUE::EvalFile& evalFile) const noexcept {
|
||||
std::size_t h = 0;
|
||||
Stockfish::hash_combine(h, evalFile.defaultName);
|
||||
Stockfish::hash_combine(h, evalFile.current);
|
||||
Stockfish::hash_combine(h, evalFile.netDescription);
|
||||
return h;
|
||||
}
|
||||
};
|
||||
|
||||
#endif // #ifndef NNUE_MISC_H_INCLUDED
|
||||
|
||||
+139
-3
@@ -37,7 +37,7 @@
|
||||
#include <vector>
|
||||
#include <cstring>
|
||||
|
||||
#include "memory.h"
|
||||
#include "shm.h"
|
||||
|
||||
// We support linux very well, but we explicitly do NOT support Android,
|
||||
// because there is no affected systems, not worth maintaining.
|
||||
@@ -945,10 +945,11 @@ class NumaConfig {
|
||||
th.join();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::set<CpuIndex>> nodes;
|
||||
std::map<CpuIndex, NumaIndex> nodeByCpu;
|
||||
CpuIndex highestCpuIndex;
|
||||
|
||||
private:
|
||||
CpuIndex highestCpuIndex;
|
||||
|
||||
bool customAffinity;
|
||||
|
||||
@@ -1263,6 +1264,141 @@ class LazyNumaReplicated: public NumaReplicatedBase {
|
||||
}
|
||||
};
|
||||
|
||||
// Utilizes shared memory.
|
||||
template<typename T>
|
||||
class LazyNumaReplicatedSystemWide: public NumaReplicatedBase {
|
||||
public:
|
||||
using ReplicatorFuncType = std::function<T(const T&)>;
|
||||
|
||||
LazyNumaReplicatedSystemWide(NumaReplicationContext& ctx) :
|
||||
NumaReplicatedBase(ctx) {
|
||||
prepare_replicate_from(std::make_unique<T>());
|
||||
}
|
||||
|
||||
LazyNumaReplicatedSystemWide(NumaReplicationContext& ctx, std::unique_ptr<T>&& source) :
|
||||
NumaReplicatedBase(ctx) {
|
||||
prepare_replicate_from(std::move(source));
|
||||
}
|
||||
|
||||
LazyNumaReplicatedSystemWide(const LazyNumaReplicatedSystemWide&) = delete;
|
||||
LazyNumaReplicatedSystemWide(LazyNumaReplicatedSystemWide&& other) noexcept :
|
||||
NumaReplicatedBase(std::move(other)),
|
||||
instances(std::exchange(other.instances, {})) {}
|
||||
|
||||
LazyNumaReplicatedSystemWide& operator=(const LazyNumaReplicatedSystemWide&) = delete;
|
||||
LazyNumaReplicatedSystemWide& operator=(LazyNumaReplicatedSystemWide&& other) noexcept {
|
||||
NumaReplicatedBase::operator=(*this, std::move(other));
|
||||
instances = std::exchange(other.instances, {});
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
LazyNumaReplicatedSystemWide& operator=(std::unique_ptr<T>&& source) {
|
||||
prepare_replicate_from(std::move(source));
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
~LazyNumaReplicatedSystemWide() override = default;
|
||||
|
||||
const T& operator[](NumaReplicatedAccessToken token) const {
|
||||
assert(token.get_numa_index() < instances.size());
|
||||
ensure_present(token.get_numa_index());
|
||||
return *(instances[token.get_numa_index()]);
|
||||
}
|
||||
|
||||
const T& operator*() const { return *(instances[0]); }
|
||||
|
||||
const T* operator->() const { return &*instances[0]; }
|
||||
|
||||
std::vector<std::pair<SystemWideSharedConstantAllocationStatus, std::optional<std::string>>>
|
||||
get_status_and_errors() const {
|
||||
std::vector<std::pair<SystemWideSharedConstantAllocationStatus, std::optional<std::string>>>
|
||||
status;
|
||||
status.reserve(instances.size());
|
||||
|
||||
for (const auto& instance : instances)
|
||||
{
|
||||
status.emplace_back(instance.get_status(), instance.get_error_message());
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
template<typename FuncT>
|
||||
void modify_and_replicate(FuncT&& f) {
|
||||
auto source = std::make_unique<T>(*instances[0]);
|
||||
std::forward<FuncT>(f)(*source);
|
||||
prepare_replicate_from(std::move(source));
|
||||
}
|
||||
|
||||
void on_numa_config_changed() override {
|
||||
// Use the first one as the source. It doesn't matter which one we use,
|
||||
// because they all must be identical, but the first one is guaranteed to exist.
|
||||
auto source = std::make_unique<T>(*instances[0]);
|
||||
prepare_replicate_from(std::move(source));
|
||||
}
|
||||
|
||||
private:
|
||||
mutable std::vector<SystemWideSharedConstant<T>> instances;
|
||||
mutable std::mutex mutex;
|
||||
|
||||
std::size_t get_discriminator(NumaIndex idx) const {
|
||||
const NumaConfig& cfg = get_numa_config();
|
||||
const NumaConfig& cfg_sys = NumaConfig::from_system(false);
|
||||
// as a descriminator, locate the hardware/system numadomain this cpuindex belongs to
|
||||
CpuIndex cpu = *cfg.nodes[idx].begin(); // get a CpuIndex from NumaIndex
|
||||
NumaIndex sys_idx = cfg_sys.is_cpu_assigned(cpu) ? cfg_sys.nodeByCpu.at(cpu) : 0;
|
||||
std::string s = cfg_sys.to_string() + "$" + std::to_string(sys_idx);
|
||||
return std::hash<std::string>{}(s);
|
||||
}
|
||||
|
||||
void ensure_present(NumaIndex idx) const {
|
||||
assert(idx < instances.size());
|
||||
|
||||
if (instances[idx] != nullptr)
|
||||
return;
|
||||
|
||||
assert(idx != 0);
|
||||
|
||||
std::unique_lock<std::mutex> lock(mutex);
|
||||
// Check again for races.
|
||||
if (instances[idx] != nullptr)
|
||||
return;
|
||||
|
||||
const NumaConfig& cfg = get_numa_config();
|
||||
cfg.execute_on_numa_node(idx, [this, idx]() {
|
||||
instances[idx] = SystemWideSharedConstant<T>(*instances[0], get_discriminator(idx));
|
||||
});
|
||||
}
|
||||
|
||||
void prepare_replicate_from(std::unique_ptr<T>&& source) {
|
||||
instances.clear();
|
||||
|
||||
const NumaConfig& cfg = get_numa_config();
|
||||
// We just need to make sure the first instance is there.
|
||||
// Note that we cannot move here as we need to reallocate the data
|
||||
// on the correct NUMA node.
|
||||
// Even in the case of a single NUMA node we have to copy since it's shared memory.
|
||||
if (cfg.requires_memory_replication())
|
||||
{
|
||||
assert(cfg.num_numa_nodes() > 0);
|
||||
|
||||
cfg.execute_on_numa_node(0, [this, &source]() {
|
||||
instances.emplace_back(SystemWideSharedConstant<T>(*source, get_discriminator(0)));
|
||||
});
|
||||
|
||||
// Prepare others for lazy init.
|
||||
instances.resize(cfg.num_numa_nodes());
|
||||
}
|
||||
else
|
||||
{
|
||||
assert(cfg.num_numa_nodes() == 1);
|
||||
instances.emplace_back(SystemWideSharedConstant<T>(*source, get_discriminator(0)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class NumaReplicationContext {
|
||||
public:
|
||||
NumaReplicationContext(NumaConfig&& cfg) :
|
||||
|
||||
+12
-12
@@ -137,19 +137,19 @@ struct LimitsType {
|
||||
// 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& optionsMap,
|
||||
ThreadPool& threadPool,
|
||||
TranspositionTable& transpositionTable,
|
||||
const LazyNumaReplicated<Eval::NNUE::Networks>& nets) :
|
||||
SharedState(const OptionsMap& optionsMap,
|
||||
ThreadPool& threadPool,
|
||||
TranspositionTable& transpositionTable,
|
||||
const LazyNumaReplicatedSystemWide<Eval::NNUE::Networks>& nets) :
|
||||
options(optionsMap),
|
||||
threads(threadPool),
|
||||
tt(transpositionTable),
|
||||
networks(nets) {}
|
||||
|
||||
const OptionsMap& options;
|
||||
ThreadPool& threads;
|
||||
TranspositionTable& tt;
|
||||
const LazyNumaReplicated<Eval::NNUE::Networks>& networks;
|
||||
const OptionsMap& options;
|
||||
ThreadPool& threads;
|
||||
TranspositionTable& tt;
|
||||
const LazyNumaReplicatedSystemWide<Eval::NNUE::Networks>& networks;
|
||||
};
|
||||
|
||||
class Worker;
|
||||
@@ -383,10 +383,10 @@ class Worker {
|
||||
|
||||
Tablebases::Config tbConfig;
|
||||
|
||||
const OptionsMap& options;
|
||||
ThreadPool& threads;
|
||||
TranspositionTable& tt;
|
||||
const LazyNumaReplicated<Eval::NNUE::Networks>& networks;
|
||||
const OptionsMap& options;
|
||||
ThreadPool& threads;
|
||||
TranspositionTable& tt;
|
||||
const LazyNumaReplicatedSystemWide<Eval::NNUE::Networks>& networks;
|
||||
|
||||
// Used by NNUE
|
||||
Eval::NNUE::AccumulatorStack accumulatorStack;
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
/*
|
||||
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
|
||||
Copyright (C) 2004-2025 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 SHM_H_INCLUDED
|
||||
#define SHM_H_INCLUDED
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <functional>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
#include <variant>
|
||||
|
||||
#if !defined(_WIN32) && !defined(__ANDROID__)
|
||||
#include "shm_linux.h"
|
||||
#endif
|
||||
|
||||
#if defined(__ANDROID__)
|
||||
#include <limits.h>
|
||||
#define SF_MAX_SEM_NAME_LEN NAME_MAX
|
||||
#endif
|
||||
|
||||
#include "types.h"
|
||||
|
||||
#include "memory.h"
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
#if _WIN32_WINNT < 0x0601
|
||||
#undef _WIN32_WINNT
|
||||
#define _WIN32_WINNT 0x0601 // Force to include needed API prototypes
|
||||
#endif
|
||||
|
||||
#if !defined(NOMINMAX)
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <windows.h>
|
||||
#else
|
||||
#include <cstring>
|
||||
#include <fcntl.h>
|
||||
#include <pthread.h>
|
||||
#include <semaphore.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
|
||||
#if defined(__APPLE__)
|
||||
#include <mach-o/dyld.h>
|
||||
#include <sys/syslimits.h>
|
||||
|
||||
#elif defined(__sun)
|
||||
#include <stdlib.h>
|
||||
|
||||
#elif defined(__FreeBSD__)
|
||||
#include <sys/sysctl.h>
|
||||
#include <sys/types.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#elif defined(__NetBSD__) || defined(__DragonFly__) || defined(__linux__)
|
||||
#include <limits.h>
|
||||
#include <unistd.h>
|
||||
#endif
|
||||
|
||||
|
||||
namespace Stockfish {
|
||||
|
||||
// argv[0] CANNOT be used because we need to identify the executable.
|
||||
// argv[0] contains the command used to invoke it, which does not involve the full path.
|
||||
// Just using a path is not fully resilient either, as the executable could
|
||||
// have changed if it wasn't locked by the OS. Ideally we would hash the executable
|
||||
// but it's not really that important at this point.
|
||||
// If the path is longer than 4095 bytes the hash will be computed from an unspecified
|
||||
// amount of bytes of the path; in particular it can a hash of an empty string.
|
||||
|
||||
inline std::string getExecutablePathHash() {
|
||||
char executable_path[4096] = {0};
|
||||
std::size_t path_length = 0;
|
||||
|
||||
#if defined(_WIN32)
|
||||
path_length = GetModuleFileNameA(NULL, executable_path, sizeof(executable_path));
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
uint32_t size = sizeof(executable_path);
|
||||
if (_NSGetExecutablePath(executable_path, &size) == 0)
|
||||
{
|
||||
path_length = std::strlen(executable_path);
|
||||
}
|
||||
|
||||
#elif defined(__sun) // Solaris
|
||||
const char* path = getexecname();
|
||||
if (path)
|
||||
{
|
||||
std::strncpy(executable_path, path, sizeof(executable_path) - 1);
|
||||
path_length = std::strlen(executable_path);
|
||||
}
|
||||
|
||||
#elif defined(__FreeBSD__)
|
||||
size_t size = sizeof(executable_path);
|
||||
int mib[4] = {CTL_KERN, KERN_PROC, KERN_PROC_PATHNAME, -1};
|
||||
if (sysctl(mib, 4, executable_path, &size, NULL, 0) == 0)
|
||||
{
|
||||
path_length = std::strlen(executable_path);
|
||||
}
|
||||
|
||||
#elif defined(__NetBSD__) || defined(__DragonFly__)
|
||||
ssize_t len = readlink("/proc/curproc/exe", executable_path, sizeof(executable_path) - 1);
|
||||
if (len >= 0)
|
||||
{
|
||||
executable_path[len] = '\0';
|
||||
path_length = len;
|
||||
}
|
||||
|
||||
#elif defined(__linux__)
|
||||
ssize_t len = readlink("/proc/self/exe", executable_path, sizeof(executable_path) - 1);
|
||||
if (len >= 0)
|
||||
{
|
||||
executable_path[len] = '\0';
|
||||
path_length = len;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
// In case of any error the path will be empty.
|
||||
return std::string(executable_path, path_length);
|
||||
}
|
||||
|
||||
enum class SystemWideSharedConstantAllocationStatus {
|
||||
NoAllocation,
|
||||
LocalMemory,
|
||||
SharedMemory
|
||||
};
|
||||
|
||||
#if defined(_WIN32)
|
||||
|
||||
inline std::string GetLastErrorAsString(DWORD error) {
|
||||
//Get the error message ID, if any.
|
||||
DWORD errorMessageID = error;
|
||||
if (errorMessageID == 0)
|
||||
{
|
||||
return std::string(); //No error message has been recorded
|
||||
}
|
||||
|
||||
LPSTR messageBuffer = nullptr;
|
||||
|
||||
//Ask Win32 to give us the string version of that message ID.
|
||||
//The parameters we pass in, tell Win32 to create the buffer that holds the message for us (because we don't yet know how long the message string will be).
|
||||
size_t size = FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM
|
||||
| FORMAT_MESSAGE_IGNORE_INSERTS,
|
||||
NULL, errorMessageID, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
|
||||
(LPSTR) &messageBuffer, 0, NULL);
|
||||
|
||||
//Copy the error message into a std::string.
|
||||
std::string message(messageBuffer, size);
|
||||
|
||||
//Free the Win32's string's buffer.
|
||||
LocalFree(messageBuffer);
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
// Utilizes shared memory to store the value. It is deduplicated system-wide (for the single user).
|
||||
template<typename T>
|
||||
class SharedMemoryBackend {
|
||||
public:
|
||||
enum class Status {
|
||||
Success,
|
||||
LargePageAllocationError,
|
||||
FileMappingError,
|
||||
MapViewError,
|
||||
MutexCreateError,
|
||||
MutexWaitError,
|
||||
MutexReleaseError,
|
||||
NotInitialized
|
||||
};
|
||||
|
||||
static constexpr DWORD IS_INITIALIZED_VALUE = 1;
|
||||
|
||||
SharedMemoryBackend() :
|
||||
status(Status::NotInitialized) {};
|
||||
|
||||
SharedMemoryBackend(const std::string& shm_name, const T& value) :
|
||||
status(Status::NotInitialized) {
|
||||
|
||||
initialize(shm_name, value);
|
||||
}
|
||||
|
||||
bool is_valid() const { return status == Status::Success; }
|
||||
|
||||
std::optional<std::string> get_error_message() const {
|
||||
switch (status)
|
||||
{
|
||||
case Status::Success :
|
||||
return std::nullopt;
|
||||
case Status::LargePageAllocationError :
|
||||
return "Failed to allocate large page memory";
|
||||
case Status::FileMappingError :
|
||||
return "Failed to create file mapping: " + last_error_message;
|
||||
case Status::MapViewError :
|
||||
return "Failed to map view: " + last_error_message;
|
||||
case Status::MutexCreateError :
|
||||
return "Failed to create mutex: " + last_error_message;
|
||||
case Status::MutexWaitError :
|
||||
return "Failed to wait on mutex: " + last_error_message;
|
||||
case Status::MutexReleaseError :
|
||||
return "Failed to release mutex: " + last_error_message;
|
||||
case Status::NotInitialized :
|
||||
return "Not initialized";
|
||||
default :
|
||||
return "Unknown error";
|
||||
}
|
||||
}
|
||||
|
||||
void* get() const { return is_valid() ? pMap : nullptr; }
|
||||
|
||||
~SharedMemoryBackend() { cleanup(); }
|
||||
|
||||
SharedMemoryBackend(const SharedMemoryBackend&) = delete;
|
||||
SharedMemoryBackend& operator=(const SharedMemoryBackend&) = delete;
|
||||
|
||||
SharedMemoryBackend(SharedMemoryBackend&& other) noexcept :
|
||||
pMap(other.pMap),
|
||||
hMapFile(other.hMapFile),
|
||||
status(other.status),
|
||||
last_error_message(std::move(other.last_error_message)) {
|
||||
|
||||
other.pMap = nullptr;
|
||||
other.hMapFile = 0;
|
||||
other.status = Status::NotInitialized;
|
||||
}
|
||||
|
||||
SharedMemoryBackend& operator=(SharedMemoryBackend&& other) noexcept {
|
||||
if (this != &other)
|
||||
{
|
||||
cleanup();
|
||||
pMap = other.pMap;
|
||||
hMapFile = other.hMapFile;
|
||||
status = other.status;
|
||||
last_error_message = std::move(other.last_error_message);
|
||||
|
||||
other.pMap = nullptr;
|
||||
other.hMapFile = 0;
|
||||
other.status = Status::NotInitialized;
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
SystemWideSharedConstantAllocationStatus get_status() const {
|
||||
return status == Status::Success ? SystemWideSharedConstantAllocationStatus::SharedMemory
|
||||
: SystemWideSharedConstantAllocationStatus::NoAllocation;
|
||||
}
|
||||
|
||||
private:
|
||||
void initialize(const std::string& shm_name, const T& value) {
|
||||
const size_t total_size = sizeof(T) + sizeof(IS_INITIALIZED_VALUE);
|
||||
|
||||
// Try allocating with large pages first.
|
||||
hMapFile = windows_try_with_large_page_priviliges(
|
||||
[&](size_t largePageSize) {
|
||||
const size_t total_size_aligned =
|
||||
(total_size + largePageSize - 1) / largePageSize * largePageSize;
|
||||
|
||||
#if defined(_WIN64)
|
||||
DWORD total_size_low = total_size_aligned & 0xFFFFFFFFu;
|
||||
DWORD total_size_high = total_size_aligned >> 32u;
|
||||
#else
|
||||
DWORD total_size_low = total_size_aligned;
|
||||
DWORD total_size_high = 0;
|
||||
#endif
|
||||
|
||||
return CreateFileMappingA(INVALID_HANDLE_VALUE, NULL,
|
||||
PAGE_READWRITE | SEC_COMMIT | SEC_LARGE_PAGES,
|
||||
total_size_high, total_size_low, shm_name.c_str());
|
||||
},
|
||||
[]() { return (void*) nullptr; });
|
||||
|
||||
// Fallback to normal allocation if no large pages available.
|
||||
if (!hMapFile)
|
||||
{
|
||||
hMapFile = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0,
|
||||
static_cast<DWORD>(total_size), shm_name.c_str());
|
||||
}
|
||||
|
||||
if (!hMapFile)
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
last_error_message = GetLastErrorAsString(err);
|
||||
status = Status::FileMappingError;
|
||||
return;
|
||||
}
|
||||
|
||||
pMap = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, total_size);
|
||||
if (!pMap)
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
last_error_message = GetLastErrorAsString(err);
|
||||
status = Status::MapViewError;
|
||||
cleanup_partial();
|
||||
return;
|
||||
}
|
||||
|
||||
// Use named mutex to ensure only one initializer
|
||||
std::string mutex_name = shm_name + "$mutex";
|
||||
HANDLE hMutex = CreateMutexA(NULL, FALSE, mutex_name.c_str());
|
||||
if (!hMutex)
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
last_error_message = GetLastErrorAsString(err);
|
||||
status = Status::MutexCreateError;
|
||||
cleanup_partial();
|
||||
return;
|
||||
}
|
||||
|
||||
DWORD wait_result = WaitForSingleObject(hMutex, INFINITE);
|
||||
if (wait_result != WAIT_OBJECT_0)
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
last_error_message = GetLastErrorAsString(err);
|
||||
status = Status::MutexWaitError;
|
||||
CloseHandle(hMutex);
|
||||
cleanup_partial();
|
||||
return;
|
||||
}
|
||||
|
||||
// Crucially, we place the object first to ensure alignment.
|
||||
volatile DWORD* is_initialized =
|
||||
std::launder(reinterpret_cast<DWORD*>(reinterpret_cast<char*>(pMap) + sizeof(T)));
|
||||
T* object = std::launder(reinterpret_cast<T*>(pMap));
|
||||
|
||||
if (*is_initialized != IS_INITIALIZED_VALUE)
|
||||
{
|
||||
// First time initialization, message for debug purposes
|
||||
new (object) T{value};
|
||||
*is_initialized = IS_INITIALIZED_VALUE;
|
||||
}
|
||||
|
||||
BOOL release_result = ReleaseMutex(hMutex);
|
||||
CloseHandle(hMutex);
|
||||
|
||||
if (!release_result)
|
||||
{
|
||||
const DWORD err = GetLastError();
|
||||
last_error_message = GetLastErrorAsString(err);
|
||||
status = Status::MutexReleaseError;
|
||||
cleanup_partial();
|
||||
return;
|
||||
}
|
||||
|
||||
status = Status::Success;
|
||||
}
|
||||
|
||||
void cleanup_partial() {
|
||||
if (pMap != nullptr)
|
||||
{
|
||||
UnmapViewOfFile(pMap);
|
||||
pMap = nullptr;
|
||||
}
|
||||
if (hMapFile)
|
||||
{
|
||||
CloseHandle(hMapFile);
|
||||
hMapFile = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void cleanup() {
|
||||
if (pMap != nullptr)
|
||||
{
|
||||
UnmapViewOfFile(pMap);
|
||||
pMap = nullptr;
|
||||
}
|
||||
if (hMapFile)
|
||||
{
|
||||
CloseHandle(hMapFile);
|
||||
hMapFile = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void* pMap = nullptr;
|
||||
HANDLE hMapFile = 0;
|
||||
Status status = Status::NotInitialized;
|
||||
std::string last_error_message;
|
||||
};
|
||||
|
||||
#elif !defined(__ANDROID__)
|
||||
|
||||
template<typename T>
|
||||
class SharedMemoryBackend {
|
||||
public:
|
||||
SharedMemoryBackend() = default;
|
||||
|
||||
SharedMemoryBackend(const std::string& shm_name, const T& value) :
|
||||
shm1(shm::create_shared<T>(shm_name, value)) {}
|
||||
|
||||
void* get() const {
|
||||
const T* ptr = &shm1->get();
|
||||
return reinterpret_cast<void*>(const_cast<T*>(ptr));
|
||||
}
|
||||
|
||||
bool is_valid() const { return shm1 && shm1->is_open() && shm1->is_initialized(); }
|
||||
|
||||
SystemWideSharedConstantAllocationStatus get_status() const {
|
||||
return is_valid() ? SystemWideSharedConstantAllocationStatus::SharedMemory
|
||||
: SystemWideSharedConstantAllocationStatus::NoAllocation;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_error_message() const {
|
||||
if (!shm1)
|
||||
return "Shared memory not initialized";
|
||||
|
||||
if (!shm1->is_open())
|
||||
return "Shared memory is not open";
|
||||
|
||||
if (!shm1->is_initialized())
|
||||
return "Not initialized";
|
||||
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<shm::SharedMemory<T>> shm1;
|
||||
};
|
||||
|
||||
#else
|
||||
|
||||
// For systems that don't have shared memory, or support is troublesome.
|
||||
// The way fallback is done is that we need a dummy backend.
|
||||
|
||||
template<typename T>
|
||||
class SharedMemoryBackend {
|
||||
public:
|
||||
SharedMemoryBackend() = default;
|
||||
|
||||
SharedMemoryBackend(const std::string& shm_name, const T& value) {}
|
||||
|
||||
void* get() const { return nullptr; }
|
||||
|
||||
bool is_valid() const { return false; }
|
||||
|
||||
SystemWideSharedConstantAllocationStatus get_status() const {
|
||||
return SystemWideSharedConstantAllocationStatus::NoAllocation;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_error_message() const { return "Dummy SharedMemoryBackend"; }
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
template<typename T>
|
||||
struct SharedMemoryBackendFallback {
|
||||
SharedMemoryBackendFallback() = default;
|
||||
|
||||
SharedMemoryBackendFallback(const std::string&, const T& value) :
|
||||
fallback_object(make_unique_large_page<T>(value)) {}
|
||||
|
||||
void* get() const { return fallback_object.get(); }
|
||||
|
||||
SharedMemoryBackendFallback(const SharedMemoryBackendFallback&) = delete;
|
||||
SharedMemoryBackendFallback& operator=(const SharedMemoryBackendFallback&) = delete;
|
||||
|
||||
SharedMemoryBackendFallback(SharedMemoryBackendFallback&& other) noexcept :
|
||||
fallback_object(std::move(other.fallback_object)) {}
|
||||
|
||||
SharedMemoryBackendFallback& operator=(SharedMemoryBackendFallback&& other) noexcept {
|
||||
fallback_object = std::move(other.fallback_object);
|
||||
return *this;
|
||||
}
|
||||
|
||||
SystemWideSharedConstantAllocationStatus get_status() const {
|
||||
return fallback_object == nullptr ? SystemWideSharedConstantAllocationStatus::NoAllocation
|
||||
: SystemWideSharedConstantAllocationStatus::LocalMemory;
|
||||
}
|
||||
|
||||
std::optional<std::string> get_error_message() const {
|
||||
if (fallback_object == nullptr)
|
||||
return "Not initialized";
|
||||
|
||||
return "Shared memory not supported by the OS. Local allocation fallback.";
|
||||
}
|
||||
|
||||
private:
|
||||
LargePagePtr<T> fallback_object;
|
||||
};
|
||||
|
||||
// Platform-independent wrapper
|
||||
template<typename T>
|
||||
struct SystemWideSharedConstant {
|
||||
private:
|
||||
static std::string createHashString(const std::string& input) {
|
||||
size_t hash = std::hash<std::string>{}(input);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::hex << std::setfill('0') << hash;
|
||||
|
||||
return ss.str();
|
||||
}
|
||||
|
||||
public:
|
||||
// We can't run the destructor because it may be in a completely different process.
|
||||
// The object stored must also be obviously in-line but we can't check for that, other than some basic checks that cover most cases.
|
||||
static_assert(std::is_trivially_destructible_v<T>);
|
||||
static_assert(std::is_trivially_move_constructible_v<T>);
|
||||
static_assert(std::is_trivially_copy_constructible_v<T>);
|
||||
|
||||
SystemWideSharedConstant() = default;
|
||||
|
||||
|
||||
// Content is addressed by its hash. An additional discriminator can be added to account for differences
|
||||
// that are not present in the content, for example NUMA node allocation.
|
||||
SystemWideSharedConstant(const T& value, std::size_t discriminator = 0) {
|
||||
std::size_t content_hash = std::hash<T>{}(value);
|
||||
std::size_t executable_hash = std::hash<std::string>{}(getExecutablePathHash());
|
||||
|
||||
std::string shm_name = std::string("Local\\sf_") + std::to_string(content_hash) + "$"
|
||||
+ std::to_string(executable_hash) + "$"
|
||||
+ std::to_string(discriminator);
|
||||
|
||||
#if !defined(_WIN32)
|
||||
// POSIX shared memory names must start with a slash
|
||||
shm_name = "/sf_" + createHashString(shm_name);
|
||||
|
||||
// hash name and make sure it is not longer than SF_MAX_SEM_NAME_LEN
|
||||
if (shm_name.size() > SF_MAX_SEM_NAME_LEN)
|
||||
{
|
||||
shm_name = shm_name.substr(0, SF_MAX_SEM_NAME_LEN - 1);
|
||||
}
|
||||
#endif
|
||||
|
||||
SharedMemoryBackend<T> shm_backend(shm_name, value);
|
||||
|
||||
if (shm_backend.is_valid())
|
||||
{
|
||||
backend = std::move(shm_backend);
|
||||
}
|
||||
else
|
||||
{
|
||||
backend = SharedMemoryBackendFallback<T>(shm_name, value);
|
||||
}
|
||||
}
|
||||
|
||||
SystemWideSharedConstant(const SystemWideSharedConstant&) = delete;
|
||||
SystemWideSharedConstant& operator=(const SystemWideSharedConstant&) = delete;
|
||||
|
||||
SystemWideSharedConstant(SystemWideSharedConstant&& other) noexcept :
|
||||
backend(std::move(other.backend)) {}
|
||||
|
||||
SystemWideSharedConstant& operator=(SystemWideSharedConstant&& other) noexcept {
|
||||
backend = std::move(other.backend);
|
||||
return *this;
|
||||
}
|
||||
|
||||
const T& operator*() const { return *std::launder(reinterpret_cast<const T*>(get_ptr())); }
|
||||
|
||||
bool operator==(std::nullptr_t) const noexcept { return get_ptr() == nullptr; }
|
||||
|
||||
bool operator!=(std::nullptr_t) const noexcept { return get_ptr() != nullptr; }
|
||||
|
||||
SystemWideSharedConstantAllocationStatus get_status() const {
|
||||
return std::visit(
|
||||
[](const auto& end) -> SystemWideSharedConstantAllocationStatus {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(end)>, std::monostate>)
|
||||
{
|
||||
return SystemWideSharedConstantAllocationStatus::NoAllocation;
|
||||
}
|
||||
else
|
||||
{
|
||||
return end.get_status();
|
||||
}
|
||||
},
|
||||
backend);
|
||||
}
|
||||
|
||||
std::optional<std::string> get_error_message() const {
|
||||
return std::visit(
|
||||
[](const auto& end) -> std::optional<std::string> {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(end)>, std::monostate>)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
else
|
||||
{
|
||||
return end.get_error_message();
|
||||
}
|
||||
},
|
||||
backend);
|
||||
}
|
||||
|
||||
private:
|
||||
auto get_ptr() const {
|
||||
return std::visit(
|
||||
[](const auto& end) -> void* {
|
||||
if constexpr (std::is_same_v<std::decay_t<decltype(end)>, std::monostate>)
|
||||
{
|
||||
return nullptr;
|
||||
}
|
||||
else
|
||||
{
|
||||
return end.get();
|
||||
}
|
||||
},
|
||||
backend);
|
||||
}
|
||||
|
||||
std::variant<std::monostate, SharedMemoryBackend<T>, SharedMemoryBackendFallback<T>> backend;
|
||||
};
|
||||
|
||||
|
||||
} // namespace Stockfish
|
||||
|
||||
#endif // #ifndef SHM_H_INCLUDED
|
||||
+658
@@ -0,0 +1,658 @@
|
||||
/*
|
||||
Stockfish, a UCI chess playing engine derived from Glaurung 2.1
|
||||
Copyright (C) 2004-2025 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 SHM_LINUX_H_INCLUDED
|
||||
#define SHM_LINUX_H_INCLUDED
|
||||
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <cerrno>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <dirent.h>
|
||||
#include <mutex>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <pthread.h>
|
||||
#include <string>
|
||||
#include <inttypes.h>
|
||||
#include <type_traits>
|
||||
#include <unordered_set>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <signal.h>
|
||||
#include <sys/file.h>
|
||||
#include <sys/mman.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if defined(__NetBSD__) || defined(__DragonFly__) || defined(__linux__)
|
||||
#include <limits.h>
|
||||
#define SF_MAX_SEM_NAME_LEN NAME_MAX
|
||||
#elif defined(__APPLE__)
|
||||
#define SF_MAX_SEM_NAME_LEN 31
|
||||
#else
|
||||
#define SF_MAX_SEM_NAME_LEN 255
|
||||
#endif
|
||||
|
||||
|
||||
namespace Stockfish::shm {
|
||||
|
||||
namespace detail {
|
||||
|
||||
struct ShmHeader {
|
||||
static constexpr uint32_t SHM_MAGIC = 0xAD5F1A12;
|
||||
pthread_mutex_t mutex;
|
||||
std::atomic<uint32_t> ref_count{0};
|
||||
std::atomic<bool> initialized{false};
|
||||
uint32_t magic = SHM_MAGIC;
|
||||
};
|
||||
|
||||
class SharedMemoryBase {
|
||||
public:
|
||||
virtual ~SharedMemoryBase() = default;
|
||||
virtual void close() noexcept = 0;
|
||||
virtual const std::string& name() const noexcept = 0;
|
||||
};
|
||||
|
||||
class SharedMemoryRegistry {
|
||||
private:
|
||||
static std::mutex registry_mutex_;
|
||||
static std::unordered_set<SharedMemoryBase*> active_instances_;
|
||||
|
||||
public:
|
||||
static void register_instance(SharedMemoryBase* instance) {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
active_instances_.insert(instance);
|
||||
}
|
||||
|
||||
static void unregister_instance(SharedMemoryBase* instance) {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
active_instances_.erase(instance);
|
||||
}
|
||||
|
||||
static void cleanup_all() noexcept {
|
||||
std::scoped_lock lock(registry_mutex_);
|
||||
for (auto* instance : active_instances_)
|
||||
instance->close();
|
||||
active_instances_.clear();
|
||||
}
|
||||
};
|
||||
|
||||
inline std::mutex SharedMemoryRegistry::registry_mutex_;
|
||||
inline std::unordered_set<SharedMemoryBase*> SharedMemoryRegistry::active_instances_;
|
||||
|
||||
class CleanupHooks {
|
||||
private:
|
||||
static std::once_flag register_once_;
|
||||
|
||||
static void handle_signal(int sig) noexcept {
|
||||
SharedMemoryRegistry::cleanup_all();
|
||||
_Exit(128 + sig);
|
||||
}
|
||||
|
||||
static void register_signal_handlers() noexcept {
|
||||
std::atexit([]() { SharedMemoryRegistry::cleanup_all(); });
|
||||
|
||||
constexpr int signals[] = {SIGHUP, SIGINT, SIGQUIT, SIGILL, SIGABRT, SIGFPE,
|
||||
SIGSEGV, SIGTERM, SIGBUS, SIGSYS, SIGXCPU, SIGXFSZ};
|
||||
|
||||
struct sigaction sa;
|
||||
sa.sa_handler = handle_signal;
|
||||
sigemptyset(&sa.sa_mask);
|
||||
sa.sa_flags = 0;
|
||||
|
||||
for (int sig : signals)
|
||||
sigaction(sig, &sa, nullptr);
|
||||
}
|
||||
|
||||
public:
|
||||
static void ensure_registered() noexcept {
|
||||
std::call_once(register_once_, register_signal_handlers);
|
||||
}
|
||||
};
|
||||
|
||||
inline std::once_flag CleanupHooks::register_once_;
|
||||
|
||||
|
||||
inline int portable_fallocate(int fd, off_t offset, off_t length) {
|
||||
#ifdef __APPLE__
|
||||
fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, offset, length, 0};
|
||||
int ret = fcntl(fd, F_PREALLOCATE, &store);
|
||||
if (ret == -1)
|
||||
{
|
||||
store.fst_flags = F_ALLOCATEALL;
|
||||
ret = fcntl(fd, F_PREALLOCATE, &store);
|
||||
}
|
||||
if (ret != -1)
|
||||
ret = ftruncate(fd, offset + length);
|
||||
return ret;
|
||||
#else
|
||||
return posix_fallocate(fd, offset, length);
|
||||
#endif
|
||||
}
|
||||
|
||||
} // namespace detail
|
||||
|
||||
template<typename T>
|
||||
class SharedMemory: public detail::SharedMemoryBase {
|
||||
static_assert(std::is_trivially_copyable_v<T>, "T must be trivially copyable");
|
||||
static_assert(!std::is_pointer_v<T>, "T cannot be a pointer type");
|
||||
|
||||
private:
|
||||
std::string name_;
|
||||
int fd_ = -1;
|
||||
void* mapped_ptr_ = nullptr;
|
||||
T* data_ptr_ = nullptr;
|
||||
detail::ShmHeader* header_ptr_ = nullptr;
|
||||
size_t total_size_ = 0;
|
||||
std::string sentinel_base_;
|
||||
std::string sentinel_path_;
|
||||
|
||||
static constexpr size_t calculate_total_size() noexcept {
|
||||
return sizeof(T) + sizeof(detail::ShmHeader);
|
||||
}
|
||||
|
||||
static std::string make_sentinel_base(const std::string& name) {
|
||||
uint64_t hash = std::hash<std::string>{}(name);
|
||||
char buf[32];
|
||||
std::snprintf(buf, sizeof(buf), "sfshm_%016" PRIx64, static_cast<uint64_t>(hash));
|
||||
return buf;
|
||||
}
|
||||
|
||||
public:
|
||||
explicit SharedMemory(const std::string& name) noexcept :
|
||||
name_(name),
|
||||
total_size_(calculate_total_size()),
|
||||
sentinel_base_(make_sentinel_base(name)) {}
|
||||
|
||||
~SharedMemory() noexcept override {
|
||||
detail::SharedMemoryRegistry::unregister_instance(this);
|
||||
close();
|
||||
}
|
||||
|
||||
SharedMemory(const SharedMemory&) = delete;
|
||||
SharedMemory& operator=(const SharedMemory&) = delete;
|
||||
|
||||
SharedMemory(SharedMemory&& other) noexcept :
|
||||
name_(std::move(other.name_)),
|
||||
fd_(other.fd_),
|
||||
mapped_ptr_(other.mapped_ptr_),
|
||||
data_ptr_(other.data_ptr_),
|
||||
header_ptr_(other.header_ptr_),
|
||||
total_size_(other.total_size_),
|
||||
sentinel_base_(std::move(other.sentinel_base_)),
|
||||
sentinel_path_(std::move(other.sentinel_path_)) {
|
||||
|
||||
detail::SharedMemoryRegistry::unregister_instance(&other);
|
||||
detail::SharedMemoryRegistry::register_instance(this);
|
||||
other.reset();
|
||||
}
|
||||
|
||||
SharedMemory& operator=(SharedMemory&& other) noexcept {
|
||||
if (this != &other)
|
||||
{
|
||||
detail::SharedMemoryRegistry::unregister_instance(this);
|
||||
close();
|
||||
|
||||
name_ = std::move(other.name_);
|
||||
fd_ = other.fd_;
|
||||
mapped_ptr_ = other.mapped_ptr_;
|
||||
data_ptr_ = other.data_ptr_;
|
||||
header_ptr_ = other.header_ptr_;
|
||||
total_size_ = other.total_size_;
|
||||
sentinel_base_ = std::move(other.sentinel_base_);
|
||||
sentinel_path_ = std::move(other.sentinel_path_);
|
||||
|
||||
detail::SharedMemoryRegistry::unregister_instance(&other);
|
||||
detail::SharedMemoryRegistry::register_instance(this);
|
||||
|
||||
other.reset();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool open(const T& initial_value) noexcept {
|
||||
detail::CleanupHooks::ensure_registered();
|
||||
|
||||
bool retried_stale = false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
if (is_open())
|
||||
return false;
|
||||
|
||||
bool created_new = false;
|
||||
fd_ = shm_open(name_.c_str(), O_CREAT | O_EXCL | O_RDWR, 0666);
|
||||
|
||||
if (fd_ == -1)
|
||||
{
|
||||
fd_ = shm_open(name_.c_str(), O_RDWR, 0666);
|
||||
if (fd_ == -1)
|
||||
return false;
|
||||
}
|
||||
else
|
||||
created_new = true;
|
||||
|
||||
if (!lock_file(LOCK_EX))
|
||||
{
|
||||
::close(fd_);
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool invalid_header = false;
|
||||
bool success =
|
||||
created_new ? setup_new_region(initial_value) : setup_existing_region(invalid_header);
|
||||
|
||||
if (!success)
|
||||
{
|
||||
if (created_new || invalid_header)
|
||||
shm_unlink(name_.c_str());
|
||||
if (mapped_ptr_)
|
||||
unmap_region();
|
||||
unlock_file();
|
||||
::close(fd_);
|
||||
reset();
|
||||
|
||||
if (!created_new && invalid_header && !retried_stale)
|
||||
{
|
||||
retried_stale = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!lock_shared_mutex())
|
||||
{
|
||||
if (created_new)
|
||||
shm_unlink(name_.c_str());
|
||||
if (mapped_ptr_)
|
||||
unmap_region();
|
||||
unlock_file();
|
||||
::close(fd_);
|
||||
reset();
|
||||
|
||||
if (!created_new && !retried_stale)
|
||||
{
|
||||
retried_stale = true;
|
||||
continue;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!create_sentinel_file_locked())
|
||||
{
|
||||
unlock_shared_mutex();
|
||||
unmap_region();
|
||||
if (created_new)
|
||||
shm_unlink(name_.c_str());
|
||||
unlock_file();
|
||||
::close(fd_);
|
||||
reset();
|
||||
return false;
|
||||
}
|
||||
|
||||
header_ptr_->ref_count.fetch_add(1, std::memory_order_acq_rel);
|
||||
|
||||
unlock_shared_mutex();
|
||||
unlock_file();
|
||||
detail::SharedMemoryRegistry::register_instance(this);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void close() noexcept override {
|
||||
if (fd_ == -1 && mapped_ptr_ == nullptr)
|
||||
return;
|
||||
|
||||
bool remove_region = false;
|
||||
bool file_locked = lock_file(LOCK_EX);
|
||||
bool mutex_locked = false;
|
||||
|
||||
if (file_locked && header_ptr_ != nullptr)
|
||||
mutex_locked = lock_shared_mutex();
|
||||
|
||||
if (mutex_locked)
|
||||
{
|
||||
if (header_ptr_)
|
||||
{
|
||||
header_ptr_->ref_count.fetch_sub(1, std::memory_order_acq_rel);
|
||||
}
|
||||
remove_sentinel_file();
|
||||
remove_region = !has_other_live_sentinels_locked();
|
||||
unlock_shared_mutex();
|
||||
}
|
||||
else
|
||||
{
|
||||
remove_sentinel_file();
|
||||
decrement_refcount_relaxed();
|
||||
}
|
||||
|
||||
unmap_region();
|
||||
|
||||
if (remove_region)
|
||||
shm_unlink(name_.c_str());
|
||||
|
||||
if (file_locked)
|
||||
unlock_file();
|
||||
|
||||
if (fd_ != -1)
|
||||
{
|
||||
::close(fd_);
|
||||
fd_ = -1;
|
||||
}
|
||||
|
||||
reset();
|
||||
}
|
||||
|
||||
const std::string& name() const noexcept override { return name_; }
|
||||
|
||||
[[nodiscard]] bool is_open() const noexcept { return fd_ != -1 && mapped_ptr_ && data_ptr_; }
|
||||
|
||||
[[nodiscard]] const T& get() const noexcept { return *data_ptr_; }
|
||||
|
||||
[[nodiscard]] const T* operator->() const noexcept { return data_ptr_; }
|
||||
|
||||
[[nodiscard]] const T& operator*() const noexcept { return *data_ptr_; }
|
||||
|
||||
[[nodiscard]] uint32_t ref_count() const noexcept {
|
||||
return header_ptr_ ? header_ptr_->ref_count.load(std::memory_order_acquire) : 0;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool is_initialized() const noexcept {
|
||||
return header_ptr_ ? header_ptr_->initialized.load(std::memory_order_acquire) : false;
|
||||
}
|
||||
|
||||
static void cleanup_all_instances() noexcept { detail::SharedMemoryRegistry::cleanup_all(); }
|
||||
|
||||
private:
|
||||
void reset() noexcept {
|
||||
fd_ = -1;
|
||||
mapped_ptr_ = nullptr;
|
||||
data_ptr_ = nullptr;
|
||||
header_ptr_ = nullptr;
|
||||
sentinel_path_.clear();
|
||||
}
|
||||
|
||||
void unmap_region() noexcept {
|
||||
if (mapped_ptr_)
|
||||
{
|
||||
munmap(mapped_ptr_, total_size_);
|
||||
mapped_ptr_ = nullptr;
|
||||
data_ptr_ = nullptr;
|
||||
header_ptr_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
[[nodiscard]] bool lock_file(int operation) noexcept {
|
||||
if (fd_ == -1)
|
||||
return false;
|
||||
|
||||
while (flock(fd_, operation) == -1)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void unlock_file() noexcept {
|
||||
if (fd_ == -1)
|
||||
return;
|
||||
|
||||
while (flock(fd_, LOCK_UN) == -1)
|
||||
{
|
||||
if (errno == EINTR)
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::string sentinel_full_path(pid_t pid) const {
|
||||
std::string path = "/dev/shm/";
|
||||
path += sentinel_base_;
|
||||
path.push_back('.');
|
||||
path += std::to_string(pid);
|
||||
return path;
|
||||
}
|
||||
|
||||
void decrement_refcount_relaxed() noexcept {
|
||||
if (!header_ptr_)
|
||||
return;
|
||||
|
||||
uint32_t expected = header_ptr_->ref_count.load(std::memory_order_relaxed);
|
||||
while (expected != 0
|
||||
&& !header_ptr_->ref_count.compare_exchange_weak(
|
||||
expected, expected - 1, std::memory_order_acq_rel, std::memory_order_relaxed))
|
||||
{}
|
||||
}
|
||||
|
||||
bool create_sentinel_file_locked() noexcept {
|
||||
if (!header_ptr_)
|
||||
return false;
|
||||
|
||||
const pid_t self_pid = getpid();
|
||||
sentinel_path_ = sentinel_full_path(self_pid);
|
||||
|
||||
for (int attempt = 0; attempt < 2; ++attempt)
|
||||
{
|
||||
int fd = ::open(sentinel_path_.c_str(), O_CREAT | O_EXCL | O_WRONLY | O_CLOEXEC, 0600);
|
||||
if (fd != -1)
|
||||
{
|
||||
::close(fd);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (errno == EEXIST)
|
||||
{
|
||||
::unlink(sentinel_path_.c_str());
|
||||
decrement_refcount_relaxed();
|
||||
continue;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
sentinel_path_.clear();
|
||||
return false;
|
||||
}
|
||||
|
||||
void remove_sentinel_file() noexcept {
|
||||
if (!sentinel_path_.empty())
|
||||
{
|
||||
::unlink(sentinel_path_.c_str());
|
||||
sentinel_path_.clear();
|
||||
}
|
||||
}
|
||||
|
||||
static bool pid_is_alive(pid_t pid) noexcept {
|
||||
if (pid <= 0)
|
||||
return false;
|
||||
|
||||
if (kill(pid, 0) == 0)
|
||||
return true;
|
||||
|
||||
return errno == EPERM;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool initialize_shared_mutex() noexcept {
|
||||
if (!header_ptr_)
|
||||
return false;
|
||||
|
||||
pthread_mutexattr_t attr;
|
||||
if (pthread_mutexattr_init(&attr) != 0)
|
||||
return false;
|
||||
|
||||
bool success = pthread_mutexattr_setpshared(&attr, PTHREAD_PROCESS_SHARED) == 0;
|
||||
#ifdef PTHREAD_MUTEX_ROBUST
|
||||
if (success)
|
||||
success = pthread_mutexattr_setrobust(&attr, PTHREAD_MUTEX_ROBUST) == 0;
|
||||
#endif
|
||||
|
||||
if (success)
|
||||
success = pthread_mutex_init(&header_ptr_->mutex, &attr) == 0;
|
||||
|
||||
pthread_mutexattr_destroy(&attr);
|
||||
return success;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool lock_shared_mutex() noexcept {
|
||||
if (!header_ptr_)
|
||||
return false;
|
||||
|
||||
while (true)
|
||||
{
|
||||
int rc = pthread_mutex_lock(&header_ptr_->mutex);
|
||||
if (rc == 0)
|
||||
return true;
|
||||
|
||||
#ifdef PTHREAD_MUTEX_ROBUST
|
||||
if (rc == EOWNERDEAD)
|
||||
{
|
||||
if (pthread_mutex_consistent(&header_ptr_->mutex) == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
#endif
|
||||
|
||||
if (rc == EINTR)
|
||||
continue;
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void unlock_shared_mutex() noexcept {
|
||||
if (header_ptr_)
|
||||
pthread_mutex_unlock(&header_ptr_->mutex);
|
||||
}
|
||||
|
||||
bool has_other_live_sentinels_locked() const noexcept {
|
||||
DIR* dir = opendir("/dev/shm");
|
||||
if (!dir)
|
||||
return false;
|
||||
|
||||
std::string prefix = sentinel_base_ + ".";
|
||||
bool found = false;
|
||||
|
||||
while (dirent* entry = readdir(dir))
|
||||
{
|
||||
std::string name = entry->d_name;
|
||||
if (name.rfind(prefix, 0) != 0)
|
||||
continue;
|
||||
|
||||
auto pid_str = name.substr(prefix.size());
|
||||
char* end = nullptr;
|
||||
long value = std::strtol(pid_str.c_str(), &end, 10);
|
||||
if (!end || *end != '\0')
|
||||
continue;
|
||||
|
||||
pid_t pid = static_cast<pid_t>(value);
|
||||
if (pid_is_alive(pid))
|
||||
{
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
|
||||
std::string stale_path = std::string("/dev/shm/") + name;
|
||||
::unlink(stale_path.c_str());
|
||||
const_cast<SharedMemory*>(this)->decrement_refcount_relaxed();
|
||||
}
|
||||
|
||||
closedir(dir);
|
||||
return found;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool setup_new_region(const T& initial_value) noexcept {
|
||||
if (ftruncate(fd_, static_cast<off_t>(total_size_)) == -1)
|
||||
return false;
|
||||
|
||||
if (detail::portable_fallocate(fd_, 0, static_cast<off_t>(total_size_)) != 0)
|
||||
return false;
|
||||
|
||||
mapped_ptr_ = mmap(nullptr, total_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
|
||||
if (mapped_ptr_ == MAP_FAILED)
|
||||
{
|
||||
mapped_ptr_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
data_ptr_ = static_cast<T*>(mapped_ptr_);
|
||||
header_ptr_ =
|
||||
reinterpret_cast<detail::ShmHeader*>(static_cast<char*>(mapped_ptr_) + sizeof(T));
|
||||
|
||||
new (header_ptr_) detail::ShmHeader{};
|
||||
new (data_ptr_) T{initial_value};
|
||||
|
||||
if (!initialize_shared_mutex())
|
||||
return false;
|
||||
|
||||
header_ptr_->ref_count.store(0, std::memory_order_release);
|
||||
header_ptr_->initialized.store(true, std::memory_order_release);
|
||||
return true;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool setup_existing_region(bool& invalid_header) noexcept {
|
||||
invalid_header = false;
|
||||
|
||||
struct stat st;
|
||||
fstat(fd_, &st);
|
||||
if (static_cast<size_t>(st.st_size) < total_size_)
|
||||
{
|
||||
invalid_header = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
mapped_ptr_ = mmap(nullptr, total_size_, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0);
|
||||
if (mapped_ptr_ == MAP_FAILED)
|
||||
{
|
||||
mapped_ptr_ = nullptr;
|
||||
return false;
|
||||
}
|
||||
|
||||
data_ptr_ = static_cast<T*>(mapped_ptr_);
|
||||
header_ptr_ = std::launder(
|
||||
reinterpret_cast<detail::ShmHeader*>(static_cast<char*>(mapped_ptr_) + sizeof(T)));
|
||||
|
||||
if (!header_ptr_->initialized.load(std::memory_order_acquire)
|
||||
|| header_ptr_->magic != detail::ShmHeader::SHM_MAGIC)
|
||||
{
|
||||
invalid_header = true;
|
||||
unmap_region();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename T>
|
||||
[[nodiscard]] std::optional<SharedMemory<T>> create_shared(const std::string& name,
|
||||
const T& initial_value) noexcept {
|
||||
SharedMemory<T> shm(name);
|
||||
if (shm.open(initial_value))
|
||||
return shm;
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace Stockfish::shm
|
||||
|
||||
#endif // #ifndef SHM_LINUX_H_INCLUDED
|
||||
Reference in New Issue
Block a user