Line data Source code
1 : #include "searcher.hpp"
2 :
3 : #include "com.hpp"
4 : #include "dynamicConfig.hpp"
5 : #include "logging.hpp"
6 : #include "moveApply.hpp"
7 : #include "xboard.hpp"
8 :
9 5075 : TimeType Searcher::getCurrentMoveMs()const{
10 5075 : if (ThreadPool::instance().main().getData().isPondering || ThreadPool::instance().main().getData().isAnalysis) { return INFINITETIME; }
11 :
12 5073 : TimeType ret = currentMoveMs;
13 5073 : if (TimeMan::msecUntilNextTC > 0) {
14 : bool extented = false;
15 2 : switch (moveDifficulty) {
16 0 : case MoveDifficultyUtil::MD_forced:
17 : // only one move in movelist !
18 0 : ret = (ret >> 4);
19 : break;
20 0 : case MoveDifficultyUtil::MD_easy:
21 : ///@todo this is not used anymore
22 0 : ret = (ret >> 3);
23 : break;
24 : case MoveDifficultyUtil::MD_std:
25 : // nothing special
26 : break;
27 0 : case MoveDifficultyUtil::MD_moobAttackIID:
28 : // score is decreasing during IID but still quite high (IID moob, sharp position)
29 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorIIDGood));
30 : extented = true;
31 : break;
32 0 : case MoveDifficultyUtil::MD_moobDefenceIID:
33 : // score is decreasing during IID and it's not smelling good (sharp position)
34 : extented = true;
35 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorIID));
36 : break;
37 : }
38 : if (!extented) {
39 2 : switch (positionEvolution) {
40 : case MoveDifficultyUtil::PE_none:
41 : case MoveDifficultyUtil::PE_std:
42 : // nothing special
43 : break;
44 0 : case MoveDifficultyUtil::PE_boomingAttack:
45 : // let's validate this a little more
46 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorBoomHistory));
47 0 : break;
48 0 : case MoveDifficultyUtil::PE_boomingDefence:
49 : // let's validate this a little more
50 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorBoomHistory));
51 0 : break;
52 0 : case MoveDifficultyUtil::PE_moobingAttack:
53 : // let's try to understand better
54 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorMoobHistory));
55 0 : break;
56 0 : case MoveDifficultyUtil::PE_moobingDefence:
57 : // let's try to defend
58 0 : ret = static_cast<TimeType>(std::min(TimeMan::msecUntilNextTC / MoveDifficultyUtil::maxStealDivisor, ret * MoveDifficultyUtil::emergencyFactorMoobHistory));
59 0 : break;
60 : }
61 : }
62 : }
63 : // take variability into account
64 5073 : ret = std::min(TimeMan::maxTime, static_cast<TimeType>(static_cast<float>(ret) * MoveDifficultyUtil::variabilityFactor())); // inside [0.5 .. 2]
65 5073 : return std::max(ret, static_cast<TimeType>(20)); // if not much time left, let's try something ...;
66 : }
67 :
68 4400925 : void Searcher::getCMHPtr(const unsigned int ply, CMHPtrArray& cmhPtr) {
69 : cmhPtr.fill(nullptr);
70 8801850 : for (unsigned int k = 0; k < MAX_CMH_PLY; ++k) {
71 4400925 : assert(static_cast<int>(ply) - static_cast<int>(2*k) < MAX_PLY && static_cast<int>(ply) - static_cast<int>(2*k) >= 0);
72 4400925 : if (ply > 2*k && isValidMove(stack[ply - 2*k].p.lastMove)) {
73 : const Position & pRef = stack[ply - 2*k].p;
74 4152185 : const Square to = correctedMove2ToKingDest(pRef.lastMove);
75 4152185 : const int ptIdx = isEnPassant(pRef.lastMove) ? PieceIdx(P_wp) : PieceIdx(pRef.board_const(to));
76 4152185 : cmhPtr[k] = &historyT.counter_history[ptIdx][to];
77 : }
78 : }
79 4400925 : }
80 :
81 208 : bool Searcher::isBooming(uint16_t halfmove){
82 : assert(halfmove >= 0);
83 208 : if (halfmove < 4) { return false; } // no booming at the beginning of the game of course
84 192 : if (stack[halfmove-2].h == nullHash) { return false; } // no record in previous state
85 191 : if (stack[halfmove-4].h == nullHash) { return false; } // no record in former state
86 : constexpr ScoreType boomMargin = 80;
87 190 : return stack[halfmove-4].eval <= stack[halfmove-2].eval + boomMargin;
88 : }
89 :
90 35 : bool Searcher::isMoobing(uint16_t halfmove){
91 : assert(halfmove >= 0);
92 35 : if (halfmove < 4) { return false; } // no moobing at the beginning of the game of course
93 19 : if (stack[halfmove-2].h == nullHash) { return false; } // no record in previous state
94 18 : if (stack[halfmove-4].h == nullHash) { return false; } // no record in former state
95 : constexpr ScoreType moobMargin = 80;
96 17 : return stack[halfmove-4].eval >= stack[halfmove-2].eval + moobMargin;
97 : }
98 :
99 22856171 : ScoreType Searcher::getCMHScore(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr) const {
100 : ScoreType ret = 0;
101 68568513 : for (int i = 0; i < MAX_CMH_PLY; i++) {
102 22856171 : if (cmhPtr[i]) { ret += (*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to]; }
103 : }
104 22856171 : return ret/MAX_CMH_PLY;
105 : }
106 :
107 0 : bool Searcher::isCMHGood(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr, const ScoreType threshold) const {
108 0 : for (int i = 0; i < MAX_CMH_PLY; i++) {
109 0 : if (cmhPtr[i]) {
110 0 : const auto cmhScore = (*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to];
111 0 : if (cmhScore >= threshold){
112 : /*
113 : std::cout << ToString(p) << std::endl;
114 : std::cout << SquareNames[from] << std::endl;
115 : std::cout << SquareNames[to] << std::endl;
116 : std::cout << cmhScore << std::endl;
117 : */
118 : return true;
119 : }
120 : }
121 : }
122 : return false;
123 : }
124 :
125 526842 : bool Searcher::isCMHBad(const Position& p, const Square from, const Square to, const CMHPtrArray& cmhPtr, const ScoreType threshold) const {
126 : int nbBad = 0;
127 1580526 : for (int i = 0; i < MAX_CMH_PLY; i++) {
128 526842 : if (cmhPtr[i]) {
129 460444 : if ((*cmhPtr[i])[PieceIdx(p.board_const(from)) * NbSquare + to] < threshold) ++nbBad;
130 : }
131 : }
132 526842 : return nbBad == MAX_CMH_PLY;
133 : }
134 :
135 6655 : ScoreType Searcher::drawScore(const Position& p, DepthType height) const {
136 : // handles chess variants
137 : ///@todo other chess variants
138 6655 : if (DynamicConfig::armageddon) {
139 0 : if (p.c == Co_White) return matedScore(height);
140 0 : else return matingScore(height-1);
141 : }
142 6655 : return static_cast<ScoreType>(-1 + 2 * ((stats.counters[Stats::sid_nodes] + stats.counters[Stats::sid_qnodes]) % 2));
143 : }
144 :
145 26 : void Searcher::idleLoop() {
146 26 : _searching = false;
147 : while (true) {
148 249 : std::unique_lock lock(_mutex);
149 249 : Logging::LogIt(Logging::logInfo) << "begin of idleloop " << id();
150 249 : _cv.notify_one(); // Wake up anyone waiting for search finished
151 498 : _cv.wait(lock, [&] { return _searching; });
152 249 : if (_exit) {
153 26 : Logging::LogIt(Logging::logInfo) << "Exiting thread loop " << id();
154 26 : return;
155 : }
156 223 : lock.unlock();
157 223 : searchLauncher(); // blocking
158 223 : Logging::LogIt(Logging::logInfo) << "end of idleloop " << id();
159 : }
160 : }
161 :
162 249 : void Searcher::startThread() {
163 249 : std::lock_guard lock(_mutex);
164 249 : Logging::LogIt(Logging::logInfo) << "Starting worker " << id();
165 249 : _searching = true;
166 249 : Logging::LogIt(Logging::logInfo) << "Setting stopflag to false on worker " << id();
167 249 : stopFlag = false;
168 249 : _cv.notify_one(); // Wake up the thread in idleLoop()
169 249 : }
170 :
171 273 : void Searcher::wait() {
172 273 : std::unique_lock lock(_mutex);
173 273 : Logging::LogIt(Logging::logInfo) << "Thread waiting " << id();
174 305 : _cv.wait(lock, [&] { return !_searching; });
175 273 : }
176 :
177 : // multi-threaded search (blocking call)
178 223 : void Searcher::searchLauncher() {
179 223 : Logging::LogIt(Logging::logInfo) << "Search launched for thread " << id();
180 : // starts other threads first but they are locked for now ...
181 208 : if (isMainThread()) { ThreadPool::instance().startOthers(); }
182 : // so here searchDriver() will update the thread _data structure
183 223 : searchDriver();
184 223 : }
185 :
186 1715 : size_t Searcher::id() const { return _index; }
187 :
188 10660799 : bool Searcher::isMainThread() const { return id() == 0; }
189 :
190 26 : Searcher::Searcher(size_t n): _index(n), _exit(false), _searching(true), _stdThread(&Searcher::idleLoop, this) {
191 26 : startTime = Clock::now();
192 26 : wait(); // wait for idleLoop to start in the _stdThread object
193 26 : }
194 :
195 26 : Searcher::~Searcher() {
196 26 : _exit = true;
197 26 : startThread();
198 26 : Logging::LogIt(Logging::logInfo) << "Waiting for thread worker to join...";
199 26 : _stdThread.join();
200 52 : }
201 :
202 223 : void Searcher::setData(const ThreadData& d) {
203 223 : _data = d; // this is a copy
204 0 : }
205 :
206 1256 : ThreadData& Searcher::getData() { return _data; }
207 :
208 0 : const ThreadData& Searcher::getData() const { return _data; }
209 :
210 4683 : SearchData& Searcher::getSearchData() { return _data.datas; }
211 :
212 0 : const SearchData& Searcher::getSearchData() const { return _data.datas; }
213 :
214 4068 : bool Searcher::searching() const { return _searching; }
215 :
216 52 : void Searcher::clearGame() {
217 : clearPawnTT();
218 52 : stats.init();
219 52 : killerT.initKillers();
220 52 : historyT.initHistory();
221 52 : counterT.initCounter();
222 52 : previousBest = INVALIDMOVE;
223 :
224 : // clear stack data
225 106548 : for (auto & d : stack){
226 106496 : d = {Position(), nullHash, 0, INVALIDMINIMOVE};
227 : }
228 106496 : }
229 :
230 223 : void Searcher::clearSearch(bool forceHistoryClear) {
231 : #ifdef REPRODUCTIBLE_RESULTS
232 : clearPawnTT();
233 : forceHistoryClear = true;
234 : #endif
235 223 : stats.init();
236 223 : killerT.initKillers();
237 223 : if (forceHistoryClear) historyT.initHistory();
238 223 : counterT.initCounter();
239 223 : previousBest = INVALIDMOVE;
240 223 : }
241 :
242 26 : void Searcher::initPawnTable() {
243 26 : Logging::LogIt(Logging::logInfo) << "Init Pawn TT (one per thread)";
244 26 : Logging::LogIt(Logging::logInfo) << "PawnEntry size " << sizeof(PawnEntry);
245 52 : ttSizePawn = powerFloor((SIZE_MULTIPLIER * DynamicConfig::ttPawnSizeMb / DynamicConfig::threads) / sizeof(PawnEntry));
246 26 : assert(BB::countBit(ttSizePawn) == 1); // a power of 2 and not 0 ...
247 3014682 : tablePawn.reset(new PawnEntry[ttSizePawn]);
248 52 : Logging::LogIt(Logging::logInfo) << "Size of Pawn TT " << ttSizePawn * sizeof(PawnEntry) / 1024 << "Kb (" << ttSizePawn << " entries)";
249 26 : }
250 :
251 0 : void Searcher::clearPawnTT() {
252 6422580 : for (unsigned int k = 0; k < ttSizePawn; ++k) tablePawn[k].h = nullHash;
253 0 : }
254 :
255 796968 : bool Searcher::getPawnEntry(Hash h, PawnEntry*& pe) {
256 796968 : assert(h != nullHash);
257 796968 : PawnEntry& _e = tablePawn[h & (ttSizePawn - 1)];
258 796968 : pe = &_e;
259 796968 : if (_e.h != Hash64to32(h)) return false;
260 : ///@todo check for hash collision ?
261 : stats.incr(Stats::sid_ttPawnhits);
262 648214 : return !DynamicConfig::disableTT;
263 : }
264 :
265 796968 : void Searcher::prefetchPawn(Hash h) {
266 796968 : void* addr = (&tablePawn[h & (ttSizePawn - 1)]);
267 : #if defined(__INTEL_COMPILER)
268 : __asm__("");
269 : #elif defined(_MSC_VER)
270 : _mm_prefetch((char*)addr, _MM_HINT_T0);
271 : #else
272 796968 : __builtin_prefetch(addr);
273 : #endif
274 796968 : }
275 :
276 : std::atomic<bool> Searcher::startLock;
277 :
278 0 : Searcher& Searcher::getCoSearcher(size_t id) {
279 0 : static std::map<size_t, std::unique_ptr<Searcher>> coSearchers;
280 : // init new co-searcher if not already present
281 0 : if (!coSearchers.contains(id)) {
282 0 : coSearchers[id] = std::unique_ptr<Searcher>(new Searcher(id + MAX_THREADS));
283 0 : coSearchers[id]->initPawnTable();
284 : }
285 0 : return *coSearchers[id];
286 : }
287 :
288 0 : Position Searcher::getQuiet(const Position& p, Searcher* searcher, ScoreType* qScore) {
289 : // fixed co-searcher if not given
290 0 : Searcher& cos = getCoSearcher(searcher ? searcher->id() : 2 * MAX_THREADS);
291 0 : cos.clearSearch(true);
292 :
293 0 : PVList pv;
294 : DepthType height = 1;
295 0 : DepthType seldepth = 0;
296 : ScoreType s = 0;
297 :
298 0 : Position pQuiet = p; // because p is given const
299 : #ifdef WITH_NNUE
300 0 : NNUEEvaluator evaluator;
301 : pQuiet.associateEvaluator(evaluator);
302 0 : pQuiet.resetNNUEEvaluator(pQuiet.evaluator());
303 : #endif
304 :
305 : // go for a qsearch (no pruning, open bounds)
306 0 : cos.stopFlag = false;
307 0 : cos.currentMoveMs = INFINITETIME;
308 0 : cos.isStoppableCoSearcher = true;
309 0 : cos.subSearch = true;
310 :
311 0 : s = cos.qsearchNoPruning(-10000, 10000, pQuiet, height, seldepth, &pv);
312 :
313 0 : cos.subSearch = false;
314 :
315 0 : if (qScore) *qScore = s;
316 :
317 : //std::cout << "pv : " << ToString(pv) << std::endl;
318 :
319 : // goto qsearch leaf
320 0 : for (const auto& m : pv) {
321 0 : Position p2 = pQuiet;
322 : //std::cout << "Applying move " << ToString(m) << std::endl;
323 0 : if (const MoveInfo moveInfo(p2,m); !applyMove(p2, moveInfo)) break;
324 0 : pQuiet = p2;
325 0 : }
326 :
327 : #ifdef WITH_NNUE
328 : pQuiet.dissociateEvaluator();
329 : #endif
330 0 : return pQuiet;
331 0 : }
332 :
333 : #ifdef WITH_GENFILE
334 :
335 0 : struct GenFENEntry{
336 : std::string fen;
337 : Move m;
338 : ScoreType s;
339 : uint16_t ply;
340 : Color stm;
341 : bool operator<(const GenFENEntry& other) const {
342 0 : return fen < other.fen;
343 : }
344 0 : void write(std::ofstream & stream, int result) const {
345 : stream << "fen " << fen << "\n"
346 0 : << "move " << ToString(m) << "\n"
347 0 : << "score " << s << "\n"
348 : //<< "eval " << e << "\n"
349 0 : << "ply " << ply << "\n"
350 0 : << "result " << (stm == Co_White ? result : -result) << "\n"
351 0 : << "e" << "\n";
352 0 : }
353 : ///@todo writeBinary
354 : };
355 :
356 0 : void Searcher::writeToGenFile(const Position& p, bool getQuietPos, const ThreadData & d, const std::optional<int> result) {
357 : static uint64_t sfensWritten = 0;
358 :
359 0 : static std::set<GenFENEntry> buffer;
360 :
361 0 : ThreadData data = d; // copy data from PV
362 0 : Position pLeaf = p; // copy current pos
363 :
364 0 : if (getQuietPos){
365 :
366 0 : Searcher& cos = getCoSearcher(id());
367 0 : Logging::LogIt(Logging::logDebug) << "Looking for quiet position";
368 :
369 0 : const int oldMinOutLvl = DynamicConfig::minOutputLevel;
370 0 : const bool oldDisableTT = DynamicConfig::disableTT;
371 0 : const unsigned int oldLevel = DynamicConfig::level;
372 0 : const unsigned int oldRandomOpen = DynamicConfig::randomOpen;
373 0 : const unsigned int oldRandomPly = DynamicConfig::randomPly;
374 :
375 : // init sub search
376 : //DynamicConfig::minOutputLevel = Logging::logMax;
377 0 : DynamicConfig::disableTT = true; // do not use TT in order to get qsearch leaf node
378 0 : DynamicConfig::level = 100;
379 0 : DynamicConfig::randomOpen = 0;
380 0 : DynamicConfig::randomPly = 0;
381 :
382 : ///@todo in the following code the evaluator will be reset 3 times ! (getQuiet, before eval, at the beginning of searchDriver) ...
383 :
384 : // look for a quiet position using qsearch
385 0 : ScoreType qScore = 0;
386 0 : pLeaf = getQuiet(p, this, &qScore);
387 :
388 0 : Logging::LogIt(Logging::logDebug) << "quiet position is " << GetFEN(pLeaf);
389 :
390 0 : ScoreType e = 0;
391 0 : if (Abs(qScore) < 1000) {
392 : #ifdef WITH_NNUE
393 0 : NNUEEvaluator evaluator;
394 : pLeaf.associateEvaluator(evaluator);
395 0 : pLeaf.resetNNUEEvaluator(pLeaf.evaluator());
396 : #endif
397 : // evaluate quiet leaf position
398 0 : EvalData eData;
399 0 : e = eval(pLeaf, eData, cos, true, false);
400 :
401 0 : DynamicConfig::disableTT = false; // use TT here
402 0 : if (Abs(e) < 1000) {
403 0 : const Hash matHash = MaterialHash::getMaterialHash(p.mat);
404 : float gp = 1;
405 0 : if (matHash != nullHash) {
406 : const MaterialHash::MaterialHashEntry& MEntry = MaterialHash::materialHashTable[matHash];
407 : gp = MEntry.gamePhase();
408 : }
409 0 : const DepthType depth = static_cast<DepthType>(clampDepth(DynamicConfig::genFenDepth) * gp + clampDepth(DynamicConfig::genFenDepthEG) * (1.f - gp));
410 :
411 0 : data.p = pLeaf;
412 0 : data.depth = depth;
413 : cos.setData(data);
414 :
415 0 : cos.stopFlag = false;
416 0 : cos.subSearch = true;
417 0 : cos.currentMoveMs = INFINITETIME;
418 0 : cos.isStoppableCoSearcher = true;
419 0 : cos.clearSearch(true); // reset node count
420 0 : cos.subSearch = true;
421 :
422 0 : cos.searchDriver(false);
423 :
424 0 : cos.subSearch = false;
425 :
426 0 : data = cos.getData();
427 : // std::cout << data << std::endl; // debug
428 : }
429 : }
430 :
431 0 : DynamicConfig::minOutputLevel = oldMinOutLvl;
432 0 : DynamicConfig::disableTT = oldDisableTT;
433 0 : DynamicConfig::level = oldLevel;
434 0 : DynamicConfig::randomOpen = oldRandomOpen;
435 0 : DynamicConfig::randomPly = oldRandomPly;
436 :
437 : // end of sub search
438 :
439 : }
440 : // skip when bestmove is capture or when you have not reached randomPly limit yet
441 : else{
442 0 : if(isCapture(data.best) || pLeaf.halfmoves <= DynamicConfig::randomPly || pLeaf.halfmoves <= 10){
443 0 : data.best = INVALIDMOVE;
444 : }
445 : }
446 :
447 0 : if (data.best != INVALIDMOVE && Abs(data.score) < 1500) {
448 0 : buffer.emplace(GetFEN(pLeaf), data.best, data.score, pLeaf.halfmoves, pLeaf.c);
449 0 : ++sfensWritten;
450 0 : if (sfensWritten % 10'000 == 0) Logging::LogIt(Logging::logInfoPrio) << "Sfens written " << sfensWritten;
451 : }
452 :
453 0 : if (result.has_value()){
454 0 : Logging::LogIt(Logging::logInfo) << "Game ended, result " << result.value();
455 0 : for (const auto & entry : buffer){
456 0 : entry.write(genStream,result.value());
457 : }
458 : buffer.clear();
459 : }
460 :
461 0 : }
462 : #endif
|