/// <summary>
        /// Makes node within the tree the root child, reorganizing the nodes and child arrays.
        ///
        /// Critically, we do this operation in situ to avoid having to transiently allocate
        /// extremely large memory objects.
        ///
        /// The operation consists of 3 stages:
        ///   - traverse the subtree breadth-first starting at the new root,
        ///     building a bitmap of which nodes are children.
        ///
        ///   - traverse the node array sequentially, processing each node that is a member of this bitmap by
        ///     moving it into the new position in the store (including modifying associated child and parent references)
        ///     Also we build a table of all the new children that need to be moved.
        ///
        ///   - using the child table built above, shift all children down. Because nodes may have children written
        ///     out of order, we don't know for sure there is enough space available. Therefore we sort the table
        ///     based on their new position in the table and then shift them down, insuring we don't overwrite
        ///     children yet to be shifted.
        ///
        /// Additionally we may have to recreate the transposition roots dictionary because
        /// the set of roots is smaller (the retined subtree only) and the node indices will change.
        /// </summary>
        /// <param name="store"></param>
        /// <param name="newRootChild"></param>
        /// <param name="newPriorMoves"></param>
        /// <param name="transpositionRoots"></param>
        public static void MakeChildNewRoot(MCTSNodeStore store, float policySoftmax,
                                            ref MCTSNodeStruct newRootChild,
                                            PositionWithHistory newPriorMoves,
                                            PositionEvalCache cacheNonRetainedNodes,
                                            TranspositionRootsDict transpositionRoots)
        {
#if DEBUG
            store.Validate();
#endif

            COUNT++;

            // Nothing to do if the requested node is already currently the root
            if (newRootChild.Index == store.RootNode.Index)
            {
                // Nothing changing in the tree, just flush the cache references
                store.ClearAllCacheIndices();
            }
            else
            {
                DoMakeChildNewRoot(store, policySoftmax, ref newRootChild, newPriorMoves, cacheNonRetainedNodes, transpositionRoots);
            }

#if DEBUG
            store.Validate();
#endif
        }
Exemple #2
0
        /// <summary>
        /// Extracts subset of nodes (filtered to those with N sufficiently large)
        /// into a cache to be reused in a subsequent interation of an iterated search.
        /// </summary>
        /// <param name="root"></param>
        /// <param name="minN"></param>
        /// <param name="maxWeightEmpiricalPolicy"></param>
        /// <param name="treeModificationType"></param>
        /// <returns></returns>
        public static PositionEvalCache ModifyNodeP(MCTSNode root, int minN, float maxWeightEmpiricalPolicy,
                                                    IteratedMCTSDef.TreeModificationType treeModificationType)
        {
            bool cache = treeModificationType == IteratedMCTSDef.TreeModificationType.DeleteNodesMoveToCache;

            PositionEvalCache posCache = cache ? new PositionEvalCache() : null;

            root.Ref.TraverseSequential(root.Context.Tree.Store, (ref MCTSNodeStruct nodeRef, MCTSNodeStructIndex index) =>
            {
                bool shouldBlend = nodeRef.N >= minN;
                if (shouldBlend || treeModificationType == IteratedMCTSDef.TreeModificationType.DeleteNodesMoveToCache)
                {
                    MCTSNode node = root.Context.Tree.GetNode(index);

                    bool rewriteInTree = treeModificationType == IteratedMCTSDef.TreeModificationType.ClearNodeVisits &&
                                         nodeRef.N >= minN;

                    float weightEmpirical = maxWeightEmpiricalPolicy * ((float)nodeRef.N / (float)root.N);

                    ProcessNode(posCache, node, weightEmpirical, cache, rewriteInTree);
                }

                return(true);
            });

            return(posCache);
        }
        /// <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?
