From eee8cddba9ec26e2fad46dd50e3d4bc3179b884c Mon Sep 17 00:00:00 2001 From: David Wu Date: Fri, 13 Sep 2024 10:41:12 -0400 Subject: [PATCH 1/6] More hint gen options, avg log policy --- cpp/command/misc.cpp | 140 ++++++++++++++++++++++++++++++++++++++++++- cpp/dataio/sgf.cpp | 17 ++++++ cpp/dataio/sgf.h | 2 + cpp/main.cpp | 2 + cpp/main.h | 1 + 5 files changed, 160 insertions(+), 2 deletions(-) diff --git a/cpp/command/misc.cpp b/cpp/command/misc.cpp index 485a51611..31db4e469 100644 --- a/cpp/command/misc.cpp +++ b/cpp/command/misc.cpp @@ -1358,6 +1358,7 @@ int MainCmds::dataminesgfs(const vector& args) { double gameModeFastThreshold; bool flipIfPassOrWFirst; bool allowGameOver; + bool manualHintOnly; double trainingWeight; int minRank; @@ -1396,6 +1397,7 @@ int MainCmds::dataminesgfs(const vector& args) { TCLAP::ValueArg gameModeFastThresholdArg("","game-mode-fast-threshold","Utility threshold for game mode fast pass",false,0.005,"UTILS"); TCLAP::SwitchArg flipIfPassOrWFirstArg("","flip-if-pass","Try to heuristically find cases where an sgf passes to simulate white<->black"); TCLAP::SwitchArg allowGameOverArg("","allow-game-over","Allow sampling game over positions in sgf"); + TCLAP::SwitchArg manualHintOnlyArg("","manual-hint-only","Allow only positions marked for hint in the sgf"); TCLAP::ValueArg trainingWeightArg("","training-weight","Scale the loss function weight from data from games that originate from this position",false,1.0,"WEIGHT"); TCLAP::ValueArg minRankArg("","min-rank","Require player making the move to have rank at least this",false,Sgf::RANK_UNKNOWN,"INT"); TCLAP::ValueArg minMinRankArg("","min-min-rank","Require both players in a game to have rank at least this",false,Sgf::RANK_UNKNOWN,"INT"); @@ -1427,6 +1429,7 @@ int MainCmds::dataminesgfs(const vector& args) { cmd.add(gameModeFastThresholdArg); cmd.add(flipIfPassOrWFirstArg); cmd.add(allowGameOverArg); + cmd.add(manualHintOnlyArg); cmd.add(trainingWeightArg); cmd.add(minRankArg); cmd.add(minMinRankArg); @@ -1459,6 +1462,7 @@ int MainCmds::dataminesgfs(const vector& args) { gameModeFastThreshold = gameModeFastThresholdArg.getValue(); flipIfPassOrWFirst = flipIfPassOrWFirstArg.getValue(); allowGameOver = allowGameOverArg.getValue(); + manualHintOnly = manualHintOnlyArg.getValue(); trainingWeight = trainingWeightArg.getValue(); minRank = minRankArg.getValue(); minMinRank = minMinRankArg.getValue(); @@ -1805,7 +1809,7 @@ int MainCmds::dataminesgfs(const vector& args) { // --------------------------------------------------------------------------------------------------- //SGF MODE - auto processSgfGame = [&logger,&gameInit,&nnEval,&expensiveEvaluateMove,autoKomi,&gameModeFastThreshold,&maxDepth,&numFilteredSgfs,&maxHandicap,&maxPolicy,allowGameOver,trainingWeight]( + auto processSgfGame = [&logger,&gameInit,&nnEval,&expensiveEvaluateMove,autoKomi,&gameModeFastThreshold,&maxDepth,&numFilteredSgfs,&maxHandicap,&maxPolicy,allowGameOver,manualHintOnly,trainingWeight]( Search* search, Rand& rand, const string& fileName, CompactSgf* sgf, bool blackOkay, bool whiteOkay ) { //Don't use the SGF rules - randomize them for a bit more entropy @@ -2286,6 +2290,10 @@ int MainCmds::dataminesgfs(const vector& args) { if((hist.moveHistory[hintIdx].pla == P_BLACK && !blackOkay) || (hist.moveHistory[hintIdx].pla == P_WHITE && !whiteOkay)) return; + bool markedAsHintPos = (comments.size() > 0 && comments.find("%HINT%") != string::npos); + if(manualHintOnly && !markedAsHintPos) + return; + //unusedSample doesn't have enough history, doesn't have hintloc the way we want it int64_t numEnqueued = 1+numPosesEnqueued.fetch_add(1); if(numEnqueued % 500 == 0) @@ -2293,7 +2301,7 @@ int MainCmds::dataminesgfs(const vector& args) { PosQueueEntry entry; entry.hist = new BoardHistory(hist); assert(hist.getCurrentTurnNumber() == unusedSample.getCurrentTurnNumber()); - entry.markedAsHintPos = (comments.size() > 0 && comments.find("%HINT%") != string::npos); + entry.markedAsHintPos = markedAsHintPos; posQueue.waitPush(entry); } ); @@ -2806,3 +2814,131 @@ int MainCmds::sampleinitializations(const vector& args) { ScoreValue::freeTables(); return 0; } + + +int MainCmds::checksgfhintpolicy(const vector& args) { + Board::initHash(); + ScoreValue::initTables(); + Rand seedRand; + + ConfigParser cfg; + string nnModelFile; + vector sgfDirs; + try { + KataGoCommandLine cmd("Check policy for hint positions in sgfs"); + cmd.addConfigFileArg("",""); + cmd.addModelFileArg(); + cmd.addOverrideConfigArg(); + + TCLAP::MultiArg sgfDirArg("","sgfdir","Directory of sgf files",true,"DIR"); + cmd.add(sgfDirArg); + cmd.parseArgs(args); + + nnModelFile = cmd.getModelFile(); + sgfDirs = sgfDirArg.getValue(); + + cmd.getConfig(cfg); + } + catch (TCLAP::ArgException &e) { + cerr << "Error: " << e.error() << " for argument " << e.argId() << endl; + return 1; + } + + const bool logToStdoutDefault = true; + Logger logger(&cfg, logToStdoutDefault); + + NNEvaluator* nnEval; + { + Setup::initializeSession(cfg); + const int expectedConcurrentEvals = 1; + const int defaultMaxBatchSize = 8; + const bool defaultRequireExactNNLen = false; + const bool disableFP16 = false; + const string expectedSha256 = ""; + nnEval = Setup::initializeNNEvaluator( + nnModelFile,nnModelFile,expectedSha256,cfg,logger,seedRand,expectedConcurrentEvals, + NNPos::MAX_BOARD_LEN,NNPos::MAX_BOARD_LEN,defaultMaxBatchSize,defaultRequireExactNNLen,disableFP16, + Setup::SETUP_FOR_ANALYSIS + ); + } + logger.write("Loaded neural net"); + + vector sgfFiles; + FileHelpers::collectSgfsFromDirs(sgfDirs, sgfFiles); + logger.write("Found " + Global::int64ToString((int64_t)sgfFiles.size()) + " sgf files!"); + + int64_t numHintPositions = 0; + double logPolicySum = 0.0; + double logPolicyWeight = 0.0; + + for(size_t i = 0; i uniqueHashes; + bool hashComments = true; + bool hashParent = true; + bool flipIfPassOrWFirst = false; + bool allowGameOver = false; + Rand rand; + + const std::vector rulesToUse = { + Rules::parseRules("chinese"), + Rules::parseRules("japanese") + }; + + logger.write("Processing sgf: " + sgfFiles[i] + " hint positions " + Global::int64ToString(numHintPositions)); + sgf->iterAllUniquePositions( + uniqueHashes, hashComments, hashParent, flipIfPassOrWFirst, allowGameOver, &rand, + [&](Sgf::PositionSample& posSample, const BoardHistory& hist, const string& comments) { + if(comments.find("%HINT%") == string::npos) + return; + (void)hist; // Ignore, we want the position before the hint move + + if(!posSample.hasPreviousPositions(1)) + return; + numHintPositions++; + Sgf::PositionSample priorPosSample = posSample.previousPosition(1.0); + + for(const Rules& rules: rulesToUse) { + Player nextPla; + BoardHistory histBefore = priorPosSample.getCurrentBoardHistory(rules,nextPla); + Board board = histBefore.getRecentBoard(0); + + for(int symmetry = 0; symmetry < 8; symmetry++) { + MiscNNInputParams nnInputParams; + NNResultBuf buf; + bool skipCache = true; + bool includeOwnerMap = false; + nnEval->evaluate(board,histBefore,nextPla,nnInputParams,buf,skipCache,includeOwnerMap); + + shared_ptr nnOutput = std::move(buf.result); + int pos = NNPos::locToPos(posSample.moves[posSample.moves.size()-1].loc, board.x_size, nnOutput->nnXLen, nnOutput->nnYLen); + double policy = nnOutput->policyProbs[pos]; + logPolicySum += log(policy + 1e-30); + logPolicyWeight += 1.0; + } + } + } + ); + + delete sgf; + } + + double averageLogPolicy = logPolicySum / logPolicyWeight; + + cout << "Total number of hint positions: " << numHintPositions << endl; + cout << "Average log policy across all hints: " << averageLogPolicy << endl; + + delete nnEval; + NeuralNet::globalCleanup(); + ScoreValue::freeTables(); + + return 0; +} diff --git a/cpp/dataio/sgf.cpp b/cpp/dataio/sgf.cpp index 5984d6948..07904d1fd 100644 --- a/cpp/dataio/sgf.cpp +++ b/cpp/dataio/sgf.cpp @@ -1111,6 +1111,23 @@ Sgf::PositionSample Sgf::PositionSample::previousPosition(double newWeight) cons return other; } +BoardHistory Sgf::PositionSample::getCurrentBoardHistory(const Rules& rules, Player& nextPlaToMove) const { + int encorePhase = 0; + Player pla = nextPla; + Board boardCopy = board; + BoardHistory hist(boardCopy,pla,rules,encorePhase); + int numSampleMoves = (int)moves.size(); + for(int i = 0; i& args return MainCmds::trystartposes(subArgs); else if(subcommand == "viewstartposes") return MainCmds::viewstartposes(subArgs); + else if(subcommand == "checksgfhintpolicy") + return MainCmds::checksgfhintpolicy(subArgs); else if(subcommand == "demoplay") return MainCmds::demoplay(subArgs); else if(subcommand == "writetrainingdata") diff --git a/cpp/main.h b/cpp/main.h index c229718d2..b4813df33 100644 --- a/cpp/main.h +++ b/cpp/main.h @@ -46,6 +46,7 @@ namespace MainCmds { int trystartposes(const std::vector& args); int viewstartposes(const std::vector& args); + int checksgfhintpolicy(const std::vector& args); int demoplay(const std::vector& args); int printclockinfo(const std::vector& args); From e2f926ebe9a210a4e64be70dacd7153e8582166d Mon Sep 17 00:00:00 2001 From: David Wu Date: Sun, 6 Oct 2024 11:28:07 -0400 Subject: [PATCH 2/6] Add friendly pass ok into book and graph hash --- cpp/game/boardhistory.cpp | 2 ++ cpp/game/rules.cpp | 4 ++++ cpp/game/rules.h | 1 + 3 files changed, 7 insertions(+) diff --git a/cpp/game/boardhistory.cpp b/cpp/game/boardhistory.cpp index c728db204..e8b6d99ce 100644 --- a/cpp/game/boardhistory.cpp +++ b/cpp/game/boardhistory.cpp @@ -1239,6 +1239,8 @@ Hash128 BoardHistory::getSituationRulesAndKoHash(const Board& board, const Board hash ^= Rules::ZOBRIST_MULTI_STONE_SUICIDE_HASH; if(hist.hasButton) hash ^= Rules::ZOBRIST_BUTTON_HASH; + if(hist.rules.friendlyPassOk) + hash ^= Rules::ZOBRIST_FRIENDLY_PASS_OK_HASH; return hash; } diff --git a/cpp/game/rules.cpp b/cpp/game/rules.cpp index 5b01c3320..ef863962f 100644 --- a/cpp/game/rules.cpp +++ b/cpp/game/rules.cpp @@ -614,3 +614,7 @@ const Hash128 Rules::ZOBRIST_MULTI_STONE_SUICIDE_HASH = //Based on sha256 hash const Hash128 Rules::ZOBRIST_BUTTON_HASH = //Based on sha256 hash of Rules::ZOBRIST_BUTTON_HASH Hash128(0xb8b914c9234ece84ULL, 0x3d759cddebe29c14ULL); + +const Hash128 Rules::ZOBRIST_FRIENDLY_PASS_OK_HASH = //Based on sha256 hash of Rules::ZOBRIST_FRIENDLY_PASS_OK_HASH + Hash128(0x0113655998ef0a25ULL, 0x99c9d04ecd964874ULL); + diff --git a/cpp/game/rules.h b/cpp/game/rules.h index 227ed0a12..1fffab834 100644 --- a/cpp/game/rules.h +++ b/cpp/game/rules.h @@ -101,6 +101,7 @@ struct Rules { static const Hash128 ZOBRIST_TAX_RULE_HASH[3]; static const Hash128 ZOBRIST_MULTI_STONE_SUICIDE_HASH; static const Hash128 ZOBRIST_BUTTON_HASH; + static const Hash128 ZOBRIST_FRIENDLY_PASS_OK_HASH; private: nlohmann::json toJsonHelper(bool omitKomi, bool omitDefaults) const; From 84346645a388bb1a3b5295690c8d7ea47f0dc1d4 Mon Sep 17 00:00:00 2001 From: David Wu Date: Sun, 6 Oct 2024 12:04:50 -0400 Subject: [PATCH 3/6] Update tests for fpok hash --- cpp/tests/results/runOutputTests.txt | 264 +++++++++++++-------------- 1 file changed, 132 insertions(+), 132 deletions(-) diff --git a/cpp/tests/results/runOutputTests.txt b/cpp/tests/results/runOutputTests.txt index 245bc856a..c75a137a4 100644 --- a/cpp/tests/results/runOutputTests.txt +++ b/cpp/tests/results/runOutputTests.txt @@ -17163,282 +17163,282 @@ rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 0 conservativePassAndIsRoot 0 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -C8DFFA0442B54EB90CDC9F5C33EB601C +51162A4A8F2306CD0DCFFA05AB046A39 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -C2389F06E895B4AB14F63FFD56399169 +5BF14F482503FCDF15E55AA4CED69B4C History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -43896A46D42356025D752B630C66D230 +DA40BA0819B51E765C664E3A9489D815 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -25698EE5CE5AB5A518199AF394845462 +BCA05EAB03CCFDD1190AFFAA0C6B5E47 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -2F8EEBE7647A4FB700333A52F156A517 +B6473BA9A9EC07C301205F0B69B9AF32 History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 0 conservativePassAndIsRoot 0 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -C8DFFA0442B54EB90CDC9F5C33EB601C +51162A4A8F2306CD0DCFFA05AB046A39 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -C2389F06E895B4AB14F63FFD56399169 +5BF14F482503FCDF15E55AA4CED69B4C History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -43896A46D42356025D752B630C66D230 +DA40BA0819B51E765C664E3A9489D815 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -25698EE5CE5AB5A518199AF394845462 +BCA05EAB03CCFDD1190AFFAA0C6B5E47 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -2F8EEBE7647A4FB700333A52F156A517 +B6473BA9A9EC07C301205F0B69B9AF32 History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 0 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -EE5360FB3B7303B990556BA10F0EC930 +779AB0B5F6E54BCD91460EF897E1C315 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -E4B405F99153F9AB887FCB006ADC3845 +7D7DD5B75CC5B1DF896CAE59F2333260 History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -6505F0B9ADE51B02C1FCDF9E30837B1C +FCCC20F760735376C0EFBAC7A86C7139 History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -03E5141AB79CF8A584906E0EA861FD4E +9A2CC4547A0AB0D185830B57308EF76B History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -090271181DBC02B79CBACEAFCDB30C3B +90CBA156D02A4AC39DA9ABF6555C061E History that net sees null null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 0 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -EE5360FB3B7303B990556BA10F0EC930 +779AB0B5F6E54BCD91460EF897E1C315 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -E4B405F99153F9AB887FCB006ADC3845 +7D7DD5B75CC5B1DF896CAE59F2333260 History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -6505F0B9ADE51B02C1FCDF9E30837B1C +FCCC20F760735376C0EFBAC7A86C7139 History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -03E5141AB79CF8A584906E0EA861FD4E +9A2CC4547A0AB0D185830B57308EF76B History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -090271181DBC02B79CBACEAFCDB30C3B +90CBA156D02A4AC39DA9ABF6555C061E History that net sees null null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 1 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -B447BE19338DC3549C7EFD55B7A0E499 +2D8E6E57FE1B8B209D6D980C2F4FEEBC History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -BEA0DB1B99AD394684545DF4D27215EC +27690B55543B7132854738AD4A9D1FC9 History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -3F112E5BA51BDBEFCDD7496A882D56B5 +A6D8FE15688D939BCCC42C3310C25C90 History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -59F1CAF8BF62384888BBF8FA10CFD0E7 +C0381AB672F4703C89A89DA38820DAC2 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -5316AFFA1542C25A9091585B751D2192 +CADF7FB4D8D48A2E91823D02EDF22BB7 History that net sees null null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 1 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -3368838144F812C6E8C96E018EB0F193 +AAA153CF896E5AB2E9DA0B58165FFBB6 History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -976AC1976F689AA26CB770868ED255EB +0EA311D9A2FED2D66DA415DF163D5FCE History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -BD50B761A383F835F8A4FBAF6F16532C +2499672F6E15B041F9B79EF6F7F95909 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -D0F821301625DCF82731D2E428DD59C9 +4931F17EDBB3948C2622B7BDB03253EC History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -B447BE19338DC3549C7EFD55B7A0E499 +2D8E6E57FE1B8B209D6D980C2F4FEEBC History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -BEA0DB1B99AD394684545DF4D27215EC +27690B55543B7132854738AD4A9D1FC9 History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -3F112E5BA51BDBEFCDD7496A882D56B5 +A6D8FE15688D939BCCC42C3310C25C90 History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -C764794E70977A4B4E86F7BB27879E02 +5EADA900BD01323F4F9592E2BF689427 History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -3D4E55D19ACA27E433F4D74B8FB26DB7 +A487859F575C6F9032E7B212175D6792 History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -59F1CAF8BF62384888BBF8FA10CFD0E7 +C0381AB672F4703C89A89DA38820DAC2 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -5316AFFA1542C25A9091585B751D2192 +CADF7FB4D8D48A2E91823D02EDF22BB7 History that net sees null null null null null (;GM[1]FF[4]SZ[9]KM[-7];B[ff];W[ee];B[dd];W[];B[];W[];B[cc];W[bb];B[];W[]) @@ -17446,282 +17446,282 @@ rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 0 conservativePassAndIsRoot 0 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -1085E4596C7ACBCDB94A375871A1E040 +894C3417A1EC83B9B8595201E94EEA65 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -1A62815BC65A31DFA16097F914731135 +83AB51150BCC79ABA073F2A08C9C1B10 History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -9BD3741BFAECD376E8E383674E2C526C +021AA455377A9B02E9F0E63ED6C35849 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -FD3390B8E09530D1AD8F32F7D6CED43E +64FA40F62D0378A5AC9C57AE4E21DE1B History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -F7D4F5BA4AB5CAC3B5A59256B31C254B +6E1D25F4872382B7B4B6F70F2BF32F6E History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 0 conservativePassAndIsRoot 0 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -1085E4596C7ACBCDB94A375871A1E040 +894C3417A1EC83B9B8595201E94EEA65 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -1A62815BC65A31DFA16097F914731135 +83AB51150BCC79ABA073F2A08C9C1B10 History that net sees pass null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -9BD3741BFAECD376E8E383674E2C526C +021AA455377A9B02E9F0E63ED6C35849 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -FD3390B8E09530D1AD8F32F7D6CED43E +64FA40F62D0378A5AC9C57AE4E21DE1B History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -F7D4F5BA4AB5CAC3B5A59256B31C254B +6E1D25F4872382B7B4B6F70F2BF32F6E History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 0 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -36097EA615BC86CD25C3C3A54D44496C +AFC0AEE8D82ACEB924D0A6FCD5AB4349 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -3CEE1BA4BF9C7CDF3DE963042896B819 +A527CBEA720A34AB3CFA065DB079B23C History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -BD5FEEE4832A9E76746A779A72C9FB40 +24963EAA4EBCD602757912C3EA26F165 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -DBBF0A4799537DD13106C60AEA2B7D12 +4276DA0954C535A53015A35372C47737 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -D1586F45337387C3292C66AB8FF98C67 +4891BF0BFEE5CFB7283F03F217168642 History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 0 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -36097EA615BC86CD25C3C3A54D44496C +AFC0AEE8D82ACEB924D0A6FCD5AB4349 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -3CEE1BA4BF9C7CDF3DE963042896B819 +A527CBEA720A34AB3CFA065DB079B23C History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -BD5FEEE4832A9E76746A779A72C9FB40 +24963EAA4EBCD602757912C3EA26F165 History that net sees pass null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -DBBF0A4799537DD13106C60AEA2B7D12 +4276DA0954C535A53015A35372C47737 History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -D1586F45337387C3292C66AB8FF98C67 +4891BF0BFEE5CFB7283F03F217168642 History that net sees pass null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 1 VERSION 6 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -6C1DA0441D42462029E85551F5EA64C5 +F5D4700AD0D40E5428FB30086D056EE0 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -66FAC546B762BC3231C2F5F0903895B0 +FF3315087AF4F44630D190A908D79F95 History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -E74B30068BD45E9B7841E16ECA67D6E9 +7E82E048464216EF795284375288DCCC History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -81ABD4A591ADBD3C3D2D50FE528550BB +186204EB5C3BF5483C3E35A7CA6A5A9E History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -8B4CB1A73B8D472E2507F05F3757A1CE +128561E9F61B0F5A24149506AFB8ABEB History that net sees null null null null null rules koPOSITIONALscoreAREAtaxNONEsui1fpok1komi7.5 enablePassingHacks 1 conservativePassAndIsRoot 1 VERSION 7 encorephase 0 finished 0 numTurnsThisPhase 0 numApproxValidTurnsThisPhase 0 -EB329DDC6A3797B25D5FC605CCFA71CF +72FB4D92A7A1DFC65C4CA35C54157BEA History that net sees null null null null null Move 0 F4 encorephase 0 finished 0 numTurnsThisPhase 1 numApproxValidTurnsThisPhase 1 -4F30DFCA41A71FD6D921D882CC98D5B7 +D6F90F848C3157A2D832BDDB5477DF92 History that net sees F4 null null null null Move 1 E5 encorephase 0 finished 0 numTurnsThisPhase 2 numApproxValidTurnsThisPhase 2 -650AA93C8D4C7D414D3253AB2D5CD370 +FCC3797240DA35354C2136F2B5B3D955 History that net sees E5 F4 null null null Move 2 D6 encorephase 0 finished 0 numTurnsThisPhase 3 numApproxValidTurnsThisPhase 3 -08A23F6D38EA598C92A77AE06A97D995 +916BEF23F57C11F893B41FB9F278D3B0 History that net sees D6 E5 F4 null null Move 3 pass encorephase 0 finished 0 numTurnsThisPhase 4 numApproxValidTurnsThisPhase 4 -6C1DA0441D42462029E85551F5EA64C5 +F5D4700AD0D40E5428FB30086D056EE0 History that net sees null null null null null Move 4 pass encorephase 0 finished 1 numTurnsThisPhase 5 numApproxValidTurnsThisPhase 5 -66FAC546B762BC3231C2F5F0903895B0 +FF3315087AF4F44630D190A908D79F95 History that net sees null null null null null Move 5 pass encorephase 0 finished 1 numTurnsThisPhase 6 numApproxValidTurnsThisPhase 2 -E74B30068BD45E9B7841E16ECA67D6E9 +7E82E048464216EF795284375288DCCC History that net sees null null null null null Move 6 C7 encorephase 0 finished 0 numTurnsThisPhase 7 numApproxValidTurnsThisPhase 2 -1F3E67135E58FF3FFB105FBF65CD1E5E +86F7B75D93CEB74BFA033AE6FD22147B History that net sees C7 pass null null null Move 7 B8 encorephase 0 finished 0 numTurnsThisPhase 8 numApproxValidTurnsThisPhase 3 -E5144B8CB405A29086627F4FCDF8EDEB +7CDD9BC27993EAE487711A165517E7CE History that net sees B8 C7 pass null null Move 8 pass encorephase 0 finished 0 numTurnsThisPhase 9 numApproxValidTurnsThisPhase 4 -81ABD4A591ADBD3C3D2D50FE528550BB +186204EB5C3BF5483C3E35A7CA6A5A9E History that net sees null null null null null Move 9 pass encorephase 0 finished 1 numTurnsThisPhase 10 numApproxValidTurnsThisPhase 5 -8B4CB1A73B8D472E2507F05F3757A1CE +128561E9F61B0F5A24149506AFB8ABEB History that net sees null null null null null Running neuralnetless search tests From b6d5285e95617a7070aa02fabbccb0c34c68b0f9 Mon Sep 17 00:00:00 2001 From: David Wu Date: Sun, 6 Oct 2024 12:23:35 -0400 Subject: [PATCH 4/6] More conservative setting of forceNonTerminal for graphhash purposes --- cpp/search/search.cpp | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/cpp/search/search.cpp b/cpp/search/search.cpp index ea7098073..58af0bec1 100644 --- a/cpp/search/search.cpp +++ b/cpp/search/search.cpp @@ -333,6 +333,15 @@ bool Search::makeMove(Loc moveLoc, Player movePla, bool preventEncore) { if(movePla != rootPla) setPlayerAndClearHistory(movePla); + //If the white handicap bonus changes due to the move, we will also need to recompute everything since this is + //basically like a change to the komi. + float oldWhiteHandicapBonusScore = rootHistory.whiteHandicapBonusScore; + + //Compute these first so we can know if we need to set forceNonTerminal below. + rootHistory.makeBoardMoveAssumeLegal(rootBoard,moveLoc,rootPla,rootKoHashTable,preventEncore); + rootPla = getOpp(rootPla); + rootKoHashTable->recompute(rootHistory); + if(rootNode != NULL) { SearchNode* child = NULL; { @@ -373,7 +382,7 @@ bool Search::makeMove(Loc moveLoc, Player movePla, bool preventEncore) { //Okay, this is now our new root! Create a copy so as to keep the root out of the node table. const bool copySubtreeValueBias = false; - const bool forceNonTerminal = true; + const bool forceNonTerminal = rootHistory.isGameFinished; // Make sure the root isn't considered terminal if game would be finished. rootNode = new SearchNode(*child, forceNonTerminal, copySubtreeValueBias); //Sweep over the new root marking it as good (calling NULL function), and then delete anything unmarked. //This will include the old copy of the child that we promoted to root. @@ -388,14 +397,6 @@ bool Search::makeMove(Loc moveLoc, Player movePla, bool preventEncore) { } } - //If the white handicap bonus changes due to the move, we will also need to recompute everything since this is - //basically like a change to the komi. - float oldWhiteHandicapBonusScore = rootHistory.whiteHandicapBonusScore; - - rootHistory.makeBoardMoveAssumeLegal(rootBoard,moveLoc,rootPla,rootKoHashTable,preventEncore); - rootPla = getOpp(rootPla); - rootKoHashTable->recompute(rootHistory); - //Explicitly clear avoid move arrays when we play a move - user needs to respecify them if they want them. avoidMoveUntilByLocBlack.clear(); avoidMoveUntilByLocWhite.clear(); @@ -704,7 +705,7 @@ void Search::beginSearch(bool pondering) { if(rootNode == NULL) { //Avoid storing the root node in the nodeTable, guarantee that it never is part of a cycle, allocate it directly. //Also force that it is non-terminal. - const bool forceNonTerminal = true; + const bool forceNonTerminal = rootHistory.isGameFinished; // Make sure the root isn't considered terminal if game would be finished. rootNode = new SearchNode(rootPla, forceNonTerminal, createMutexIdxForNode(dummyThread)); } else { @@ -1124,9 +1125,10 @@ bool Search::playoutDescend( bool isRoot ) { //Hit terminal node, finish - //forceNonTerminal marks special nodes where we cannot end the game. This includes the root, since if we are searching a position + //forceNonTerminal marks special nodes where we cannot end the game, and is set IF they would normally be finished. + //This includes the root if the root would be game-ended, since if we are searching a position //we presumably want to actually explore deeper and get a result. Also it includes the node following a pass from the root in - //the case where we are conservativePass. For friendlyPassOk rules, it may include deeper nodes. + //the case where we are conservativePass and the game would be ended. For friendlyPassOk rules, it may include deeper nodes. //Note that we also carefully clear the search when a pass from the root would be terminal, so nodes should never need to switch //status after tree reuse in the latter case. if(thread.history.isGameFinished && !node.forceNonTerminal) { @@ -1259,8 +1261,9 @@ bool Search::playoutDescend( (void)childrenCapacity; //We can only test this before we make the move, so do it now. - const bool forceNonTerminalDueToFriendlyPass = - bestChildMoveLoc == Board::PASS_LOC && thread.history.shouldSuppressEndGameFromFriendlyPass(thread.board, thread.pla); + const bool canForceNonTerminalDueToFriendlyPass = + bestChildMoveLoc == Board::PASS_LOC && + thread.history.shouldSuppressEndGameFromFriendlyPass(thread.board, thread.pla); //Make the move! We need to make the move before we create the node so we can see the new state and get the right graphHash. thread.history.makeBoardMoveAssumeLegal(thread.board,bestChildMoveLoc,thread.pla,rootKoHashTable); @@ -1272,9 +1275,9 @@ bool Search::playoutDescend( //If conservative pass, passing from the root is always non-terminal //If friendly passing rules, we might also be non-terminal - const bool forceNonTerminal = bestChildMoveLoc == Board::PASS_LOC && ( + const bool forceNonTerminal = bestChildMoveLoc == Board::PASS_LOC && thread.history.isGameFinished && ( (searchParams.conservativePass && (&node == rootNode)) || - forceNonTerminalDueToFriendlyPass + canForceNonTerminalDueToFriendlyPass ); child = allocateOrFindNode(thread, thread.pla, bestChildMoveLoc, forceNonTerminal, thread.graphHash); child->virtualLosses.fetch_add(1,std::memory_order_release); From 0fe296eb013836d7d86062e18456b56e70c950e8 Mon Sep 17 00:00:00 2001 From: David Wu Date: Sun, 6 Oct 2024 12:51:31 -0400 Subject: [PATCH 5/6] Track graph hash on nodes plus forceNonTerminal hack --- cpp/search/search.cpp | 64 +++++++++++++++++++++------------------ cpp/search/searchnode.cpp | 4 ++- cpp/search/searchnode.h | 11 ++++++- 3 files changed, 47 insertions(+), 32 deletions(-) diff --git a/cpp/search/search.cpp b/cpp/search/search.cpp index 58af0bec1..2053fb18b 100644 --- a/cpp/search/search.cpp +++ b/cpp/search/search.cpp @@ -63,6 +63,9 @@ SearchThread::~SearchThread() { //----------------------------------------------------------------------------------------- +//Based on sha256 of "search.cpp FORCE_NON_TERMINAL_HASH" +static const Hash128 FORCE_NON_TERMINAL_HASH = Hash128(0xd4c31800cb8809e2ULL,0xf75f9d2083f2ffcaULL); + static const double VALUE_WEIGHT_DEGREES_OF_FREEDOM = 3.0; Search::Search(SearchParams params, NNEvaluator* nnEval, Logger* lg, const string& rSeed) @@ -342,6 +345,25 @@ bool Search::makeMove(Loc moveLoc, Player movePla, bool preventEncore) { rootPla = getOpp(rootPla); rootKoHashTable->recompute(rootHistory); + //Explicitly clear avoid move arrays when we play a move - user needs to respecify them if they want them. + avoidMoveUntilByLocBlack.clear(); + avoidMoveUntilByLocWhite.clear(); + + //If we're newly inferring some moves as handicap that we weren't before, clear since score will be wrong. + if(rootHistory.whiteHandicapBonusScore != oldWhiteHandicapBonusScore) + clearSearch(); + + //In the case that we are conservativePass and a pass would end the game, need to clear the search. + //This is because deeper in the tree, such a node would have been explored as ending the game, but now that + //it's a root pass, it needs to be treated as if it no longer ends the game. + if(searchParams.conservativePass && rootHistory.passWouldEndGame(rootBoard,rootPla)) + clearSearch(); + + //In the case that we're preventing encore, and the phase would have ended, we also need to clear the search + //since the search was conducted on the assumption that we're going into encore now. + if(preventEncore && rootHistory.passWouldEndPhase(rootBoard,rootPla)) + clearSearch(); + if(rootNode != NULL) { SearchNode* child = NULL; { @@ -397,25 +419,6 @@ bool Search::makeMove(Loc moveLoc, Player movePla, bool preventEncore) { } } - //Explicitly clear avoid move arrays when we play a move - user needs to respecify them if they want them. - avoidMoveUntilByLocBlack.clear(); - avoidMoveUntilByLocWhite.clear(); - - //If we're newly inferring some moves as handicap that we weren't before, clear since score will be wrong. - if(rootHistory.whiteHandicapBonusScore != oldWhiteHandicapBonusScore) - clearSearch(); - - //In the case that we are conservativePass and a pass would end the game, need to clear the search. - //This is because deeper in the tree, such a node would have been explored as ending the game, but now that - //it's a root pass, it needs to be treated as if it no longer ends the game. - if(searchParams.conservativePass && rootHistory.passWouldEndGame(rootBoard,rootPla)) - clearSearch(); - - //In the case that we're preventing encore, and the phase would have ended, we also need to clear the search - //since the search was conducted on the assumption that we're going into encore now. - if(preventEncore && rootHistory.passWouldEndPhase(rootBoard,rootPla)) - clearSearch(); - return true; } @@ -700,15 +703,25 @@ void Search::beginSearch(bool pondering) { rootSymmetries.push_back(0); } + //If we're using graph search, we recompute the graph hash from scratch at the start of search. + if(searchParams.useGraphSearch) + rootGraphHash = GraphHash::getGraphHashFromScratch(rootHistory, rootPla, searchParams.graphSearchRepBound, searchParams.drawEquivalentWinsForWhite); + else + rootGraphHash = Hash128(); + SearchThread dummyThread(-1, *this); if(rootNode == NULL) { //Avoid storing the root node in the nodeTable, guarantee that it never is part of a cycle, allocate it directly. //Also force that it is non-terminal. const bool forceNonTerminal = rootHistory.isGameFinished; // Make sure the root isn't considered terminal if game would be finished. - rootNode = new SearchNode(rootPla, forceNonTerminal, createMutexIdxForNode(dummyThread)); + Hash128 graphHashMaybeForceNonTerminal = forceNonTerminal ? (rootGraphHash ^ FORCE_NON_TERMINAL_HASH) : rootGraphHash; + rootNode = new SearchNode(rootPla, forceNonTerminal, createMutexIdxForNode(dummyThread), graphHashMaybeForceNonTerminal); } else { + //Update root graph hash in case forceNonTerminal changed. + rootNode->graphHashMaybeForceNonTerminal = rootNode->forceNonTerminal ? (rootGraphHash ^ FORCE_NON_TERMINAL_HASH) : rootGraphHash; + //If the root node has any existing children, then prune things down if there are moves that should not be allowed at the root. SearchNode& node = *rootNode; SearchNodeChildrenReference children = node.getChildren(); @@ -814,9 +827,6 @@ uint32_t Search::createMutexIdxForNode(SearchThread& thread) const { return thread.rand.nextUInt() & (mutexPool->getNumMutexes()-1); } -//Based on sha256 of "search.cpp FORCE_NON_TERMINAL_HASH" -static const Hash128 FORCE_NON_TERMINAL_HASH = Hash128(0xd4c31800cb8809e2ULL,0xf75f9d2083f2ffcaULL); - //Must be called AFTER making the bestChildMoveLoc in the thread board and hist. SearchNode* Search::allocateOrFindNode(SearchThread& thread, Player nextPla, Loc bestChildMoveLoc, bool forceNonTerminal, Hash128 graphHash) { //Hash to use as a unique id for this node in the table, for transposition detection. @@ -850,7 +860,7 @@ SearchNode* Search::allocateOrFindNode(SearchThread& thread, Player nextPla, Loc child = insertLoc->second; } else { - child = new SearchNode(nextPla, forceNonTerminal, createMutexIdxForNode(thread)); + child = new SearchNode(nextPla, forceNonTerminal, createMutexIdxForNode(thread), childHash); //Also perform subtree value bias and pattern bonus handling under the mutex. These parameters are no atomic, so //if the node is accessed concurrently by other nodes through the table, we need to make sure these parameters are fully @@ -1081,12 +1091,6 @@ void Search::computeRootValues() { recentScoreCenter = expectedScore - cap; } - //If we're using graph search, we recompute the graph hash from scratch at the start of search. - if(searchParams.useGraphSearch) - rootGraphHash = GraphHash::getGraphHashFromScratch(rootHistory, rootPla, searchParams.graphSearchRepBound, searchParams.drawEquivalentWinsForWhite); - else - rootGraphHash = Hash128(); - Player opponentWasMirroringPla = mirroringPla; //Update mirroringPla, mirrorAdvantage, mirrorCenterSymmetryError updateMirroring(); diff --git a/cpp/search/searchnode.cpp b/cpp/search/searchnode.cpp index 9cfa48597..363005590 100644 --- a/cpp/search/searchnode.cpp +++ b/cpp/search/searchnode.cpp @@ -152,7 +152,7 @@ void SearchChildPointer::setMoveLocRelaxed(Loc loc) { //Makes a search node resulting from prevPla playing prevLoc -SearchNode::SearchNode(Player pla, bool fnt, uint32_t mIdx) +SearchNode::SearchNode(Player pla, bool fnt, uint32_t mIdx, Hash128 gh) :nextPla(pla), forceNonTerminal(fnt), patternBonusHash(), @@ -169,6 +169,7 @@ SearchNode::SearchNode(Player pla, bool fnt, uint32_t mIdx) lastSubtreeValueBiasDeltaSum(0.0), lastSubtreeValueBiasWeight(0.0), subtreeValueBiasTableEntry(), + graphHashMaybeForceNonTerminal(gh), dirtyCounter(0) { } @@ -190,6 +191,7 @@ SearchNode::SearchNode(const SearchNode& other, bool fnt, bool copySubtreeValueB lastSubtreeValueBiasDeltaSum(0.0), lastSubtreeValueBiasWeight(0.0), subtreeValueBiasTableEntry(), + graphHashMaybeForceNonTerminal(other.graphHashMaybeForceNonTerminal), dirtyCounter(other.dirtyCounter.load(std::memory_order_acquire)) { { diff --git a/cpp/search/searchnode.h b/cpp/search/searchnode.h index 5303a5f42..7dee0c068 100644 --- a/cpp/search/searchnode.h +++ b/cpp/search/searchnode.h @@ -225,10 +225,19 @@ struct SearchNode { double lastSubtreeValueBiasWeight; std::shared_ptr subtreeValueBiasTableEntry; + //Only valid if useGraphSearch is true. + //Graph hash of this node except with an additional factor hashed in if this node would normally be a terminal node + //and we are forcing it not to be due to being the root, or a child of the root after passing (if conservativePass) + //or after any pass in the tree that friendlyPassOk marks as should not be ending the game. + //Note that this is NOT a unique key for evaluations due to special behavior of the root where it affects how a + //child handles passes after a pass move, this property is not encoded in the hash! + //On the root, this might not be up to date outside of the search regarding forceNonTerminal. + Hash128 graphHashMaybeForceNonTerminal; + std::atomic dirtyCounter; //-------------------------------------------------------------------------------- - SearchNode(Player prevPla, bool forceNonTerminal, uint32_t mutexIdx); + SearchNode(Player prevPla, bool forceNonTerminal, uint32_t mutexIdx, Hash128 graphHashMaybeForceNonTerminal); SearchNode(const SearchNode&, bool forceNonTerminal, bool copySubtreeValueBias); ~SearchNode(); From cc95761320d9c57067bc45ebf5d59c83176c8bcc Mon Sep 17 00:00:00 2001 From: David Wu Date: Sun, 6 Oct 2024 00:54:30 -0400 Subject: [PATCH 6/6] Experimental eval cache implementation --- cpp/CMakeLists.txt | 1 + cpp/program/setup.cpp | 8 ++ cpp/search/evalcache.cpp | 121 +++++++++++++++++++++++++++ cpp/search/evalcache.h | 43 ++++++++++ cpp/search/search.cpp | 32 +++++++ cpp/search/search.h | 15 ++++ cpp/search/searchexplorehelpers.cpp | 50 +++++++++++ cpp/search/searchnode.cpp | 2 + cpp/search/searchnode.h | 2 + cpp/search/searchparams.cpp | 10 +++ cpp/search/searchparams.h | 3 + cpp/search/searchupdatehelpers.cpp | 80 +++++++++++++++++- cpp/tests/results/runOutputTests.txt | 16 ++++ 13 files changed, 379 insertions(+), 4 deletions(-) create mode 100644 cpp/search/evalcache.cpp create mode 100644 cpp/search/evalcache.h diff --git a/cpp/CMakeLists.txt b/cpp/CMakeLists.txt index 53d285556..aa475bf1c 100644 --- a/cpp/CMakeLists.txt +++ b/cpp/CMakeLists.txt @@ -227,6 +227,7 @@ add_executable(katago search/localpattern.cpp search/searchnodetable.cpp search/subtreevaluebiastable.cpp + search/evalcache.cpp search/patternbonustable.cpp search/analysisdata.cpp search/reportedsearchvalues.cpp diff --git a/cpp/program/setup.cpp b/cpp/program/setup.cpp index fb03869b6..d0bf7053b 100644 --- a/cpp/program/setup.cpp +++ b/cpp/program/setup.cpp @@ -702,6 +702,14 @@ vector Setup::loadParams( else if(cfg.contains("subtreeValueBiasWeightExponent")) params.subtreeValueBiasWeightExponent = cfg.getDouble("subtreeValueBiasWeightExponent", 0.0, 1.0); else params.subtreeValueBiasWeightExponent = 0.85; + if(cfg.contains("useEvalCache"+idxStr)) params.useEvalCache = cfg.getBool("useEvalCache"+idxStr); + else if(cfg.contains("useEvalCache")) params.useEvalCache = cfg.getBool("useEvalCache"); + else params.useEvalCache = false; + + if(cfg.contains("evalCacheMinVisits"+idxStr)) params.evalCacheMinVisits = cfg.getInt64("evalCacheMinVisits"+idxStr, (int64_t)1, (int64_t)1 << 50); + else if(cfg.contains("evalCacheMinVisits")) params.evalCacheMinVisits = cfg.getInt64("evalCacheMinVisits", (int64_t)1, (int64_t)1 << 50); + else params.evalCacheMinVisits = 100; + if(cfg.contains("nodeTableShardsPowerOfTwo"+idxStr)) params.nodeTableShardsPowerOfTwo = cfg.getInt("nodeTableShardsPowerOfTwo"+idxStr, 8, 24); else if(cfg.contains("nodeTableShardsPowerOfTwo")) params.nodeTableShardsPowerOfTwo = cfg.getInt("nodeTableShardsPowerOfTwo", 8, 24); else params.nodeTableShardsPowerOfTwo = 16; diff --git a/cpp/search/evalcache.cpp b/cpp/search/evalcache.cpp new file mode 100644 index 000000000..8e5db1a21 --- /dev/null +++ b/cpp/search/evalcache.cpp @@ -0,0 +1,121 @@ +#include "../search/evalcache.h" + +#include "../search/searchnode.h" + +//------------------------ +#include "../core/using.h" +//------------------------ + +FirstExploreEval::FirstExploreEval() + :avgWinLoss(0.0f), + avgScoreMean(0.0f), + cacheWeight(0.0f) +{} +FirstExploreEval::FirstExploreEval(float avgWL, float avgSM, float w) + :avgWinLoss(avgWL), + avgScoreMean(avgSM), + cacheWeight(w) +{} + + +EvalCacheEntry::EvalCacheEntry() + :avgWinLoss(0.0f), + avgNoResult(0.0f), + avgScoreMean(0.0f), + avgLead(0.0f), + cacheWeight(0.0f), + firstExploreEvals() +{} + +EvalCacheTable::EvalCacheTable(int32_t numShards) { + mutexPool = new MutexPool(numShards); + entries.resize(numShards); +} +EvalCacheTable::~EvalCacheTable() { + for(const std::map& map: entries) { + for(const auto& pair: map) { + delete pair.second; + } + } + delete mutexPool; +} + +EvalCacheEntry* EvalCacheTable::find(Hash128 graphHash) { + uint32_t subMapIdx = (uint32_t)(graphHash.hash0 % entries.size()); + auto iter = entries[subMapIdx].find(graphHash); + if(iter == entries[subMapIdx].end()) + return NULL; + return iter->second; +} + +void EvalCacheTable::update(Hash128 graphHash, const SearchNode* node, int64_t evalCacheMinVisits, bool isRootNode) { + uint32_t subMapIdx = (uint32_t)(graphHash.hash0 % entries.size()); + std::mutex& mutex = mutexPool->getMutex(subMapIdx); + std::lock_guard lock(mutex); + + EvalCacheEntry*& entry = entries[subMapIdx][graphHash]; + if(entry == NULL) + entry = new EvalCacheEntry(); + + ConstSearchNodeChildrenReference children = node->getChildren(); + int childrenCapacity = children.getCapacity(); + for(int i = 0; istats.visits.load(std::memory_order_acquire); + float childWinLossAvg = (float)(child->stats.winLossValueAvg.load(std::memory_order_acquire)); + float childScoreMeanAvg = (float)(child->stats.scoreMeanAvg.load(std::memory_order_acquire)); + Loc moveLoc = children[i].getMoveLocRelaxed(); + if(childNumVisits >= evalCacheMinVisits) { + float childCacheWeight = (float)childNumVisits; + FirstExploreEval& eval = entry->firstExploreEvals[moveLoc]; + if(childCacheWeight >= eval.cacheWeight) { + eval = FirstExploreEval(childWinLossAvg,childScoreMeanAvg,childCacheWeight); + } + } + } + + float newCacheWeight = (float)(node->stats.visits.load(std::memory_order_acquire)); + if(newCacheWeight < entry->cacheWeight * 0.75f) + return; + + //Should we record this node's aggregate evals in the cache? + //Or should we only leave it at recording initial first-play evals for exploring children? + bool shouldRecordEvals = true; + if(isRootNode) { + //On the root node, due to it handling passing differently than other nodes, we do NOT record it if passing + //is near the highest utility move or is at least a third of the visits. + int64_t totalEdgeVisits = 0; + int64_t passEdgeVisits = 0; + double maxSelfUtility = -1e50; + double passSelfUtility = -1e50; + for(int i = 0; istats.utilityAvg.load(std::memory_order_acquire); + double selfUtility = node->nextPla == P_WHITE ? childUtility : -childUtility; + totalEdgeVisits += edgeVisits; + maxSelfUtility = std::max(maxSelfUtility,selfUtility); + if(childPointer.getMoveLocRelaxed() == Board::PASS_LOC) { + passEdgeVisits += edgeVisits; + passSelfUtility = selfUtility; + } + } + if(passEdgeVisits * 3 >= totalEdgeVisits || passSelfUtility + 0.01 >= maxSelfUtility) { + shouldRecordEvals = false; + } + } + + if(shouldRecordEvals) { + entry->cacheWeight = newCacheWeight; + entry->avgWinLoss = (float)(node->stats.winLossValueAvg.load(std::memory_order_acquire)); + entry->avgNoResult = (float)(node->stats.noResultValueAvg.load(std::memory_order_acquire)); + entry->avgScoreMean = (float)(node->stats.scoreMeanAvg.load(std::memory_order_acquire)); + entry->avgLead = (float)(node->stats.leadAvg.load(std::memory_order_acquire)); + } +} diff --git a/cpp/search/evalcache.h b/cpp/search/evalcache.h new file mode 100644 index 000000000..25127aab7 --- /dev/null +++ b/cpp/search/evalcache.h @@ -0,0 +1,43 @@ +#ifndef SEARCH_EVALCACHE_H_ +#define SEARCH_EVALCACHE_H_ + +#include "../core/global.h" +#include "../core/hash.h" +#include "../game/board.h" +#include "../search/mutexpool.h" + +struct FirstExploreEval { + float avgWinLoss; + float avgScoreMean; + float cacheWeight; + FirstExploreEval(); + FirstExploreEval(float avgWinLoss, float avgScoreMean, float cacheWeight); +}; + +struct SearchNode; + +struct EvalCacheEntry { + float avgWinLoss; + float avgNoResult; + float avgScoreMean; + float avgLead; + float cacheWeight; + std::map firstExploreEvals; + + EvalCacheEntry(); +}; + +struct EvalCacheTable { + std::vector> entries; + MutexPool* mutexPool; + + EvalCacheTable(int32_t numShards); + ~EvalCacheTable(); + + EvalCacheEntry* find(Hash128 graphHash); + void update(Hash128 graphHash, const SearchNode* node, int64_t evalCacheMinVisits, bool isRootNode); +}; + + + +#endif // SEARCH_EVALCACHE_H_ diff --git a/cpp/search/search.cpp b/cpp/search/search.cpp index 2053fb18b..d53ff60f0 100644 --- a/cpp/search/search.cpp +++ b/cpp/search/search.cpp @@ -95,6 +95,7 @@ Search::Search(SearchParams params, NNEvaluator* nnEval, NNEvaluator* humanEval, valueWeightDistribution(NULL), patternBonusTable(NULL), externalPatternBonusTable(nullptr), + evalCache(NULL), nonSearchRand(rSeed + string("$nonSearchRand")), logger(lg), nnEvaluator(nnEval), @@ -156,6 +157,7 @@ Search::~Search() { delete mutexPool; delete subtreeValueBiasTable; delete patternBonusTable; + delete evalCache; killThreads(); } @@ -614,6 +616,10 @@ void Search::runWholeSearch( } } + if(searchParams.useEvalCache && searchParams.useGraphSearch && evalCache != NULL && rootNode != NULL && mirroringPla == C_EMPTY) { + recursivelyRecordEvalCache(*rootNode); + } + //Relaxed load is fine since numPlayoutsShared should be synchronized already due to the joins lastSearchNumPlayouts = numPlayoutsShared.load(std::memory_order_relaxed); effectiveSearchTimeCarriedOver += timer.getSeconds() - actualSearchStartTime; @@ -670,6 +676,11 @@ void Search::beginSearch(bool pondering) { if(searchParams.subtreeValueBiasFactor != 0 && subtreeValueBiasTable == NULL && !(searchParams.antiMirror && mirroringPla != C_EMPTY)) subtreeValueBiasTable = new SubtreeValueBiasTable(searchParams.subtreeValueBiasTableNumShards); + //Prepare eval cache if we need it + if(searchParams.useEvalCache && searchParams.useGraphSearch && evalCache == NULL && mirroringPla == C_EMPTY) { + evalCache = new EvalCacheTable(searchParams.subtreeValueBiasTableNumShards); + } + //Refresh pattern bonuses if needed if(patternBonusTable != NULL) { delete patternBonusTable; @@ -717,10 +728,14 @@ void Search::beginSearch(bool pondering) { const bool forceNonTerminal = rootHistory.isGameFinished; // Make sure the root isn't considered terminal if game would be finished. Hash128 graphHashMaybeForceNonTerminal = forceNonTerminal ? (rootGraphHash ^ FORCE_NON_TERMINAL_HASH) : rootGraphHash; rootNode = new SearchNode(rootPla, forceNonTerminal, createMutexIdxForNode(dummyThread), graphHashMaybeForceNonTerminal); + if(searchParams.useEvalCache && searchParams.useGraphSearch && evalCache != NULL && mirroringPla == C_EMPTY) + rootNode->evalCacheEntry = evalCache->find(rootNode->graphHashMaybeForceNonTerminal); } else { //Update root graph hash in case forceNonTerminal changed. rootNode->graphHashMaybeForceNonTerminal = rootNode->forceNonTerminal ? (rootGraphHash ^ FORCE_NON_TERMINAL_HASH) : rootGraphHash; + if(searchParams.useEvalCache && searchParams.useGraphSearch && evalCache != NULL && mirroringPla == C_EMPTY) + rootNode->evalCacheEntry = evalCache->find(rootNode->graphHashMaybeForceNonTerminal); //If the root node has any existing children, then prune things down if there are moves that should not be allowed at the root. SearchNode& node = *rootNode; @@ -876,6 +891,9 @@ SearchNode* Search::allocateOrFindNode(SearchThread& thread, Player nextPla, Loc } } + if(searchParams.useEvalCache && searchParams.useGraphSearch && evalCache != NULL && mirroringPla == C_EMPTY) + child->evalCacheEntry = evalCache->find(child->graphHashMaybeForceNonTerminal); + if(patternBonusTable != NULL) child->patternBonusHash = patternBonusTable->getHash(getOpp(thread.pla), bestChildMoveLoc, thread.history.getRecentBoard(1)); @@ -1045,6 +1063,20 @@ void Search::recursivelyRecomputeStats(SearchNode& n) { delete dummyThreads[threadIdx]; } +void Search::recursivelyRecordEvalCache(SearchNode& n) { + std::function f = [&](SearchNode* node, int threadIdx) { + (void)threadIdx; + int64_t numVisits = node->stats.visits.load(std::memory_order_acquire); + if(numVisits >= searchParams.evalCacheMinVisits) { + bool isRootNode = node==rootNode; + evalCache->update(node->graphHashMaybeForceNonTerminal, node, searchParams.evalCacheMinVisits, isRootNode); + } + }; + vector nodes; + nodes.push_back(&n); + applyRecursivelyPostOrderMulithreaded(nodes,&f); +} + void Search::computeRootValues() { //rootSafeArea is strictly pass-alive groups and strictly safe territory. diff --git a/cpp/search/search.h b/cpp/search/search.h index f327112ee..6da092a5b 100644 --- a/cpp/search/search.h +++ b/cpp/search/search.h @@ -15,6 +15,7 @@ #include "../game/rules.h" #include "../neuralnet/nneval.h" #include "../search/analysisdata.h" +#include "../search/evalcache.h" #include "../search/mutexpool.h" #include "../search/reportedsearchvalues.h" #include "../search/searchparams.h" @@ -137,6 +138,8 @@ struct Search { PatternBonusTable* patternBonusTable; std::unique_ptr externalPatternBonusTable; + EvalCacheTable* evalCache; + Rand nonSearchRand; //only for use not in search, since rand isn't threadsafe //================================================================================================================ @@ -616,6 +619,17 @@ struct Search { void updateStatsAfterPlayout(SearchNode& node, SearchThread& thread, bool isRoot); void recomputeNodeStats(SearchNode& node, SearchThread& thread, int32_t numVisitsToAdd, bool isRoot); + void adjustEvalsFromCacheHelper( + EvalCacheEntry* evalCacheEntry, + int64_t thisNodeVisits, + double& winLossValueAvg, + double& noResultValueAvg, + double& scoreMeanAvg, + double& scoreMeanSqAvg, + double& leadAvg, + double* utilityAvg + ); + void downweightBadChildrenAndNormalizeWeight( int numChildren, double currentTotalWeight, @@ -645,6 +659,7 @@ struct Search { //---------------------------------------------------------------------------------------- void computeRootValues(); // Helper for begin search void recursivelyRecomputeStats(SearchNode& node); // Helper for search initialization + void recursivelyRecordEvalCache(SearchNode& n); bool playoutDescend( SearchThread& thread, SearchNode& node, diff --git a/cpp/search/searchexplorehelpers.cpp b/cpp/search/searchexplorehelpers.cpp index f9235d17d..7edc36aa6 100644 --- a/cpp/search/searchexplorehelpers.cpp +++ b/cpp/search/searchexplorehelpers.cpp @@ -495,6 +495,56 @@ void Search::selectBestChildToDescend( const std::vector& avoidMoveUntilByLoc = thread.pla == P_BLACK ? avoidMoveUntilByLocBlack : avoidMoveUntilByLocWhite; + //Try all the things in the eval cache that are moves we haven't visited yet. + if(searchParams.useEvalCache && searchParams.useGraphSearch && node.evalCacheEntry != NULL && mirroringPla == C_EMPTY) { + for(const auto& pair: node.evalCacheEntry->firstExploreEvals) { + Loc moveLoc = pair.first; + int movePos = getPos(moveLoc); + bool alreadyTried = posesWithChildBuf[movePos]; + if(alreadyTried) + continue; + + //Special logic for the root + if(isRoot) { + assert(thread.board.pos_hash == rootBoard.pos_hash); + assert(thread.pla == rootPla); + if(!isAllowedRootMove(moveLoc)) + continue; + } + if(avoidMoveUntilByLoc.size() > 0) { + assert(avoidMoveUntilByLoc.size() >= Board::MAX_ARR_SIZE); + int untilDepth = avoidMoveUntilByLoc[moveLoc]; + if(thread.history.moveHistory.size() - rootHistory.moveHistory.size() < untilDepth) + continue; + } + + //Quit immediately for illegal moves + float nnPolicyProb = policyProbs[movePos]; + if(nnPolicyProb < 0) + continue; + + FirstExploreEval eval = pair.second; + double cacheAvgUtility = + getResultUtility(eval.avgWinLoss,0.0) + + getScoreUtility(eval.avgScoreMean, eval.avgScoreMean * eval.avgScoreMean); + + double selectionValue = getNewExploreSelectionValue( + node, + exploreScaling, + nnPolicyProb,cacheAvgUtility, + parentWeightPerVisit, + maxChildWeight, + countEdgeVisit, + &thread + ); + if(selectionValue > maxSelectionValue) { + maxSelectionValue = selectionValue; + bestChildIdx = numChildrenFound; + bestChildMoveLoc = moveLoc; + } + } + } + //Try the new child with the best policy value Loc bestNewMoveLoc = Board::NULL_LOC; float bestNewNNPolicyProb = -1.0f; diff --git a/cpp/search/searchnode.cpp b/cpp/search/searchnode.cpp index 363005590..cc0dff5d4 100644 --- a/cpp/search/searchnode.cpp +++ b/cpp/search/searchnode.cpp @@ -170,6 +170,7 @@ SearchNode::SearchNode(Player pla, bool fnt, uint32_t mIdx, Hash128 gh) lastSubtreeValueBiasWeight(0.0), subtreeValueBiasTableEntry(), graphHashMaybeForceNonTerminal(gh), + evalCacheEntry(NULL), dirtyCounter(0) { } @@ -192,6 +193,7 @@ SearchNode::SearchNode(const SearchNode& other, bool fnt, bool copySubtreeValueB lastSubtreeValueBiasWeight(0.0), subtreeValueBiasTableEntry(), graphHashMaybeForceNonTerminal(other.graphHashMaybeForceNonTerminal), + evalCacheEntry(other.evalCacheEntry), dirtyCounter(other.dirtyCounter.load(std::memory_order_acquire)) { { diff --git a/cpp/search/searchnode.h b/cpp/search/searchnode.h index 7dee0c068..feb5d1d76 100644 --- a/cpp/search/searchnode.h +++ b/cpp/search/searchnode.h @@ -7,6 +7,7 @@ #include "../game/boardhistory.h" #include "../neuralnet/nneval.h" #include "../search/subtreevaluebiastable.h" +#include "../search/evalcache.h" typedef int SearchNodeState; // See SearchNode::STATE_* @@ -233,6 +234,7 @@ struct SearchNode { //child handles passes after a pass move, this property is not encoded in the hash! //On the root, this might not be up to date outside of the search regarding forceNonTerminal. Hash128 graphHashMaybeForceNonTerminal; + EvalCacheEntry* evalCacheEntry; std::atomic dirtyCounter; diff --git a/cpp/search/searchparams.cpp b/cpp/search/searchparams.cpp index 78f0a0f3f..6e66bca47 100644 --- a/cpp/search/searchparams.cpp +++ b/cpp/search/searchparams.cpp @@ -80,6 +80,8 @@ SearchParams::SearchParams() subtreeValueBiasTableNumShards(65536), subtreeValueBiasFreeProp(0.8), subtreeValueBiasWeightExponent(0.5), + useEvalCache(false), + evalCacheMinVisits(100), nodeTableShardsPowerOfTwo(16), numVirtualLossesPerThread(3.0), numThreads(1), @@ -211,6 +213,9 @@ bool SearchParams::operator==(const SearchParams& other) const { subtreeValueBiasFreeProp == other.subtreeValueBiasFreeProp && subtreeValueBiasWeightExponent == other.subtreeValueBiasWeightExponent && + useEvalCache == other.useEvalCache && + evalCacheMinVisits == other.evalCacheMinVisits && + nodeTableShardsPowerOfTwo == other.nodeTableShardsPowerOfTwo && numVirtualLossesPerThread == other.numVirtualLossesPerThread && @@ -457,6 +462,9 @@ json SearchParams::changeableParametersToJson() const { ret["subtreeValueBiasFreeProp"] = subtreeValueBiasFreeProp; ret["subtreeValueBiasWeightExponent"] = subtreeValueBiasWeightExponent; + ret["useEvalCache"] = useEvalCache; + ret["evalCacheMinVisits"] = evalCacheMinVisits; + // ret["nodeTableShardsPowerOfTwo"] = nodeTableShardsPowerOfTwo; ret["numVirtualLossesPerThread"] = numVirtualLossesPerThread; @@ -602,6 +610,8 @@ void SearchParams::printParams(std::ostream& out) const { PRINTPARAM(subtreeValueBiasFreeProp); PRINTPARAM(subtreeValueBiasWeightExponent); + PRINTPARAM(useEvalCache); + PRINTPARAM(evalCacheMinVisits); PRINTPARAM(nodeTableShardsPowerOfTwo); PRINTPARAM(numVirtualLossesPerThread); diff --git a/cpp/search/searchparams.h b/cpp/search/searchparams.h index e5d9e0d73..0ad0f06a1 100644 --- a/cpp/search/searchparams.h +++ b/cpp/search/searchparams.h @@ -111,6 +111,9 @@ struct SearchParams { double subtreeValueBiasFreeProp; //When a node is no longer part of the relevant search tree, only decay this proportion of the weight. double subtreeValueBiasWeightExponent; //When computing empiricial bias, weight subtree results by childvisits to this power. + bool useEvalCache; + int64_t evalCacheMinVisits; + //Threading-related int nodeTableShardsPowerOfTwo; //Controls number of shards of node table for graph search transposition lookup double numVirtualLossesPerThread; //Number of virtual losses for one thread to add diff --git a/cpp/search/searchupdatehelpers.cpp b/cpp/search/searchupdatehelpers.cpp index ff39b208c..1827f541e 100644 --- a/cpp/search/searchupdatehelpers.cpp +++ b/cpp/search/searchupdatehelpers.cpp @@ -8,7 +8,7 @@ //------------------------ - +// TODO I think 1-visit doesn't properly use eval cache due to it using this and not update after playout void Search::addLeafValue( SearchNode& node, double winLossValue, @@ -85,14 +85,31 @@ void Search::addCurrentNNOutputAsLeafValue(SearchNode& node, bool assumeNoExisti const NNOutput* nnOutput = node.getNNOutput(); assert(nnOutput != NULL); //Values in the search are from the perspective of white positive always - double winProb = (double)nnOutput->whiteWinProb; - double lossProb = (double)nnOutput->whiteLossProb; + double winLossValue = (double)nnOutput->whiteWinProb - (double)nnOutput->whiteLossProb; double noResultProb = (double)nnOutput->whiteNoResultProb; double scoreMean = (double)nnOutput->whiteScoreMean; double scoreMeanSq = (double)nnOutput->whiteScoreMeanSq; double lead = (double)nnOutput->whiteLead; + + //Note that root node does not use eval cache for its aggregate values + if(searchParams.useEvalCache && searchParams.useGraphSearch && node.evalCacheEntry != NULL && mirroringPla == C_EMPTY && (&node != rootNode)) { + // For raw NN value for purposes of cache weighting, always treat the node as 1 visit since + // repeating the same nn eval over and over is only 1 visit of eval info. + int64_t thisNodeVisitsForCache = 1; + adjustEvalsFromCacheHelper( + node.evalCacheEntry, + thisNodeVisitsForCache, + winLossValue, + noResultProb, + scoreMean, + scoreMeanSq, + lead, + NULL + ); + } + double weight = computeWeightFromNNOutput(nnOutput); - addLeafValue(node,winProb-lossProb,noResultProb,scoreMean,scoreMeanSq,lead,weight,false,assumeNoExistingWeight); + addLeafValue(node,winLossValue,noResultProb,scoreMean,scoreMeanSq,lead,weight,false,assumeNoExistingWeight); } double Search::computeWeightFromNNOutput(const NNOutput* nnOutput) const { @@ -156,6 +173,7 @@ void Search::recomputeNodeStats(SearchNode& node, SearchThread& thread, int numV ConstSearchNodeChildrenReference children = node.getChildren(); int childrenCapacity = children.getCapacity(); double origTotalChildWeight = 0.0; + int64_t thisNodeVisits = 1; // 1 = own visit for(int i = 0; iavgWinLoss; + double cacheAvgNoResult = evalCacheEntry->avgNoResult; + double cacheAvgScoreMean = evalCacheEntry->avgScoreMean; + double cacheAvgLead = evalCacheEntry->avgLead; + double cacheWeight = evalCacheEntry->cacheWeight; + // Squish down very heavily-weighted entries that are likely to have cache entries ahead of them, to somewhat + // reduce the impact of double-counting since child nodes will also apply their own averaging. + if(cacheWeight > searchParams.evalCacheMinVisits) + cacheWeight = sqrt(searchParams.evalCacheMinVisits * cacheWeight); + double visitsToCacheRatio = thisNodeVisits / cacheWeight; + double cacheFrac = 1.0 / (1.0 + 3.0 * visitsToCacheRatio * (1.0 + 2.0 * visitsToCacheRatio * visitsToCacheRatio)); + + winLossValueAvg += cacheFrac * (cacheAvgWinLoss - winLossValueAvg); + noResultValueAvg += cacheFrac * (cacheAvgNoResult - noResultValueAvg); + double oldScoreMeanAvg = scoreMeanAvg; + scoreMeanAvg += cacheFrac * (cacheAvgScoreMean - scoreMeanAvg); + scoreMeanSqAvg = std::max(0.0, scoreMeanSqAvg - oldScoreMeanAvg * oldScoreMeanAvg + scoreMeanAvg * scoreMeanAvg); + leadAvg += cacheFrac * (cacheAvgLead - leadAvg); + + if(utilityAvg != NULL) { + double cacheAvgUtility = + getResultUtility(cacheAvgWinLoss,cacheAvgNoResult) + + getScoreUtility( + cacheAvgScoreMean, std::max(0.0, scoreMeanSqAvg - scoreMeanAvg * scoreMeanAvg + cacheAvgScoreMean * cacheAvgScoreMean) + ); + *utilityAvg += cacheFrac * (cacheAvgUtility - *utilityAvg); + } +} + + void Search::downweightBadChildrenAndNormalizeWeight( int numChildren, double currentTotalWeight, //The current sum of statsBuf[i].weightAdjusted diff --git a/cpp/tests/results/runOutputTests.txt b/cpp/tests/results/runOutputTests.txt index c75a137a4..aba105b13 100644 --- a/cpp/tests/results/runOutputTests.txt +++ b/cpp/tests/results/runOutputTests.txt @@ -20635,6 +20635,8 @@ subtreeValueBiasFactor: 0 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.5 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 3 numThreads: 1 @@ -20741,6 +20743,8 @@ subtreeValueBiasFactor: 0 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.5 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 3 numThreads: 1 @@ -20847,6 +20851,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1 @@ -20953,6 +20959,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1 @@ -21059,6 +21067,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1 @@ -21165,6 +21175,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1 @@ -21271,6 +21283,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1 @@ -21377,6 +21391,8 @@ subtreeValueBiasFactor: 0.45 subtreeValueBiasTableNumShards: 65536 subtreeValueBiasFreeProp: 0.8 subtreeValueBiasWeightExponent: 0.85 +useEvalCache: 0 +evalCacheMinVisits: 100 nodeTableShardsPowerOfTwo: 16 numVirtualLossesPerThread: 1 numThreads: 1