/// <summary> /// Actually runs a search with specified limits. /// </summary> /// <param name="searchLimit"></param> /// <returns></returns> private GameEngineSearchResultCeres RunSearch(SearchLimit searchLimit) { DateTime lastInfoUpdate = DateTime.Now; int numUpdatesSent = 0; MCTSManager.MCTSProgressCallback callback = (manager) => { curManager = manager; DateTime now = DateTime.Now; float timeSinceLastUpdate = (float)(now - lastInfoUpdate).TotalSeconds; bool isFirstUpdate = numUpdatesSent == 0; float UPDATE_INTERVAL_SECONDS = isFirstUpdate ? 0.1f : 0.5f; if (curManager != null && timeSinceLastUpdate > UPDATE_INTERVAL_SECONDS && curManager.Root.N > 0) { Send(UCIInfoString(curManager)); numUpdatesSent++; lastInfoUpdate = now; } }; GameEngineCeresInProcess.ProgressCallback callbackPlain = obj => callback((MCTSManager)obj); // use this? movesSinceNewGame // Search from this position (possibly with tree reuse) GameEngineSearchResultCeres result = CeresEngine.Search(curPositionAndMoves, searchLimit, gameMoveHistory, callbackPlain) as GameEngineSearchResultCeres; GameMoveStat moveStat = new GameMoveStat(gameMoveHistory.Count, curPositionAndMoves.FinalPosition.MiscInfo.SideToMove, result.ScoreQ, result.ScoreCentipawns, float.NaN, //engine1.CumulativeSearchTimeSeconds, curPositionAndMoves.FinalPosition.PieceCount, result.MAvg, result.FinalN, result.FinalN - result.StartingN, searchLimit, (float)result.TimingStats.ElapsedTimeSecs); gameMoveHistory.Add(moveStat); if (SearchFinishedEvent != null) { SearchFinishedEvent(result.Search.Manager); } // Send the final info string (unless this was an instamove). Send(UCIInfoString(result.Search.Manager, result.Search.BestMoveRoot)); // Send the best move Send("bestmove " + result.Search.BestMove.MoveStr(MGMoveNotationStyle.LC0Coordinate)); if (debug) { Send("info string " + result.Search.BestMoveRoot.BestMoveInfo(false)); } return(result); }
/// <summary> /// Runs all iterations of the iterated search. /// </summary> /// <param name="manager"></param> /// <param name="progressCallback"></param> /// <param name="iterationsDefinition"></param> /// <returns></returns> public (TimingStats, MCTSNode) IteratedSearch(MCTSManager manager, MCTSManager.MCTSProgressCallback progressCallback, IteratedMCTSDef iterationsDefinition) { TimingStats fullSearchTimingStats = new TimingStats(); MCTSNode fullSearchNode = null; using (new TimingBlock(fullSearchTimingStats, TimingBlock.LoggingType.None)) { iterationsDefinition.SetForSearchLimit(manager.SearchLimit); // Temporarily disable the primary/secondary pruning aggressivenss so we get pure policy distribution bool saveEarlyStop = manager.Context.ParamsSearch.FutilityPruningStopSearchEnabled; float saveSecondaryAgg = manager.Context.ParamsSearch.MoveFutilityPruningAggressiveness; manager.Context.ParamsSearch.FutilityPruningStopSearchEnabled = false; manager.Context.ParamsSearch.MoveFutilityPruningAggressiveness = 0; string cacheFileName = $"Ceres.imcts_{DateTime.Now.Ticks}_.cache"; // Loop thru the steps (except last one) PositionEvalCache lastCache = null; for (int step = 0; step < iterationsDefinition.StepDefs.Length - 1; step++) { IteratedMCTSStepDef thisStep = iterationsDefinition.StepDefs[step]; // On the second and subsequent steps configure so that we reuse the case saved from prior iteration if (step > 0 && iterationsDefinition.TreeModification == IteratedMCTSDef.TreeModificationType.DeleteNodesMoveToCache) { manager.Context.EvaluatorDef.CacheMode = PositionEvalCache.CacheMode.MemoryAndDisk; manager.Context.EvaluatorDef.CacheFileName = null; manager.Context.EvaluatorDef.PreloadedCache = lastCache; } // Set the search limit as requested for this step manager.SearchLimit = thisStep.Limit; // Run this step of the search (disable progress callback) (TimingStats, MCTSNode)stepResult = manager.DoSearch(thisStep.Limit, null); // Extract a cache with a small subset of nodes with largest N and a blended policy int minN = (int)(thisStep.NodeNFractionCutoff * manager.Root.N); if (minN < 100) { minN = int.MaxValue; // do not modify policies on very small trees } lastCache = IteratedMCTSBlending.ModifyNodeP(manager.Context.Root, minN, thisStep.WeightFractionNewPolicy, iterationsDefinition.TreeModification); //cache.SaveToDisk(cacheFileName); if (iterationsDefinition.TreeModification == IteratedMCTSDef.TreeModificationType.ClearNodeVisits) { const bool MATERIALIZE_TRANSPOSITIONS = true; // ** TODO: can we safely remove this? manager.ResetTreeState(MATERIALIZE_TRANSPOSITIONS); } } // Restore original pruning aggressiveness manager.Context.ParamsSearch.FutilityPruningStopSearchEnabled = saveEarlyStop; manager.Context.ParamsSearch.MoveFutilityPruningAggressiveness = saveSecondaryAgg; manager.SearchLimit = iterationsDefinition.StepDefs[^ 1].Limit; // TODO: duplicated with the next call?
/// <summary> /// Runs a new search. /// </summary> /// <param name="nnEvaluators"></param> /// <param name="paramsSelect"></param> /// <param name="paramsSearch"></param> /// <param name="limitManager"></param> /// <param name="paramsSearchExecutionPostprocessor"></param> /// <param name="reuseOtherContextForEvaluatedNodes"></param> /// <param name="priorMoves"></param> /// <param name="searchLimit"></param> /// <param name="verbose"></param> /// <param name="startTime"></param> /// <param name="gameMoveHistory"></param> /// <param name="progressCallback"></param> /// <param name="possiblyUsePositionCache"></param> /// <param name="isFirstMoveOfGame"></param> public void Search(NNEvaluatorSet nnEvaluators, ParamsSelect paramsSelect, ParamsSearch paramsSearch, IManagerGameLimit limitManager, ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor, MCTSIterator reuseOtherContextForEvaluatedNodes, PositionWithHistory priorMoves, SearchLimit searchLimit, bool verbose, DateTime startTime, List <GameMoveStat> gameMoveHistory, MCTSManager.MCTSProgressCallback progressCallback = null, bool possiblyUsePositionCache = false, bool isFirstMoveOfGame = false) { searchLimit = AdjustedSearchLimit(searchLimit, paramsSearch); int maxNodes; if (MCTSParamsFixed.STORAGE_USE_INCREMENTAL_ALLOC) { // In this mode, we are just reserving virtual address space // from a very large pool (e.g. 256TB for Windows). // Therefore it is safe to reserve a very large block. maxNodes = (int)(1.1f * MCTSNodeStore.MAX_NODES); } else { if (searchLimit.SearchCanBeExpanded) { throw new Exception("STORAGE_USE_INCREMENTAL_ALLOC must be true when SearchCanBeExpanded."); } if (searchLimit.Type != SearchLimitType.NodesPerMove) { maxNodes = (int)searchLimit.Value + 5_000; } else { throw new Exception("STORAGE_USE_INCREMENTAL_ALLOC must be true when using time search limits."); } } MCTSNodeStore store = new MCTSNodeStore(maxNodes, priorMoves); SearchLimit searchLimitToUse = ConvertedSearchLimit(priorMoves.FinalPosition, searchLimit, 0, 0, paramsSearch, limitManager, gameMoveHistory, isFirstMoveOfGame); Manager = new MCTSManager(store, reuseOtherContextForEvaluatedNodes, null, null, nnEvaluators, paramsSearch, paramsSelect, searchLimitToUse, paramsSearchExecutionPostprocessor, limitManager, startTime, null, gameMoveHistory, isFirstMoveOfGame); using (new SearchContextExecutionBlock(Manager.Context)) { (BestMove, TimingInfo) = MCTSManager.Search(Manager, verbose, progressCallback, possiblyUsePositionCache); } }
SearchOnFEN(NNEvaluatorSet nnEvaluators, ParamsSelect paramsChildSelect, ParamsSearch paramsSearch, IManagerGameLimit timeManager, ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor, MCTSIterator reuseOtherContextForEvaluatedNodes, string fen, string movesStr, SearchLimit searchLimit, bool verbose = false, MCTSManager.MCTSProgressCallback progressCallback = null, bool possiblyEnablePositionCache = false, List <GameMoveStat> gameMoveHistory = null) { PositionWithHistory priorMoves = PositionWithHistory.FromFENAndMovesUCI(fen, movesStr); return(Search(nnEvaluators, paramsChildSelect, paramsSearch, timeManager, paramsSearchExecutionPostprocessor, reuseOtherContextForEvaluatedNodes, priorMoves, searchLimit, verbose, DateTime.Now, gameMoveHistory, progressCallback, possiblyEnablePositionCache)); }
/// <summary> /// Runs a search, possibly continuing from node /// nested in a prior search (tree reuse). /// </summary> /// <param name="priorSearch"></param> /// <param name="reuseOtherContextForEvaluatedNodes"></param> /// <param name="moves"></param> /// <param name="newPositionAndMoves"></param> /// <param name="gameMoveHistory"></param> /// <param name="searchLimit"></param> /// <param name="verbose"></param> /// <param name="startTime"></param> /// <param name="progressCallback"></param> /// <param name="thresholdMinFractionNodesRetained"></param> /// <param name="isFirstMoveOfGame"></param> public void SearchContinue(MCTSearch priorSearch, MCTSIterator reuseOtherContextForEvaluatedNodes, IEnumerable <MGMove> moves, PositionWithHistory newPositionAndMoves, List <GameMoveStat> gameMoveHistory, SearchLimit searchLimit, bool verbose, DateTime startTime, MCTSManager.MCTSProgressCallback progressCallback, float thresholdMinFractionNodesRetained, bool isFirstMoveOfGame = false) { CountSearchContinuations = priorSearch.CountSearchContinuations; Manager = priorSearch.Manager; MCTSIterator priorContext = Manager.Context; MCTSNodeStore store = priorContext.Tree.Store; int numNodesInitial = Manager == null ? 0 : Manager.Root.N; MCTSNodeStructIndex newRootIndex; using (new SearchContextExecutionBlock(priorContext)) { MCTSNode newRoot = FollowMovesToNode(Manager.Root, moves); // New root is not useful if contained no search // (for example if it was resolved via tablebase) // thus in that case we pretend as if we didn't find it if (newRoot != null && (newRoot.N == 0 || newRoot.NumPolicyMoves == 0)) { newRoot = null; } // Check for possible instant move (MCTSManager, MGMove, TimingStats)instamove = CheckInstamove(Manager, searchLimit, newRoot); if (instamove != default) { // Modify in place to point to the new root continationSubroot = newRoot; BestMove = instamove.Item2; TimingInfo = new TimingStats(); return; } else { CountSearchContinuations = 0; } // TODO: don't reuse tree if it would cause the nodes in use // to exceed a reasonable value for this machine #if NOT // NOTE: abandoned, small subtrees will be fast to rewrite so we can always do this // Only rewrite the store with the subtree reused // if it is not tiny relative to the current tree // (otherwise the scan/rewrite is not worth it float fracTreeReuse = newRoot.N / store.Nodes.NumUsedNodes; const float THRESHOLD_REUSE_TREE = 0.02f; #endif // Inform contempt manager about the opponents move // (compared to the move we believed was optimal) if (newRoot != null && newRoot.Depth == 2) { MCTSNode opponentsPriorMove = newRoot; MCTSNode bestMove = opponentsPriorMove.Parent.ChildrenSorted(n => (float)n.Q)[0]; if (bestMove.N > opponentsPriorMove.N / 10) { float bestQ = (float)bestMove.Q; float actualQ = (float)opponentsPriorMove.Q; Manager.Context.ContemptManager.RecordOpponentMove(actualQ, bestQ); //Console.WriteLine("Record " + actualQ + " vs best " + bestQ + " target contempt " + priorManager.Context.ContemptManager.TargetContempt); } } bool storeIsAlmostFull = priorContext.Tree.Store.FractionInUse > 0.9f; bool newRootIsBigEnoughForReuse = newRoot != null && newRoot.N >= (priorContext.Root.N * thresholdMinFractionNodesRetained); if (priorContext.ParamsSearch.TreeReuseEnabled && newRootIsBigEnoughForReuse && !storeIsAlmostFull) { SearchLimit searchLimitAdjusted = searchLimit; if (Manager.Context.ParamsSearch.Execution.TranspositionMode != TranspositionMode.None) { // The MakeChildNewRoot method is not able to handle transposition linkages // (this would be complicated and could involve linkages to nodes no longer in the retained subtree). // Therefore we first materialize any transposition linked nodes in the subtree. // Since this is not currently multithreaded we can turn off tree node locking for the duration. newRoot.Tree.ChildCreateLocks.LockingActive = false; newRoot.MaterializeAllTranspositionLinks(); newRoot.Tree.ChildCreateLocks.LockingActive = true; } // Now rewrite the tree nodes and children "in situ" PositionEvalCache reusePositionCache = null; if (Manager.Context.ParamsSearch.TreeReuseRetainedPositionCacheEnabled) { reusePositionCache = new PositionEvalCache(0); } TranspositionRootsDict newTranspositionRoots = null; if (priorContext.Tree.TranspositionRoots != null) { int estNumNewTranspositionRoots = newRoot.N + newRoot.N / 3; // somewhat oversize to allow for growth in subsequent search newTranspositionRoots = new TranspositionRootsDict(estNumNewTranspositionRoots); } // TODO: Consider sometimes or always skip rebuild via MakeChildNewRoot, // instead just set a new root (move it into place as first node). // Perhaps rebuild only if the MCTSNodeStore would become excessively large. TimingStats makeNewRootTimingStats = new TimingStats(); using (new TimingBlock(makeNewRootTimingStats, TimingBlock.LoggingType.None)) { MCTSNodeStructStorage.MakeChildNewRoot(store, Manager.Context.ParamsSelect.PolicySoftmax, ref newRoot.Ref, newPositionAndMoves, reusePositionCache, newTranspositionRoots); } MCTSManager.TotalTimeSecondsInMakeNewRoot += (float)makeNewRootTimingStats.ElapsedTimeSecs; CeresEnvironment.LogInfo("MCTS", "MakeChildNewRoot", $"Select {newRoot.N:N0} from {numNodesInitial:N0} " + $"in {(int)(makeNewRootTimingStats.ElapsedTimeSecs/1000.0)}ms"); // Finally if nodes adjust based on current nodes if (searchLimit.Type == SearchLimitType.NodesPerMove) { searchLimitAdjusted = new SearchLimit(SearchLimitType.NodesPerMove, searchLimit.Value + store.RootNode.N); } // Construct a new search manager reusing this modified store and modified transposition roots MCTSManager manager = new MCTSManager(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, newTranspositionRoots, priorContext.NNEvaluators, priorContext.ParamsSearch, priorContext.ParamsSelect, searchLimitAdjusted, Manager.ParamsSearchExecutionPostprocessor, Manager.LimitManager, startTime, Manager, gameMoveHistory, isFirstMoveOfGame: isFirstMoveOfGame); manager.Context.ContemptManager = priorContext.ContemptManager; bool possiblyUsePositionCache = false; // TODO could this be relaxed? (MGMove move, TimingStats stats)result = MCTSManager.Search(manager, verbose, progressCallback, possiblyUsePositionCache); BestMove = result.move; TimingInfo = result.stats; Manager = manager; } else { // We decided not to (or couldn't find) that path in the existing tree // Just run the search from scratch if (verbose) { Console.WriteLine("\r\nFailed nSearchFollowingMoves."); } Search(Manager.Context.NNEvaluators, Manager.Context.ParamsSelect, Manager.Context.ParamsSearch, Manager.LimitManager, null, reuseOtherContextForEvaluatedNodes, newPositionAndMoves, searchLimit, verbose, startTime, gameMoveHistory, progressCallback, false); } } #if NOT // This code partly or completely works // We don't rely upon it because it could result in uncontained growth of the store, // since detached nodes are left // But if the subtree chosen is almost the whole tree, maybe we could indeed use this techinque as an alternate in these cases if (store.ResetRootAssumingMovesMade(moves, thresholdFractionNodesRetained)) { SearchManager manager = new SearchManager(store, priorContext.ParamsNN, priorContext.ParamsSearch, priorContext.ParamsSelect, null, limit); manager.Context.TranspositionRoots = priorContext.TranspositionRoots; return(Search(manager, false, verbose, progressCallback, false)); } #endif }
/// <summary> /// Overriden virtual method which executes search. /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="gameMoveHistory"></param> /// <param name="callback"></param> /// <returns></returns> protected override GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory, ProgressCallback callback, bool verbose) { if (LastSearch != null && curPositionAndMoves.InitialPosMG != LastSearch.Manager.Context.StartPosAndPriorMoves.InitialPosMG) { throw new Exception("ResetGame must be called if not continuing same line"); } MCTSearch searchResult; // Set up callback passthrough if provided MCTSManager.MCTSProgressCallback callbackMCTS = null; if (callback != null) { callbackMCTS = callbackContext => callback((MCTSManager)callbackContext); } // Possibly use the context of opponent to reuse position evaluations MCTSIterator shareContext = null; if (OpponentEngine is GameEngineCeresInProcess) { GameEngineCeresInProcess ceresOpponentEngine = OpponentEngine as GameEngineCeresInProcess; if (LastSearch is not null && LastSearch.Manager.Context.ParamsSearch.ReusePositionEvaluationsFromOtherTree && ceresOpponentEngine?.LastSearch.Manager != null && LeafEvaluatorReuseOtherTree.ContextsCompatibleForReuse(LastSearch.Manager.Context, ceresOpponentEngine.LastSearch.Manager.Context)) { shareContext = ceresOpponentEngine.LastSearch.Manager.Context; // Clear any prior shared context from the shared context // to prevent unlimited backward chaining (keeping unneeded prior contexts alive) shareContext.ClearSharedContext(); } } void InnerCallback(MCTSManager manager) { callbackMCTS?.Invoke(manager); } // Run the search searchResult = RunSearchPossiblyTreeReuse(shareContext, curPositionAndMoves, gameMoveHistory, searchLimit, InnerCallback, verbose); int scoreCeresCP = (int)Math.Round(EncodedEvalLogistic.LogisticToCentipawn((float)searchResult.Manager.Root.Q), 0); MGMove bestMoveMG = searchResult.BestMove; int N = (int)searchResult.SearchRootNode.N; // Save (do not dispose) last search in case we can reuse it next time LastSearch = searchResult; isFirstMoveOfGame = false; // TODO is the RootNWhenSearchStarted correct because we may be following a continuation (BestMoveRoot) GameEngineSearchResultCeres result = new GameEngineSearchResultCeres(bestMoveMG.MoveStr(MGMoveNotationStyle.LC0Coordinate), (float)searchResult.SearchRootNode.Q, scoreCeresCP, searchResult.SearchRootNode.MAvg, searchResult.Manager.SearchLimit, default,