Merge commit 'a2a7edf4c8fa145667135bf1bc7f4f67016f7608' into cluster

This is the last commit before there are more conflicts.
This commit is contained in:
Steinar H. Gunderson
2025-12-24 16:19:29 +01:00
14 changed files with 396 additions and 273 deletions
+1
View File
@@ -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
+7 -8
View File
@@ -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<PAWN>();
int pawnCount = pos.count<PAWN>();
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<PAWN>() + 350 * pos.count<KNIGHT>() + 400 * pos.count<BISHOP>()
int material = 300 * pos.count<PAWN>() + 350 * pos.count<KNIGHT>() + 400 * pos.count<BISHOP>()
+ 640 * pos.count<ROOK>() + 1200 * pos.count<QUEEN>();
v = (nnue * (34000 + material + 135 * pos.count<PAWN>())
+ optimism * (4400 + material + 99 * pos.count<PAWN>()))
/ 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);
+2 -2
View File
@@ -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;
+34 -30
View File
@@ -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 <atomic>
#include <charconv>
#include <cctype>
#include <cmath>
#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <limits>
#include <iterator>
#include <mutex>
#include <sstream>
#include <string_view>
#include <system_error>
#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<size_t>::max())
std::exit(EXIT_FAILURE);
return static_cast<size_t>(value);
}
return value;
std::optional<std::string> 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<char>(f), std::istreambuf_iterator<char>());
}
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) {
+12 -16
View File
@@ -77,6 +77,8 @@ using AlignedPtr = std::unique_ptr<T, AlignedDeleter<T>>;
template<typename T>
using LargePagePtr = std::unique_ptr<T, LargePageDeleter<T>>;
#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<std::string> get_system_command_output(const std::string& command) {
std::unique_ptr<FILE, PipeDeleter> 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<std::string> 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<std::string> split(const std::string& s, const std::string& delimiter) {
size_t begin = 0;
std::vector<std::string> 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<std::string> split(const std::string& s, const std::string& d
return res;
}
void remove_whitespace(std::string& s);
enum SyncCout {
IO_LOCK,
IO_UNLOCK
+4 -8
View File
@@ -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
+282 -165
View File
@@ -33,15 +33,19 @@
#include <utility>
#include <vector>
// 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 <sched.h>
#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 <windows.h>
#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<CpuIndex>(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<CpuIndex>(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<CpuIndex> get_process_affinity() {
std::set<CpuIndex> 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<CpuIndex> 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<std::streamsize>::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<std::set<CpuIndex>> 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<CpuIndex>(procGroup) * WIN_PROCESSOR_GROUP_SIZE
+ static_cast<CpuIndex>(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<GROUP_AFFINITY[]>(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<WORD>(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<WORD>(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<CpuIndex> get_process_affinity() {
std::set<CpuIndex> 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<std::set<CpuIndex>> 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<CpuIndex> 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<GROUP_AFFINITY[]>(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<CpuIndex> 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<uint64_t>(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<size_t> indices_from_shortened_string(const std::string& s) {
std::vector<size_t> 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;
+25 -20
View File
@@ -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<NonPV>(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;
}
+1 -1
View File
@@ -218,7 +218,7 @@ class SearchManager: public ISearchManager {
Depth depth) const;
Stockfish::TimeManagement tm;
int originalPly;
double originalTimeAdjust;
int callsCnt;
std::atomic_bool ponder;
+6 -4
View File
@@ -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();
}
+10 -10
View File
@@ -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);
}
+5 -2
View File
@@ -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;
+1 -1
View File
@@ -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
+6 -6
View File
@@ -389,12 +389,12 @@ WinRateParams win_rate_params(const Position& pos) {
int material = pos.count<PAWN>() + 3 * pos.count<KNIGHT>() + 3 * pos.count<BISHOP>()
+ 5 * pos.count<ROOK>() + 9 * pos.count<QUEEN>();
// 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);