Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,12 @@ if(IRONPHOENIX_BUILD_TESTS)
target_compile_features(ironphoenix_search_stack PRIVATE cxx_std_20)
add_test(NAME ironphoenix_search_stack COMMAND ironphoenix_search_stack)

add_executable(ironphoenix_lazy_smp tests/lazy_smp.cpp)
target_link_libraries(ironphoenix_lazy_smp PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_lazy_smp PRIVATE cxx_std_20)
add_test(NAME ironphoenix_lazy_smp COMMAND ironphoenix_lazy_smp)
set_target_properties(ironphoenix_lazy_smp PROPERTIES FOLDER "Tests")

add_executable(ironphoenix_mate_stop tests/mate_stop.cpp)
target_link_libraries(ironphoenix_mate_stop PRIVATE IronPhoenix::Core)
target_compile_features(ironphoenix_mate_stop PRIVATE cxx_std_20)
Expand Down
36 changes: 36 additions & 0 deletions docs/lazy-smp.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# IronPhoenix Lazy SMP (4PC Teams)

Lazy SMP is being developed on `feature/lazy-smp` to avoid breaking the
independent IIR experiments or the stable `main` search.

Use `setoption name Threads value N` in UCI (`1..64`, default `1`).
For example:

```text
uci
setoption name Threads value 8
setoption name Hash value 512
position startpos
go movetime 10000
stop
```

The primary search handles time management, aspiration/PVS and final UCI
`bestmove`. Additional workers execute independent iterative-deepening
searches from the same four-player team position. Helpers start at staggered
depths to diversify their exploration and **do not emit** UCI output.
Workers have separate PV buffers, SearchStack and history. A shared TT
is protected by striped cluster mutexes (uncontended when `Threads=1`).

The reported `nodes`/`nps` include batched counts from helpers as well as
the main search. Fixed node budgets use this aggregate (up to a small
batching overshoot). Stopping or starting a new search joins every helper;
`setoption` and `ucinewgame` stop workers before changing hash or
configuration. `MultiPV` keeps one authoritative primary output and
uses standard search helpers to populate the shared TT.

**Note:** Multi-thread search can increase total throughput while hurting
single-thread NPS due to synchronization, cache, and CPU contention. Verify
strength via paired SPRT/benchmarks across thread counts. Test with a
reasonable Hash allocation: helper workers share a TT but have private
small allocation for initialization. Does not include IIR.
21 changes: 19 additions & 2 deletions include/ironphoenix/search.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
#include <cstddef>
#include <cstdint>
#include <iosfwd>
#include <memory>
#include <mutex>
#include <thread>

