diff --git a/AUTHORS b/AUTHORS index 36b2b6f79..a232e115f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -68,6 +68,7 @@ Douglas Matos Gomes (dsmsgms) Dubslow Eduardo Cáceres (eduherminio) Eelco de Groot (KingDefender) +Ehsan Rashid (erashid) Elvin Liu (solarlight2) erbsenzaehler Ernesto Gatti diff --git a/src/evaluate.cpp b/src/evaluate.cpp index 13a3f2117..eaf7ab5f9 100644 --- a/src/evaluate.cpp +++ b/src/evaluate.cpp @@ -46,7 +46,8 @@ int Eval::simple_eval(const Position& pos, Color c) { bool Eval::use_smallnet(const Position& pos) { int simpleEval = simple_eval(pos, pos.side_to_move()); - return std::abs(simpleEval) > 992 + 6 * pos.count(); + int pawnCount = pos.count(); + return std::abs(simpleEval) > 992 + 6 * pawnCount * pawnCount / 16; } // Evaluate is the evaluator for the outer world. It returns a static evaluation @@ -67,7 +68,7 @@ Value Eval::evaluate(const Eval::NNUE::Networks& networks, : networks.big.evaluate(pos, &caches.big, true, &nnueComplexity); // Re-evaluate the position when higher eval accuracy is worth the time spent - if (smallNet && nnue * simpleEval < 0) + if (smallNet && (nnue * simpleEval < 0 || std::abs(nnue) < 250)) { nnue = networks.big.evaluate(pos, &caches.big, true, &nnueComplexity); smallNet = false; @@ -75,17 +76,15 @@ Value Eval::evaluate(const Eval::NNUE::Networks& networks, // Blend optimism and eval with nnue complexity optimism += optimism * nnueComplexity / 470; - nnue -= nnue * (nnueComplexity * 5 / 3) / 32621; + nnue -= nnue * nnueComplexity / 20000; - int material = 200 * pos.count() + 350 * pos.count() + 400 * pos.count() + int material = 300 * pos.count() + 350 * pos.count() + 400 * pos.count() + 640 * pos.count() + 1200 * pos.count(); - v = (nnue * (34000 + material + 135 * pos.count()) - + optimism * (4400 + material + 99 * pos.count())) - / 35967; + v = (nnue * (34300 + material) + optimism * (4400 + material)) / 36672; // Damp down the evaluation linearly when shuffling - v = v * (204 - pos.rule50_count()) / 208; + v -= v * pos.rule50_count() / 212; // Guarantee evaluation does not hit the tablebase range v = std::clamp(v, VALUE_TB_LOSS_IN_MAX_PLY + 1, VALUE_TB_WIN_IN_MAX_PLY - 1); diff --git a/src/evaluate.h b/src/evaluate.h index 4b3e91acf..bdef9ceb6 100644 --- a/src/evaluate.h +++ b/src/evaluate.h @@ -33,8 +33,8 @@ namespace Eval { // for the build process (profile-build and fishtest) to work. Do not change the // name of the macro or the location where this macro is defined, as it is used // in the Makefile/Fishtest. -#define EvalFileDefaultNameBig "nn-c721dfca8cd3.nnue" -#define EvalFileDefaultNameSmall "nn-baff1ede1f90.nnue" +#define EvalFileDefaultNameBig "nn-ddcfb9224cdb.nnue" +#define EvalFileDefaultNameSmall "nn-37f18f62d772.nnue" namespace NNUE { struct Networks; diff --git a/src/misc.cpp b/src/misc.cpp index d48b75e1c..aa22e61f2 100644 --- a/src/misc.cpp +++ b/src/misc.cpp @@ -34,30 +34,25 @@ // 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 fun1_t = bool (*)(LOGICAL_PROCESSOR_RELATIONSHIP, - PSYSTEM_LOGICAL_PROCESSOR_INFORMATION_EX, - PDWORD); -using fun2_t = bool (*)(USHORT, PGROUP_AFFINITY); -using fun3_t = bool (*)(HANDLE, CONST GROUP_AFFINITY*, PGROUP_AFFINITY); -using fun4_t = bool (*)(USHORT, PGROUP_AFFINITY, USHORT, PUSHORT); -using fun5_t = WORD (*)(); -using fun6_t = bool (*)(HANDLE, DWORD, PHANDLE); -using fun7_t = bool (*)(LPCSTR, LPCSTR, PLUID); -using fun8_t = bool (*)(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD); +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 #include -#include +#include #include #include #include #include #include +#include +#include #include #include #include -#include #include "types.h" @@ -488,23 +483,25 @@ static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize if (!hAdvapi32) hAdvapi32 = LoadLibrary(TEXT("advapi32.dll")); - auto fun6 = fun6_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken")); - if (!fun6) + auto OpenProcessToken_f = + OpenProcessToken_t((void (*)()) GetProcAddress(hAdvapi32, "OpenProcessToken")); + if (!OpenProcessToken_f) return nullptr; - auto fun7 = fun7_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA")); - if (!fun7) + auto LookupPrivilegeValueA_f = + LookupPrivilegeValueA_t((void (*)()) GetProcAddress(hAdvapi32, "LookupPrivilegeValueA")); + if (!LookupPrivilegeValueA_f) return nullptr; - auto fun8 = fun8_t((void (*)()) GetProcAddress(hAdvapi32, "AdjustTokenPrivileges")); - if (!fun8) + 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 (!fun6( // OpenProcessToken() + if (!OpenProcessToken_f( // OpenProcessToken() GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY, &hProcessToken)) return nullptr; - if (fun7( // LookupPrivilegeValue(nullptr, SE_LOCK_MEMORY_NAME, &luid) - nullptr, "SeLockMemoryPrivilege", &luid)) + if (LookupPrivilegeValueA_f(nullptr, "SeLockMemoryPrivilege", &luid)) { TOKEN_PRIVILEGES tp{}; TOKEN_PRIVILEGES prevTp{}; @@ -516,8 +513,8 @@ static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize // 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 (fun8( // AdjustTokenPrivileges() - hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, &prevTpLen) + if (AdjustTokenPrivileges_f(hProcessToken, FALSE, &tp, sizeof(TOKEN_PRIVILEGES), &prevTp, + &prevTpLen) && GetLastError() == ERROR_SUCCESS) { // Round up size to full pages and allocate @@ -526,8 +523,7 @@ static void* aligned_large_pages_alloc_windows([[maybe_unused]] size_t allocSize PAGE_READWRITE); // Privilege no longer needed, restore previous state - fun8( // AdjustTokenPrivileges () - hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr); + AdjustTokenPrivileges_f(hProcessToken, FALSE, &prevTp, 0, nullptr, nullptr); } } @@ -603,13 +599,21 @@ void aligned_large_pages_free(void* mem) { std_aligned_free(mem); } #endif size_t str_to_size_t(const std::string& s) { - size_t value; - auto result = std::from_chars(s.data(), s.data() + s.size(), value); - - if (result.ec != std::errc()) + unsigned long long value = std::stoull(s); + if (value > std::numeric_limits::max()) std::exit(EXIT_FAILURE); + return static_cast(value); +} - return value; +std::optional read_file_to_string(const std::string& path) { + std::ifstream f(path, std::ios_base::binary); + if (!f) + return std::nullopt; + return std::string(std::istreambuf_iterator(f), std::istreambuf_iterator()); +} + +void remove_whitespace(std::string& s) { + s.erase(std::remove_if(s.begin(), s.end(), [](char c) { return std::isspace(c); }), s.end()); } std::string CommandLine::get_binary_directory(std::string argv0) { diff --git a/src/misc.h b/src/misc.h index 99cbecfdd..5c0bde44e 100644 --- a/src/misc.h +++ b/src/misc.h @@ -77,6 +77,8 @@ using AlignedPtr = std::unique_ptr>; template using LargePagePtr = std::unique_ptr>; +#if defined(__linux__) + struct PipeDeleter { void operator()(FILE* file) const { if (file != nullptr) @@ -86,23 +88,12 @@ struct PipeDeleter { } }; -#if defined(__linux__) - -inline std::optional get_system_command_output(const std::string& command) { - std::unique_ptr pipe(popen(command.c_str(), "r")); - if (!pipe) - return std::nullopt; - - std::string result; - char buffer[1024]; - while (fgets(buffer, sizeof(buffer), pipe.get()) != nullptr) - result += buffer; - - return result; -} - #endif +// Reads the file as bytes. +// Returns std::nullopt if the file does not exist. +std::optional read_file_to_string(const std::string& path); + void dbg_hit_on(bool cond, int slot = 0); void dbg_mean_of(int64_t value, int slot = 0); void dbg_stdev_of(int64_t value, int slot = 0); @@ -118,9 +109,12 @@ inline TimePoint now() { } inline std::vector split(const std::string& s, const std::string& delimiter) { - size_t begin = 0; std::vector res; + if (s.empty()) + return res; + + size_t begin = 0; for (;;) { const size_t end = s.find(delimiter, begin); @@ -136,6 +130,8 @@ inline std::vector split(const std::string& s, const std::string& d return res; } +void remove_whitespace(std::string& s); + enum SyncCout { IO_LOCK, IO_UNLOCK diff --git a/src/movepick.cpp b/src/movepick.cpp index 7def0ce84..d33359075 100644 --- a/src/movepick.cpp +++ b/src/movepick.cpp @@ -178,7 +178,7 @@ void MovePicker::score() { Square to = m.to_sq(); // histories - m.value = 2 * (*mainHistory)[pos.side_to_move()][m.from_to()]; + m.value = (*mainHistory)[pos.side_to_move()][m.from_to()]; m.value += 2 * (*pawnHistory)[pawn_structure_index(pos)][pc][to]; m.value += 2 * (*continuationHistory[0])[pc][to]; m.value += (*continuationHistory[1])[pc][to]; @@ -197,13 +197,9 @@ void MovePicker::score() { : 0; // malus for putting piece en prise - m.value -= !(threatenedPieces & from) - ? (pt == QUEEN ? bool(to & threatenedByRook) * 48150 - + bool(to & threatenedByMinor) * 10650 - : pt == ROOK ? bool(to & threatenedByMinor) * 24335 - : pt != PAWN ? bool(to & threatenedByPawn) * 14950 - : 0) - : 0; + m.value -= (pt == QUEEN ? bool(to & threatenedByRook) * 49000 + : pt == ROOK ? bool(to & threatenedByMinor) * 24335 + : bool(to & threatenedByPawn) * 14900); } else // Type == EVASIONS diff --git a/src/numa.h b/src/numa.h index c04292daf..5934a0cd8 100644 --- a/src/numa.h +++ b/src/numa.h @@ -33,15 +33,19 @@ #include #include -// We support linux very well, but we explicitly do NOT support Android, partially because -// there are potential issues with `lscpu`, `popen` availability, and partially because -// there's no NUMA environments running Android and there probably won't be. +// We support linux very well, but we explicitly do NOT support Android, because there's +// no affected systems, not worth maintaining. #if defined(__linux__) && !defined(__ANDROID__) #if !defined(_GNU_SOURCE) #define _GNU_SOURCE #endif #include -#elif defined(_WIN32) +#elif defined(_WIN64) + + #if _WIN32_WINNT < 0x0601 + #undef _WIN32_WINNT + #define _WIN32_WINNT 0x0601 // Force to include needed API prototypes + #endif // On Windows each processor group can have up to 64 processors. // https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups @@ -51,6 +55,9 @@ static constexpr size_t WIN_PROCESSOR_GROUP_SIZE = 64; #define NOMINMAX #endif #include + #if defined small + #undef small + #endif // https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-setthreadselectedcpusetmasks using SetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT); @@ -58,6 +65,18 @@ using SetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT) // https://learn.microsoft.com/en-us/windows/win32/api/processtopologyapi/nf-processtopologyapi-setthreadgroupaffinity using SetThreadGroupAffinity_t = BOOL (*)(HANDLE, const GROUP_AFFINITY*, PGROUP_AFFINITY); +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadselectedcpusetmasks +using GetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT, PUSHORT); + +// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getprocessaffinitymask +using GetProcessAffinityMask_t = BOOL (*)(HANDLE, PDWORD_PTR, PDWORD_PTR); + +// https://learn.microsoft.com/en-us/windows/win32/api/processtopologyapi/nf-processtopologyapi-getprocessgroupaffinity +using GetProcessGroupAffinity_t = BOOL (*)(HANDLE, PUSHORT, PUSHORT); + +// https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-getactiveprocessorcount +using GetActiveProcessorCount_t = DWORD (*)(WORD); + #endif #include "misc.h" @@ -67,8 +86,28 @@ namespace Stockfish { using CpuIndex = size_t; using NumaIndex = size_t; -inline const CpuIndex SYSTEM_THREADS_NB = - std::max(1, std::thread::hardware_concurrency()); +inline CpuIndex get_hardware_concurrency() { + CpuIndex concurrency = std::thread::hardware_concurrency(); + + // Get all processors across all processor groups on windows, since ::hardware_concurrency + // only returns the number of processors in the first group, because only these + // are available to std::thread. +#ifdef _WIN64 + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto GetActiveProcessorCount_f = + GetActiveProcessorCount_t((void (*)()) GetProcAddress(k32, "GetActiveProcessorCount")); + + if (GetActiveProcessorCount_f != nullptr) + { + concurrency = GetActiveProcessorCount_f(ALL_PROCESSOR_GROUPS); + } +#endif + + return concurrency; +} + +inline const CpuIndex SYSTEM_THREADS_NB = std::max(1, get_hardware_concurrency()); + // We want to abstract the purpose of storing the numa node index somewhat. // Whoever is using this does not need to know the specifics of the replication @@ -91,8 +130,6 @@ class NumaReplicatedAccessToken { // in a way that doesn't require recreating it completely, and it would be complex and expensive // to maintain class invariants. // The CPU (processor) numbers always correspond to the actual numbering used by the system. -// NOTE: the numbering is only valid within the process, as for example on Windows -// every process gets a "virtualized" set of processors that respects the current affinity // The NUMA node numbers MAY NOT correspond to the system's numbering of the NUMA nodes. // In particular, empty nodes may be removed, or the user may create custom nodes. // It is guaranteed that NUMA nodes are NOT empty, i.e. every node exposed by NumaConfig @@ -109,144 +146,91 @@ class NumaConfig { add_cpu_range_to_node(NumaIndex{0}, CpuIndex{0}, numCpus - 1); } - static std::set get_process_affinity() { - std::set cpus; - - // For unsupported systems, or in case of a soft error, we may assume all processors - // are available for use. - [[maybe_unused]] auto set_to_all_cpus = [&]() { - for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) - cpus.insert(c); - }; - -#if defined(__linux__) && !defined(__ANDROID__) - - // cpu_set_t by default holds 1024 entries. This may not be enough soon, - // but there is no easy way to determine how many threads there actually is. - // In this case we just choose a reasonable upper bound. - static constexpr CpuIndex MaxNumCpus = 1024 * 64; - - cpu_set_t* mask = CPU_ALLOC(MaxNumCpus); - if (mask == nullptr) - std::exit(EXIT_FAILURE); - - const size_t masksize = CPU_ALLOC_SIZE(MaxNumCpus); - - CPU_ZERO_S(masksize, mask); - - const int status = sched_getaffinity(0, masksize, mask); - - if (status != 0) - { - CPU_FREE(mask); - std::exit(EXIT_FAILURE); - } - - for (CpuIndex c = 0; c < MaxNumCpus; ++c) - if (CPU_ISSET_S(c, masksize, mask)) - cpus.insert(c); - - CPU_FREE(mask); - -#elif defined(_WIN32) - - // Windows is problematic and weird due to multiple ways of setting affinity, processor groups, - // and behaviour changes between versions. It's unclear if we can support this feature - // on Windows in the same way we do on Linux. - // Apparently when affinity is set via either start /affinity or msys2 taskset - // the function GetNumaProcessorNodeEx completely disregards the processors that we do not - // have affinity more. Moreover, the indices are shifted to start from 0, indicating that Windows - // is providing a whole new mapping of processors to this process. This is problematic in some cases - // but it at least allows us to [probably] support this affinity restriction feature by default. - // So overall, Windows appears to "virtualize" a set of processors and processor groups for every - // process. It's unclear if this assignment can change while the process is running. - // std::thread::hardware_concurrency() returns the number of processors that's consistent - // with GetNumaProcessorNodeEx, so we can just add all of them. - - set_to_all_cpus(); - -#else - - // For other systems we assume the process is allowed to execute on all processors. - set_to_all_cpus(); - -#endif - - return cpus; - } - // This function queries the system for the mapping of processors to NUMA nodes. - // On Linux we utilize `lscpu` to avoid libnuma. + // On Linux we read from standardized kernel sysfs, with a fallback to single NUMA node. // On Windows we utilize GetNumaProcessorNodeEx, which has its quirks, see // comment for Windows implementation of get_process_affinity - static NumaConfig from_system(bool respectProcessAffinity = true) { + static NumaConfig from_system([[maybe_unused]] bool respectProcessAffinity = true) { NumaConfig cfg = empty(); +#if defined(__linux__) && !defined(__ANDROID__) + std::set allowedCpus; if (respectProcessAffinity) allowedCpus = get_process_affinity(); - else - { - for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) - allowedCpus.insert(c); - } - auto is_cpu_allowed = [&](CpuIndex c) { return allowedCpus.count(c) == 1; }; - -#if defined(__linux__) && !defined(__ANDROID__) + auto is_cpu_allowed = [respectProcessAffinity, &allowedCpus](CpuIndex c) { + return !respectProcessAffinity || allowedCpus.count(c) == 1; + }; // On Linux things are straightforward, since there's no processor groups and // any thread can be scheduled on all processors. - // This command produces output in the following form - // CPU NODE - // 0 0 - // 1 0 - // 2 1 - // 3 1 - // - // On some systems it may use '-' to signify no NUMA node, in which case we assume it's in node 0. - auto lscpuOpt = get_system_command_output("lscpu -e=cpu,node"); - if (lscpuOpt.has_value()) + + // We try to gather this information from the sysfs first + // https://www.kernel.org/doc/Documentation/ABI/stable/sysfs-devices-node + + bool useFallback = false; + auto fallback = [&]() { + useFallback = true; + cfg = empty(); + }; + + // /sys/devices/system/node/online contains information about active NUMA nodes + auto nodeIdsStr = read_file_to_string("/sys/devices/system/node/online"); + if (!nodeIdsStr.has_value() || nodeIdsStr->empty()) { - - std::istringstream ss(*lscpuOpt); - - // skip the list header - ss.ignore(std::numeric_limits::max(), '\n'); - - while (true) - { - CpuIndex c; - NumaIndex n; - - ss >> c; - - if (!ss) - break; - - ss >> n; - - if (!ss) - { - ss.clear(); - std::string dummy; - ss >> dummy; - n = 0; - } - - if (is_cpu_allowed(c)) - cfg.add_cpu_to_node(n, c); - } + fallback(); } else + { + remove_whitespace(*nodeIdsStr); + for (size_t n : indices_from_shortened_string(*nodeIdsStr)) + { + // /sys/devices/system/node/node.../cpulist + std::string path = + std::string("/sys/devices/system/node/node") + std::to_string(n) + "/cpulist"; + auto cpuIdsStr = read_file_to_string(path); + // Now, we only bail if the file does not exist. Some nodes may be empty, that's fine. + // An empty node still has a file that appears to have some whitespace, so we need + // to handle that. + if (!cpuIdsStr.has_value()) + { + fallback(); + break; + } + else + { + remove_whitespace(*cpuIdsStr); + for (size_t c : indices_from_shortened_string(*cpuIdsStr)) + { + if (is_cpu_allowed(c)) + cfg.add_cpu_to_node(n, c); + } + } + } + } + + if (useFallback) { for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) if (is_cpu_allowed(c)) cfg.add_cpu_to_node(NumaIndex{0}, c); } -#elif defined(_WIN32) +#elif defined(_WIN64) + + std::optional> allowedCpus; + + if (respectProcessAffinity) + allowedCpus = get_process_affinity(); + + // The affinity can't be determined in all cases on Windows, but we at least guarantee + // that the number of allowed processors is >= number of processors in the affinity mask. + // In case the user is not satisfied they must set the processor numbers explicitly. + auto is_cpu_allowed = [&allowedCpus](CpuIndex c) { + return !allowedCpus.has_value() || allowedCpus->count(c) == 1; + }; // Since Windows 11 and Windows Server 2022 thread affinities can span // processor groups and can be set as such by a new WinAPI function. @@ -268,14 +252,6 @@ class NumaConfig { procnum.Reserved = 0; USHORT nodeNumber; - // When start /affinity or taskset was used to run this process with restricted affinity - // GetNumaProcessorNodeEx will NOT correspond to the system's processor setup, instead - // it appears to follow a completely new processor assignment, made specifically for this process, - // in which processors that this process has affinity for are remapped, and only those are remapped, - // to form a new set of processors. In other words, we can only get processors - // which we have affinity for this way. This means that the behaviour for - // `respectProcessAffinity == false` may be unexpected when affinity is set from outside, - // while the behaviour for `respectProcessAffinity == true` is given by default. const BOOL status = GetNumaProcessorNodeEx(&procnum, &nodeNumber); const CpuIndex c = static_cast(procGroup) * WIN_PROCESSOR_GROUP_SIZE + static_cast(number); @@ -323,8 +299,7 @@ class NumaConfig { // Fallback for unsupported systems. for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) - if (is_cpu_allowed(c)) - cfg.add_cpu_to_node(NumaIndex{0}, c); + cfg.add_cpu_to_node(NumaIndex{0}, c); #endif @@ -344,38 +319,17 @@ class NumaConfig { NumaIndex n = 0; for (auto&& nodeStr : split(s, ":")) { - bool addedAnyCpuInThisNode = false; - - for (const std::string& cpuStr : split(nodeStr, ",")) + auto indices = indices_from_shortened_string(nodeStr); + if (!indices.empty()) { - if (cpuStr.empty()) - continue; - - auto parts = split(cpuStr, "-"); - if (parts.size() == 1) + for (auto idx : indices) { - const CpuIndex c = CpuIndex{str_to_size_t(parts[0])}; - if (!cfg.add_cpu_to_node(n, c)) + if (!cfg.add_cpu_to_node(n, CpuIndex(idx))) std::exit(EXIT_FAILURE); } - else if (parts.size() == 2) - { - const CpuIndex cfirst = CpuIndex{str_to_size_t(parts[0])}; - const CpuIndex clast = CpuIndex{str_to_size_t(parts[1])}; - if (!cfg.add_cpu_range_to_node(n, cfirst, clast)) - std::exit(EXIT_FAILURE); - } - else - { - std::exit(EXIT_FAILURE); - } - - addedAnyCpuInThisNode = true; - } - - if (addedAnyCpuInThisNode) n += 1; + } } cfg.customAffinity = true; @@ -549,7 +503,7 @@ class NumaConfig { // This is defensive, allowed because this code is not performance critical. sched_yield(); -#elif defined(_WIN32) +#elif defined(_WIN64) // Requires Windows 11. No good way to set thread affinity spanning processor groups before that. HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); @@ -561,8 +515,8 @@ class NumaConfig { if (SetThreadSelectedCpuSetMasks_f != nullptr) { // Only available on Windows 11 and Windows Server 2022 onwards. - const USHORT numProcGroups = - ((highestCpuIndex + 1) + WIN_PROCESSOR_GROUP_SIZE - 1) / WIN_PROCESSOR_GROUP_SIZE; + const USHORT numProcGroups = USHORT( + ((highestCpuIndex + 1) + WIN_PROCESSOR_GROUP_SIZE - 1) / WIN_PROCESSOR_GROUP_SIZE); auto groupAffinities = std::make_unique(numProcGroups); std::memset(groupAffinities.get(), 0, sizeof(GROUP_AFFINITY) * numProcGroups); for (WORD i = 0; i < numProcGroups; ++i) @@ -603,14 +557,14 @@ class NumaConfig { // See https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support GROUP_AFFINITY affinity; std::memset(&affinity, 0, sizeof(GROUP_AFFINITY)); - affinity.Group = static_cast(n); // We use an ordered set so we're guaranteed to get the smallest cpu number here. const size_t forcedProcGroupIndex = *(nodes[n].begin()) / WIN_PROCESSOR_GROUP_SIZE; + affinity.Group = static_cast(forcedProcGroupIndex); for (CpuIndex c : nodes[n]) { const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; const size_t idxWithinProcGroup = c % WIN_PROCESSOR_GROUP_SIZE; - // We skip processors that are not in the same proccessor group. + // We skip processors that are not in the same processor group. // If everything was set up correctly this will never be an issue, // but we have to account for bad NUMA node specification. if (procGroupIndex != forcedProcGroupIndex) @@ -709,6 +663,169 @@ class NumaConfig { return true; } + +#if defined(__linux__) && !defined(__ANDROID__) + + static std::set get_process_affinity() { + + std::set cpus; + + // For unsupported systems, or in case of a soft error, we may assume all processors + // are available for use. + [[maybe_unused]] auto set_to_all_cpus = [&]() { + for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) + cpus.insert(c); + }; + + // cpu_set_t by default holds 1024 entries. This may not be enough soon, + // but there is no easy way to determine how many threads there actually is. + // In this case we just choose a reasonable upper bound. + static constexpr CpuIndex MaxNumCpus = 1024 * 64; + + cpu_set_t* mask = CPU_ALLOC(MaxNumCpus); + if (mask == nullptr) + std::exit(EXIT_FAILURE); + + const size_t masksize = CPU_ALLOC_SIZE(MaxNumCpus); + + CPU_ZERO_S(masksize, mask); + + const int status = sched_getaffinity(0, masksize, mask); + + if (status != 0) + { + CPU_FREE(mask); + std::exit(EXIT_FAILURE); + } + + for (CpuIndex c = 0; c < MaxNumCpus; ++c) + if (CPU_ISSET_S(c, masksize, mask)) + cpus.insert(c); + + CPU_FREE(mask); + + return cpus; + } + +#elif defined(_WIN64) + + // On Windows there are two ways to set affinity, and therefore 2 ways to get it. + // These are not consistent, so we have to check both. + // In some cases it is actually not possible to determine affinity. + // For example when two different threads have affinity on different processor groups, + // set using SetThreadAffinityMask, we can't retrieve the actual affinities. + // From documentation on GetProcessAffinityMask: + // > If the calling process contains threads in multiple groups, + // > the function returns zero for both affinity masks. + // In such cases we just give up and assume we have affinity for all processors. + // nullopt means no affinity is set, that is, all processors are allowed + static std::optional> get_process_affinity() { + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto GetThreadSelectedCpuSetMasks_f = GetThreadSelectedCpuSetMasks_t( + (void (*)()) GetProcAddress(k32, "GetThreadSelectedCpuSetMasks")); + auto GetProcessAffinityMask_f = + GetProcessAffinityMask_t((void (*)()) GetProcAddress(k32, "GetProcessAffinityMask")); + auto GetProcessGroupAffinity_f = + GetProcessGroupAffinity_t((void (*)()) GetProcAddress(k32, "GetProcessGroupAffinity")); + + if (GetThreadSelectedCpuSetMasks_f != nullptr) + { + std::set cpus; + + USHORT RequiredMaskCount; + GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), nullptr, 0, &RequiredMaskCount); + + // If RequiredMaskCount then these affinities were never set, but it's not consistent + // so GetProcessAffinityMask may still return some affinity. + if (RequiredMaskCount > 0) + { + auto groupAffinities = std::make_unique(RequiredMaskCount); + + GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), groupAffinities.get(), + RequiredMaskCount, &RequiredMaskCount); + + for (USHORT i = 0; i < RequiredMaskCount; ++i) + { + const size_t procGroupIndex = groupAffinities[i].Group; + + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (groupAffinities[i].Mask & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + } + + return cpus; + } + } + + if (GetProcessAffinityMask_f != nullptr && GetProcessGroupAffinity_f != nullptr) + { + std::set cpus; + + DWORD_PTR proc, sys; + BOOL status = GetProcessAffinityMask_f(GetCurrentProcess(), &proc, &sys); + if (status == 0) + return std::nullopt; + + // We can't determine affinity because it spans processor groups. + if (proc == 0) + return std::nullopt; + + // We are expecting a single group. + USHORT GroupCount = 1; + alignas(4) USHORT GroupArray[1]; + status = GetProcessGroupAffinity_f(GetCurrentProcess(), &GroupCount, GroupArray); + if (status == 0 || GroupCount != 1) + return std::nullopt; + + const size_t procGroupIndex = GroupArray[0]; + + uint64_t mask = static_cast(proc); + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (mask & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + + return cpus; + } + + return std::nullopt; + } + +#endif + + static std::vector indices_from_shortened_string(const std::string& s) { + std::vector indices; + + if (s.empty()) + return indices; + + for (const std::string& ss : split(s, ",")) + { + if (ss.empty()) + continue; + + auto parts = split(ss, "-"); + if (parts.size() == 1) + { + const CpuIndex c = CpuIndex{str_to_size_t(parts[0])}; + indices.emplace_back(c); + } + else if (parts.size() == 2) + { + const CpuIndex cfirst = CpuIndex{str_to_size_t(parts[0])}; + const CpuIndex clast = CpuIndex{str_to_size_t(parts[1])}; + for (size_t c = cfirst; c <= clast; ++c) + { + indices.emplace_back(c); + } + } + } + + return indices; + } }; class NumaReplicationContext; diff --git a/src/search.cpp b/src/search.cpp index 9aabc2f86..c8c1bcaba 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -162,7 +162,7 @@ void Search::Worker::start_searching() { } main_manager()->tm.init(limits, rootPos.side_to_move(), rootPos.game_ply(), options, - main_manager()->originalPly); + main_manager()->originalTimeAdjust); tt.new_search(); if (rootMoves.empty()) @@ -598,11 +598,12 @@ Value Search::Worker::search( Key posKey; Move ttMove, move, excludedMove, bestMove; Depth extension, newDepth; - Value bestValue, value, ttValue, eval, maxValue, probCutBeta; + Value bestValue, value, ttValue, eval, maxValue, probCutBeta, singularValue; bool givesCheck, improving, priorCapture, opponentWorsening; bool capture, moveCountPruning, ttCapture; Piece movedPiece; int moveCount, captureCount, quietCount; + Bound singularBound; // Step 1. Initialize node Worker* thisThread = this; @@ -667,7 +668,7 @@ Value Search::Worker::search( ss->ttPv = PvNode || (ss->ttHit && tte->is_pv()); // At non-PV nodes we check for an early TT cutoff - if (!PvNode && !excludedMove && tte->depth() > depth + if (!PvNode && !excludedMove && tte->depth() > depth - (ttValue <= beta) && ttValue != VALUE_NONE // Possible in case of TT access race or if !ttHit && (tte->bound() & (ttValue >= beta ? BOUND_LOWER : BOUND_UPPER))) { @@ -973,6 +974,8 @@ moves_loop: // When in check, search starts here value = bestValue; moveCountPruning = false; + singularValue = VALUE_INFINITE; + singularBound = BOUND_NONE; // Step 13. Loop through all pseudo-legal moves until no moves remain // or a beta cutoff occurs. @@ -1022,7 +1025,9 @@ moves_loop: // When in check, search starts here if (!rootNode && pos.non_pawn_material(us) && bestValue > VALUE_TB_LOSS_IN_MAX_PLY) { // Skip quiet moves if movecount exceeds our FutilityMoveCount threshold (~8 Elo) - moveCountPruning = moveCount >= futility_move_count(improving, depth); + moveCountPruning = + moveCount >= futility_move_count(improving, depth) + - (singularBound == BOUND_UPPER && singularValue < alpha - 50); // Reduced depth of the next LMR search int lmrDepth = newDepth - r; @@ -1108,19 +1113,18 @@ moves_loop: // When in check, search starts here Depth singularDepth = newDepth / 2; ss->excludedMove = move; - value = + value = singularValue = search(pos, ss, singularBeta - 1, singularBeta, singularDepth, cutNode); + singularBound = singularValue >= singularBeta ? BOUND_LOWER : BOUND_UPPER; ss->excludedMove = Move::none(); if (value < singularBeta) { int doubleMargin = 304 * PvNode - 203 * !ttCapture; int tripleMargin = 117 + 259 * PvNode - 296 * !ttCapture + 97 * ss->ttPv; - int quadMargin = 486 + 343 * PvNode - 273 * !ttCapture + 232 * ss->ttPv; extension = 1 + (value < singularBeta - doubleMargin) - + (value < singularBeta - tripleMargin) - + (value < singularBeta - quadMargin); + + (value < singularBeta - tripleMargin); depth += ((!PvNode) && (depth < 16)); } @@ -1201,17 +1205,17 @@ moves_loop: // When in check, search starts here if ((ss + 1)->cutoffCnt > 3) r++; - // Set reduction to 0 for first picked move (ttMove) (~2 Elo) - // Nullifies all previous reduction adjustments to ttMove and leaves only history to do them + // For first picked move (ttMove) reduce reduction + // but never allow it to go below 0 (~3 Elo) else if (move == ttMove) - r = 0; + r = std::max(0, r - 2); ss->statScore = 2 * thisThread->mainHistory[us][move.from_to()] + (*contHist[0])[movedPiece][move.to_sq()] + (*contHist[1])[movedPiece][move.to_sq()] - 5169; // Decrease/increase reduction for moves with a good/bad history (~8 Elo) - r -= ss->statScore / (12219 - std::min(depth, 13) * 120); + r -= ss->statScore / 11049; // Step 17. Late moves reduction / extension (LMR, ~117 Elo) if (depth >= 2 && moveCount > 1 + rootNode) @@ -1341,7 +1345,7 @@ moves_loop: // When in check, search starts here if (value >= beta) { - ss->cutoffCnt += 1 + !ttMove; + ss->cutoffCnt += 1 + !ttMove - (extension >= 2); assert(value >= beta); // Fail high break; } @@ -1391,18 +1395,19 @@ moves_loop: // When in check, search starts here // Bonus for prior countermove that caused the fail low else if (!priorCapture && prevSq != SQ_NONE) { - int bonus = (depth > 4) + (depth > 5) + (PvNode || cutNode) + ((ss - 1)->statScore < -14144) - + ((ss - 1)->moveCount > 9) + (!ss->inCheck && bestValue <= ss->staticEval - 115) - + (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 81); + int bonus = (116 * (depth > 5) + 115 * (PvNode || cutNode) + + 186 * ((ss - 1)->statScore < -14144) + 121 * ((ss - 1)->moveCount > 9) + + 64 * (!ss->inCheck && bestValue <= ss->staticEval - 115) + + 137 * (!(ss - 1)->inCheck && bestValue <= -(ss - 1)->staticEval - 81)); update_continuation_histories(ss - 1, pos.piece_on(prevSq), prevSq, - stat_bonus(depth) * bonus); + stat_bonus(depth) * bonus / 100); thisThread->mainHistory[~us][((ss - 1)->currentMove).from_to()] - << stat_bonus(depth) * bonus / 2; + << stat_bonus(depth) * bonus / 200; if (type_of(pos.piece_on(prevSq)) != PAWN && ((ss - 1)->currentMove).type_of() != PROMOTION) thisThread->pawnHistory[pawn_structure_index(pos)][pos.piece_on(prevSq)][prevSq] - << stat_bonus(depth) * bonus * 4; + << stat_bonus(depth) * bonus / 25; } if (PvNode) @@ -1536,7 +1541,7 @@ Value Search::Worker::qsearch(Position& pos, Stack* ss, Value alpha, Value beta, to_corrected_static_eval(unadjustedStaticEval, *thisThread, pos); // ttValue can be used as a better position evaluation (~13 Elo) - if (ttValue != VALUE_NONE + if (std::abs(ttValue) < VALUE_TB_WIN_IN_MAX_PLY && (tte->bound() & (ttValue > bestValue ? BOUND_LOWER : BOUND_UPPER))) bestValue = ttValue; } diff --git a/src/search.h b/src/search.h index 329e80a2c..25fab902a 100644 --- a/src/search.h +++ b/src/search.h @@ -218,7 +218,7 @@ class SearchManager: public ISearchManager { Depth depth) const; Stockfish::TimeManagement tm; - int originalPly; + double originalTimeAdjust; int callsCnt; std::atomic_bool ponder; diff --git a/src/thread.cpp b/src/thread.cpp index a80bb84b9..a4a46dded 100644 --- a/src/thread.cpp +++ b/src/thread.cpp @@ -216,11 +216,13 @@ void ThreadPool::clear() { for (auto&& th : threads) th->wait_for_search_finished(); - main_manager()->callsCnt = 0; - main_manager()->bestPreviousScore = VALUE_INFINITE; + // These two affect the time taken on the first move of a game: main_manager()->bestPreviousAverageScore = VALUE_INFINITE; - main_manager()->originalPly = -1; - main_manager()->previousTimeReduction = 1.0; + main_manager()->previousTimeReduction = 0.85; + + main_manager()->callsCnt = 0; + main_manager()->bestPreviousScore = VALUE_INFINITE; + main_manager()->originalTimeAdjust = -1; main_manager()->tm.clear(); } diff --git a/src/timeman.cpp b/src/timeman.cpp index f389e082d..9de70fdc6 100644 --- a/src/timeman.cpp +++ b/src/timeman.cpp @@ -44,8 +44,11 @@ void TimeManagement::advance_nodes_time(std::int64_t nodes) { // the bounds of time allowed for the current game ply. We currently support: // 1) x basetime (+ z increment) // 2) x moves in y seconds (+ z increment) -void TimeManagement::init( - Search::LimitsType& limits, Color us, int ply, const OptionsMap& options, int& originalPly) { +void TimeManagement::init(Search::LimitsType& limits, + Color us, + int ply, + const OptionsMap& options, + double& originalTimeAdjust) { TimePoint npmsec = TimePoint(options["nodestime"]); // If we have no time, we don't need to fully initialize TM. @@ -56,9 +59,6 @@ void TimeManagement::init( if (limits.time[us] == 0) return; - if (originalPly == -1) - originalPly = ply; - TimePoint moveOverhead = TimePoint(options["Move Overhead"]); // optScale is a percentage of available time to use for the current move. @@ -105,10 +105,9 @@ void TimeManagement::init( // game time for the current move, so also cap to a percentage of available game time. if (limits.movestogo == 0) { - // Use extra time with larger increments - double optExtra = scaledInc < 500 ? 1.0 : 1.13; - if (ply - originalPly < 2) - optExtra *= 0.95; + // Extra time according to timeLeft + if (originalTimeAdjust < 0) + originalTimeAdjust = 0.3285 * std::log10(timeLeft) - 0.4830; // Calculate time constants based on current time left. double logTimeInSec = std::log10(scaledTime / 1000.0); @@ -117,7 +116,8 @@ void TimeManagement::init( optScale = std::min(0.0122 + std::pow(ply + 2.95, 0.462) * optConstant, 0.213 * limits.time[us] / timeLeft) - * optExtra; + * originalTimeAdjust; + maxScale = std::min(6.64, maxConstant + ply / 12.0); } diff --git a/src/timeman.h b/src/timeman.h index 5e0b8369c..016103470 100644 --- a/src/timeman.h +++ b/src/timeman.h @@ -36,8 +36,11 @@ struct LimitsType; // the maximum available time, the game move number, and other parameters. class TimeManagement { public: - void init( - Search::LimitsType& limits, Color us, int ply, const OptionsMap& options, int& originalPly); + void init(Search::LimitsType& limits, + Color us, + int ply, + const OptionsMap& options, + double& originalTimeAdjust); TimePoint optimum() const; TimePoint maximum() const; diff --git a/src/tt.cpp b/src/tt.cpp index 79274f525..f95170e94 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -124,7 +124,7 @@ TTEntry* TranspositionTable::probe(const Key key, bool& found) const { const uint16_t key16 = uint16_t(key); // Use the low 16 bits as key inside the cluster for (int i = 0; i < ClusterSize; ++i) - if (tte[i].key16 == key16 || !tte[i].depth8) + if (tte[i].key16 == key16) return found = bool(tte[i].depth8), &tte[i]; // Find an entry to be replaced according to the replacement strategy diff --git a/src/uci.cpp b/src/uci.cpp index e2405371d..14f8a67dc 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -389,12 +389,12 @@ WinRateParams win_rate_params(const Position& pos) { int material = pos.count() + 3 * pos.count() + 3 * pos.count() + 5 * pos.count() + 9 * pos.count(); - // The fitted model only uses data for material counts in [10, 78], and is anchored at count 58. - double m = std::clamp(material, 10, 78) / 58.0; + // The fitted model only uses data for material counts in [17, 78], and is anchored at count 58. + double m = std::clamp(material, 17, 78) / 58.0; // Return a = p_a(material) and b = p_b(material), see github.com/official-stockfish/WDL_model - constexpr double as[] = {-150.77043883, 394.96159472, -321.73403766, 406.15850091}; - constexpr double bs[] = {62.33245393, -91.02264855, 45.88486850, 51.63461272}; + constexpr double as[] = {-41.25712052, 121.47473115, -124.46958843, 411.84490997}; + constexpr double bs[] = {84.92998051, -143.66658718, 80.09988253, 49.80869370}; double a = (((as[0] * m + as[1]) * m + as[2]) * m) + as[3]; double b = (((bs[0] * m + bs[1]) * m + bs[2]) * m) + bs[3]; @@ -435,8 +435,8 @@ std::string UCIEngine::format_score(const Score& s) { // without treatment of mate and similar special scores. int UCIEngine::to_cp(Value v, const Position& pos) { - // In general, the score can be defined via the the WDL as - // (log(1/L - 1) - log(1/W - 1)) / ((log(1/L - 1) + log(1/W - 1)) + // In general, the score can be defined via the WDL as + // (log(1/L - 1) - log(1/W - 1)) / (log(1/L - 1) + log(1/W - 1)). // Based on our win_rate_model, this simply yields v / a. auto [a, b] = win_rate_params(pos);