Exemple #1
0
        /// <summary>
        ///
        /// </summary>
        /// <param name="store"></param>
        /// <param name="reuseOtherContextForEvaluatedNodes"></param>
        /// <param name="reusePositionCache"></param>
        /// <param name="reuseTranspositionRoots"></param>
        /// <param name="nnEvaluators"></param>
        /// <param name="searchParams"></param>
        /// <param name="childSelectParams"></param>
        /// <param name="searchLimit"></param>
        /// <param name="paramsSearchExecutionPostprocessor"></param>
        /// <param name="limitManager"></param>
        /// <param name="startTime"></param>
        /// <param name="priorManager"></param>
        /// <param name="gameMoveHistory"></param>
        /// <param name="isFirstMoveOfGame"></param>
        public MCTSManager(MCTSNodeStore store,
                           MCTSIterator reuseOtherContextForEvaluatedNodes,
                           PositionEvalCache reusePositionCache,
                           TranspositionRootsDict reuseTranspositionRoots,
                           NNEvaluatorSet nnEvaluators,
                           ParamsSearch searchParams,
                           ParamsSelect childSelectParams,
                           SearchLimit searchLimit,
                           ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor,
                           IManagerGameLimit limitManager,
                           DateTime startTime,
                           MCTSManager priorManager,
                           List <GameMoveStat> gameMoveHistory,
                           bool isFirstMoveOfGame)
        {
            if (searchLimit.IsPerGameLimit)
            {
                throw new Exception("Per game search limits not supported");
            }

            StartTimeThisSearch                = startTime;
            RootNWhenSearchStarted             = store.Nodes.nodes[store.RootIndex.Index].N;
            ParamsSearchExecutionPostprocessor = paramsSearchExecutionPostprocessor;
            IsFirstMoveOfGame = isFirstMoveOfGame;
            SearchLimit       = searchLimit;


            // Make our own copy of move history.
            PriorMoveStats = new List <GameMoveStat>();
            if (gameMoveHistory != null)
            {
                PriorMoveStats.AddRange(gameMoveHistory);
            }

            // Possibly autoselect new optimal parameters
            ParamsSearchExecutionChooser paramsChooser = new ParamsSearchExecutionChooser(nnEvaluators.EvaluatorDef,
                                                                                          searchParams, childSelectParams, searchLimit);

            // TODO: technically this is overwriting the params belonging to the prior search, that's ugly (but won't actually cause a problem)
            paramsChooser.ChooseOptimal(searchLimit.EstNumNodes(50_000, false), paramsSearchExecutionPostprocessor); // TODO: make 50_000 smarter


            int estNumNodes = EstimatedNumSearchNodesForEvaluator(searchLimit, nnEvaluators);

            // Adjust the nodes estimate if we are continuing an existing search
            if (searchLimit.Type == SearchLimitType.NodesPerMove && RootNWhenSearchStarted > 0)
            {
                estNumNodes = Math.Max(0, estNumNodes - RootNWhenSearchStarted);
            }
            Context = new MCTSIterator(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, reuseTranspositionRoots,
                                       nnEvaluators, searchParams, childSelectParams, searchLimit, estNumNodes);
            ThreadSearchContext = Context;

            TerminationManager = new MCTSFutilityPruning(this, searchLimit.SearchMoves);
            LimitManager       = limitManager;

            CeresEnvironment.LogInfo("MCTS", "Init", $"SearchManager created for store {store}", InstanceID);
        }
Exemple #2
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
        }
Exemple #3
0
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="nnParams"></param>
        /// <param name="searchParams"></param>
        /// <param name="childSelectParams"></param>
        /// <param name="priorMoves">if null, the prior moves are taken from the passed store</param>
        /// <param name="searchLimit"></param>
        public MCTSManager(MCTSNodeStore store,
                           MCTSIterator reuseOtherContextForEvaluatedNodes,
                           PositionEvalCache reusePositionCache,
                           TranspositionRootsDict reuseTranspositionRoots,
                           NNEvaluatorSet nnEvaluators,
                           ParamsSearch searchParams,
                           ParamsSelect childSelectParams,
                           SearchLimit searchLimit,
                           ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor,
                           IManagerGameLimit timeManager,
                           DateTime startTime,
                           MCTSManager priorManager,
                           List <GameMoveStat> gameMoveHistory,
                           bool isFirstMoveOfGame)
        {
            StartTimeThisSearch                = startTime;
            RootNWhenSearchStarted             = store.Nodes.nodes[store.RootIndex.Index].N;
            ParamsSearchExecutionPostprocessor = paramsSearchExecutionPostprocessor;
            IsFirstMoveOfGame = isFirstMoveOfGame;

            PriorMoveStats = new List <GameMoveStat>();

            // Make our own copy of move history.
            if (gameMoveHistory != null)
            {
                PriorMoveStats.AddRange(gameMoveHistory);
            }

            // Possibly convert time limit per game into time for this move.
            if (searchLimit.IsPerGameLimit)
            {
                SearchLimitType type = searchLimit.Type == SearchLimitType.SecondsForAllMoves
                                                       ? SearchLimitType.SecondsPerMove
                                                       : SearchLimitType.NodesPerMove;
                float rootQ = priorManager == null ? float.NaN : (float)store.RootNode.Q;
                ManagerGameLimitInputs timeManagerInputs = new(store.Nodes.PriorMoves.FinalPosition,
                                                               searchParams, PriorMoveStats,
                                                               type, store.RootNode.N, rootQ,
                                                               searchLimit.Value, searchLimit.ValueIncrement,
                                                               float.NaN, float.NaN,
                                                               maxMovesToGo : searchLimit.MaxMovesToGo,
                                                               isFirstMoveOfGame : isFirstMoveOfGame);

                ManagerGameLimitOutputs timeManagerOutputs = timeManager.ComputeMoveAllocation(timeManagerInputs);
                SearchLimit = timeManagerOutputs.LimitTarget;
            }
            else
            {
                SearchLimit = searchLimit;
            }

            // Possibly autoselect new optimal parameters
            ParamsSearchExecutionChooser paramsChooser = new ParamsSearchExecutionChooser(nnEvaluators.EvaluatorDef,
                                                                                          searchParams, childSelectParams, searchLimit);

            // TODO: technically this is overwriting the params belonging to the prior search, that's ugly (but won't actually cause a problem)
            paramsChooser.ChooseOptimal(searchLimit.EstNumNodes(50_000), paramsSearchExecutionPostprocessor); // TODO: make 50_000 smarter


            int estNumNodes = EstimatedNumSearchNodesForEvaluator(searchLimit, nnEvaluators);

            // Adjust the nodes estimate if we are continuing an existing search
            if (searchLimit.Type == SearchLimitType.NodesPerMove && RootNWhenSearchStarted > 0)
            {
                estNumNodes = Math.Max(0, estNumNodes - RootNWhenSearchStarted);
            }
            Context = new MCTSIterator(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, reuseTranspositionRoots,
                                       nnEvaluators, searchParams, childSelectParams, searchLimit, estNumNodes);
            ThreadSearchContext = Context;

            TerminationManager = new MCTSFutilityPruning(this, Context);
            LimitManager       = timeManager;

            CeresEnvironment.LogInfo("MCTS", "Init", $"SearchManager created for store {store}", InstanceID);
        }