/// <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 }
/// <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?
/// <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); }
// -------------------------------------------------------------------------------------------- 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); } } }
/// <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); } }
/// <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 }
/// <summary> /// Runs a search, possibly continuing from node /// nested in a prior search (tree reuse). /// </summary> /// <param name="priorSearch"></param> /// <param name="reuseOtherContextForEvaluatedNodes"></param> /// <param name="moves"></param> /// <param name="newPositionAndMoves"></param> /// <param name="gameMoveHistory"></param> /// <param name="searchLimit"></param> /// <param name="verbose"></param> /// <param name="startTime"></param> /// <param name="progressCallback"></param> /// <param name="thresholdMinFractionNodesRetained"></param> /// <param name="isFirstMoveOfGame"></param> public void SearchContinue(MCTSearch priorSearch, MCTSIterator reuseOtherContextForEvaluatedNodes, IEnumerable <MGMove> moves, PositionWithHistory newPositionAndMoves, List <GameMoveStat> gameMoveHistory, SearchLimit searchLimit, bool verbose, DateTime startTime, MCTSManager.MCTSProgressCallback progressCallback, float thresholdMinFractionNodesRetained, bool isFirstMoveOfGame = false) { CountSearchContinuations = priorSearch.CountSearchContinuations; Manager = priorSearch.Manager; MCTSIterator priorContext = Manager.Context; MCTSNodeStore store = priorContext.Tree.Store; int numNodesInitial = Manager == null ? 0 : Manager.Root.N; MCTSNodeStructIndex newRootIndex; using (new SearchContextExecutionBlock(priorContext)) { MCTSNode newRoot = FollowMovesToNode(Manager.Root, moves); // New root is not useful if contained no search // (for example if it was resolved via tablebase) // thus in that case we pretend as if we didn't find it if (newRoot != null && (newRoot.N == 0 || newRoot.NumPolicyMoves == 0)) { newRoot = null; } // Check for possible instant move (MCTSManager, MGMove, TimingStats)instamove = CheckInstamove(Manager, searchLimit, newRoot); if (instamove != default) { // Modify in place to point to the new root continationSubroot = newRoot; BestMove = instamove.Item2; TimingInfo = new TimingStats(); return; } else { CountSearchContinuations = 0; } // TODO: don't reuse tree if it would cause the nodes in use // to exceed a reasonable value for this machine #if NOT // NOTE: abandoned, small subtrees will be fast to rewrite so we can always do this // Only rewrite the store with the subtree reused // if it is not tiny relative to the current tree // (otherwise the scan/rewrite is not worth it float fracTreeReuse = newRoot.N / store.Nodes.NumUsedNodes; const float THRESHOLD_REUSE_TREE = 0.02f; #endif // Inform contempt manager about the opponents move // (compared to the move we believed was optimal) if (newRoot != null && newRoot.Depth == 2) { MCTSNode opponentsPriorMove = newRoot; MCTSNode bestMove = opponentsPriorMove.Parent.ChildrenSorted(n => (float)n.Q)[0]; if (bestMove.N > opponentsPriorMove.N / 10) { float bestQ = (float)bestMove.Q; float actualQ = (float)opponentsPriorMove.Q; Manager.Context.ContemptManager.RecordOpponentMove(actualQ, bestQ); //Console.WriteLine("Record " + actualQ + " vs best " + bestQ + " target contempt " + priorManager.Context.ContemptManager.TargetContempt); } } bool storeIsAlmostFull = priorContext.Tree.Store.FractionInUse > 0.9f; bool newRootIsBigEnoughForReuse = newRoot != null && newRoot.N >= (priorContext.Root.N * thresholdMinFractionNodesRetained); if (priorContext.ParamsSearch.TreeReuseEnabled && newRootIsBigEnoughForReuse && !storeIsAlmostFull) { SearchLimit searchLimitAdjusted = searchLimit; if (Manager.Context.ParamsSearch.Execution.TranspositionMode != TranspositionMode.None) { // The MakeChildNewRoot method is not able to handle transposition linkages // (this would be complicated and could involve linkages to nodes no longer in the retained subtree). // Therefore we first materialize any transposition linked nodes in the subtree. // Since this is not currently multithreaded we can turn off tree node locking for the duration. newRoot.Tree.ChildCreateLocks.LockingActive = false; newRoot.MaterializeAllTranspositionLinks(); newRoot.Tree.ChildCreateLocks.LockingActive = true; } // Now rewrite the tree nodes and children "in situ" PositionEvalCache reusePositionCache = null; if (Manager.Context.ParamsSearch.TreeReuseRetainedPositionCacheEnabled) { reusePositionCache = new PositionEvalCache(0); } TranspositionRootsDict newTranspositionRoots = null; if (priorContext.Tree.TranspositionRoots != null) { int estNumNewTranspositionRoots = newRoot.N + newRoot.N / 3; // somewhat oversize to allow for growth in subsequent search newTranspositionRoots = new TranspositionRootsDict(estNumNewTranspositionRoots); } // TODO: Consider sometimes or always skip rebuild via MakeChildNewRoot, // instead just set a new root (move it into place as first node). // Perhaps rebuild only if the MCTSNodeStore would become excessively large. TimingStats makeNewRootTimingStats = new TimingStats(); using (new TimingBlock(makeNewRootTimingStats, TimingBlock.LoggingType.None)) { MCTSNodeStructStorage.MakeChildNewRoot(store, Manager.Context.ParamsSelect.PolicySoftmax, ref newRoot.Ref, newPositionAndMoves, reusePositionCache, newTranspositionRoots); } MCTSManager.TotalTimeSecondsInMakeNewRoot += (float)makeNewRootTimingStats.ElapsedTimeSecs; CeresEnvironment.LogInfo("MCTS", "MakeChildNewRoot", $"Select {newRoot.N:N0} from {numNodesInitial:N0} " + $"in {(int)(makeNewRootTimingStats.ElapsedTimeSecs/1000.0)}ms"); // Finally if nodes adjust based on current nodes if (searchLimit.Type == SearchLimitType.NodesPerMove) { searchLimitAdjusted = new SearchLimit(SearchLimitType.NodesPerMove, searchLimit.Value + store.RootNode.N); } // Construct a new search manager reusing this modified store and modified transposition roots MCTSManager manager = new MCTSManager(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, newTranspositionRoots, priorContext.NNEvaluators, priorContext.ParamsSearch, priorContext.ParamsSelect, searchLimitAdjusted, Manager.ParamsSearchExecutionPostprocessor, Manager.LimitManager, startTime, Manager, gameMoveHistory, isFirstMoveOfGame: isFirstMoveOfGame); manager.Context.ContemptManager = priorContext.ContemptManager; bool possiblyUsePositionCache = false; // TODO could this be relaxed? (MGMove move, TimingStats stats)result = MCTSManager.Search(manager, verbose, progressCallback, possiblyUsePositionCache); BestMove = result.move; TimingInfo = result.stats; Manager = manager; } else { // We decided not to (or couldn't find) that path in the existing tree // Just run the search from scratch if (verbose) { Console.WriteLine("\r\nFailed nSearchFollowingMoves."); } Search(Manager.Context.NNEvaluators, Manager.Context.ParamsSelect, Manager.Context.ParamsSearch, Manager.LimitManager, null, reuseOtherContextForEvaluatedNodes, newPositionAndMoves, searchLimit, verbose, startTime, gameMoveHistory, progressCallback, false); } } #if NOT // This code partly or completely works // We don't rely upon it because it could result in uncontained growth of the store, // since detached nodes are left // But if the subtree chosen is almost the whole tree, maybe we could indeed use this techinque as an alternate in these cases if (store.ResetRootAssumingMovesMade(moves, thresholdFractionNodesRetained)) { SearchManager manager = new SearchManager(store, priorContext.ParamsNN, priorContext.ParamsSearch, priorContext.ParamsSelect, null, limit); manager.Context.TranspositionRoots = priorContext.TranspositionRoots; return(Search(manager, false, verbose, progressCallback, false)); } #endif }
/// <summary> /// 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); }
/// <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++; } } }