Exemple #1
0
        /// <summary>
        /// Constructor to begin iteration within specified at a specified node.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="root"></param>
        public MCTSNodeSequentialVisitor(MCTSNodeStore store, MCTSNodeStructIndex root)
        {
            Store = store;
            Root  = root;

            currentNode     = root;
            pendingBranches = new SortedSet <MCTSNodeStructIndex>();
        }
Exemple #2
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);
            }
        }
Exemple #3
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);
        }
        /// <summary>
        /// Prunes cache down to approximately specified target size.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="targetSize">target numer of nodes, or -1 to use default sizing</param>
        /// <returns></returns>
        internal int Prune(MCTSNodeStore store, int targetSize)
        {
            int startNumInUse = numInUse;

            // Default target size is 70% of maximum.
            if (targetSize == -1)
            {
                targetSize = (nodes.Length * 70) / 100;
            }
            if (numInUse <= targetSize)
            {
                return(0);
            }

            lock (lockObj)
            {
                int count = 0;
                for (int i = 0; i < nodes.Length; i++)
                {
                    // TODO: the long is cast to int, could we possibly overflow? make long?
                    if (nodes[i] != null)
                    {
                        pruneSequenceNums[count++] = (int)nodes[i].LastAccessedSequenceCounter;
                    }
                }

                Span <int> slice = new Span <int>(pruneSequenceNums).Slice(0, count);

                // Compute the minimum sequence number an entry must have
                // to be retained (to enforce LRU eviction)
                //float cutoff = KthSmallestValue.CalcKthSmallestValue(keyPrioritiesForSorting, numToPrune);
                int cutoff;

                cutoff = KthSmallestValueInt.CalcKthSmallestValue(slice, numInUse - targetSize);
                //Console.WriteLine(slice.Length + " " + (numInUse-targetSize) + " --> "
                //                 + cutoff + " correct " + slice[numInUse-targetSize] + " avg " + slice[numInUse/2]);

                int maxEntries = pruneCount == 0 ? (numInUse + 1) : nodes.Length;
                for (int i = 1; i < maxEntries; i++)
                {
                    MCTSNode node = nodes[i];
                    if (node != null && node.LastAccessedSequenceCounter < cutoff)
                    {
                        MCTSNodeStructIndex nodeIndex = new MCTSNodeStructIndex(node.Index);
                        nodes[i] = null;

                        ref MCTSNodeStruct refNode = ref store.Nodes.nodes[nodeIndex.Index];
                        refNode.CacheIndex = 0;

                        numInUse--;
                    }
                }
                pruneCount++;
            }
Exemple #5
0
 public void Traverse(MCTSNodeStore store, VisitorFunc visitorFunc, TreeTraversalType traversalType)
 {
     if (traversalType == TreeTraversalType.Unspecified ||
         traversalType == TreeTraversalType.Sequential)
     {
         DoTraverseSequential(store, visitorFunc);
     }
     else
     {
         DoTraverse(store, visitorFunc, traversalType);
     }
 }
Exemple #6
0
        /// <summary>
        /// Constructor.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="context"></param>
        /// <param name="maxNodesBound"></param>
        /// <param name="positionCache"></param>
        public MCTSTree(MCTSNodeStore store, MCTSIterator context,
                        int maxNodesBound, int estimatedNumNodes,
                        PositionEvalCache positionCache)
        {
            if (context.ParamsSearch.DrawByRepetitionLookbackPlies > MAX_LENGTH_POS_HISTORY)
            {
                throw new Exception($"DrawByRepetitionLookbackPlies exceeds maximum length of {MAX_LENGTH_POS_HISTORY}");
            }

            Store         = store;
            Context       = context;
            PositionCache = positionCache;

            ChildCreateLocks = new LockSet(128);

            const int ANNOTATION_MIN_CACHE_SIZE = 50_000;
            int       annotationCacheSize       = Math.Min(maxNodesBound, context.ParamsSearch.Execution.NodeAnnotationCacheSize);

            if (annotationCacheSize < ANNOTATION_MIN_CACHE_SIZE &&
                annotationCacheSize < maxNodesBound)
            {
                throw new Exception($"NODE_ANNOTATION_CACHE_SIZE is below minimum size of {ANNOTATION_MIN_CACHE_SIZE}");
            }

            if (maxNodesBound <= annotationCacheSize && !context.ParamsSearch.TreeReuseEnabled)
            {
                // We know with certainty the maximum size, and it will fit inside the cache
                // without purging needed - so just use a simple fixed size cache
                cache = new MCTSNodeCacheArrayFixed(this, maxNodesBound);
            }
            else
            {
                cache = new MCTSNodeCacheArrayPurgeableSet(this, annotationCacheSize, estimatedNumNodes);
            }

            // Populate EncodedPriorPositions with encoded boards
            // corresponding to possible prior moves (before the root of this search)
            EncodedPriorPositions = new List <EncodedPositionBoard>();
            Position[] priorPositions = new Position[9];

            // Get prior positions (last position has highest index)
            priorPositions = PositionHistoryGatherer.DoGetHistoryPositions(PriorMoves, priorPositions, 0, 8, false).ToArray();


            for (int i = priorPositions.Length - 1; i >= 0; i--)
            {
                EncodedPositionBoard thisBoard = EncodedPositionBoard.GetBoard(in priorPositions[i], priorPositions[i].MiscInfo.SideToMove, false);
                EncodedPriorPositions.Add(thisBoard);
            }
        }
Exemple #7
0
 /// <summary>
 /// Possibly prunes the cache to remove some of the least recently accessed nodes.
 /// </summary>
 /// <param name="store"></param>
 public void PossiblyPruneCache(MCTSNodeStore store)
 {
     if (numCachePrunesInProgress == 0 && nodeCache.Count > MaxCacheSize)
     {
         // Reduce to 80% of prior size
         Task.Run(() =>
         {
             //using (new TimingBlock("Prune"))
             {
                 Interlocked.Increment(ref numCachePrunesInProgress);
                 DictionaryUtils.PruneDictionary(nodeCache, a => a.LastAccessedSequenceCounter,
                                                 (MaxCacheSize * 8) / 10);
                 Interlocked.Decrement(ref numCachePrunesInProgress);
             };
         });
     }
 }
        /// <summary>
        /// Possibly prunes the cache to remove some of the least recently accessed nodes.
        /// </summary>
        /// <param name="store"></param>
        public void PossiblyPruneCache(MCTSNodeStore store)
        {
            bool almostFull = numInUse > (nodes.Length * 90) / 100;

            if (numCachePrunesInProgress == 0 && almostFull)
            {
                // Reduce to 80% of prior size
                Task.Run(() =>
                {
                    //using (new TimingBlock("Prune"))
                    {
                        Interlocked.Increment(ref numCachePrunesInProgress);
                        int targetSize = (nodes.Length * 70) / 100;
                        //using (new TimingBlock("xx"))
                        Prune(store, targetSize);
                        Interlocked.Decrement(ref numCachePrunesInProgress);
                    };
                });
            }
        }
        /// <summary>
        /// Possibly prunes the cache to remove some of the least recently accessed nodes.
        /// </summary>
        /// <param name="store"></param>
        public void PossiblyPruneCache(MCTSNodeStore store)
        {
            bool almostFull = NumInUse > (MaxCacheSize * 85) / 100;

            if (numCachePrunesInProgress == 0 && almostFull)
            {
                Interlocked.Increment(ref numCachePrunesInProgress);

                int countPurged = 0;
                Parallel.ForEach(Enumerable.Range(0, MAX_SUBCACHES),
                                 new ParallelOptions()
                {
                    MaxDegreeOfParallelism = 4
                },                                                             // memory access already saturated at 4
                                 i =>
                {
                    Interlocked.Add(ref countPurged, subCaches[i].Prune(store, -1));
                });

                Interlocked.Decrement(ref numCachePrunesInProgress);
            }
        }
Exemple #10
0
        public static MCTSNodeStore Restore(string directory, string id, bool clearSearchInProgressState = true)
        {
            // Read in miscellaneous information file
            string miscInfoFN = Path.Combine(directory, id + FN_POSTFIX_MISC_INFO);
            MCTSNodeStoreSerializeMiscInfo miscInfo = SysMisc.ReadObj <MCTSNodeStoreSerializeMiscInfo>(miscInfoFN);

            MCTSNodeStore store = new MCTSNodeStore(miscInfo.NumNodesReserved);

            store.Nodes.InsureAllocated(miscInfo.NumNodesAllocated);
            store.RootIndex = miscInfo.RootIndex;
            store.Nodes.Reset(miscInfo.PriorMoves, true);

            long numNodes = SysMisc.ReadFileIntoSpan <MCTSNodeStruct>(Path.Combine(directory, id + FN_POSTFIX_NODES), store.Nodes.Span);

            //store.Nodes.InsureAllocated((int)numNodes);
            store.Nodes.nextFreeIndex = (int)numNodes;

            store.Children.InsureAllocated((int)miscInfo.NumChildrenAllocated);
            long numChildren = SysMisc.ReadFileIntoSpan <MCTSNodeStructChild>(Path.Combine(directory, id + FN_POSTFIX_CHILDREN), store.Children.Span);

            if (numChildren > int.MaxValue)
            {
                throw new NotImplementedException("Implementation restriction: cannot read stores with number of children exceeding int.MaxValue.");
            }
            store.Children.nextFreeBlockIndex = (int)numChildren / MCTSNodeStructChildStorage.NUM_CHILDREN_PER_BLOCK;

            if (clearSearchInProgressState)
            {
                // Reset the search state fields
                MemoryBufferOS <MCTSNodeStruct> nodes = store.Nodes.nodes;
                for (int i = 1; i < store.Nodes.NumTotalNodes; i++)
                {
                    nodes[i].ResetSearchInProgressState();
                }
            }

            return(store);
        }
Exemple #11
0
        public static void Save(MCTSNodeStore store, string directory, string id)
        {
            if (store.Children.NumAllocatedChildren >= int.MaxValue)
            {
                throw new NotImplementedException("Implementation restriction: cannot write stores with number of children exceeding int.MaxValue.");
            }

            SysMisc.WriteSpanToFile(GetPath(directory, id, FN_POSTFIX_NODES), store.Nodes.Span.Slice(0, store.Nodes.NumTotalNodes));
            SysMisc.WriteSpanToFile(GetPath(directory, id, FN_POSTFIX_CHILDREN), store.Children.Span.Slice(0, (int)store.Children.NumAllocatedChildren));

            MCTSNodeStoreSerializeMiscInfo miscInfo = new MCTSNodeStoreSerializeMiscInfo()
            {
                Description = "",
                PriorMoves  = store.Nodes.PriorMoves,
                RootIndex   = store.RootIndex,

                NumNodesReserved     = store.MaxNodes,
                NumNodesAllocated    = store.Nodes.NumTotalNodes,
                NumChildrenAllocated = store.Children.NumAllocatedChildren
            };

            SysMisc.WriteObj(GetPath(directory, id, FN_POSTFIX_MISC_INFO), miscInfo);
        }
Exemple #12
0
 /// Traverses node sequentially in order of creation (returning the node and its index).
 /// This traversal will typically be much faster because it is cache friendly.
 ///
 ///
 /// </summary>
 /// <param name="visitorFunc"></param>
 public void TraverseSequential(MCTSNodeStore store, VisitorSequentialFunc visitorFunc)
 {
     ref MCTSNodeStruct node = ref this;
Exemple #13
0
 /// <summary>
 /// Possibly prunes the cache to remove some of the least recently accessed nodes.
 /// </summary>
 /// <param name="store"></param>
 public void PossiblyPruneCache(MCTSNodeStore store)
 {
     // Nothing to do since fixed caches do not purge.
 }
Exemple #14
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 #15
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);
        }