Exemple #4
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 #5
0
        // --------------------------------------------------------------------------------------------
        static void ProcessNode(PositionEvalCache cache, MCTSNode node, float weightEmpirical,
                                bool saveToCache, bool rewriteNodeInTree)
        {
            Span <MCTSNodeStructChild> children = node.Ref.Children;

            // TODO: optimize this away if saveToCache is false
            ushort[] probabilities = new ushort[node.NumPolicyMoves];
            ushort[] indices       = new ushort[node.NumPolicyMoves];

            // Compute empirical visit distribution
            float[] nodeFractions = new float[node.NumPolicyMoves];
            for (int i = 0; i < node.NumChildrenExpanded; i++)
            {
                nodeFractions[i] = (float)node.ChildAtIndex(i).N / (float)node.N;
            }

            // Determine P of first unexpanded node
            // We can't allow any child to have a new P less than this
            // since we need to keep them in order by P and the resorting logic below
            // can only operate over expanded nodes
            float minP = 0;

            if (node.NumChildrenExpanded < node.NumPolicyMoves)
            {
                minP = node.ChildAtIndexInfo(node.NumChildrenExpanded).p;
            }

            // Add each move to the policy vector with blend of prior and empirical values
            for (int i = 0; i < node.NumChildrenExpanded; i++)
            {
                (MCTSNode node, EncodedMove move, FP16 p)info = node.ChildAtIndexInfo(i);
                indices[i] = (ushort)info.move.IndexNeuralNet;

                float newValue = (1.0f - weightEmpirical) * info.p
                                 + weightEmpirical * nodeFractions[i];
                if (newValue < minP)
                {
                    newValue = minP;
                }
                probabilities[i] = CompressedPolicyVector.EncodedProbability(newValue);

                if (rewriteNodeInTree && weightEmpirical != 0)
                {
                    MCTSNodeStructChild thisChild = children[i];
                    if (thisChild.IsExpanded)
                    {
                        ref MCTSNodeStruct childNodeRef = ref thisChild.ChildRef;
                        thisChild.ChildRef.P = (FP16)newValue;
                    }
                    else
                    {
                        node.Ref.ChildAtIndex(i).SetUnexpandedPolicyValues(thisChild.Move, (FP16)newValue);
                    }
                }
            }
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>
        /// Constructor for a NN evaluator (either local or remote) with specified parameters.
        /// </summary>
        /// <param name="paramsNN"></param>
        /// <param name="saveToCache"></param>
        /// <param name="instanceID"></param>
        /// <param name="lowPriority"></param>
        public LeafEvaluatorNN(NNEvaluatorDef evaluatorDef, NNEvaluator evaluator,
                               bool saveToCache,
                               bool lowPriority,
                               PositionEvalCache cache,
                               Func <MCTSIterator, int> batchEvaluatorIndexDynamicSelector)
        {
            rawPosArray = posArrayPool.Rent(NNEvaluatorDef.MAX_BATCH_SIZE);

            EvaluatorDef = evaluatorDef;
            SaveToCache  = saveToCache;
            LowPriority  = lowPriority;
            Cache        = cache;
            this.BatchEvaluatorIndexDynamicSelector = batchEvaluatorIndexDynamicSelector;

            Batch = new EncodedPositionBatchFlat(EncodedPositionType.PositionOnly, NNEvaluatorDef.MAX_BATCH_SIZE);

            if (evaluatorDef.Location == NNEvaluatorDef.LocationType.Local)
            {
                localEvaluator = evaluator;// isEvaluator1 ? Params.Evaluator1 : Params.Evaluator2;
            }
            else
            {
                throw new NotImplementedException();
            }

            // TODO: auto-estimate performance
#if SOMEDAY
            for (int i = 0; i < 10; i++)
            {
//        using (new TimingBlock("benchmark"))
                {
                    float[] splits = WFEvalNetBenchmark.GetBigBatchNPSFractions(((WFEvalNetCompound)localEvaluator).Evaluators);
                    Console.WriteLine(splits[0] + " " + splits[1] + " " + splits[2] + " " + splits[3]);
                    (float estNPSSingletons, float estNPSBigBatch) = WFEvalNetBenchmark.EstNPS(localEvaluator);
                    Console.WriteLine(estNPSSingletons + " " + estNPSBigBatch);
                }
            }
#endif
        }
Exemple #8
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 #9
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);
        }
