From ea7d81e5217495b2e54d78c312026bcb5b031d65 Mon Sep 17 00:00:00 2001 From: Nick Date: Fri, 23 Oct 2026 07:42:41 -0500 Subject: [PATCH] Search: implement Lazy SMP with shared synchronized TT and configurable UCI Threads Add worker-local search instances with staggered iterative deepening, striped TT locks, aggregated node accounting, cooperative stop/join, isolated helper UCI output, MultiPV support, and threading regression tests. Keep IIR outside main. --- CMakeLists.txt | 6 ++ docs/lazy-smp.md | 36 ++++++++ include/ironphoenix/search.hpp | 21 ++++- include/ironphoenix/tt.hpp | 12 +++ src/multipv.cpp | 17 ++-- src/search.cpp | 116 +++++++++++++++++++++----- src/tt.cpp | 12 ++- src/uci.cpp | 16 ++++ tests/lazy_smp.cpp | 146 +++++++++++++++++++++++++++++++++ 9 files changed, 352 insertions(+), 30 deletions(-) create mode 100644 docs/lazy-smp.md create mode 100644 tests/lazy_smp.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index d03a6a9..8372b9f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/docs/lazy-smp.md b/docs/lazy-smp.md new file mode 100644 index 0000000..916f3d0 --- /dev/null +++ b/docs/lazy-smp.md @@ -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. diff --git a/include/ironphoenix/search.hpp b/include/ironphoenix/search.hpp index 84da1c4..c638319 100644 --- a/include/ironphoenix/search.hpp +++ b/include/ironphoenix/search.hpp @@ -11,6 +11,7 @@ #include #include #include +#include #include #include @@ -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; @@ -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; @@ -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 groupNodes_{0}; + std::atomic* sharedNodes_ = nullptr; + const std::atomic* groupStop_ = nullptr; + bool helperMode_ = false; + int helperIndex_ = 0; + int threads_ = 1; HistoryTables history_{}; std::thread worker_; std::atomic stopRequested_{ false }; @@ -105,7 +119,10 @@ namespace ironphoenix { std::array 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); diff --git a/include/ironphoenix/tt.hpp b/include/ironphoenix/tt.hpp index 5ef0220..7b31d77 100644 --- a/include/ironphoenix/tt.hpp +++ b/include/ironphoenix/tt.hpp @@ -2,8 +2,10 @@ #include "move.hpp" +#include #include #include +#include #include namespace ironphoenix { @@ -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, @@ -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 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; diff --git a/src/multipv.cpp b/src/multipv.cpp index 428c26f..f72b89c 100644 --- a/src/multipv.cpp +++ b/src/multipv.cpp @@ -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([ @@ -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); }); } @@ -65,8 +68,8 @@ void SearchEngine::emitMultiPVInfo( const std::int64_t ms = elapsedMs(); const std::uint64_t nps = ms > 0 - ? nodes_ * 1000ull / static_cast(ms) - : nodes_ * 1000ull; + ? totalNodes() * 1000ull / static_cast(ms) + : totalNodes() * 1000ull; std::ostringstream line; line << "info depth " << depth @@ -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"; @@ -140,7 +143,7 @@ void SearchEngine::runMultiPV(Position position, int multiPV, std::ostream& out) bool havePreviousIteration = false; int stableBestDepths = 0; std::vector lastComplete; - int depth = 1; + int depth = helperMode_ ? 1 + (helperIndex_ % 3) : 1; while (!shouldStop()) { if (requestedDepth > 0 && depth > requestedDepth) @@ -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; diff --git a/src/search.cpp b/src/search.cpp index 770f84c..640ca22 100644 --- a/src/search.cpp +++ b/src/search.cpp @@ -9,7 +9,11 @@ #include #include +#include +#include #include +#include +#include #include #include @@ -53,12 +57,21 @@ namespace ironphoenix { } - SearchEngine::SearchEngine() = default; + SearchEngine::SearchEngine(bool helper) + : tt_(helper ? 1u : 64u), helperMode_(helper) {} SearchEngine::~SearchEngine() { stopAndWait(); } + bool SearchEngine::setThreads(int threads) { + if (threads < 1 || threads > MAX_THREADS) + return false; + stopAndWait(); + threads_ = threads; + return true; + } + bool SearchEngine::setHashSizeMB(std::size_t megabytes) { stopAndWait(); return tt_.resizeMB(megabytes); @@ -80,15 +93,79 @@ namespace ironphoenix { stopRequested_.store(false, std::memory_order_relaxed); ponderHitRequested_.store(false, std::memory_order_relaxed); searching_.store(true, std::memory_order_relaxed); + if (!helperMode_) { + tt_.setConcurrent(threads_ > 1); + groupNodes_.store(0, std::memory_order_relaxed); + sharedNodes_ = threads_ > 1 ? &groupNodes_ : nullptr; + } tt_.newGeneration(); worker_ = std::thread([this, position = std::move(position), limits, &out]() mutable { limits_ = limits; - run(std::move(position), out); + runWithHelpers(std::move(position), 1, out); searching_.store(false, std::memory_order_relaxed); }); } + namespace { + // Helpers must never emit info/bestmove onto the primary UCI stream. + class NullStreamBuffer final : public std::streambuf { + protected: + int_type overflow(int_type ch) override { + return traits_type::not_eof(ch); + } + }; + } + + void SearchEngine::runWithHelpers(Position position, int multiPV, std::ostream& out) { + struct Sink { + NullStreamBuffer buffer; + std::ostream stream{&buffer}; + }; + // Each helper writes only to its own discarded stream. + std::vector> sinks; + std::vector> helpers; + if (!helperMode_ && threads_ > 1) { + sinks.reserve(static_cast(threads_ - 1)); + helpers.reserve(static_cast(threads_ - 1)); + for (int i = 1; i < threads_; ++i) { + auto sink = std::make_unique(); + auto helper = std::make_unique(true); + helper->activeTT_ = &tt_; + helper->groupStop_ = &stopRequested_; + helper->sharedNodes_ = &groupNodes_; + helper->helperIndex_ = i; + // Independent iterative deepening with staggered root depths + // creates natural Lazy SMP diversity; only main emits moves. + helper->start(position, limits_, sink->stream); + sinks.push_back(std::move(sink)); + helpers.push_back(std::move(helper)); + } + } + + if (multiPV > 1) + runMultiPV(std::move(position), multiPV, out); + else + run(std::move(position), out); + + // Stop and join every helper before releasing the shared table/stream. + for (auto& helper : helpers) + helper->stopAndWait(); + } + + void SearchEngine::countNode() noexcept { + ++nodes_; + // Keep atomic contention out of the hot loop by reporting in batches. + if (sharedNodes_ && (nodes_ & 255u) == 0) + sharedNodes_->fetch_add(256u, std::memory_order_relaxed); + } + + std::uint64_t SearchEngine::totalNodes() const noexcept { + return sharedNodes_ + ? sharedNodes_->load(std::memory_order_relaxed) + (nodes_ & 255u) + : nodes_; + } + void SearchEngine::stop() noexcept { stopRequested_.store(true, std::memory_order_relaxed); } @@ -242,10 +319,11 @@ namespace ironphoenix { } bool SearchEngine::shouldStop() noexcept { - if (stopRequested_.load(std::memory_order_relaxed)) + if (stopRequested_.load(std::memory_order_relaxed) + || (groupStop_ && groupStop_->load(std::memory_order_relaxed))) return true; - if (limits_.nodes > 0 && nodes_ >= limits_.nodes) { + if (limits_.nodes > 0 && totalNodes() >= limits_.nodes) { stopRequested_.store(true, std::memory_order_relaxed); return true; } @@ -500,7 +578,7 @@ namespace ironphoenix { auto& ss = searchStack_[ply]; ss.beginNode(pos.sideToMove(), 0, pvNode); - ++nodes_; + countNode(); selDepth_ = std::max(selDepth_, ply); pvLength_[ply] = ply; @@ -518,7 +596,7 @@ namespace ironphoenix { ss.inCheck = inCheckNode; TTProbe ttProbe; - const bool ttHit = tt_.probe(pos.key(), ply, ttProbe); + const bool ttHit = activeTT_->probe(pos.key(), ply, ttProbe); ss.recordTT(ttProbe, ttHit); const Move ttMove = ss.ttMove; @@ -539,7 +617,7 @@ namespace ironphoenix { recordStackEvaluation(ply, bestScore); if (bestScore >= beta) { - tt_.store(pos.key(), 0, bestScore, TTBound::Lower, Move{}, pvNode, ply); + activeTT_->store(pos.key(), 0, bestScore, TTBound::Lower, Move{}, pvNode, ply); return bestScore; } @@ -614,7 +692,7 @@ namespace ironphoenix { pvLength_[ply] = std::max(ply + 1, pvLength_[ply + 1]); if (alpha >= beta) { - tt_.store(pos.key(), 0, bestScore, TTBound::Lower, bestMove, pvNode, ply); + activeTT_->store(pos.key(), 0, bestScore, TTBound::Lower, bestMove, pvNode, ply); return bestScore; } } @@ -625,7 +703,7 @@ namespace ironphoenix { if (inCheckNode && legalMoves == 0) { const int mateScore = -MATE_SCORE + ply; - tt_.store(pos.key(), 0, mateScore, TTBound::Exact, Move{}, pvNode, ply); + activeTT_->store(pos.key(), 0, mateScore, TTBound::Exact, Move{}, pvNode, ply); return mateScore; } @@ -640,7 +718,7 @@ namespace ironphoenix { else if (bestScore >= beta) bound = TTBound::Lower; - tt_.store(pos.key(), 0, bestScore, bound, bestMove, pvNode, ply); + activeTT_->store(pos.key(), 0, bestScore, bound, bestMove, pvNode, ply); return bestScore; } @@ -699,7 +777,7 @@ namespace ironphoenix { auto& ss = searchStack_[ply]; ss.beginNode(pos.sideToMove(), depth, pvNode); - ++nodes_; + countNode(); selDepth_ = std::max(selDepth_, ply); pvLength_[ply] = ply; @@ -713,7 +791,7 @@ namespace ironphoenix { const int originalAlpha = alpha; TTProbe ttProbe; - const bool ttHit = tt_.probe(pos.key(), ply, ttProbe); + const bool ttHit = activeTT_->probe(pos.key(), ply, ttProbe); ss.recordTT(ttProbe, ttHit); const Move ttMove = ss.ttMove; @@ -1022,7 +1100,7 @@ namespace ironphoenix { else if (bestScore >= beta) bound = TTBound::Lower; - tt_.store(pos.key(), depth, bestScore, bound, bestMove, pvNode, ply); + activeTT_->store(pos.key(), depth, bestScore, bound, bestMove, pvNode, ply); return bestScore; } @@ -1038,8 +1116,8 @@ namespace ironphoenix { const std::uint64_t nps = ms > 0 - ? nodes_ * 1000ull / static_cast(ms) - : nodes_ * 1000ull; + ? totalNodes() * 1000ull / static_cast(ms) + : totalNodes() * 1000ull; std::ostringstream line; @@ -1064,9 +1142,9 @@ namespace ironphoenix { else if (bound == ScoreBound::Upper) line << " upperbound"; - line << " nodes " << nodes_ + line << " nodes " << totalNodes() << " nps " << nps - << " hashfull " << tt_.hashfull() + << " hashfull " << activeTT_->hashfull() << " time " << ms << " pv"; @@ -1125,7 +1203,7 @@ namespace ironphoenix { const auto& tuning = Tuning::search(); const int requestedDepth = limits_.depth > 0 ? std::min(limits_.depth, MAX_PLY - 1) : 0; - int depth = 1; + int depth = helperMode_ ? 1 + (helperIndex_ % 3) : 1; while (!shouldStop()) { if (requestedDepth > 0 && depth > requestedDepth) @@ -1289,7 +1367,7 @@ namespace ironphoenix { 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; diff --git a/src/tt.cpp b/src/tt.cpp index 8d03866..40b7855 100644 --- a/src/tt.cpp +++ b/src/tt.cpp @@ -86,7 +86,10 @@ bool TranspositionTable::probe(Key key, int ply, TTProbe& out) const noexcept { if (table_.empty()) return false; - const Cluster& cluster = table_[static_cast(key) & mask_]; + const std::size_t index = static_cast(key) & mask_; + std::unique_lock guard(locks_[index % LOCK_STRIPES], std::defer_lock); + if (concurrent_) guard.lock(); + const Cluster& cluster = table_[index]; for (const Entry& entry : cluster.entry) { if (entry.key == key && entry.key != 0) { out.hit = true; @@ -111,7 +114,10 @@ void TranspositionTable::store(Key key, if (table_.empty()) return; - Cluster& cluster = table_[static_cast(key) & mask_]; + const std::size_t index = static_cast(key) & mask_; + std::unique_lock guard(locks_[index % LOCK_STRIPES], std::defer_lock); + if (concurrent_) guard.lock(); + Cluster& cluster = table_[index]; Entry* replace = &cluster.entry[0]; for (Entry& entry : cluster.entry) { @@ -155,6 +161,8 @@ int TranspositionTable::hashfull() const noexcept { std::size_t used = 0; std::size_t total = samples * 4; for (std::size_t i = 0; i < samples; ++i) { + std::unique_lock guard(locks_[i % LOCK_STRIPES], std::defer_lock); + if (concurrent_) guard.lock(); for (const Entry& entry : table_[i].entry) { if (entry.key != 0 && generationOf(entry) == generation_) ++used; diff --git a/src/uci.cpp b/src/uci.cpp index 4742d39..d2024ca 100644 --- a/src/uci.cpp +++ b/src/uci.cpp @@ -255,6 +255,7 @@ namespace ironphoenix { " setoption name Setup value \n" " setoption name StartFEN value \n" " setoption name MultiPV value <1..32>\n" + " setoption name Threads value <1..64>\n" " setoption name Ponder value \n" " setoption name Move Overhead value <0..5000>\n" " setoption name EvalFile value \n" @@ -361,6 +362,20 @@ namespace ironphoenix { return true; } + constexpr std::string_view threadsPrefix = "name Threads value "; + if (startsWith(args, threadsPrefix)) { + std::int64_t value = 0; + if (!parseNonNegative64(trim(args.substr(threadsPrefix.size())), value) + || value < 1 || value > SearchEngine::MAX_THREADS) { + out << "info string Threads must be between 1 and " + << SearchEngine::MAX_THREADS << '\n'; + return false; + } + search_.setThreads(static_cast(value)); + out << "info string Threads set to " << search_.threads() << '\n'; + return true; + } + constexpr std::string_view multiPvPrefix = "name MultiPV value "; if (startsWith(args, multiPvPrefix)) { std::int64_t value = 0; @@ -666,6 +681,7 @@ namespace ironphoenix { << "option name Setup type combo default Modern var Modern var Classic var BY var BYG var RG var Custom\n" << "option name StartFEN type string default \n" << "option name Hash type spin default 64 min 1 max 4096\n" + << "option name Threads type spin default 1 min 1 max " << SearchEngine::MAX_THREADS << "\n" << "option name MultiPV type spin default 1 min 1 max " << SearchEngine::MAX_MULTI_PV << "\n" << "option name Ponder type check default false\n" << "option name Move Overhead type spin default 50 min 0 max 5000\n" diff --git a/tests/lazy_smp.cpp b/tests/lazy_smp.cpp new file mode 100644 index 0000000..0fcec4f --- /dev/null +++ b/tests/lazy_smp.cpp @@ -0,0 +1,146 @@ +#include "ironphoenix/geometry.hpp" +#include "ironphoenix/movegen.hpp" +#include "ironphoenix/search.hpp" +#include "ironphoenix/tt.hpp" +#include "ironphoenix/uci.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace ironphoenix; +namespace { + +int failures = 0; +void check(bool ok, const char* reason) { + if (!ok) { std::cerr << "FAIL: " << reason << '\n'; ++failures; } +} +Square sq(char f, int r) { return Geometry::square(f-'a', r-1); } + +Position position() { + Position p; + p.clear(); + p.placePiece(sq('g',1), makePiece(RED, KING)); + p.placePiece(sq('a',7), makePiece(BLUE, KING)); + p.placePiece(sq('g',14), makePiece(YELLOW, KING)); + p.placePiece(sq('n',7), makePiece(GREEN, KING)); + p.placePiece(sq('e',4), makePiece(RED, BISHOP)); + p.placePiece(sq('h',11), makePiece(YELLOW, BISHOP)); + p.placePiece(sq('c',6), makePiece(BLUE, ROOK)); + p.placePiece(sq('l',9), makePiece(GREEN, ROOK)); + p.placePiece(sq('f',3), makePiece(RED, PAWN)); + p.placePiece(sq('i',12), makePiece(YELLOW, PAWN)); + p.placePiece(sq('c',5), makePiece(BLUE, PAWN)); + p.placePiece(sq('l',10), makePiece(GREEN, PAWN)); + p.setSideToMove(RED); + p.finalizeSetup(); + return p; +} + +bool waitDone(SearchEngine& engine, int attempts=700) { + for(int i=0; i(); + check(engine->setThreads(threads), "valid Threads accepted"); + check(engine->threads()==threads, "Threads stores value"); + const Position pos=position(); + const auto key=pos.key(); + SearchLimits limits; + limits.nodes=12000; + std::ostringstream out; + if(multiPV) engine->startMultiPV(pos, limits, 2, out); + else engine->start(pos, limits, out); + const bool finished=waitDone(*engine); + engine->stopAndWait(); + check(finished, "SMP search finishes with node budget"); + check(count(out.str(), "bestmove ")==1, "one and only one bestmove"); + check(out.str().find("info depth ")!=std::string::npos, "primary emits depth info"); + if(multiPV) check(out.str().find("multipv 1")!=std::string::npos,"MultiPV primary line"); + check(pos.key()==key && pos.verify(), "caller position unchanged"); +} + +void testStop() { + auto engine=std::make_unique(); + engine->setThreads(3); + SearchLimits limits; + limits.infinite=true; + std::ostringstream out; + engine->start(position(),limits,out); + std::this_thread::sleep_for(std::chrono::milliseconds(40)); + engine->stopAndWait(); + check(!engine->searching(), "stop joins all helpers"); + check(count(out.str(),"bestmove ")==1,"stop produces one bestmove"); + engine->newGame(); + check(engine->setThreads(2),"Threads can change after stop"); + check(engine->setHashSizeMB(16),"Hash safely resizes when stopped"); +} + +void testTT() { + TranspositionTable tt(1); + tt.setConcurrent(true); + std::atomic start{false}; + std::vector threads; + for(int worker=0;worker<4;++worker){ + threads.emplace_back([&tt,&start,worker]{ + while(!start.load(std::memory_order_acquire)) std::this_thread::yield(); + for(int i=0;i<8000;++i){ + const Key key=static_cast(1+(i*71+worker*47)%8192); + tt.store(key,i%24,100+(i%250),TTBound::Lower,Move{},false,1); + TTProbe found{}; + if(tt.probe(key,1,found)) { + if(found.bound!=TTBound::Lower || found.depth<0) + std::terminate(); + } + } + }); + } + start.store(true,std::memory_order_release); + for(auto& t:threads)t.join(); + check(tt.hashfull()>=0 && tt.hashfull()<=1000, "concurrent TT hashfull valid"); + tt.setConcurrent(false); + tt.clear(); + TTProbe found{}; + check(!tt.probe(123,1,found),"TT clear after workers joined"); +} + +void testUci() { + auto shell=std::make_unique(); + std::istringstream input("uci\nsetoption name Threads value 3\nquit\n"); + std::ostringstream output; + shell->run(input,output); + check(output.str().find("option name Threads type spin default 1 min 1 max 64")!=std::string::npos, + "UCI advertises Threads"); + check(output.str().find("info string Threads set to 3")!=std::string::npos, + "UCI accepts Threads"); +} + +} // namespace +int main() { + Geometry::init(); + testUci(); + testTT(); + testSearch(1,false); + testSearch(3,false); + testSearch(2,true); + testStop(); + if(!failures)std::cout<<"Lazy SMP smoke/concurrency: PASS\n"; + return failures?1:0; +}