From 54df0c451cacbdadf0a5170e21bc100c3b51ad98 Mon Sep 17 00:00:00 2001 From: yaneurao Date: Fri, 11 Oct 2024 22:31:34 +0900 Subject: [PATCH] =?UTF-8?q?-=20Stockfish=E3=81=8B=E3=82=89numa.h=20/=20cpp?= =?UTF-8?q?=E3=82=92porting=20=E4=BD=9C=E6=A5=AD=E4=B8=AD=E3=81=9D?= =?UTF-8?q?=E3=81=AE1=20-=20ThreadIdOffset=E3=82=A8=E3=83=B3=E3=82=B8?= =?UTF-8?q?=E3=83=B3=E3=82=AA=E3=83=97=E3=82=B7=E3=83=A7=E3=83=B3=E5=BB=83?= =?UTF-8?q?=E6=AD=A2=20-=20Stockfish=E3=81=AEmisc.h=E3=81=AEsplit()?= =?UTF-8?q?=E3=81=A8str=5Fto=5Fsize=5Ft()=E3=82=92porting=20-=20StringExte?= =?UTF-8?q?nsion::Split()=E3=82=92Stockfish=E3=81=A7=E5=AE=9F=E8=A3=85?= =?UTF-8?q?=E3=81=95=E3=82=8C=E3=81=9Fsplit()=E3=81=AB=E5=B7=AE=E3=81=97?= =?UTF-8?q?=E6=9B=BF=E3=81=88=E3=80=82=E3=81=9D=E3=82=8C=E3=81=AB=E4=BC=B4?= =?UTF-8?q?=E3=81=86=E4=BF=AE=E6=AD=A3=E3=80=82=20-=20TranspositionTable?= =?UTF-8?q?=E3=82=92struct=E3=81=8B=E3=82=89class=E3=81=AB=E5=A4=89?= =?UTF-8?q?=E6=9B=B4=E3=80=82=20-=20ThreadPool=E3=82=92struct=E3=81=8B?= =?UTF-8?q?=E3=82=89class=E3=81=AB=E5=A4=89=E6=9B=B4=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- source/YaneuraOu.vcxproj | 1 + source/YaneuraOu.vcxproj.filters | 3 + source/book/makebook2023.cpp | 2 +- .../yaneuraou-engine/yaneuraou-search.cpp | 4 - source/misc.cpp | 53 +- source/misc.h | 12 +- source/numa.h | 1642 +++++++++++++++++ source/search.h | 24 + source/thread.h | 57 +- source/tt.h | 4 +- source/usi_option.cpp | 16 +- 11 files changed, 1772 insertions(+), 46 deletions(-) create mode 100644 source/numa.h diff --git a/source/YaneuraOu.vcxproj b/source/YaneuraOu.vcxproj index efccb79ca..5627bca6a 100644 --- a/source/YaneuraOu.vcxproj +++ b/source/YaneuraOu.vcxproj @@ -670,6 +670,7 @@ + diff --git a/source/YaneuraOu.vcxproj.filters b/source/YaneuraOu.vcxproj.filters index 3d1a9a7c1..95299e50b 100644 --- a/source/YaneuraOu.vcxproj.filters +++ b/source/YaneuraOu.vcxproj.filters @@ -364,6 +364,9 @@ リソース ファイル + + リソース ファイル + diff --git a/source/book/makebook2023.cpp b/source/book/makebook2023.cpp index bd3b32340..41449d4c6 100644 --- a/source/book/makebook2023.cpp +++ b/source/book/makebook2023.cpp @@ -603,7 +603,7 @@ namespace MakeBook2023 auto& token = splited2[0]; if (token == "NOE" && splited2.size() == 2) // numbers of entires { - size_t noe = StringExtension::to_int(splited2[1],0); + size_t noe = StringExtension::to_int(string(splited2[1]),0); cout << "Number of Sfen Entries = " << noe << endl; // エントリー数が事前にわかったので、その分だけそれぞれの構造体配列を確保する。 diff --git a/source/engine/yaneuraou-engine/yaneuraou-search.cpp b/source/engine/yaneuraou-engine/yaneuraou-search.cpp index 5d1ddeb44..37b2ea1b4 100644 --- a/source/engine/yaneuraou-engine/yaneuraou-search.cpp +++ b/source/engine/yaneuraou-engine/yaneuraou-search.cpp @@ -193,10 +193,6 @@ namespace { // 探索用の定数 // ----------------------- -// Different node types, used as a template parameter -// 探索しているnodeの種類 -enum NodeType { NonPV, PV , Root}; - // Futility margin // depth(残り探索深さ)に応じたfutility margin。 // ※ RazoringはStockfish12で効果がないとされてしまい除去された。 diff --git a/source/misc.cpp b/source/misc.cpp index e8a17edff..037dc4093 100644 --- a/source/misc.cpp +++ b/source/misc.cpp @@ -657,10 +657,6 @@ namespace WinProcGroup { void bindThisThread(size_t idx) { -#if defined(_WIN32) - idx += Options["ThreadIdOffset"]; -#endif - // Use only local variables to be thread-safe // 使うべきプロセッサグループ番号が返ってくる。 @@ -2052,25 +2048,27 @@ namespace StringExtension return s; } - // sを文字列sepで分割した文字列集合を返す。 - std::vector Split(const std::string& s, const std::string& sep) - { - std::vector v; - string ss = s; - size_t p = 0; // 前回の分割場所 - while (true) + // sを文字列spで分割した文字列集合を返す。 + std::vector Split(std::string_view s, std::string_view delimiter) { + std::vector res; + + if (s.empty()) + return res; + + size_t begin = 0; + for (;;) { - size_t pos = ss.find(sep , p); - if (pos == string::npos) - { - // sepが見つからなかったのでこれでおしまい。 - v.emplace_back(ss.substr(p)); + const size_t end = s.find(delimiter, begin); + if (end == std::string::npos) break; - } - v.emplace_back(ss.substr(p, pos - p)); - p = pos + sep.length(); + + res.emplace_back(s.substr(begin, end - begin)); + begin = end + delimiter.size(); } - return v; + + res.emplace_back(s.substr(begin)); + + return res; } // Pythonの delemiter.join(v) みたいなの。 @@ -2089,6 +2087,21 @@ namespace StringExtension }; +// sを文字列spで分割した文字列集合を返す。 +// ※ Stockfishとの互換性のために用意。 +std::vector split(std::string_view s, std::string_view delimiter) +{ + return StringExtension::Split(s, delimiter); +} + +// "123"みたいな文字列を123のように数値型(size_t)に変換する。 +size_t str_to_size_t(const std::string& s) { + unsigned long long value = std::stoull(s); + if (value > std::numeric_limits::max()) + std::exit(EXIT_FAILURE); + return static_cast(value); +} + // ---------------------------- // working directory // ---------------------------- diff --git a/source/misc.h b/source/misc.h index 7c4699c4f..ecaf38b87 100644 --- a/source/misc.h +++ b/source/misc.h @@ -11,6 +11,7 @@ #include #include #include +#include #include "types.h" @@ -951,6 +952,7 @@ namespace Math { // 文字列 拡張 // -------------------- +// 文字列拡張(やねうら王独自) namespace StringExtension { // 大文字・小文字を無視して文字列の比較を行う。 @@ -1004,13 +1006,21 @@ namespace StringExtension extern std::string ToUpper(std::string const& value); // sを文字列spで分割した文字列集合を返す。 - extern std::vector Split(const std::string& s , const std::string& sep); + extern std::vector Split(std::string_view s, std::string_view delimiter); // Pythonの delemiter.join(v) みたいなの。 // 例: v = [1,2,3] に対して ' '.join(v) == "1 2 3" extern std::string Join(const std::vector& v , const std::string& delimiter); }; +// sを文字列spで分割した文字列集合を返す。 +// ※ Stockfishとの互換性のために用意。 +extern std::vector split(std::string_view s, std::string_view delimiter); + +// "123"みたいな文字列を123のように数値型(size_t)に変換する。 +extern size_t str_to_size_t(const std::string& s); + + // -------------------- // Concurrent // -------------------- diff --git a/source/numa.h b/source/numa.h new file mode 100644 index 000000000..f79bfb0fa --- /dev/null +++ b/source/numa.h @@ -0,0 +1,1642 @@ +/* + Stockfish, a UCI chess playing engine derived from Glaurung 2.1 + Copyright (C) 2004-2024 The Stockfish developers (see AUTHORS file) + + Stockfish is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Stockfish is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . +*/ + +#ifndef NUMA_H_INCLUDED +#define NUMA_H_INCLUDED + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "memory.h" + +// We support linux very well, but we explicitly do NOT support Android, +// because there is no affected systems, not worth maintaining. +#if defined(__linux__) && !defined(__ANDROID__) + #if !defined(_GNU_SOURCE) + #define _GNU_SOURCE + #endif + #include +#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 +static constexpr size_t WIN_PROCESSOR_GROUP_SIZE = 64; + + #if !defined(NOMINMAX) + #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); + +// https://learn.microsoft.com/en-us/windows/win32/api/processthreadsapi/nf-processthreadsapi-getthreadselectedcpusetmasks +using GetThreadSelectedCpuSetMasks_t = BOOL (*)(HANDLE, PGROUP_AFFINITY, USHORT, PUSHORT); + +#endif + +#include "misc.h" + +//namespace Stockfish { + +using CpuIndex = size_t; +using NumaIndex = size_t; + +// NUMAも考慮して、このPCで使えるスレッド数を返す。 +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. + + // Windowsで全てのプロセッサグループにまたがる全プロセッサを取得する。 + // なぜなら、hardware_concurrency() は最初のグループ内のプロセッサ数しか返さない。 + // これは std::thread で使用できるのがこのグループのプロセッサのみだからである。 + +#ifdef _WIN64 + concurrency = std::max(concurrency, GetActiveProcessorCount(ALL_PROCESSOR_GROUPS)); +#endif + + return concurrency; +} + +inline const CpuIndex SYSTEM_THREADS_NB = std::max(1, get_hardware_concurrency()); + +#if defined(_WIN64) + +struct WindowsAffinity { + std::optional> oldApi; + std::optional> newApi; + + // We also provide diagnostic for when the affinity is set to nullopt + // whether it was due to being indeterminate. If affinity is indeterminate + // it is best to assume it is not set at all, so consistent with the meaning + // of the nullopt affinity. + + // アフィニティが nullopt に設定された場合、その原因が不定であったかどうかの診断も提供します。 + // アフィニティが不定である場合、アフィニティが全く設定されていないと仮定するのが最善です。 + // これは、nullopt のアフィニティの意味と一貫性があります。 + + bool isNewDeterminate = true; + bool isOldDeterminate = true; + + std::optional> get_combined() const { + if (!oldApi.has_value()) + return newApi; + if (!newApi.has_value()) + return oldApi; + + std::set intersect; + std::set_intersection(oldApi->begin(), oldApi->end(), newApi->begin(), newApi->end(), + std::inserter(intersect, intersect.begin())); + return intersect; + } + + // Since Windows 11 and Windows Server 2022 thread affinities can span + // processor groups and can be set as such by a new WinAPI function. However, + // we may need to force using the old API if we detect that the process has + // affinity set by the old API already and we want to override that. Due to the + // limitations of the old API we cannot detect its use reliably. There will be + // cases where we detect not use but it has actually been used and vice versa. + + // Windows 11およびWindows Server 2022以降、スレッドのアフィニティはプロセッサグループをまたいで設定でき、 + // 新しいWinAPI関数でそのように設定可能です。しかし、既に古いAPIでアフィニティが設定されている場合、 + // それを上書きしたい場合には古いAPIを強制的に使用する必要があるかもしれません。 + // ただし、古いAPIの制限により、その使用を確実に検出することはできません。 + // 実際には使用されているのに検出できなかったり、逆に検出されたが実際には使用されていない場合が発生します。 + + bool likely_used_old_api() const { return oldApi.has_value() || !isOldDeterminate; } +}; + +inline std::pair> get_process_group_affinity() { + + // GetProcessGroupAffinity requires the GroupArray argument to be + // aligned to 4 bytes instead of just 2. + + // GetProcessGroupAffinity は、GroupArray 引数を2バイトではなく、 + // 4バイトにアライメントさせる必要があります。 + + static constexpr size_t GroupArrayMinimumAlignment = 4; + static_assert(GroupArrayMinimumAlignment >= alignof(USHORT)); + + // The function should succeed the second time, but it may fail if the group + // affinity has changed between GetProcessGroupAffinity calls. In such case + // we consider this a hard error, as we Cannot work with unstable affinities + // anyway. + + // この関数は2回目には成功するはずですが、GetProcessGroupAffinityの呼び出し間で + // グループアフィニティが変更された場合、失敗する可能性があります。 + // その場合、これを重大なエラーと見なし、不安定なアフィニティでは動作できないためです。 + + static constexpr int MAX_TRIES = 2; + USHORT GroupCount = 1; + for (int i = 0; i < MAX_TRIES; ++i) + { + auto GroupArray = std::make_unique( + GroupCount + (GroupArrayMinimumAlignment / alignof(USHORT) - 1)); + + USHORT* GroupArrayAligned = align_ptr_up(GroupArray.get()); + + const BOOL status = + GetProcessGroupAffinity(GetCurrentProcess(), &GroupCount, GroupArrayAligned); + + if (status == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + break; + } + + if (status != 0) + { + return std::make_pair(status, + std::vector(GroupArrayAligned, GroupArrayAligned + GroupCount)); + } + } + + return std::make_pair(0, std::vector()); +} + +// 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 cannot +// 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 + +// Windowsでは、アフィニティを設定する方法が2つあり、それに応じて取得する方法も2つあります。 +// これらは一貫性がないため、両方を確認する必要があります。 +// 場合によっては、アフィニティを特定することが実際に不可能なことがあります。 +// 例えば、異なるスレッドが異なるプロセッサグループに対してSetThreadAffinityMaskを使って +// アフィニティを設定している場合、実際のアフィニティを取得できません。 +// GetProcessAffinityMask のドキュメントには以下のように記載されています: +// > 呼び出し元プロセスが複数のグループにスレッドを含んでいる場合、 +// > 関数は両方のアフィニティマスクに対してゼロを返します。 +// こうした場合、すべてのプロセッサに対してアフィニティがあると仮定し、諦めます。 +// nullopt はアフィニティが設定されていないことを意味し、つまりすべてのプロセッサが許可されていることを示します。 + +inline WindowsAffinity get_process_affinity() { + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto GetThreadSelectedCpuSetMasks_f = GetThreadSelectedCpuSetMasks_t( + (void (*)()) GetProcAddress(k32, "GetThreadSelectedCpuSetMasks")); + + BOOL status = 0; + + WindowsAffinity affinity; + + if (GetThreadSelectedCpuSetMasks_f != nullptr) + { + USHORT RequiredMaskCount; + status = GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), nullptr, 0, &RequiredMaskCount); + + // We expect ERROR_INSUFFICIENT_BUFFER from GetThreadSelectedCpuSetMasks, + // but other failure is an actual error. + + // GetThreadSelectedCpuSetMasks からは ERROR_INSUFFICIENT_BUFFER を予期していますが、 + // それ以外の失敗は実際のエラーです。 + + if (status == 0 && GetLastError() != ERROR_INSUFFICIENT_BUFFER) + { + affinity.isNewDeterminate = false; + } + else if (RequiredMaskCount > 0) + { + // If RequiredMaskCount then these affinities were never set, but it's + // not consistent so GetProcessAffinityMask may still return some affinity. + + // RequiredMaskCount が非ゼロの場合、これらのアフィニティは設定されていなかったことを示しますが、 + // 一貫性がないため、GetProcessAffinityMask がいまだに何らかのアフィニティを返すことがあります。 + + auto groupAffinities = std::make_unique(RequiredMaskCount); + + status = GetThreadSelectedCpuSetMasks_f(GetCurrentThread(), groupAffinities.get(), + RequiredMaskCount, &RequiredMaskCount); + + if (status == 0) + { + affinity.isNewDeterminate = false; + } + else + { + std::set cpus; + + 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); + } + } + + affinity.newApi = std::move(cpus); + } + } + } + + // NOTE: There is no way to determine full affinity using the old API if + // individual threads set affinity on different processor groups. + + // 注意: 古いAPIでは、各スレッドが異なるプロセッサグループにアフィニティを設定している場合、 + // 完全なアフィニティを特定する方法はありません。 + + DWORD_PTR proc, sys; + status = GetProcessAffinityMask(GetCurrentProcess(), &proc, &sys); + + // If proc == 0 then we cannot determine affinity because it spans processor groups. + // On Windows 11 and Server 2022 it will instead + // > If, however, hHandle specifies a handle to the current process, the function + // > always uses the calling thread's primary group (which by default is the same + // > as the process' primary group) in order to set the + // > lpProcessAffinityMask and lpSystemAffinityMask. + // So it will never be indeterminate here. We can only make assumptions later. + + // proc が 0 の場合、アフィニティがプロセッサグループをまたがっているため、アフィニティを特定できません。 + // Windows 11 と Server 2022では、代わりに次のように動作します: + // > ただし、hHandle が現在のプロセスのハンドルを指定している場合、 + // > 関数は常に呼び出し元スレッドのプライマリグループ(デフォルトではプロセスのプライマリグループと同じ) + // > を使用して、lpProcessAffinityMask と lpSystemAffinityMask を設定します。 + // したがって、この場合アフィニティが不定になることはありません。この後は仮定に基づいて動作します。 + + if (status == 0 || proc == 0) + { + affinity.isOldDeterminate = false; + return affinity; + } + + // If SetProcessAffinityMask was never called the affinity must span + // all processor groups, but if it was called it must only span one. + + // SetProcessAffinityMask が一度も呼ばれていない場合、アフィニティはすべてのプロセッサグループに + // またがっているはずです。しかし、呼ばれていた場合は、1つのグループにのみ限定されているはずです。 + + std::vector groupAffinity; + // We need to capture this later and capturing + // from structured bindings requires c++20. + + // これを後でキャプチャする必要があり、構造化束縛からのキャプチャにはC++20が必要です。 + + std::tie(status, groupAffinity) = get_process_group_affinity(); + if (status == 0) + { + affinity.isOldDeterminate = false; + return affinity; + } + + if (groupAffinity.size() == 1) + { + // We detect the case when affinity is set to all processors and correctly + // leave affinity.oldApi as nullopt. + + // アフィニティがすべてのプロセッサに設定されている場合を検出し、 + // 正しく affinity.oldApi を nullopt のままにします。 + + if (GetActiveProcessorGroupCount() != 1 || proc != sys) + { + std::set cpus; + + const size_t procGroupIndex = groupAffinity[0]; + + const 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); + } + + affinity.oldApi = std::move(cpus); + } + } + else + { + // If we got here it means that either SetProcessAffinityMask was never set + // or we're on Windows 11/Server 2022. + + // Since Windows 11 and Windows Server 2022 the behaviour of + // GetProcessAffinityMask changed: + // > If, however, hHandle specifies a handle to the current process, + // > the function always uses the calling thread's primary group + // > (which by default is the same as the process' primary group) + // > in order to set the lpProcessAffinityMask and lpSystemAffinityMask. + // In which case we can actually retrieve the full affinity. + + // ここに到達したということは、SetProcessAffinityMask が一度も設定されていないか、 + // Windows 11/Server 2022 上で動作しているということです。 + + // Windows 11 および Windows Server 2022 以降、GetProcessAffinityMask の動作が変更されました: + // > ただし、hHandle が現在のプロセスのハンドルを指定している場合、 + // > 関数は常に呼び出し元スレッドのプライマリグループ + // > (デフォルトではプロセスのプライマリグループと同じ)を使用して + // > lpProcessAffinityMask および lpSystemAffinityMask を設定します。 + // この場合、実際に全てのアフィニティを取得できます。 + + if (GetThreadSelectedCpuSetMasks_f != nullptr) + { + std::thread th([&]() { + std::set cpus; + bool isAffinityFull = true; + + for (auto procGroupIndex : groupAffinity) + { + const int numActiveProcessors = + GetActiveProcessorCount(static_cast(procGroupIndex)); + + // We have to schedule to two different processors + // and & the affinities we get. Otherwise our processor + // choice could influence the resulting affinity. + // We assume the processor IDs within the group are + // filled sequentially from 0. + + // 2つの異なるプロセッサにスケジューリングし、取得したアフィニティを AND 演算する必要があります。 + // そうしないと、プロセッサの選択が結果のアフィニティに影響を与える可能性があります。 + // 同じグループ内のプロセッサIDは0から順に埋められていると仮定しています。 + + uint64_t procCombined = std::numeric_limits::max(); + uint64_t sysCombined = std::numeric_limits::max(); + + for (int i = 0; i < std::min(numActiveProcessors, 2); ++i) + { + GROUP_AFFINITY GroupAffinity; + std::memset(&GroupAffinity, 0, sizeof(GROUP_AFFINITY)); + GroupAffinity.Group = static_cast(procGroupIndex); + + GroupAffinity.Mask = static_cast(1) << i; + + status = + SetThreadGroupAffinity(GetCurrentThread(), &GroupAffinity, nullptr); + if (status == 0) + { + affinity.isOldDeterminate = false; + return; + } + + SwitchToThread(); + + DWORD_PTR proc2, sys2; + status = GetProcessAffinityMask(GetCurrentProcess(), &proc2, &sys2); + if (status == 0) + { + affinity.isOldDeterminate = false; + return; + } + + procCombined &= static_cast(proc2); + sysCombined &= static_cast(sys2); + } + + if (procCombined != sysCombined) + isAffinityFull = false; + + for (size_t j = 0; j < WIN_PROCESSOR_GROUP_SIZE; ++j) + { + if (procCombined & (KAFFINITY(1) << j)) + cpus.insert(procGroupIndex * WIN_PROCESSOR_GROUP_SIZE + j); + } + } + + // We have to detect the case where the affinity was not set, + // or is set to all processors so that we correctly produce as + // std::nullopt result. + + // アフィニティが設定されていない場合や、すべてのプロセッサに設定されている場合を検出し、 + // 正しく std::nullopt の結果を生成する必要があります。 + + if (!isAffinityFull) + { + affinity.oldApi = std::move(cpus); + } + }); + + th.join(); + } + } + + return affinity; +} + +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + +inline 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. + + // cpu_set_t はデフォルトで1024エントリを保持します。これは近い将来十分でない可能性がありますが、 + // 実際にいくつのスレッドがあるかを簡単に特定する方法はありません。 + // この場合、合理的な上限を選択します。 + + 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; +} + +#endif + +#if defined(__linux__) && !defined(__ANDROID__) + +inline static const auto STARTUP_PROCESSOR_AFFINITY = get_process_affinity(); + +#elif defined(_WIN64) + +inline static const auto STARTUP_PROCESSOR_AFFINITY = get_process_affinity(); +inline static const auto STARTUP_USE_OLD_AFFINITY_API = + STARTUP_PROCESSOR_AFFINITY.likely_used_old_api(); + +#endif + +// 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 +// machinery to be able to access NUMA replicated memory. + +// NUMAノードのインデックスを保存する目的をある程度抽象化したいと考えています。 +// これを使用する人は、NUMAで複製されたメモリにアクセスするために、 +// 複製メカニズムの詳細を知る必要はありません。 + +class NumaReplicatedAccessToken { + public: + NumaReplicatedAccessToken() : + n(0) {} + + explicit NumaReplicatedAccessToken(NumaIndex idx) : + n(idx) {} + + NumaIndex get_numa_index() const { return n; } + + private: + NumaIndex n; +}; + +// Designed as immutable, because there is no good reason to alter an already +// existing config 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. 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: every node exposed by NumaConfig has at least one processor assigned. +// +// We use startup affinities so as not to modify its own behaviour in time. +// +// Since Stockfish doesn't support exceptions all places where an exception +// should be thrown are replaced by std::exit. + +// これは不変(immutable)として設計されています。なぜなら、既存の設定を完全に再作成せずに +// 変更する理由がなく、クラスの不変条件を維持するのは複雑でコストがかかるためです。 +// CPU(プロセッサ)番号は常にシステムで使用されている実際の番号に対応しています。 +// NUMAノード番号はシステムのNUMAノード番号と一致しない場合があります。 +// 特に、空のノードが削除されたり、ユーザーがカスタムノードを作成したりすることがあります。 +// NumaConfigによって公開されているNUMAノードは必ず空ではなく、少なくとも1つのプロセッサが割り当てられています。 +// +// 初期のアフィニティを使用して、その動作が時間とともに変更されないようにします。 +// +// Stockfishは例外をサポートしていないため、例外がスローされるべき場所ではすべてstd::exitに置き換えています。 + +class NumaConfig { + public: + NumaConfig() : + highestCpuIndex(0), + customAffinity(false) { + const auto numCpus = SYSTEM_THREADS_NB; + add_cpu_range_to_node(NumaIndex{0}, CpuIndex{0}, numCpus - 1); + } + + // This function queries the system for the mapping of processors to NUMA nodes. + // 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. + + // この関数は、プロセッサとNUMAノードのマッピングをシステムに問い合わせます。 + // Linuxでは標準化されたカーネルのsysfsから読み取り、フォールバックとして単一のNUMAノードを使用します。 + // WindowsではGetNumaProcessorNodeExを使用しますが、これにはいくつかのクセがあります。 + // 詳細は、get_process_affinity のWindows実装のコメントを参照してください。 + + static NumaConfig from_system([[maybe_unused]] bool respectProcessAffinity = true) { + NumaConfig cfg = empty(); + +#if defined(__linux__) && !defined(__ANDROID__) + + std::set allowedCpus; + + if (respectProcessAffinity) + allowedCpus = STARTUP_PROCESSOR_AFFINITY; + + 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. + // We try to gather this information from the sysfs first + + // Linuxではプロセッサグループが存在せず、 + // すべてのスレッドが全てのプロセッサにスケジュールされるため、処理は単純です。 + // まず、sysfsからこの情報を取得しようとします。 + + // 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()) + { + 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(_WIN64) + + std::optional> allowedCpus; + + if (respectProcessAffinity) + allowedCpus = STARTUP_PROCESSOR_AFFINITY.get_combined(); + + // The affinity cannot 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. + + // Windowsでは、すべてのケースでアフィニティを特定できるわけではありませんが、 + // 少なくとも許可されたプロセッサの数がアフィニティマスク内のプロセッサ数以上であることは保証します。 + // ユーザーが満足しない場合、プロセッサ番号を明示的に設定する必要があります。 + + auto is_cpu_allowed = [&allowedCpus](CpuIndex c) { + return !allowedCpus.has_value() || allowedCpus->count(c) == 1; + }; + + WORD numProcGroups = GetActiveProcessorGroupCount(); + for (WORD procGroup = 0; procGroup < numProcGroups; ++procGroup) + { + for (BYTE number = 0; number < WIN_PROCESSOR_GROUP_SIZE; ++number) + { + PROCESSOR_NUMBER procnum; + procnum.Group = procGroup; + procnum.Number = number; + procnum.Reserved = 0; + USHORT nodeNumber; + + const BOOL status = GetNumaProcessorNodeEx(&procnum, &nodeNumber); + const CpuIndex c = static_cast(procGroup) * WIN_PROCESSOR_GROUP_SIZE + + static_cast(number); + if (status != 0 && nodeNumber != std::numeric_limits::max() + && is_cpu_allowed(c)) + { + cfg.add_cpu_to_node(nodeNumber, c); + } + } + } + + // Split the NUMA nodes to be contained within a group if necessary. + // This is needed between Windows 10 Build 20348 and Windows 11, because + // the new NUMA allocation behaviour was introduced while there was + // still no way to set thread affinity spanning multiple processor groups. + // See https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support + // We also do this is if need to force old API for some reason. + // + // 2024-08-26: It appears that we need to actually always force this behaviour. + // While Windows allows this to work now, such assignments have bad interaction + // with the scheduler - in particular it still prefers scheduling on the thread's + // "primary" node, even if it means scheduling SMT processors first. + // See https://github.com/official-stockfish/Stockfish/issues/5551 + // See https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups + // + // Each process is assigned a primary group at creation, and by default all + // of its threads' primary group is the same. Each thread's ideal processor + // is in the thread's primary group, so threads will preferentially be + // scheduled to processors on their primary group, but they are able to + // be scheduled to processors on any other group. + // + // used to be guarded by if (STARTUP_USE_OLD_AFFINITY_API) + + // 必要に応じて、NUMAノードをグループ内に分割します。 + // これは、Windows 10 Build 20348 から Windows 11 にかけて必要です。 + // なぜなら、新しいNUMA割り当ての動作が導入されたものの、複数のプロセッサグループにまたがる + // スレッドのアフィニティを設定する方法がまだなかったためです。 + // 詳細は https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support を参照してください。 + // また、何らかの理由で古いAPIを強制的に使用する必要がある場合にもこの処理を行います。 + // + // 2024-08-26: 実際には、この動作を常に強制する必要があるようです。 + // Windowsでは現在この動作が許可されていますが、そのような割り当てはスケジューラとの相性が悪くなります。 + // 特に、スレッドの「プライマリ」ノードでのスケジューリングが優先されるため、SMTプロセッサでの + // スケジューリングが優先されてしまうことがあります。 + // 詳細は https://github.com/official-stockfish/Stockfish/issues/5551 および + // https://learn.microsoft.com/en-us/windows/win32/procthread/processor-groups を参照してください。 + // + // 各プロセスは作成時にプライマリグループに割り当てられ、デフォルトではすべてのスレッドの + // プライマリグループは同じです。各スレッドの理想的なプロセッサはそのスレッドの + // プライマリグループ内にあり、スレッドはプライマリグループのプロセッサで優先的に + // スケジュールされますが、他のグループのプロセッサにもスケジュールされることが可能です。 + // + // 以前は、if (STARTUP_USE_OLD_AFFINITY_API) によってガードされていました。 + + { + NumaConfig splitCfg = empty(); + + NumaIndex splitNodeIndex = 0; + for (const auto& cpus : cfg.nodes) + { + if (cpus.empty()) + continue; + + size_t lastProcGroupIndex = *(cpus.begin()) / WIN_PROCESSOR_GROUP_SIZE; + for (CpuIndex c : cpus) + { + const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; + if (procGroupIndex != lastProcGroupIndex) + { + splitNodeIndex += 1; + lastProcGroupIndex = procGroupIndex; + } + splitCfg.add_cpu_to_node(splitNodeIndex, c); + } + splitNodeIndex += 1; + } + + cfg = std::move(splitCfg); + } + +#else + + // Fallback for unsupported systems. + // サポートされていないシステムのためのフォールバック処理。 + + for (CpuIndex c = 0; c < SYSTEM_THREADS_NB; ++c) + cfg.add_cpu_to_node(NumaIndex{0}, c); + +#endif + + // We have to ensure no empty NUMA nodes persist. + // 空のNUMAノードが残らないようにする必要があります。 + + cfg.remove_empty_numa_nodes(); + + // If the user explicitly opts out from respecting the current process affinity + // then it may be inconsistent with the current affinity (obviously), so we + // consider it custom. + + // ユーザーが現在のプロセスアフィニティを無視することを明示的に選択した場合、 + // それは現在のアフィニティと矛盾する可能性があります(当然のことですが)、 + // したがってそれをカスタムとして扱います。 + + if (!respectProcessAffinity) + cfg.customAffinity = true; + + return cfg; + } + + // ':'-separated numa nodes + // ','-separated cpu indices + // supports "first-last" range syntax for cpu indices + // For example "0-15,128-143:16-31,144-159:32-47,160-175:48-63,176-191" + + // NUMAノードは ':' で区切り + // CPUインデックスは ',' で区切り + // CPUインデックスには "first-last" 形式の範囲指定がサポートされています + // 例えば "0-15,128-143:16-31,144-159:32-47,160-175:48-63,176-191" + + static NumaConfig from_string(const std::string& s) { + NumaConfig cfg = empty(); + + NumaIndex n = 0; + for (auto&& nodeStr : split(s, ":")) + { + auto indices = indices_from_shortened_string(std::string(nodeStr)); + if (!indices.empty()) + { + for (auto idx : indices) + { + if (!cfg.add_cpu_to_node(n, CpuIndex(idx))) + std::exit(EXIT_FAILURE); + } + + n += 1; + } + } + + cfg.customAffinity = true; + + return cfg; + } + + NumaConfig(const NumaConfig&) = delete; + NumaConfig(NumaConfig&&) = default; + NumaConfig& operator=(const NumaConfig&) = delete; + NumaConfig& operator=(NumaConfig&&) = default; + + bool is_cpu_assigned(CpuIndex n) const { return nodeByCpu.count(n) == 1; } + + NumaIndex num_numa_nodes() const { return nodes.size(); } + + CpuIndex num_cpus_in_numa_node(NumaIndex n) const { + assert(n < nodes.size()); + return nodes[n].size(); + } + + CpuIndex num_cpus() const { return nodeByCpu.size(); } + + bool requires_memory_replication() const { return customAffinity || nodes.size() > 1; } + + std::string to_string() const { + std::string str; + + bool isFirstNode = true; + for (auto&& cpus : nodes) + { + if (!isFirstNode) + str += ":"; + + bool isFirstSet = true; + auto rangeStart = cpus.begin(); + for (auto it = cpus.begin(); it != cpus.end(); ++it) + { + auto next = std::next(it); + if (next == cpus.end() || *next != *it + 1) + { + // cpus[i] is at the end of the range (may be of size 1) + // cpus[i] は範囲の末尾に位置しています(サイズが1の場合もあります) + + if (!isFirstSet) + str += ","; + + const CpuIndex last = *it; + + if (it != rangeStart) + { + const CpuIndex first = *rangeStart; + + str += std::to_string(first); + str += "-"; + str += std::to_string(last); + } + else + str += std::to_string(last); + + rangeStart = next; + isFirstSet = false; + } + } + + isFirstNode = false; + } + + return str; + } + + bool suggests_binding_threads(CpuIndex numThreads) const { + // If we can reasonably determine that the threads cannot be contained + // by the OS within the first NUMA node then we advise distributing + // and binding threads. When the threads are not bound we can only use + // NUMA memory replicated objects from the first node, so when the OS + // has to schedule on other nodes we lose performance. We also suggest + // binding if there's enough threads to distribute among nodes with minimal + // disparity. We try to ignore small nodes, in particular the empty ones. + + // If the affinity set by the user does not match the affinity given by + // the OS then binding is necessary to ensure the threads are running on + // correct processors. + + // スレッドが最初のNUMAノード内に収まらないと合理的に判断できる場合、 + // スレッドを分散させ、バインドすることを推奨します。 + // スレッドがバインドされていない場合、NUMAメモリ複製オブジェクトは最初のノードからのみ使用可能であり、 + // OSが他のノードにスケジュールする必要がある場合、パフォーマンスが低下します。 + // また、スレッドがノード間で最小限の不均衡で分散可能な場合にも、バインドを推奨します。 + // 小さなノード、特に空のノードは無視するよう努めます。 + + // ユーザーが設定したアフィニティがOSによって提供されたアフィニティと一致しない場合、 + // スレッドが正しいプロセッサ上で実行されることを保証するためにバインドが必要です。 + + if (customAffinity) + return true; + + // We obviously cannot distribute a single thread, so a single thread + // should never be bound. + + // 明らかに1つのスレッドを分散させることはできないため、 + // 単一のスレッドは決してバインドされるべきではありません。 + + if (numThreads <= 1) + return false; + + size_t largestNodeSize = 0; + for (auto&& cpus : nodes) + if (cpus.size() > largestNodeSize) + largestNodeSize = cpus.size(); + + auto is_node_small = [largestNodeSize](const std::set& node) { + static constexpr double SmallNodeThreshold = 0.6; + return static_cast(node.size()) / static_cast(largestNodeSize) + <= SmallNodeThreshold; + }; + + size_t numNotSmallNodes = 0; + for (auto&& cpus : nodes) + if (!is_node_small(cpus)) + numNotSmallNodes += 1; + + return (numThreads > largestNodeSize / 2 || numThreads >= numNotSmallNodes * 4) + && nodes.size() > 1; + } + + std::vector distribute_threads_among_numa_nodes(CpuIndex numThreads) const { + std::vector ns; + + if (nodes.size() == 1) + { + // Special case for when there's no NUMA nodes. This doesn't buy us + // much, but let's keep the default path simple. + + // NUMAノードが存在しない場合の特別なケース。 + // これ自体はあまりメリットがありませんが、デフォルトの処理をシンプルに保つために対応します。 + + ns.resize(numThreads, NumaIndex{0}); + } + else + { + std::vector occupation(nodes.size(), 0); + for (CpuIndex c = 0; c < numThreads; ++c) + { + NumaIndex bestNode{0}; + float bestNodeFill = std::numeric_limits::max(); + for (NumaIndex n = 0; n < nodes.size(); ++n) + { + float fill = + static_cast(occupation[n] + 1) / static_cast(nodes[n].size()); + // NOTE: Do we want to perhaps fill the first available node + // up to 50% first before considering other nodes? + // Probably not, because it would interfere with running + // multiple instances. We basically shouldn't favor any + // particular node. + + // 注意: 最初に使用可能なノードを、他のノードを考慮する前に + // まず50%まで埋めるべきでしょうか? + // 多分そうすべきではありません。 + // なぜなら、複数のインスタンスを実行する際に干渉してしまうためです。 + // 基本的に、特定のノードを優先すべきではありません。 + + if (fill < bestNodeFill) + { + bestNode = n; + bestNodeFill = fill; + } + } + ns.emplace_back(bestNode); + occupation[bestNode] += 1; + } + } + + return ns; + } + + NumaReplicatedAccessToken bind_current_thread_to_numa_node(NumaIndex n) const { + if (n >= nodes.size() || nodes[n].size() == 0) + std::exit(EXIT_FAILURE); + +#if defined(__linux__) && !defined(__ANDROID__) + + cpu_set_t* mask = CPU_ALLOC(highestCpuIndex + 1); + if (mask == nullptr) + std::exit(EXIT_FAILURE); + + const size_t masksize = CPU_ALLOC_SIZE(highestCpuIndex + 1); + + CPU_ZERO_S(masksize, mask); + + for (CpuIndex c : nodes[n]) + CPU_SET_S(c, masksize, mask); + + const int status = sched_setaffinity(0, masksize, mask); + + CPU_FREE(mask); + + if (status != 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. + // This is defensive, allowed because this code is not performance critical. + sched_yield(); + +#elif defined(_WIN64) + + // Requires Windows 11. No good way to set thread affinity spanning + // processor groups before that. + + // このスレッドが再スケジュールされることを確認するために、一旦このスレッドを譲ります。 + // これは防御的な処理であり、このコードはパフォーマンスに関わる重要な部分ではないため許容されています。 + + HMODULE k32 = GetModuleHandle(TEXT("Kernel32.dll")); + auto SetThreadSelectedCpuSetMasks_f = SetThreadSelectedCpuSetMasks_t( + (void (*)()) GetProcAddress(k32, "SetThreadSelectedCpuSetMasks")); + + // We ALWAYS set affinity with the new API if available, because + // there's no downsides, and we forcibly keep it consistent with + // the old API should we need to use it. I.e. we always keep this + // as a superset of what we set with SetThreadGroupAffinity. + + // 新しいAPIが利用可能であれば、常にアフィニティを設定します。 + // なぜなら、デメリットがなく、必要に応じて古いAPIとの一貫性を強制的に保つからです。 + // つまり、SetThreadGroupAffinity で設定した内容を常にそのスーパーセットとして維持します。 + + if (SetThreadSelectedCpuSetMasks_f != nullptr) + { + // Only available on Windows 11 and Windows Server 2022 onwards + // Windows 11 および Windows Server 2022 以降でのみ利用可能 + + 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) + groupAffinities[i].Group = i; + + for (CpuIndex c : nodes[n]) + { + const size_t procGroupIndex = c / WIN_PROCESSOR_GROUP_SIZE; + const size_t idxWithinProcGroup = c % WIN_PROCESSOR_GROUP_SIZE; + groupAffinities[procGroupIndex].Mask |= KAFFINITY(1) << idxWithinProcGroup; + } + + HANDLE hThread = GetCurrentThread(); + + const BOOL status = + SetThreadSelectedCpuSetMasks_f(hThread, groupAffinities.get(), numProcGroups); + if (status == 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. + // This is defensive, allowed because this code is not performance critical. + + // このスレッドが再スケジュールされることを確認するために、一旦このスレッドを譲ります。 + // これは防御的な処理であり、このコードはパフォーマンスに関わる重要な部分ではないため許容されています。 + + SwitchToThread(); + } + + // Sometimes we need to force the old API, but do not use it unless necessary. + // 古いAPIを強制的に使用する必要がある場合もありますが、必要でない限り使用しないようにします。 + + if (SetThreadSelectedCpuSetMasks_f == nullptr || STARTUP_USE_OLD_AFFINITY_API) + { + // On earlier windows version (since windows 7) we cannot run a single thread + // on multiple processor groups, so we need to restrict the group. + // We assume the group of the first processor listed for this node. + // Processors from outside this group will not be assigned for this thread. + // Normally this won't be an issue because windows used to assign NUMA nodes + // such that they cannot span processor groups. However, since Windows 10 + // Build 20348 the behaviour changed, so there's a small window of versions + // between this and Windows 11 that might exhibit problems with not all + // processors being utilized. + // + // We handle this in NumaConfig::from_system by manually splitting the + // nodes when we detect that there is no function to set affinity spanning + // processor nodes. This is required because otherwise our thread distribution + // code may produce suboptimal results. + // + // Windows 7以降の初期のWindowsバージョンでは、1つのスレッドを複数のプロセッサグループにまたがって + // 実行することができないため、グループを制限する必要があります。 + // このノードにリストされている最初のプロセッサのグループを使用すると仮定します。 + // このグループ外のプロセッサは、このスレッドには割り当てられません。 + // 通常、これは問題にならないはずです。というのも、以前のWindowsではNUMAノードが + // プロセッサグループをまたがないように割り当てられていたからです。 + // しかし、Windows 10 Build 20348以降、この動作が変更されたため、Windows 11までのバージョン間で + // すべてのプロセッサが利用されない問題が発生する可能性があります。 + // + // この問題は、NumaConfig::from_system 内で、アフィニティをプロセッサノード間にまたがって設定する + // 関数がないことを検出した場合に、ノードを手動で分割することで対処しています。 + // これは、そうしないとスレッド分散コードが最適でない結果を生む可能性があるため、必要な処理です。 + + // See https://learn.microsoft.com/en-us/windows/win32/procthread/numa-support + GROUP_AFFINITY affinity; + std::memset(&affinity, 0, sizeof(GROUP_AFFINITY)); + + // We use an ordered set to be sure to get the smallest cpu number here. + // 最小のCPU番号を取得するために、順序付きセットを使用しています。 + 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 processor group. + // If everything was set up correctly this will never be an issue, + // but we have to account for bad NUMA node specification. + + // 同じプロセッサグループに属していないプロセッサはスキップします。 + // すべてが正しく設定されていれば問題にはなりませんが、不適切なNUMAノード指定が + // ある可能性も考慮する必要があります。 + + if (procGroupIndex != forcedProcGroupIndex) + continue; + + affinity.Mask |= KAFFINITY(1) << idxWithinProcGroup; + } + + HANDLE hThread = GetCurrentThread(); + + const BOOL status = SetThreadGroupAffinity(hThread, &affinity, nullptr); + if (status == 0) + std::exit(EXIT_FAILURE); + + // We yield this thread just to be sure it gets rescheduled. This is + // defensive, allowed because this code is not performance critical. + + // このスレッドが再スケジュールされることを確認するために、一旦このスレッドを譲ります。 + // これは防御的な処理であり、このコードはパフォーマンスに関わる重要な部分ではないため許容されています。 + + SwitchToThread(); + } + +#endif + + return NumaReplicatedAccessToken(n); + } + + template + void execute_on_numa_node(NumaIndex n, FuncT&& f) const { + std::thread th([this, &f, n]() { + bind_current_thread_to_numa_node(n); + std::forward(f)(); + }); + + th.join(); + } + + private: + std::vector> nodes; + std::map nodeByCpu; + CpuIndex highestCpuIndex; + + bool customAffinity; + + static NumaConfig empty() { return NumaConfig(EmptyNodeTag{}); } + + struct EmptyNodeTag {}; + + NumaConfig(EmptyNodeTag) : + highestCpuIndex(0), + customAffinity(false) {} + + void remove_empty_numa_nodes() { + std::vector> newNodes; + for (auto&& cpus : nodes) + if (!cpus.empty()) + newNodes.emplace_back(std::move(cpus)); + nodes = std::move(newNodes); + } + + // Returns true if successful + // Returns false if failed, i.e. when the cpu is already present + // strong guarantee, the structure remains unmodified + + // 成功した場合は true を返します。 + // 失敗した場合、すなわち、すでにCPUが存在する場合は false を返します。 + // 強い保証: 構造は変更されません。 + + bool add_cpu_to_node(NumaIndex n, CpuIndex c) { + if (is_cpu_assigned(c)) + return false; + + while (nodes.size() <= n) + nodes.emplace_back(); + + nodes[n].insert(c); + nodeByCpu[c] = n; + + if (c > highestCpuIndex) + highestCpuIndex = c; + + return true; + } + + // Returns true if successful + // Returns false if failed, i.e. when any of the cpus is already present + // strong guarantee, the structure remains unmodified + + // 成功した場合は true を返します。 + // 失敗した場合、すなわち、いずれかのCPUがすでに存在する場合は false を返します。 + // 強い保証: 構造は変更されません。 + + bool add_cpu_range_to_node(NumaIndex n, CpuIndex cfirst, CpuIndex clast) { + for (CpuIndex c = cfirst; c <= clast; ++c) + if (is_cpu_assigned(c)) + return false; + + while (nodes.size() <= n) + nodes.emplace_back(); + + for (CpuIndex c = cfirst; c <= clast; ++c) + { + nodes[n].insert(c); + nodeByCpu[c] = n; + } + + if (clast > highestCpuIndex) + highestCpuIndex = clast; + + return true; + } + + static std::vector indices_from_shortened_string(const std::string& s) { + std::vector indices; + + if (s.empty()) + return indices; + + for (const auto& ss : split(s, ",")) + { + if (ss.empty()) + continue; + + auto parts = split(ss, "-"); + if (parts.size() == 1) + { + const CpuIndex c = CpuIndex{str_to_size_t(std::string(parts[0]))}; + indices.emplace_back(c); + } + else if (parts.size() == 2) + { + const CpuIndex cfirst = CpuIndex{str_to_size_t(std::string(parts[0]))}; + const CpuIndex clast = CpuIndex{str_to_size_t(std::string(parts[1]))}; + for (size_t c = cfirst; c <= clast; ++c) + { + indices.emplace_back(c); + } + } + } + + return indices; + } +}; + +class NumaReplicationContext; + +// Instances of this class are tracked by the NumaReplicationContext instance. +// NumaReplicationContext informs all tracked instances when NUMA configuration changes. + +// このクラスのインスタンスは NumaReplicationContext インスタンスによって追跡されます。 +// NumaReplicationContext は、NUMA構成が変更されたときに、追跡されたすべてのインスタンスに通知します。 + +class NumaReplicatedBase { + public: + NumaReplicatedBase(NumaReplicationContext& ctx); + + NumaReplicatedBase(const NumaReplicatedBase&) = delete; + NumaReplicatedBase(NumaReplicatedBase&& other) noexcept; + + NumaReplicatedBase& operator=(const NumaReplicatedBase&) = delete; + NumaReplicatedBase& operator=(NumaReplicatedBase&& other) noexcept; + + virtual void on_numa_config_changed() = 0; + virtual ~NumaReplicatedBase(); + + const NumaConfig& get_numa_config() const; + + private: + NumaReplicationContext* context; +}; + +// We force boxing with a unique_ptr. If this becomes an issue due to added +// indirection we may need to add an option for a custom boxing type. When the +// NUMA config changes the value stored at the index 0 is replicated to other nodes. + +// unique_ptr を使用して強制的にボクシングします。もし追加の間接参照によって問題が生じる場合は、 +// カスタムボクシング型を追加するオプションが必要かもしれません。 +// NUMA構成が変更されると、インデックス0に格納された値が他のノードに複製されます。 + +template +class NumaReplicated: public NumaReplicatedBase { + public: + using ReplicatorFuncType = std::function; + + NumaReplicated(NumaReplicationContext& ctx) : + NumaReplicatedBase(ctx) { + replicate_from(T{}); + } + + NumaReplicated(NumaReplicationContext& ctx, T&& source) : + NumaReplicatedBase(ctx) { + replicate_from(std::move(source)); + } + + NumaReplicated(const NumaReplicated&) = delete; + NumaReplicated(NumaReplicated&& other) noexcept : + NumaReplicatedBase(std::move(other)), + instances(std::exchange(other.instances, {})) {} + + NumaReplicated& operator=(const NumaReplicated&) = delete; + NumaReplicated& operator=(NumaReplicated&& other) noexcept { + NumaReplicatedBase::operator=(*this, std::move(other)); + instances = std::exchange(other.instances, {}); + + return *this; + } + + NumaReplicated& operator=(T&& source) { + replicate_from(std::move(source)); + + return *this; + } + + ~NumaReplicated() override = default; + + const T& operator[](NumaReplicatedAccessToken token) const { + assert(token.get_numa_index() < instances.size()); + return *(instances[token.get_numa_index()]); + } + + const T& operator*() const { return *(instances[0]); } + + const T* operator->() const { return instances[0].get(); } + + template + void modify_and_replicate(FuncT&& f) { + auto source = std::move(instances[0]); + std::forward(f)(*source); + 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::move(instances[0]); + replicate_from(std::move(*source)); + } + + private: + std::vector> instances; + + void replicate_from(T&& source) { + instances.clear(); + + const NumaConfig& cfg = get_numa_config(); + if (cfg.requires_memory_replication()) + { + for (NumaIndex n = 0; n < cfg.num_numa_nodes(); ++n) + { + cfg.execute_on_numa_node( + n, [this, &source]() { instances.emplace_back(std::make_unique(source)); }); + } + } + else + { + assert(cfg.num_numa_nodes() == 1); + // We take advantage of the fact that replication is not required + // and reuse the source value, avoiding one copy operation. + + // 複製が不要であることを利用し、ソースの値を再利用してコピー操作を1回回避します。 + + instances.emplace_back(std::make_unique(std::move(source))); + } + } +}; + +// We force boxing with a unique_ptr. If this becomes an issue due to added +// indirection we may need to add an option for a custom boxing type. + +// unique_ptrを使用して強制的にボクシングします。追加の間接参照によって問題が発生する場合は、 +// カスタムボクシング型のオプションを追加する必要があるかもしれません。 + +template +class LazyNumaReplicated: public NumaReplicatedBase { + public: + using ReplicatorFuncType = std::function; + + LazyNumaReplicated(NumaReplicationContext& ctx) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(T{}); + } + + LazyNumaReplicated(NumaReplicationContext& ctx, T&& source) : + NumaReplicatedBase(ctx) { + prepare_replicate_from(std::move(source)); + } + + LazyNumaReplicated(const LazyNumaReplicated&) = delete; + LazyNumaReplicated(LazyNumaReplicated&& other) noexcept : + NumaReplicatedBase(std::move(other)), + instances(std::exchange(other.instances, {})) {} + + LazyNumaReplicated& operator=(const LazyNumaReplicated&) = delete; + LazyNumaReplicated& operator=(LazyNumaReplicated&& other) noexcept { + NumaReplicatedBase::operator=(*this, std::move(other)); + instances = std::exchange(other.instances, {}); + + return *this; + } + + LazyNumaReplicated& operator=(T&& source) { + prepare_replicate_from(std::move(source)); + + return *this; + } + + ~LazyNumaReplicated() 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].get(); } + + template + void modify_and_replicate(FuncT&& f) { + auto source = std::move(instances[0]); + std::forward(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::move(instances[0]); + prepare_replicate_from(std::move(*source)); + } + + private: + mutable std::vector> instances; + mutable std::mutex mutex; + + void ensure_present(NumaIndex idx) const { + assert(idx < instances.size()); + + if (instances[idx] != nullptr) + return; + + assert(idx != 0); + + std::unique_lock 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] = std::make_unique(*instances[0]); }); + } + + void prepare_replicate_from(T&& source) { + instances.clear(); + + const NumaConfig& cfg = get_numa_config(); + if (cfg.requires_memory_replication()) + { + assert(cfg.num_numa_nodes() > 0); + + // 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. + + // 最初のインスタンスが存在することを確認する必要があります。 + // ここではデータを正しいNUMAノードに再割り当てする必要があるため、move操作はできません。 + + cfg.execute_on_numa_node( + 0, [this, &source]() { instances.emplace_back(std::make_unique(source)); }); + + // Prepare others for lazy init. + // 他のインスタンスを遅延初期化に備えます。 + + instances.resize(cfg.num_numa_nodes()); + } + else + { + assert(cfg.num_numa_nodes() == 1); + + // We take advantage of the fact that replication is not required + // and reuse the source value, avoiding one copy operation. + + // 複製が不要であることを利用して、ソース値を再利用し、コピー操作を1回回避します。 + + instances.emplace_back(std::make_unique(std::move(source))); + } + } +}; + +class NumaReplicationContext { + public: + NumaReplicationContext(NumaConfig&& cfg) : + config(std::move(cfg)) {} + + NumaReplicationContext(const NumaReplicationContext&) = delete; + NumaReplicationContext(NumaReplicationContext&&) = delete; + + NumaReplicationContext& operator=(const NumaReplicationContext&) = delete; + NumaReplicationContext& operator=(NumaReplicationContext&&) = delete; + + ~NumaReplicationContext() { + + // The context must outlive replicated objects + // コンテキストは複製されたオブジェクトよりも長く生存している必要があります。 + + if (!trackedReplicatedObjects.empty()) + std::exit(EXIT_FAILURE); + } + + void attach(NumaReplicatedBase* obj) { + assert(trackedReplicatedObjects.count(obj) == 0); + trackedReplicatedObjects.insert(obj); + } + + void detach(NumaReplicatedBase* obj) { + assert(trackedReplicatedObjects.count(obj) == 1); + trackedReplicatedObjects.erase(obj); + } + + // oldObj may be invalid at this point + // この時点で oldObj は無効になっている可能性があります。 + + void move_attached([[maybe_unused]] NumaReplicatedBase* oldObj, NumaReplicatedBase* newObj) { + assert(trackedReplicatedObjects.count(oldObj) == 1); + assert(trackedReplicatedObjects.count(newObj) == 0); + trackedReplicatedObjects.erase(oldObj); + trackedReplicatedObjects.insert(newObj); + } + + void set_numa_config(NumaConfig&& cfg) { + config = std::move(cfg); + for (auto&& obj : trackedReplicatedObjects) + obj->on_numa_config_changed(); + } + + const NumaConfig& get_numa_config() const { return config; } + + private: + NumaConfig config; + + // std::set uses std::less by default, which is required for pointer comparison + // std::set はデフォルトで std::less を使用しており、ポインタの比較にはこれが必要です。 + + std::set trackedReplicatedObjects; +}; + +inline NumaReplicatedBase::NumaReplicatedBase(NumaReplicationContext& ctx) : + context(&ctx) { + context->attach(this); +} + +inline NumaReplicatedBase::NumaReplicatedBase(NumaReplicatedBase&& other) noexcept : + context(std::exchange(other.context, nullptr)) { + context->move_attached(&other, this); +} + +inline NumaReplicatedBase& NumaReplicatedBase::operator=(NumaReplicatedBase&& other) noexcept { + context = std::exchange(other.context, nullptr); + + context->move_attached(&other, this); + + return *this; +} + +inline NumaReplicatedBase::~NumaReplicatedBase() { + if (context != nullptr) + context->detach(this); +} + +inline const NumaConfig& NumaReplicatedBase::get_numa_config() const { + return context->get_numa_config(); +} + +//} // namespace Stockfish + + +#endif // #ifndef NUMA_H_INCLUDED diff --git a/source/search.h b/source/search.h index 267c926dd..b797775d7 100644 --- a/source/search.h +++ b/source/search.h @@ -7,8 +7,25 @@ #include "config.h" #include "misc.h" #include "movepick.h" +#include "numa.h" #include "position.h" +// ----------------------- +// 探索用の定数 +// ----------------------- + +// Different node types, used as a template parameter +// テンプレートパラメータとして使用される異なるノードタイプ +enum NodeType { + NonPV, + PV, + Root +}; + +class TranspositionTable; +class ThreadPool; +class OptionsMap; + // 探索関係 namespace Search { @@ -18,6 +35,13 @@ namespace Search { // 探索のときに使うStack // ----------------------- +// Stack struct keeps track of the information we need to remember from nodes +// shallower and deeper in the tree during the search. Each search thread has +// its own array of Stack objects, indexed by the current ply. + +// Stack構造体は、検索中にツリーの浅いノードや深いノードから記憶する必要がある情報を管理します。 +// 各検索スレッドは、現在の深さ(ply)に基づいてインデックスされた、独自のStackオブジェクトの配列を持っています。 + struct Stack { Move* pv; // PVへのポインター。RootMovesのvector pvを指している。 PieceToHistory* continuationHistory;// historyのうち、counter moveに関するhistoryへのポインタ。実体はThreadが持っている。 diff --git a/source/thread.h b/source/thread.h index c72282d8e..53a8597bf 100644 --- a/source/thread.h +++ b/source/thread.h @@ -9,6 +9,7 @@ #include #include "movepick.h" +#include "numa.h" #include "position.h" #include "search.h" #include "thread_win32_osx.h" @@ -19,13 +20,59 @@ #include "tt.h" #endif +// -------------------- +// スレッドの属するNumaを管理する +// -------------------- + +// Sometimes we don't want to actually bind the threads, but the recipient still +// needs to think it runs on *some* NUMA node, such that it can access structures +// that rely on NUMA node knowledge. This class encapsulates this optional process +// such that the recipient does not need to know whether the binding happened or not. + +// 時にはスレッドを実際にバインドしたくない場合もありますが、 +// 受け手側は、それが 何らかの NUMAノード上で実行されていると認識する必要があります。 +// これは、NUMAノードに関する情報を必要とする構造体にアクセスするためです。 +// このクラスは、このバインドが行われたかどうかを受け手が知る必要がないように、 +// このオプションのプロセスをカプセル化します。 + +class OptionalThreadToNumaNodeBinder { +public: + OptionalThreadToNumaNodeBinder(NumaIndex n) : + numaConfig(nullptr), + numaId(n) {} + + OptionalThreadToNumaNodeBinder(const NumaConfig& cfg, NumaIndex n) : + numaConfig(&cfg), + numaId(n) {} + + NumaReplicatedAccessToken operator()() const { + if (numaConfig != nullptr) + return numaConfig->bind_current_thread_to_numa_node(numaId); + else + return NumaReplicatedAccessToken(numaId); + } + +private: + const NumaConfig* numaConfig; + NumaIndex numaId; +}; + + // -------------------- // 探索時に用いるスレッド // -------------------- -// 探索時に用いる、それぞれのスレッド -// これを思考スレッド数だけ確保する。 -// ただしメインスレッドはこのclassを継承してMainThreadにして使う。 +// Abstraction of a thread. It contains a pointer to the worker and a native thread. +// After construction, the native thread is started with idle_loop() +// waiting for a signal to start searching. +// When the signal is received, the thread starts searching and when +// the search is finished, it goes back to idle_loop() waiting for a new signal. + +// (探索用の)スレッドの抽象化です。これはワーカーへのポインタとネイティブスレッドを含みます。 +// 構築後、ネイティブスレッドは idle_loop() で開始され、開始信号を待ちます。 +// 信号を受け取るとスレッドは検索を開始し、検索が終了すると再び idle_loop() に戻り、新しい信号を待ちます。 +// ⇨ 探索時に用いる、それぞれのスレッド。これを探索用スレッド数だけ確保する。 +// ただしメインスレッドはこのclassを継承してMainThreadにして使う。 class Thread { // exitフラグやsearchingフラグの状態を変更するときのmutex @@ -252,8 +299,10 @@ struct MainThread: public Thread // Threads(スレッドオブジェクト)はglobalに配置するし、スレッドの初期化の際には // スレッドが保持する思考エンジンが使う変数等がすべてが初期化されていて欲しいからである。 // スレッドの生成はset(options["Threads"])で行い、スレッドの終了はset(0)で行なう。 -struct ThreadPool +class ThreadPool { +public: + // mainスレッドに思考を開始させる。 void start_thinking(const Position& pos, StateListPtr& states , const Search::LimitsType& limits , bool ponderMode = false); diff --git a/source/tt.h b/source/tt.h index bb7069848..0e3d5ca85 100644 --- a/source/tt.h +++ b/source/tt.h @@ -63,7 +63,7 @@ struct TTEntry { #endif private: - friend struct TranspositionTable; + friend class TranspositionTable; // save()の内部実装用 void save_(TTEntry::KEY_TYPE key_for_ttentry, Value v, bool pv , Bound b, Depth d, Move m, Value ev); @@ -98,7 +98,7 @@ struct TTEntry { // TT_ENTRYをClusterSize個並べて、クラスターをつくる。 // このクラスターのTT_ENTRYは同じhash keyに対する保存場所である。(保存場所が被ったときに後続のTT_ENTRYを使う) // このクラスターが、clusterCount個だけ確保されている。 -struct TranspositionTable { +class TranspositionTable { // 1クラスターにおけるTTEntryの数 // TT_CLUSTER_SIZE == 2のとき、TTEntry 10bytes×3つ + 2(padding) = 32bytes diff --git a/source/usi_option.cpp b/source/usi_option.cpp index c2ff870a7..777785fa1 100644 --- a/source/usi_option.cpp +++ b/source/usi_option.cpp @@ -65,7 +65,7 @@ namespace USI { // ※ やねうら王独自改良 // スレッド数の変更やUSI_Hashのメモリ確保をそのハンドラでやってしまうと、 - // そのあとThreadIdOffsetや、LargePageEnableを送られても困ることになる。 + // そのあとLargePageEnableを送られても困ることになる。 // ゆえにこれらは、"isready"に対する応答で行うことにする。 // そもそもで言うとsetoptionに対してそんなに時間のかかることをするとGUI側がtimeoutになる懸念もある。 // Stockfishもこうすべきだと思う。 @@ -217,18 +217,6 @@ namespace USI { o["SkipLoadingEval"] << Option(false); #endif -#if defined(_WIN32) - // 3990XのようなWindows上で複数のプロセッサグループを持つCPUで、思考エンジンを同時起動したときに - // 同じプロセッサグループに割り当てられてしまうのを避けるために、スレッドオフセットを - // 指定できるようにしておく。 - // 例) 128スレッドあって、4つ思考エンジンを起動してそれぞれにThreads = 32を指定する場合、 - // それぞれの思考エンジンにはThreadIdOffset = 0,32,64,96をそれぞれ指定する。 - // (プロセッサグループは64論理コアごとに1つ作られる。上のケースでは、ThreadIdOffset = 0,0,64,64でも同じ意味。) - // ※ 1つのPCで複数の思考エンジンを同時に起動して対局させる場合はこれを適切に設定すべき。 - - o["ThreadIdOffset"] << Option(0, 0, std::thread::hardware_concurrency() - 1); -#endif - #if defined(_WIN64) // LargePageを有効化するか。 // これを無効化できないと自己対局の時に片側のエンジンだけがLargePageを使うことがあり、 @@ -468,7 +456,7 @@ namespace USI { // ";"で区切って複数指定できるものとする。 auto v = StringExtension::Split(options, ";"); for (auto line : v) - build_option(line); + build_option(string(line)); } // カレントフォルダに"engine_options.txt"(これは引数で指定されている)が