Exemple #10
0
 /// <summary>
 /// Constructor for a cache built on top of specified PositionEvalCache.
 /// </summary>
 /// <param name="cache"></param>
 public LeafEvaluatorCache(PositionEvalCache cache)
 {
     this.cache = cache;
 }
        static void DoMakeChildNewRoot(MCTSNodeStore store, float policySoftmax, ref MCTSNodeStruct newRootChild,
                                       PositionWithHistory newPriorMoves,
                                       PositionEvalCache cacheNonRetainedNodes,
                                       TranspositionRootsDict transpositionRoots)
        {
            ChildStartIndexToNodeIndex[] childrenToNodes;

            uint     numNodesUsed;
            uint     numChildrenUsed;
            BitArray includedNodes;

            int newRootChildIndex   = newRootChild.Index.Index;
            int newIndexOfNewParent = -1;

            int nextAvailableNodeIndex = 1;

            // Traverse this subtree, building a bit array of visited nodes
            includedNodes = MCTSNodeStructUtils.BitArrayNodesInSubtree(store, ref newRootChild, out numNodesUsed);

            //using (new TimingBlock("Build position cache "))
            if (cacheNonRetainedNodes != null)
            {
                long estNumNodes = store.RootNode.N - numNodesUsed;
                cacheNonRetainedNodes.InitializeWithSize((int)estNumNodes);
                ExtractPositionCacheNonRetainedNodes(store, policySoftmax, includedNodes, in newRootChild, cacheNonRetainedNodes);
            }

            // We will constract a table indicating the starting index and length of
            // children associated with the nodes we are extracting
            childrenToNodes = GC.AllocateUninitializedArray <ChildStartIndexToNodeIndex>((int)numNodesUsed);

            void RewriteNodes()
            {
                // TODO: Consider that the above is possibly all we need to do in some case
                //       Suppose the subtree is very large relative to the whole
                //       This approach would be much faster, and orphan an only small part of the storage

                // Now scan all above nodes.
                // If they don't belong, ignore.
                // If they do belong, swap them down to the next available lower location
                // Note that this can't be parallelized, since we have to do it strictly in order of node index
                int numRewrittenNodesDone = 0;

                for (int i = 2; i < store.Nodes.nextFreeIndex; i++)
                {
                    if (includedNodes.Get(i))
                    {
                        ref MCTSNodeStruct thisNode = ref store.Nodes.nodes[i];

                        // Reset any cache entry
                        thisNode.CacheIndex = 0;

                        // Not possible to support transposition linked nodes,
                        // since the root may be in a part of the tree that is not retained
                        // and possibly already overwritten.
                        // We expect them to have already been materialized by the time we reach this point.
                        Debug.Assert(!thisNode.IsTranspositionLinked);
                        Debug.Assert(thisNode.NumNodesTranspositionExtracted == 0);

                        // Remember this location if this is the new parent
                        if (i == newRootChildIndex)
                        {
                            newIndexOfNewParent = nextAvailableNodeIndex;
                        }

                        // Move the actual node
                        MoveNodePosition(store, new MCTSNodeStructIndex(i), new MCTSNodeStructIndex(nextAvailableNodeIndex));

                        // Reset all transposition information
                        thisNode.NextTranspositionLinked = 0;

                        childrenToNodes[numRewrittenNodesDone] = new ChildStartIndexToNodeIndex(thisNode.childStartBlockIndex, nextAvailableNodeIndex, thisNode.NumPolicyMoves);

                        // Re-insert this into the transpositionRoots (with the updated node index)
                        if (transpositionRoots != null)
                        {
                            transpositionRoots.TryAdd(thisNode.ZobristHash, nextAvailableNodeIndex);
                        }

                        Debug.Assert(thisNode.NumNodesTranspositionExtracted == 0);

                        numRewrittenNodesDone++;
                        nextAvailableNodeIndex++;
                    }
                }
            }