Expand Down Expand Up @@ -40,8 +41,9 @@ namespace ironphoenix {
class SearchEngine {
public:
static constexpr int MAX_MULTI_PV = 32;
static constexpr int MAX_THREADS = 64;

SearchEngine();
explicit SearchEngine(bool helper = false);
~SearchEngine();

SearchEngine(const SearchEngine&) = delete;
Expand All @@ -59,6 +61,9 @@ namespace ironphoenix {

[[nodiscard]] bool searching() const noexcept { return searching_.load(std::memory_order_relaxed); }

bool setThreads(int threads);
[[nodiscard]] int threads() const noexcept { return threads_; }

bool setHashSizeMB(std::size_t megabytes);
[[nodiscard]] std::size_t hashSizeMB() const noexcept { return tt_.sizeMB(); }
void clearHash() noexcept;
Expand All @@ -82,7 +87,16 @@ namespace ironphoenix {
static constexpr int MATE_THRESHOLD = 29000;
static constexpr std::uint64_t TIME_CHECK_NODE_INTERVAL = 1024;

TranspositionTable tt_{ 64 };
TranspositionTable tt_;
// Helpers share only the primary's synchronized TT; all search state
// and history remain worker-local.
TranspositionTable* activeTT_ = &tt_;
std::atomic<std::uint64_t> groupNodes_{0};
std::atomic<std::uint64_t>* sharedNodes_ = nullptr;
const std::atomic<bool>* groupStop_ = nullptr;
bool helperMode_ = false;
int helperIndex_ = 0;
int threads_ = 1;
HistoryTables history_{};
std::thread worker_;
std::atomic<bool> stopRequested_{ false };
Expand All @@ -105,7 +119,10 @@ namespace ironphoenix {
std::array<SearchStackEntry, MAX_PLY> searchStack_{};
mutable std::mutex outputMutex_;

void runWithHelpers(Position position, int multiPV, std::ostream& out);
void run(Position position, std::ostream& out);
void countNode() noexcept;
[[nodiscard]] std::uint64_t totalNodes() const noexcept;
void runMultiPV(Position position, int multiPV, std::ostream& out);

int negamax(Position& pos, int depth, int alpha, int beta, int ply, bool pvNode, bool allowNull = true);
Expand Down
12 changes: 12 additions & 0 deletions include/ironphoenix/tt.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@

#include "move.hpp"

#include <array>
#include <cstddef>
#include <cstdint>
#include <mutex>
#include <vector>

namespace ironphoenix {
Expand Down Expand Up @@ -34,6 +36,10 @@ class TranspositionTable {
void clear() noexcept;
void newGeneration() noexcept;

// Enable striped cluster locks before launching any SMP worker.
// Never change this setting while a search is active.
void setConcurrent(bool enabled) noexcept { concurrent_ = enabled; }

[[nodiscard]] bool probe(Key key, int ply, TTProbe& out) const noexcept;
void store(Key key,
int depth,
Expand Down Expand Up @@ -67,6 +73,12 @@ class TranspositionTable {
std::size_t sizeMB_ = 0;
std::uint8_t generation_ = 0;

// A 64-byte cluster is a coherent TT update unit. Striped locks prevent
// torn concurrent reads/writes without slowing down Threads=1 searches.
static constexpr std::size_t LOCK_STRIPES = 4096;
mutable std::array<std::mutex, LOCK_STRIPES> locks_{};
bool concurrent_ = false;

[[nodiscard]] static int scoreToTT(int score, int ply) noexcept;
[[nodiscard]] static int scoreFromTT(int score, int ply) noexcept;
[[nodiscard]] static TTBound boundOf(const Entry& e) noexcept;
Expand Down
17 changes: 10 additions & 7 deletions src/multipv.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ void SearchEngine::startMultiPV(
stopRequested_.store(false, std::memory_order_relaxed);
ponderHitRequested_.store(false, std::memory_order_relaxed);
searching_.store(true, std::memory_order_relaxed);
tt_.setConcurrent(threads_ > 1);
groupNodes_.store(0, std::memory_order_relaxed);
sharedNodes_ = threads_ > 1 ? &groupNodes_ : nullptr;
tt_.newGeneration();

worker_ = std::thread([
Expand All @@ -50,7 +53,7 @@ void SearchEngine::startMultiPV(
&out]() mutable {

limits_ = limits;
runMultiPV(std::move(position), multiPV, out);
runWithHelpers(std::move(position), multiPV, out);
searching_.store(false, std::memory_order_relaxed);
});
}
Expand All @@ -65,8 +68,8 @@ void SearchEngine::emitMultiPVInfo(

const std::int64_t ms = elapsedMs();
const std::uint64_t nps = ms > 0
? nodes_ * 1000ull / static_cast<std::uint64_t>(ms)
: nodes_ * 1000ull;
? totalNodes() * 1000ull / static_cast<std::uint64_t>(ms)
: totalNodes() * 1000ull;

std::ostringstream line;
line << "info depth " << depth
Expand All @@ -83,9 +86,9 @@ void SearchEngine::emitMultiPVInfo(
line << " score cp " << score;
}

line << " nodes " << nodes_
line << " nodes " << totalNodes()
<< " nps " << nps
<< " hashfull " << tt_.hashfull()
<< " hashfull " << activeTT_->hashfull()
<< " time " << ms
<< " pv";

Expand Down Expand Up @@ -140,7 +143,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out)
bool havePreviousIteration = false;
int stableBestDepths = 0;
std::vector<RootLine> lastComplete;
int depth = 1;
int depth = helperMode_ ? 1 + (helperIndex_ % 3) : 1;

while (!shouldStop()) {
if (requestedDepth > 0 && depth > requestedDepth)
Expand Down Expand Up @@ -284,7 +287,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out)

if (requestedDepth > 0 && depth == requestedDepth)
break;
if (limits_.nodes > 0 && nodes_ >= limits_.nodes)
if (limits_.nodes > 0 && totalNodes() >= limits_.nodes)
break;
if (iterationTimeExpired(stableBestDepths, bestMoveChanged, scoreDelta))
break;
Expand Down
Loading
Loading