/// <summary> /// Constructor to create a store of specified maximum size. /// </summary> /// <param name="maxNodes"></param> /// <param name="priorMoves"></param> public MCTSNodeStore(int maxNodes, PositionWithHistory priorMoves = null) { if (priorMoves == null) { priorMoves = PositionWithHistory.StartPosition; } MaxNodes = maxNodes; int allocNodes = maxNodes; Nodes = new MCTSNodeStructStorage(allocNodes, null, MCTSParamsFixed.STORAGE_USE_INCREMENTAL_ALLOC, MCTSParamsFixed.STORAGE_LARGE_PAGES, MCTSParamsFixed.STORAGE_USE_EXISTING_SHARED_MEM); long reserveChildren = maxNodes * (long)AVG_CHILDREN_PER_NODE; Children = new MCTSNodeStructChildStorage(this, reserveChildren); // Save a copy of the prior moves Nodes.PriorMoves = new PositionWithHistory(priorMoves); CeresEnvironment.LogInfo("NodeStore", "Init", $"MCTSNodeStore created with max {maxNodes} nodes, max {reserveChildren} children"); MCTSNodeStruct.ValidateMCTSNodeStruct(); RootIndex = new MCTSNodeStructIndex(1); }
/// <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> /// 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); }
// Algorithm // priorEvaluateTask <- null // priorNodesNN <- null // do // { // // Select new nodes // newNodes <- Select() // // // Remove any which may have been already selected by alternate selector // newNodes <- Deduplicate(newNodes, priorNodesNN) // // // Check for those that can be immediately evaluated and split them out // (newNodesNN, newNodesImm) <- TryEvalImmediateAndPartition(newNodes) // if (OUT_OF_ORDER_ENABLED) BackupApply(newNodesImm) // // // Launch evaluation of new nodes which need NN evaluation // newEvaluateTask <- new Task(Evaluate(newNodesNN)) // // // Wait for prior NN evaluation to finish and apply nodes // if (priorEvaluateTask != null) // { // priorNodesNN <- Wait(priorEvaluateTask) // BackupApply(priorNodesNN) // } // // if (!OUT_OF_ORDER_ENABLED) BackupApply(newNodesImm) // // // Prepare to cycle again // priorEvaluateTask <- newEvaluateTask // } until (end of search) // // // Finalize last batch // priorNodesNN <- Wait(priorEvaluateTask) // BackupApply(priorNodesNN) public void ProcessDirectOverlapped(MCTSManager manager, int hardLimitNumNodes, int startingBatchSequenceNum, int?forceBatchSize) { Debug.Assert(!manager.Root.IsInFlight); if (hardLimitNumNodes == 0) { hardLimitNumNodes = 1; } bool overlappingAllowed = Context.ParamsSearch.Execution.FlowDirectOverlapped; int initialRootN = Context.Root.N; int guessMaxNumLeaves = MCTSParamsFixed.MAX_NN_BATCH_SIZE; ILeafSelector selector1; ILeafSelector selector2; selector1 = new LeafSelectorMulti(Context, 0, Context.StartPosAndPriorMoves, guessMaxNumLeaves); int secondSelectorID = Context.ParamsSearch.Execution.FlowDualSelectors ? 1 : 0; selector2 = overlappingAllowed ? new LeafSelectorMulti(Context, secondSelectorID, Context.StartPosAndPriorMoves, guessMaxNumLeaves) : null; MCTSNodesSelectedSet[] nodesSelectedSets = new MCTSNodesSelectedSet[overlappingAllowed ? 2 : 1]; for (int i = 0; i < nodesSelectedSets.Length; i++) { nodesSelectedSets[i] = new MCTSNodesSelectedSet(Context, i == 0 ? (LeafSelectorMulti)selector1 : (LeafSelectorMulti)selector2, guessMaxNumLeaves, guessMaxNumLeaves, BlockApply, Context.ParamsSearch.Execution.InFlightThisBatchLinkageEnabled, Context.ParamsSearch.Execution.InFlightOtherBatchLinkageEnabled); } int selectorID = 0; int batchSequenceNum = startingBatchSequenceNum; Task <MCTSNodesSelectedSet> overlappingTask = null; MCTSNodesSelectedSet pendingOverlappedNodes = null; int numOverlappedNodesImmediateApplied = 0; int iterationCount = 0; int numSelected = 0; int nodesLastSecondaryNetEvaluation = 0; while (true) { // Only start overlapping past 1000 nodes because // CPU latency will be very small at small tree sizes, // obviating the overlapping beneifts of hiding this latency bool overlapThisSet = overlappingAllowed && Context.Root.N > 2000; iterationCount++; ILeafSelector selector = selectorID == 0 ? selector1 : selector2; float thisBatchDynamicVLossBoost = batchingManagers[selectorID].VLossDynamicBoostForSelector(); // Call progress callback and check if reached search limit Context.ProgressCallback?.Invoke(manager); Manager.UpdateSearchStopStatus(); if (Manager.StopStatus != MCTSManager.SearchStopStatus.Continue) { break; } int numCurrentlyOverlapped = Context.Root.NInFlight + Context.Root.NInFlight2; int numApplied = Context.Root.N - initialRootN; int hardLimitNumNodesThisBatch = int.MaxValue; if (hardLimitNumNodes > 0) { // Subtract out number already applied or in flight hardLimitNumNodesThisBatch = hardLimitNumNodes - (numApplied + numCurrentlyOverlapped); // Stop search if we have already exceeded search limit // or if remaining number is very small relative to full search // (this avoids incurring latency with a few small batches at end of a search). if (hardLimitNumNodesThisBatch <= numApplied / 1000) { break; } } // Console.WriteLine($"Remap {targetThisBatch} ==> {Context.Root.N} {TargetBatchSize(Context.EstimatedNumSearchNodes, Context.Root.N)}"); int targetThisBatch = OptimalBatchSizeCalculator.CalcOptimalBatchSize(Manager.EstimatedNumSearchNodes, Context.Root.N, overlapThisSet, Context.ParamsSearch.Execution.FlowDualSelectors, Context.ParamsSearch.Execution.MaxBatchSize, Context.ParamsSearch.BatchSizeMultiplier); targetThisBatch = Math.Min(targetThisBatch, Manager.MaxBatchSizeDueToPossibleNearTimeExhaustion); if (forceBatchSize.HasValue) { targetThisBatch = forceBatchSize.Value; } if (targetThisBatch > hardLimitNumNodesThisBatch) { targetThisBatch = hardLimitNumNodesThisBatch; } int thisBatchTotalNumLeafsTargeted = 0; // Compute number of dynamic nodes to add (do not add any when tree is very small and impure child selection is particularly deleterious) int numNodesPadding = 0; if (manager.Root.N > 50 && manager.Context.ParamsSearch.PaddedBatchSizing) { numNodesPadding = manager.Context.ParamsSearch.PaddedExtraNodesBase + (int)(targetThisBatch * manager.Context.ParamsSearch.PaddedExtraNodesMultiplier); } int numVisitsTryThisBatch = targetThisBatch + numNodesPadding; numVisitsTryThisBatch = (int)(numVisitsTryThisBatch * batchingManagers[selectorID].BatchSizeDynamicScaleForSelector()); // Select a batch using this selector // It will select a set of Leafs completely independent of what a possibly other selector already selected // It may find some unevaluated leafs in the tree (extant but N = 0) due to action of the other selector // These leafs will nevertheless be recorded but specifically ignored later MCTSNodesSelectedSet nodesSelectedSet = nodesSelectedSets[selectorID]; nodesSelectedSet.Reset(pendingOverlappedNodes); // Select the batch of nodes if (numVisitsTryThisBatch < 5 || !Context.ParamsSearch.Execution.FlowSplitSelects) { thisBatchTotalNumLeafsTargeted += numVisitsTryThisBatch; ListBounded <MCTSNode> selectedNodes = selector.SelectNewLeafBatchlet(Context.Root, numVisitsTryThisBatch, thisBatchDynamicVLossBoost); nodesSelectedSet.AddSelectedNodes(selectedNodes, true); } else { // Set default assumed max batch size nodesSelectedSet.MaxNodesNN = numVisitsTryThisBatch; // In first attempt try to get 60% of target int numTry1 = Math.Max(1, (int)(numVisitsTryThisBatch * 0.60f)); int numTry2 = (int)(numVisitsTryThisBatch * 0.40f); thisBatchTotalNumLeafsTargeted += numTry1; ListBounded <MCTSNode> selectedNodes1 = selector.SelectNewLeafBatchlet(Context.Root, numTry1, thisBatchDynamicVLossBoost); nodesSelectedSet.AddSelectedNodes(selectedNodes1, true); int numGot1 = nodesSelectedSet.NumNewLeafsAddedNonDuplicates; nodesSelectedSet.ApplyImmeditateNotYetApplied(); // In second try target remaining 40% if (Context.ParamsSearch.Execution.SmartSizeBatches && Context.EvaluatorDef.NumDevices == 1 && Context.NNEvaluators.PerfStatsPrimary != null) // TODO: somehow handle this for multiple GPUs { int[] optimalBatchSizeBreaks; if (Context.NNEvaluators.PerfStatsPrimary.Breaks != null) { optimalBatchSizeBreaks = Context.NNEvaluators.PerfStatsPrimary.Breaks; } else { optimalBatchSizeBreaks = Context.GetOptimalBatchSizeBreaks(Context.EvaluatorDef.DeviceIndices[0]); } // Make an educated guess about the total number of NN nodes that will be sent // to the NN (resulting from both try1 and try2) // We base this on the fraction of nodes in try1 which actually are going to NN // then discounted by 0.8 because the yield on the second try is typically lower const float TRY2_SUCCESS_DISCOUNT_FACTOR = 0.8f; float fracNodesFirstTryGoingToNN = (float)nodesSelectedSet.NodesNN.Count / (float)numTry1; int estimatedAdditionalNNNodesTry2 = (int)(numTry2 * fracNodesFirstTryGoingToNN * TRY2_SUCCESS_DISCOUNT_FACTOR); int estimatedTotalNNNodes = nodesSelectedSet.NodesNN.Count + estimatedAdditionalNNNodesTry2; const float NEARBY_BREAK_FRACTION = 0.20f; int? closeByBreak = NearbyBreak(optimalBatchSizeBreaks, estimatedTotalNNNodes, NEARBY_BREAK_FRACTION); if (closeByBreak is not null) { nodesSelectedSet.MaxNodesNN = closeByBreak.Value; } } // Only try to collect the second half of the batch if the first one yielded // a good fraction of desired nodes (otherwise too many collisions to profitably continue) const float THRESHOLD_SUCCESS_TRY1 = 0.667f; bool shouldProcessTry2 = numTry1 < 10 || ((float)numGot1 / (float)numTry1) >= THRESHOLD_SUCCESS_TRY1; if (shouldProcessTry2) { thisBatchTotalNumLeafsTargeted += numTry2; ListBounded <MCTSNode> selectedNodes2 = selector.SelectNewLeafBatchlet(Context.Root, numTry2, thisBatchDynamicVLossBoost); // TODO: clean this up // - Note that ideally we might not apply immeidate nodes here (i.e. pass false instead of true in next line) // - This is because once done selecting nodes for this batch, we want to get it launched as soon as possible, // we could defer and call ApplyImmeditateNotYetApplied only later (below) // *** WARNING*** However, setting this to false causes NInFlight errors (seen when running test matches within 1 or 2 minutes) nodesSelectedSet.AddSelectedNodes(selectedNodes2, true); // MUST BE true; see above } } // Possibly pad with "preload nodes" if (rootPreloader != null && nodesSelectedSet.NodesNN.Count <= MCTSRootPreloader.PRELOAD_THRESHOLD_BATCH_SIZE) { // TODO: do we need to update thisBatchTotalNumLeafsTargeted ? TryAddRootPreloadNodes(manager, MAX_PRELOAD_NODES_PER_BATCH, nodesSelectedSet, selector); } // TODO: make flow private belows if (Context.EvaluatorDef.SECONDARY_NETWORK_ID != null && (manager.Root.N - nodesLastSecondaryNetEvaluation > 500)) { manager.RunSecondaryNetEvaluations(8, manager.flow.BlockNNEvalSecondaryNet); nodesLastSecondaryNetEvaluation = manager.Root.N; } // Update statistics UpdateStatistics(selectorID, thisBatchTotalNumLeafsTargeted, nodesSelectedSet); // Convert any excess nodes to CacheOnly if (Context.ParamsSearch.PaddedBatchSizing) { throw new Exception("Needs remediation"); // Mark nodes not eligible to be applied as "cache only" //for (int i = numApplyThisBatch; i < selectedNodes.Count; i++) // selectedNodes[i].ActionType = MCTSNode.NodeActionType.CacheOnly; } CeresEnvironment.LogInfo("MCTS", "Batch", $"Batch Target={numVisitsTryThisBatch} " + $"yields NN={nodesSelectedSet.NodesNN.Count} Immediate= {nodesSelectedSet.NodesImmediateNotYetApplied.Count} " + $"[CacheOnly={nodesSelectedSet.NumCacheOnly} None={nodesSelectedSet.NumNotApply}]", manager.InstanceID); // Now launch NN evaluation on the non-immediate nodes bool isPrimary = selectorID == 0; if (overlapThisSet) { Task <MCTSNodesSelectedSet> priorOverlappingTask = overlappingTask; numOverlappedNodesImmediateApplied = nodesSelectedSet.NodesImmediateNotYetApplied.Count; // Launch a new task to preprocess and evaluate these nodes overlappingTask = Task.Run(() => LaunchEvaluate(manager, targetThisBatch, isPrimary, nodesSelectedSet)); nodesSelectedSet.ApplyImmeditateNotYetApplied(); pendingOverlappedNodes = nodesSelectedSet; WaitEvaluationDoneAndApply(priorOverlappingTask, nodesSelectedSet.NodesNN.Count); } else { LaunchEvaluate(manager, targetThisBatch, isPrimary, nodesSelectedSet); nodesSelectedSet.ApplyAll(); //Console.WriteLine("applied " + selector.Leafs.Count + " " + manager.Root); } RunPeriodicMaintenance(manager, batchSequenceNum, iterationCount); // Advance (rotate) selector if (overlappingAllowed) { selectorID = (selectorID + 1) % 2; } batchSequenceNum++; } WaitEvaluationDoneAndApply(overlappingTask); // Debug.Assert(!manager.Root.IsInFlight); if ((manager.Root.NInFlight != 0 || manager.Root.NInFlight2 != 0) && !haveWarned) { Console.WriteLine($"Internal error: search ended with N={manager.Root.N} NInFlight={manager.Root.NInFlight} NInFlight2={manager.Root.NInFlight2} " + manager.Root); int count = 0; manager.Root.Ref.TraverseSequential(manager.Root.Context.Tree.Store, delegate(ref MCTSNodeStruct node, MCTSNodeStructIndex index) { if (node.IsInFlight && node.NumChildrenVisited == 0 && count++ < 20) { Console.WriteLine(" " + index.Index + " " + node.Terminal + " " + node.N + " " + node.IsTranspositionLinked + " " + node.NumNodesTranspositionExtracted); } return(true); }); haveWarned = true; } selector1.Shutdown(); selector2?.Shutdown(); }