/// <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>(); }
/// <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); } }
/// <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++; }
public void Traverse(MCTSNodeStore store, VisitorFunc visitorFunc, TreeTraversalType traversalType) { if (traversalType == TreeTraversalType.Unspecified || traversalType == TreeTraversalType.Sequential) { DoTraverseSequential(store, visitorFunc); } else { DoTraverse(store, visitorFunc, traversalType); } }
/// <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> /// 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); } }
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); }
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); }
/// 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;
/// <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. }
/// <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); }