Example #1
0
        /// <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?
Example #3
0
        /// <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);
            }
        }
Example #4
0
        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));
        }
Example #5
0
        /// <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,