/// <summary> /// Overridden virtual method that executs the search /// by issuing UCI commands to the LC0 engine with appropriate search limit parameters. /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="gameMoveHistory"></param> /// <param name="callback"></param> /// <returns></returns> protected override GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory, ProgressCallback callback, bool verbose) { DoSearchPrepare(); if (SetupAction != null) { SetupAction(); } string fen = curPositionAndMoves.InitialPosition.FEN; string endFEN = curPositionAndMoves.FinalPosition.FEN; string moveStr = curPositionAndMoves.MovesStr; // Run the analysis LC0VerboseMoveStats lc0Analysis = LC0Engine.AnalyzePositionFromFENAndMoves(fen, moveStr, endFEN, searchLimit); if (verbose) { lc0Analysis.Dump(); } float scoreLC0 = (int)MathF.Round(EncodedEvalLogistic.LogisticToCentipawn(lc0Analysis.SearchEvalLogistic), 0); // TODO: can we somehow correctly set the staring N arugment here? return(new GameEngineSearchResult(lc0Analysis.BestMove, lc0Analysis.SearchEvalLogistic, scoreLC0, float.NaN, searchLimit, default, 0, (int)lc0Analysis.NumNodes, (int)lc0Analysis.UCIInfo.Depth));
/// <summary> /// Constructor for selector over specified MCTSIterator. /// </summary> /// <param name="context"></param> /// <param name="selectorID"></param> /// <param name="priorSequence"></param> /// <param name="guessNumLeaves"></param> public LeafSelectorMulti(MCTSIterator context, int selectorID, PositionWithHistory priorSequence, int guessNumLeaves) { Debug.Assert(selectorID < ILeafSelector.MAX_SELECTORS); if (USE_CUSTOM_THREADPOOL) { tpm = tpmPool.Value.GetFromPool(); } SelectorID = selectorID; PriorSequence = priorSequence; paramsExecution = context.ParamsSearch.Execution; int maxNodesPerBatchForRootPreload = context.ParamsSearch.Execution.RootPreloadDepth > 0 ? MCTSSearchFlow.MAX_PRELOAD_NODES_PER_BATCH : 0; int extraLeafsDynamic = 0; if (context.ParamsSearch.PaddedBatchSizing) { extraLeafsDynamic = context.ParamsSearch.PaddedExtraNodesBase + (int)(context.ParamsSearch.PaddedExtraNodesMultiplier * guessNumLeaves); } leafs = new ListBounded <MCTSNode>(guessNumLeaves + maxNodesPerBatchForRootPreload + extraLeafsDynamic); Context = context; }
/// <summary> /// Runs a search, calling DoSearch and adjusting the cumulative search time /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="callback"></param> /// <returns></returns> public GameEngineSearchResult Search(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory = null, ProgressCallback callback = null, bool verbose = false) { // Execute any preparation which should not be counted against thinking time // For example, Stockfish can require hundreds of milliseconds to process "ucinewgame" // which is used to reset state/hash table when the tree reuse option is enabled. DoSearchPrepare(); TimingStats stats = new TimingStats(); GameEngineSearchResult result; using (new TimingBlock(stats, TimingBlock.LoggingType.None)) { result = DoSearch(curPositionAndMoves, searchLimit, gameMoveHistory, callback, verbose); } CumulativeSearchTimeSeconds += (float)stats.ElapsedTimeSecs; CumulativeNodes += result.FinalN; // XXY Console.WriteLine(this.GetType() + " limit " + searchLimit + " elapsed " + stats.ElapsedTimeSecs); result.TimingStats = stats; return(result); }
/// <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> /// 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> /// Runs a search, calling DoSearch and adjusting the cumulative search time /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="callback"></param> /// <returns></returns> public GameEngineSearchResult Search(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory = null, ProgressCallback callback = null, bool verbose = false) { if (inSearch) { throw new Exception("GameEngine.Search cannot be called concurrently by more than one thread."); } inSearch = true; // Execute any preparation which should not be counted against thinking time // For example, Stockfish can require hundreds of milliseconds to process "ucinewgame" // which is used to reset state/hash table when the tree reuse option is enabled. DoSearchPrepare(); TimingStats stats = new TimingStats(); GameEngineSearchResult result; using (new TimingBlock(stats, TimingBlock.LoggingType.None)) { result = DoSearch(curPositionAndMoves, searchLimit, gameMoveHistory, callback, verbose); } CumulativeSearchTimeSeconds += (float)stats.ElapsedTimeSecs; CumulativeNodes += result.FinalN; result.TimingStats = stats; inSearch = false; return(result); }
/// <summary> /// Processes a specified position command, /// with the side effect of resetting the curPositionAndMoves. /// </summary> /// <param name="command"></param> private void ProcessPosition(string command) { command = StringUtils.WhitespaceRemoved(command); string commandLower = command.ToLower(); string posString; if (commandLower.StartsWith("position fen ")) { posString = command.Substring(13); } else if (commandLower.StartsWith("position startpos")) { posString = command.Substring(9); } else { throw new Exception($"Illegal position command, expected to start with position fen or position startpos"); } PositionWithHistory newPositionAndMoves = PositionWithHistory.FromFENAndMovesUCI(posString); curPositionIsContinuationOfPrior = newPositionAndMoves.IsIdenticalToPriorToLastMove(curPositionAndMoves); if (!curPositionIsContinuationOfPrior && CeresEngine != null) { CeresEngine.ResetGame(); } // Switch to the new position and moves curPositionAndMoves = newPositionAndMoves; }
/// <summary> /// Runs a search, calling DoSearch and adjusting the cumulative search time /// (convenience method with same functionality but returns the as the subclass /// GameEngineSearchResultCeres. /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="callback"></param> /// <returns></returns> public GameEngineSearchResultCeres SearchCeres(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory = null, ProgressCallback callback = null, bool verbose = false) { return(Search(curPositionAndMoves, searchLimit, gameMoveHistory, callback, verbose) as GameEngineSearchResultCeres); }
/// <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> /// Analyzes a position until a specified search limit is exhausted. /// </summary> /// <param name="fenAndMovesStr"></param> /// <param name="searchLimit"></param> /// <returns></returns> public LC0VerboseMoveStats AnalyzePositionFromFENAndMoves(string fenAndMovesStr, SearchLimit searchLimit) { List <LC0VerboseMoveStat> moves = new List <LC0VerboseMoveStat>(); PositionWithHistory pwh = PositionWithHistory.FromFENAndMovesUCI(fenAndMovesStr); UCISearchInfo searchInfo; int searchValueMilliseconds = (int)((float)searchLimit.Value * 1000.0f); switch (searchLimit.Type) { case SearchLimitType.NodesPerMove: searchInfo = Runner.EvalPositionToNodes(fenAndMovesStr, (int)searchLimit.Value); break; case SearchLimitType.SecondsPerMove: searchInfo = Runner.EvalPositionToMovetime(fenAndMovesStr, searchValueMilliseconds); break; case SearchLimitType.NodesForAllMoves: throw new Exception("NodesForAllMoves not supported for Leela Chess Zero"); case SearchLimitType.SecondsForAllMoves: bool weAreWhite = pwh.FinalPosition.MiscInfo.SideToMove == SideType.White; searchInfo = Runner.EvalPositionRemainingTime(fenAndMovesStr, weAreWhite, searchLimit.MaxMovesToGo, (int)(searchLimit.Value * 1000), (int)(searchLimit.ValueIncrement * 1000)); break; default: throw new Exception($"Unknown SeachLimit.Type {searchLimit.Type}"); } double elapsed = 0;//engine.EngineProcess.TotalProcessorTime.TotalSeconds - startTime; // no more, we now assume win_percentages is requested LeelaVerboseMoveStats ret = new LeelaVerboseMoveStats(positionEnd, searchInfo.BestMove, elapsed, searchInfo.Nodes, LZPositionEvalLogistic.CentipawnToLogistic2018(searchInfo.Score)); float scoreLogistic = searchInfo.ScoreLogistic; LC0VerboseMoveStats ret = new LC0VerboseMoveStats(pwh.FinalPosition, searchInfo.BestMove, elapsed, searchInfo.Nodes, scoreLogistic, searchInfo); searchInfo.Infos.Reverse(); foreach (string info in searchInfo.Infos) { if (info.Contains("P:")) { moves.Add(new LC0VerboseMoveStat(ret, info)); } } ret.SetMoves(moves); return(LastAnalyzedPositionStats = ret); }
protected override GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory, ProgressCallback callback, bool verbose) { DoSearchPrepare(); bool weAreWhite = curPositionAndMoves.FinalPosition.MiscInfo.SideToMove == SideType.White; UCISearchInfo gameInfo; switch (searchLimit.Type) { case SearchLimitType.SecondsPerMove: gameInfo = UCIRunner.EvalPositionToMovetime(curPositionAndMoves.FENAndMovesString, (int)(searchLimit.Value * 1000)); break; case SearchLimitType.NodesPerMove: gameInfo = UCIRunner.EvalPositionToNodes(curPositionAndMoves.FENAndMovesString, (int)(searchLimit.Value)); break; case SearchLimitType.NodesForAllMoves: using (new TimingBlock(new TimingStats(), TimingBlock.LoggingType.None)) { gameInfo = UCIRunner.EvalPositionRemainingNodes(curPositionAndMoves.FENAndMovesString, weAreWhite, searchLimit.MaxMovesToGo, (int)(searchLimit.Value), (int)(searchLimit.ValueIncrement)); } break; case SearchLimitType.SecondsForAllMoves: using (new TimingBlock(new TimingStats(), TimingBlock.LoggingType.None)) { gameInfo = UCIRunner.EvalPositionRemainingTime(curPositionAndMoves.FENAndMovesString, weAreWhite, searchLimit.MaxMovesToGo, (int)(searchLimit.Value * 1000), (int)(searchLimit.ValueIncrement * 1000)); } break; default: throw new NotSupportedException($"Unsupported MoveType {searchLimit.Type}"); } float q = EncodedEvalLogistic.CentipawnToLogistic(gameInfo.ScoreCentipawns); return(new GameEngineSearchResult(gameInfo.BestMove, q, gameInfo.ScoreCentipawns, float.NaN, searchLimit, default, 0, (int)gameInfo.Nodes, gameInfo.Depth));
public static void DumpPV(PositionWithHistory priorMoves, MCTSNode node, bool fullDetail, List <MGMove> subvariation = null) { if (subvariation != null) { List <MGMove> allMoves = new List <MGMove>(); allMoves.AddRange(priorMoves.Moves); allMoves.AddRange(subvariation); DumpPV(new PositionWithHistory(priorMoves.InitialPosMG, allMoves), DescendMovesToNode(node, subvariation), fullDetail); } Console.WriteLine(); WriteHeaders(fullDetail); List <Position> seenPositions = new List <Position>(); int CountDuplicatePos(Position pos) { int count = 0; foreach (Position priorPos in seenPositions) { if (pos.EqualAsRepetition(priorPos)) { count++; } } return(count); } int depth = 0; while (true) { node.Context.Tree.Annotate(node); seenPositions.Add(node.Annotation.Pos); int countSeen = CountDuplicatePos(node.Annotation.Pos); DumpNodeStr(priorMoves, node, depth, countSeen, fullDetail); if (node.NumChildrenVisited == 0) { return; } node = node.BestMove(false); depth++; } }
/// <summary> /// Processes a specified position command, /// with the side effect of resetting the curPositionAndMoves. /// </summary> /// <param name="command"></param> private void ProcessPosition(string command) { string[] parts = command.Split(" "); string fen; int nextIndex; string startFEN; switch (parts[1]) { case "fen": fen = command.Substring(command.IndexOf("fen") + 4); nextIndex = 2; while (parts.Length > nextIndex && parts[nextIndex] != "moves") { fen = fen + " " + parts[nextIndex++]; } startFEN = fen; break; case "startpos": startFEN = Position.StartPosition.FEN; nextIndex = 2; break; default: throw new Exception("invalid " + command); } string movesSubstring = ""; if (parts.Length > nextIndex && parts[nextIndex] == "moves") { for (int i = nextIndex + 1; i < parts.Length; i++) { movesSubstring += parts[i] + " "; } } PositionWithHistory newPositionAndMoves = PositionWithHistory.FromFENAndMovesUCI(startFEN, movesSubstring); curPositionIsContinuationOfPrior = newPositionAndMoves.IsIdenticalToPriorToLastMove(curPositionAndMoves); if (!curPositionIsContinuationOfPrior && CeresEngine != null) { CeresEngine.ResetGame(); } // Switch to the new position and moves curPositionAndMoves = newPositionAndMoves; }
/// <summary> /// Analyzes a specified position until a specified limit is exhausted. /// /// TODO: This method is highly redundant (and inferior to?) the next method AnalyzePositionFromFENAndMoves, delete it. /// </summary> /// <param name="fenOrFENAndMoves">a FEN</param> /// <param name="nodes"></param> /// <returns></returns> public UCISearchInfo AnalyzePositionFromFEN(string fenAndMovesString, SearchLimit searchLimit) { List <LC0VerboseMoveStat> moves = new List <LC0VerboseMoveStat>(); Runner.EvalPositionPrepare(); UCISearchInfo searchInfo; if (searchLimit.Type == SearchLimitType.SecondsPerMove) { searchInfo = Runner.EvalPositionToMovetime(fenAndMovesString, (int)(searchLimit.Value * 1000.0f)); } else if (searchLimit.Type == SearchLimitType.NodesPerMove) { searchInfo = Runner.EvalPositionToNodes(fenAndMovesString, (int)searchLimit.Value); } else { throw new Exception("Unknown search limit " + searchLimit.Type); } double elapsed = searchInfo.EngineReportedSearchTime / 1000.0f; // no more, we now assume win_percentages is requested LeelaVerboseMoveStats ret = new LeelaVerboseMoveStats(positionEnd, searchInfo.BestMove, elapsed, searchInfo.Nodes, LZPositionEvalLogistic.CentipawnToLogistic2018(searchInfo.Score)); float scoreConverted = 2.0f * (((float)searchInfo.ScoreCentipawns / 10_000f) - 0.5f); PositionWithHistory pwh = PositionWithHistory.FromFENAndMovesUCI(fenAndMovesString); LC0VerboseMoveStats ret = new LC0VerboseMoveStats(pwh.FinalPosition, searchInfo.BestMove, elapsed, searchInfo.Nodes, scoreConverted, searchInfo); foreach (string info in searchInfo.Infos) { if (info.Contains("P:")) { moves.Add(new LC0VerboseMoveStat(ret, info)); } } ret.SetMoves(moves); // TODO: Someday perhaps make LeelaVerboseMoveStats a subclass of UCISearchInfo so this is more elegant UCISearchInfo uciInfo = new UCISearchInfo(null, ret.BestMove, null); uciInfo.Nodes = ret.NumNodes; uciInfo.EngineReportedSearchTime = (int)(1000.0f * ret.ElapsedTime); uciInfo.ExtraInfo = ret; uciInfo.BestMove = ret.BestMove; return(uciInfo); }
SearchOnFEN(NNEvaluatorSet nnEvaluators, ParamsSelect paramsChildSelect, ParamsSearch paramsSearch, IManagerGameLimit timeManager, ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor, MCTSIterator reuseOtherContextForEvaluatedNodes, string fen, string movesStr, SearchLimit searchLimit, bool verbose = false, MCTSManager.MCTSProgressCallback progressCallback = null, bool possiblyEnablePositionCache = false, List <GameMoveStat> gameMoveHistory = null) { PositionWithHistory priorMoves = PositionWithHistory.FromFENAndMovesUCI(fen, movesStr); return(Search(nnEvaluators, paramsChildSelect, paramsSearch, timeManager, paramsSearchExecutionPostprocessor, reuseOtherContextForEvaluatedNodes, priorMoves, searchLimit, verbose, DateTime.Now, gameMoveHistory, progressCallback, possiblyEnablePositionCache)); }
/// <summary> /// Attempts to find a subnode by following specified moves from root. /// </summary> /// <param name="priorRoot"></param> /// <param name="movesMade"></param> /// <returns></returns> static MCTSNode FollowMovesToNode(MCTSNode priorRoot, IEnumerable <MGMove> movesMade) { PositionWithHistory startingPriorMove = priorRoot.Context.StartPosAndPriorMoves; MGPosition position = startingPriorMove.FinalPosMG; MCTSIterator context = priorRoot.Context; // Advance root node and update prior moves MCTSNode newRoot = priorRoot; foreach (MGMove moveMade in movesMade) { bool foundChild = false; // Find this new root node (after these moves) foreach (MCTSNodeStructChild child in newRoot.Ref.Children) { if (child.IsExpanded) { MGMove thisChildMove = ConverterMGMoveEncodedMove.EncodedMoveToMGChessMove(child.Move, in position); if (thisChildMove == moveMade) { // Advance new root to reflect this move newRoot = context.Tree.GetNode(child.ChildIndex, newRoot); // Advance position position.MakeMove(thisChildMove); // Done looking for match foundChild = true; break; } } } if (!foundChild) { return(null); } } // Found it return(newRoot); }
/// <summary> /// Runs the UCI loop. /// </summary> public void PlayUCI() { // Default to the startpos. curPositionAndMoves = PositionWithHistory.FromFENAndMovesUCI(Position.StartPosition.FEN, null); curManager = null; gameMoveHistory = new List <GameMoveStat>(); while (true) { string command = InStream.ReadLine(); switch (command) { case null: case "": break; case "uci": Send("id name Ceres"); // TODO: Add executable version Send("id author David Elliott and the Ceres Authors"); // todo output options such as: // option name Logfile type check default false Send("uciok"); break; case "setoption": OutStream.WriteLine("Not processing option " + command); return; case "stop": if (taskSearchCurrentlyExecuting != null && !stopIsPending) { stopIsPending = true; // TODO: cleanup // Possible race condition, curManager is only set in search callback which may not have hit yet // Fix eventually by rewriting SerchManager to have a constructor and then non-static Search method, // os we can get the context in this class directly after construction while (curManager == null) { Thread.Sleep(1); // **** TEMPORARY *** } curManager.ExternalStopRequested = true; if (taskSearchCurrentlyExecuting != null) { taskSearchCurrentlyExecuting.Wait(); if (!debug && taskSearchCurrentlyExecuting != null) { taskSearchCurrentlyExecuting.Result?.Manager?.Dispose(); } taskSearchCurrentlyExecuting = null; } } curManager = null; stopIsPending = false; break; case "ponderhit": throw new NotImplementedException("Ceres does not yet support UCI ponder mode."); return; case "xboard": // ignore break; case "debug on": debug = true; break; case "debug off": debug = false; break; case "isready": InitializeEngineIfNeeded(); Send("readyok"); break; case "ucinewgame": gameMoveHistory = new List <GameMoveStat>(); CeresEngine?.ResetGame(); break; case "quit": if (curManager != null) { curManager.ExternalStopRequested = true; taskSearchCurrentlyExecuting?.Wait(); } if (CeresEngine != null) { CeresEngine.Dispose(); } System.Environment.Exit(0); break; case string c when c.StartsWith("go"): if (taskSearchCurrentlyExecuting != null) { throw new Exception("Received go command when another search was running and not stopped first"); } InitializeEngineIfNeeded(); taskSearchCurrentlyExecuting = ProcessGo(command); break; case string c when c.StartsWith("position"): try { ProcessPosition(c); } catch (Exception e) { Send($"Illegal position command: \"{c}\"" + System.Environment.NewLine + e.ToString()); } break; // Proprietary commands case "lc0-config": if (curManager != null) { string netID = EvaluatorDef.Nets[0].Net.NetworkID; INNWeightsFileInfo netDef = NNWeightsFiles.LookupNetworkFile(netID); (string exe, string options) = LC0EngineConfigured.GetLC0EngineOptions(null, null, curContext.EvaluatorDef, netDef, false, false); Console.WriteLine("info string " + exe + " " + options); } else { Console.WriteLine("info string No search manager created"); } break; case "dump-params": if (curManager != null) { curManager.DumpParams(); } else { Console.WriteLine("info string No search manager created"); } break; case "dump-processor": HardwareManager.DumpProcessorInfo(); break; case "dump-time": if (curManager != null) { curManager.DumpTimeInfo(); } else { Console.WriteLine("info string No search manager created"); } break; case "dump-store": if (curManager != null) { using (new SearchContextExecutionBlock(curContext)) curManager.Context.Tree.Store.Dump(true); } else { Console.WriteLine("info string No search manager created"); } break; case "dump-move-stats": if (curManager != null) { using (new SearchContextExecutionBlock(curContext)) curManager.Context.Root.Dump(1, 1, prefixString: "info string "); } else { Console.WriteLine("info string No search manager created"); } break; case "dump-pv": DumpPV(false); break; case "dump-pv-detail": DumpPV(true); break; case "dump-nvidia": NVML.DumpInfo(); break; case "waitdone": // proprietary verb taskSearchCurrentlyExecuting?.Wait(); break; default: Console.WriteLine($"error Unknown command: {command}"); break; } } }
/// <summary> /// /// </summary> /// <param name="movesMade"></param> /// <param name="thresholdFractionNodesRetained"></param> /// <returns></returns> public bool ResetRootAssumingMovesMade(IEnumerable <MGMove> movesMade, float thresholdFractionNodesRetained) { PositionWithHistory staringPriorMove = Nodes.PriorMoves; MGPosition position = Nodes.PriorMoves.FinalPosMG; ref MCTSNodeStruct priorRoot = ref RootNode;
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++; } } }
public static void Analyze(string fenAndMoves, SearchLimit searchLimit, NNEvaluatorDef evaluatorDef, bool forceDisablePruning, LC0Engine lc0Engine = null, GameEngine comparisonEngine = null, bool verbose = false) { Console.WriteLine("============================================================================="); Console.WriteLine("Analyzing FEN : " + fenAndMoves); Console.WriteLine("Search limit : " + searchLimit.ToString()); Console.WriteLine("Ceres evaluator : " + evaluatorDef.ToString()); if (comparisonEngine != null) { Console.WriteLine("Opponent : " + comparisonEngine.ToString()); } Console.WriteLine(); Console.WriteLine(); NNEvaluatorSet nnEvaluators = new NNEvaluatorSet(evaluatorDef); // Warmup (in parallel) lc0Engine?.DoSearchPrepare(); Parallel.Invoke( () => nnEvaluators.Warmup(true), () => comparisonEngine?.Warmup()); bool ceresDone = false; lastInfoUpdate = DateTime.Now; UCISearchInfo lastCeresInfo = null; // Launch Ceres MCTSearch ceresResults = null; Task searchCeres = Task.Run(() => { ParamsSearch searchParams = new ParamsSearch(); searchParams.FutilityPruningStopSearchEnabled = !forceDisablePruning; PositionWithHistory positionWithHistory = PositionWithHistory.FromFENAndMovesUCI(fenAndMoves); ceresResults = new MCTSearch(); ceresResults.Search(nnEvaluators, new ParamsSelect(), searchParams, null, null, null, positionWithHistory, searchLimit, verbose, DateTime.Now, null, manager => lastCeresInfo = new UCISearchInfo(UCIInfo.UCIInfoString(manager), null, null), false, true); }); // Possibly launch search for other engine Task searchComparison = null; if (lc0Engine != null || comparisonEngine != null) { searchComparison = Task.Run(() => { if (lc0Engine != null) { lc0Engine.DoSearchPrepare(); lc0Engine.AnalyzePositionFromFENAndMoves(fenAndMoves, searchLimit); } else { comparisonEngine.Search(PositionWithHistory.FromFENAndMovesUCI(fenAndMoves), searchLimit, verbose: true); } }); } ; while (!searchCeres.IsCompleted || (searchComparison != null && !searchComparison.IsCompleted)) { Thread.Sleep(1000); //Console.WriteLine(DateTime.Now + " --> " + lastCeresInfo?.PVString + " OTHER " + comparisonEngine?.UCIInfo?.RawString); int numCharactersSame = int.MaxValue; if (lastCeresInfo?.PVString != null || comparisonEngine?.UCIInfo?.RawString != null) { if (lastCeresInfo != null && comparisonEngine?.UCIInfo != null) { numCharactersSame = 0; string pv1 = lastCeresInfo.PVString; UCISearchInfo lastComparisonInfo = comparisonEngine.UCIInfo; string pv2 = lastComparisonInfo.PVString; while (pv1.Length > numCharactersSame && pv2.Length > numCharactersSame && pv1[numCharactersSame] == pv2[numCharactersSame]) { numCharactersSame++; } } } if (lastCeresInfo != null) { WriteUCI("Ceres", lastCeresInfo, numCharactersSame); } if (comparisonEngine != null) { WriteUCI(comparisonEngine.ID, comparisonEngine.UCIInfo, numCharactersSame); } Console.WriteLine(); } searchCeres.Wait(); searchComparison?.Wait(); string infoUpdate = UCIInfo.UCIInfoString(ceresResults.Manager); double q2 = ceresResults.SearchRootNode.Q; //SearchPrincipalVariation pv2 = new SearchPrincipalVariation(worker2.Root); MCTSPosTreeNodeDumper.DumpPV(ceresResults.SearchRootNode, true); }
protected abstract GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory, ProgressCallback callback, bool verbose);
/// <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> /// Overriden virtual method which executes search. /// </summary> /// <param name="curPositionAndMoves"></param> /// <param name="searchLimit"></param> /// <param name="gameMoveHistory"></param> /// <param name="callback"></param> /// <returns></returns> protected override GameEngineSearchResult DoSearch(PositionWithHistory curPositionAndMoves, SearchLimit searchLimit, List <GameMoveStat> gameMoveHistory, ProgressCallback callback, bool verbose) { if (LastSearch != null && curPositionAndMoves.InitialPosMG != LastSearch.Manager.Context.StartPosAndPriorMoves.InitialPosMG) { throw new Exception("ResetGame must be called if not continuing same line"); } MCTSearch searchResult; // Set up callback passthrough if provided MCTSManager.MCTSProgressCallback callbackMCTS = null; if (callback != null) { callbackMCTS = callbackContext => callback((MCTSManager)callbackContext); } // Possibly use the context of opponent to reuse position evaluations MCTSIterator shareContext = null; if (OpponentEngine is GameEngineCeresInProcess) { GameEngineCeresInProcess ceresOpponentEngine = OpponentEngine as GameEngineCeresInProcess; if (LastSearch is not null && LastSearch.Manager.Context.ParamsSearch.ReusePositionEvaluationsFromOtherTree && ceresOpponentEngine?.LastSearch.Manager != null && LeafEvaluatorReuseOtherTree.ContextsCompatibleForReuse(LastSearch.Manager.Context, ceresOpponentEngine.LastSearch.Manager.Context)) { shareContext = ceresOpponentEngine.LastSearch.Manager.Context; // Clear any prior shared context from the shared context // to prevent unlimited backward chaining (keeping unneeded prior contexts alive) shareContext.ClearSharedContext(); } } void InnerCallback(MCTSManager manager) { callbackMCTS?.Invoke(manager); } // Run the search searchResult = RunSearchPossiblyTreeReuse(shareContext, curPositionAndMoves, gameMoveHistory, searchLimit, InnerCallback, verbose); int scoreCeresCP = (int)Math.Round(EncodedEvalLogistic.LogisticToCentipawn((float)searchResult.Manager.Root.Q), 0); MGMove bestMoveMG = searchResult.BestMove; int N = (int)searchResult.SearchRootNode.N; // Save (do not dispose) last search in case we can reuse it next time LastSearch = searchResult; isFirstMoveOfGame = false; // TODO is the RootNWhenSearchStarted correct because we may be following a continuation (BestMoveRoot) GameEngineSearchResultCeres result = new GameEngineSearchResultCeres(bestMoveMG.MoveStr(MGMoveNotationStyle.LC0Coordinate), (float)searchResult.SearchRootNode.Q, scoreCeresCP, searchResult.SearchRootNode.MAvg, searchResult.Manager.SearchLimit, default,
void ProcessEPD(int epdNum, EPDEntry epd, bool outputDetail, ObjectPool <object> otherEngines) { UCISearchInfo otherEngineAnalysis2 = default; EPDEntry epdToUse = epd; Task RunNonCeres() { if (Def.ExternalEngineDef != null) { object engineObj = otherEngines.GetFromPool(); if (engineObj is LC0Engine) { LC0Engine le = (LC0Engine)engineObj; // Run test 2 first since that's the one we dump in detail, to avoid any possible caching effect from a prior run otherEngineAnalysis2 = le.AnalyzePositionFromFEN(epdToUse.FEN, epdToUse.StartMoves, Def.ExternalEngineDef.SearchLimit); // leelaAnalysis2 = le.AnalyzePositionFromFEN(epdToUse.FEN, new SearchLimit(SearchLimit.LimitType.NodesPerMove, 2)); // **** TEMP otherEngines.RestoreToPool(le); } else { UCIGameRunner runner = (engineObj is UCIGameRunner) ? (engineObj as UCIGameRunner) : (engineObj as GameEngineUCI).UCIRunner; string moveType = Def.ExternalEngineDef.SearchLimit.Type == SearchLimitType.NodesPerMove ? "nodes" : "movetime"; int moveValue = moveType == "nodes" ? (int)Def.ExternalEngineDef.SearchLimit.Value : (int)Def.ExternalEngineDef.SearchLimit.Value * 1000; runner.EvalPositionPrepare(); otherEngineAnalysis2 = runner.EvalPosition(epdToUse.FEN, epdToUse.StartMoves, moveType, moveValue, null); otherEngines.RestoreToPool(runner); // public UCISearchInfo EvalPosition(int engineNum, string fenOrPositionCommand, string moveType, int moveMetric, bool shouldCache = false) } } return(Task.CompletedTask); } bool EXTERNAL_CONCURRENT = numConcurrentSuiteThreads > 1; Task lzTask = EXTERNAL_CONCURRENT ? Task.Run(RunNonCeres) : RunNonCeres(); // Comptue search limit // If possible, adjust for the fact that LC0 "cheats" by going slightly over node budget SearchLimit ceresSearchLimit1 = Def.CeresEngine1Def.SearchLimit; SearchLimit ceresSearchLimit2 = Def.CeresEngine2Def?.SearchLimit; if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove && otherEngineAnalysis2 != null && !Def.Engine1Def.SearchParams.FutilityPruningStopSearchEnabled) { if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove) { ceresSearchLimit1 = new SearchLimit(SearchLimitType.NodesPerMove, otherEngineAnalysis2.Nodes); } if (Def.CeresEngine1Def.SearchLimit.Type == SearchLimitType.NodesPerMove) { ceresSearchLimit2 = new SearchLimit(SearchLimitType.NodesPerMove, otherEngineAnalysis2.Nodes); } } PositionWithHistory pos = PositionWithHistory.FromFENAndMovesSAN(epdToUse.FEN, epdToUse.StartMoves); // TODO: should this be switched to GameEngineCeresInProcess? // Note that if we are running both Ceres1 and Ceres2 we alternate which search goes first. // This prevents any systematic difference/benefit that might come from order // (for example if we reuse position evaluations from the other tree, which can benefit only one of the two searches). MCTSearch search1 = null; MCTSearch search2 = null; if (epdNum % 2 == 0 || Def.CeresEngine2Def == null) { search1 = new MCTSearch(); search1.Search(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, null, pos, ceresSearchLimit1, false, DateTime.Now, null, null, true); MCTSIterator shareContext = null; if (Def.RunCeres2Engine) { if (Def.Engine2Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = search1.Manager.Context; } search2 = new MCTSearch(); search2.Search(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, shareContext, pos, ceresSearchLimit2, false, DateTime.Now, null, null, true); } } else { search2 = new MCTSearch(); search2.Search(evaluatorSet2, Def.Engine2Def.SelectParams, Def.Engine2Def.SearchParams, null, null, null, pos, ceresSearchLimit2, false, DateTime.Now, null, null, true); MCTSIterator shareContext = null; if (Def.Engine1Def.SearchParams.ReusePositionEvaluationsFromOtherTree) { shareContext = search2.Manager.Context; } search1 = new MCTSearch(); search1.Search(evaluatorSet1, Def.Engine1Def.SelectParams, Def.Engine1Def.SearchParams, null, null, shareContext, pos, ceresSearchLimit1, false, DateTime.Now, null, null, true); } // Wait for LZ analysis if (EXTERNAL_CONCURRENT) { lzTask.Wait(); } Move bestMoveOtherEngine = default; if (Def.ExternalEngineDef != null) { MGPosition thisPosX = PositionWithHistory.FromFENAndMovesUCI(epdToUse.FEN, epdToUse.StartMoves).FinalPosMG; MGMove lzMoveMG1 = MGMoveFromString.ParseMove(thisPosX, otherEngineAnalysis2.BestMove); bestMoveOtherEngine = MGMoveConverter.ToMove(lzMoveMG1); } Move bestMoveCeres1 = MGMoveConverter.ToMove(search1.BestMove); Move bestMoveCeres2 = search2 == null ? default : MGMoveConverter.ToMove(search2.BestMove); char CorrectStr(Move move) => epdToUse.CorrectnessScore(move, 10) == 10 ? '+' : '.'; int scoreCeres1 = epdToUse.CorrectnessScore(bestMoveCeres1, 10); int scoreCeres2 = epdToUse.CorrectnessScore(bestMoveCeres2, 10); int scoreOtherEngine = epdToUse.CorrectnessScore(bestMoveOtherEngine, 10); SearchResultInfo result1 = new SearchResultInfo(search1.Manager, search1.BestMove); SearchResultInfo result2 = search2 == null ? null : new SearchResultInfo(search2.Manager, search2.BestMove); accCeres1 += scoreCeres1; accCeres2 += scoreCeres2; // Accumulate how many nodes were required to find one of the correct moves // (in the cases where both succeeded) if (scoreCeres1 > 0 && (search2 == null || scoreCeres2 > 0)) { accWCeres1 += (scoreCeres1 == 0) ? result1.N : result1.NumNodesWhenChoseTopNNode; if (search2 != null) { accWCeres2 += (scoreCeres2 == 0) ? result2.N : result2.NumNodesWhenChoseTopNNode; } numSearchesBothFound++; } this.avgOther += scoreOtherEngine; numSearches++; float avgCeres1 = (float)accCeres1 / numSearches; float avgCeres2 = (float)accCeres2 / numSearches; float avgWCeres1 = (float)accWCeres1 / numSearchesBothFound; float avgWCeres2 = (float)accWCeres2 / numSearchesBothFound; float avgOther = (float)this.avgOther / numSearches; string MoveIfWrong(Move m) => m.IsNull || epdToUse.CorrectnessScore(m, 10) == 10 ? " " : m.ToString().ToLower(); int diff1 = scoreCeres1 - scoreOtherEngine; //NodeEvaluatorNeuralNetwork int evalNumBatches1 = result1.NumNNBatches; int evalNumPos1 = result1.NumNNNodes; int evalNumBatches2 = search2 == null ? 0 : result2.NumNNBatches; int evalNumPos2 = search2 == null ? 0 : result2.NumNNNodes; string correctMove = null; if (epdToUse.AMMoves != null) { correctMove = "-" + epdToUse.AMMoves[0]; } else if (epdToUse.BMMoves != null) { correctMove = epdToUse.BMMoves[0]; } float otherEngineTime = otherEngineAnalysis2 == null ? 0 : (float)otherEngineAnalysis2.EngineReportedSearchTime / 1000.0f; totalTimeOther += otherEngineTime; totalTimeCeres1 += (float)search1.TimingInfo.ElapsedTimeSecs; totalNodesOther += otherEngineAnalysis2 == null ? 0 : (int)otherEngineAnalysis2.Nodes; totalNodes1 += (int)result1.N; sumEvalNumPosOther += otherEngineAnalysis2 == null ? 0 : (int)otherEngineAnalysis2.Nodes; sumEvalNumBatches1 += evalNumBatches1; sumEvalNumPos1 += evalNumPos1; if (Def.RunCeres2Engine) { totalTimeCeres2 += (float)search2.TimingInfo.ElapsedTimeSecs; totalNodes2 += (int)result2.N; sumEvalNumBatches2 += evalNumBatches2; sumEvalNumPos2 += evalNumPos2; } float Adjust(int score, float frac) => score == 0 ? 0 : Math.Max(1.0f, MathF.Round(frac * 100.0f, 0)); string worker1PickedNonTopNMoveStr = result1.PickedNonTopNMoveStr; string worker2PickedNonTopNMoveStr = result2?.PickedNonTopNMoveStr; bool ex = otherEngineAnalysis2 != null; bool c2 = search2 != null; Writer writer = new Writer(epdNum == 0); writer.Add("#", $"{epdNum,4}", 6); if (ex) { writer.Add("CEx", $"{avgOther,5:F2}", 7); } writer.Add("CC", $"{avgCeres1,5:F2}", 7); if (c2) { writer.Add("CC2", $"{avgCeres2,5:F2}", 7); } writer.Add("P", $" {0.001f * avgWCeres1,7:f2}", 9); if (c2) { writer.Add("P2", $" {0.001f * avgWCeres2,7:f2}", 9); } if (ex) { writer.Add("SEx", $"{scoreOtherEngine,3}", 5); } writer.Add("SC", $"{scoreCeres1,3}", 5); if (c2) { writer.Add("SC2", $"{scoreCeres2,3}", 5); } if (ex) { writer.Add("MEx", $"{otherEngineAnalysis2.BestMove,7}", 9); } writer.Add("MC", $"{search1.Manager.BestMoveMG,7}", 9); if (c2) { writer.Add("MC2", $"{search2.Manager.BestMoveMG,7}", 9); } writer.Add("Fr", $"{worker1PickedNonTopNMoveStr}{ 100.0f * result1.TopNNodeN / result1.N,3:F0}%", 9); if (c2) { writer.Add("Fr2", $"{worker2PickedNonTopNMoveStr}{ 100.0f * result2?.TopNNodeN / result2?.N,3:F0}%", 9); } writer.Add("Yld", $"{result1.NodeSelectionYieldFrac,6:f3}", 9); if (c2) { writer.Add("Yld2", $"{result2.NodeSelectionYieldFrac,6:f3}", 9); } // Search time if (ex) { writer.Add("TimeEx", $"{otherEngineTime,7:F2}", 9); } writer.Add("TimeC", $"{search1.TimingInfo.ElapsedTimeSecs,7:F2}", 9); if (c2) { writer.Add("TimeC2", $"{search2.TimingInfo.ElapsedTimeSecs,7:F2}", 9); } writer.Add("Dep", $"{result1.AvgDepth,5:f1}", 7); if (c2) { writer.Add("Dep2", $"{result2.AvgDepth,5:f1}", 7); } // Nodes if (ex) { writer.Add("NEx", $"{otherEngineAnalysis2.Nodes,12:N0}", 14); } writer.Add("Nodes", $"{result1.N,12:N0}", 14); if (c2) { writer.Add("Nodes2", $"{result2.N,12:N0}", 14); } // Fraction when chose top N writer.Add("Frac", $"{Adjust(scoreCeres1, result1.FractionNumNodesWhenChoseTopNNode),4:F0}", 6); if (c2) { writer.Add("Frac2", $"{Adjust(scoreCeres2, result2.FractionNumNodesWhenChoseTopNNode),4:F0}", 6); } // Score (Q) if (ex) { writer.Add("QEx", $"{otherEngineAnalysis2.ScoreLogistic,6:F3}", 8); } writer.Add("QC", $"{result1.Q,6:F3}", 8); if (c2) { writer.Add("QC2", $"{result2.Q,6:F3}", 8); } // Num batches&positions writer.Add("Batches", $"{evalNumBatches1,8:N0}", 10); writer.Add("NNEvals", $"{evalNumPos1,11:N0}", 13); if (c2) { writer.Add("Batches2", $"{evalNumBatches2,8:N0}", 10); writer.Add("NNEvals2", $"{evalNumPos2,11:N0}", 13); } // Tablebase hits writer.Add("TBase", $"{(search1.CountSearchContinuations > 0 ? 0 : search1.Manager.CountTablebaseHits),8:N0}", 10); if (c2) { writer.Add("TBase2", $"{(search2.CountSearchContinuations > 0 ? 0 : search2.Manager.CountTablebaseHits),8:N0}", 10); } // writer.Add("EPD", $"{epdToUse.ID,-30}", 32); if (outputDetail) { if (epdNum == 0) { Def.Output.WriteLine(writer.ids.ToString()); Def.Output.WriteLine(writer.dividers.ToString()); } Def.Output.WriteLine(writer.text.ToString()); } // MCTSNodeStorageSerialize.Save(worker1.Context.Store, @"c:\temp", "TESTSORE"); search1?.Manager?.Dispose(); if (!object.ReferenceEquals(search1?.Manager, search2?.Manager)) { search2?.Manager?.Dispose(); } }
/// <summary> /// Runs the UCI loop. /// </summary> public void PlayUCI() { // Default to the startpos. curPositionAndMoves = PositionWithHistory.FromFENAndMovesUCI(Position.StartPosition.FEN); gameMoveHistory = new List <GameMoveStat>(); while (true) { string command = InStream.ReadLine(); if (uciLogWriter != null) { LogWriteLine("IN:", command); } switch (command) { case null: case "": break; case "uci": UCIWriteLine($"id name Ceres {CeresVersion.VersionString}"); UCIWriteLine("id author David Elliott and the Ceres Authors"); UCIWriteLine(SetOptionUCIDescriptions); UCIWriteLine("uciok"); break; case string c when c.StartsWith("setoption"): ProcessSetOption(command); break; case "stop": if (taskSearchCurrentlyExecuting != null && !stopIsPending) { stopIsPending = true; // Avoid race condition by mkaing sure the search is already created. while (CeresEngine.Search?.Manager == null) { Thread.Sleep(20); } CeresEngine.Search.Manager.ExternalStopRequested = true; if (taskSearchCurrentlyExecuting != null) { taskSearchCurrentlyExecuting.Wait(); // if (!debug && taskSearchCurrentlyExecuting != null) taskSearchCurrentlyExecuting.Result?.Search?.Manager?.Dispose(); taskSearchCurrentlyExecuting = null; } } stopIsPending = false; break; case "ponderhit": throw new NotImplementedException("Ceres does not yet support UCI ponder mode."); return; case "xboard": // ignore break; case "debug on": debug = true; break; case "debug off": debug = false; break; case "isready": InitializeEngineIfNeeded(); UCIWriteLine("readyok"); break; case "ucinewgame": gameMoveHistory = new List <GameMoveStat>(); CeresEngine?.ResetGame(); break; case "quit": if (taskSearchCurrentlyExecuting != null) { CeresEngine.Search.Manager.ExternalStopRequested = true; taskSearchCurrentlyExecuting?.Wait(); } if (CeresEngine != null) { CeresEngine.Dispose(); } System.Environment.Exit(0); break; case string c when c.StartsWith("go"): // Possibly another search is already executing. // The UCI specification is unclear about what to do in this situation. // Some engines seem to enqueue these for later execution (e.g. Stockfish) // whereas others (e.g. Python chess) report this as an error condition. // Currently Ceres waits only a short while for any possible pending search // to finish (e.g. to avoid a race condition if it is in the process of being shutdown) // and aborts with an error if search is still in progress. // It is not viable to wait indefinitely, since (among other reasons) // the engine needs to monitor for stop commands. const int MAX_MILLISECONDS_WAIT = 500; taskSearchCurrentlyExecuting?.Wait(MAX_MILLISECONDS_WAIT); if (taskSearchCurrentlyExecuting != null && !taskSearchCurrentlyExecuting.IsCompleted) { throw new Exception("Received go command when another search was running and not stopped first."); } InitializeEngineIfNeeded(); taskSearchCurrentlyExecuting = ProcessGo(command); break; case string c when c.StartsWith("position"): try { ProcessPosition(c); } catch (Exception e) { UCIWriteLine($"Illegal position command: \"{c}\"" + System.Environment.NewLine + e.ToString()); } break; // Proprietary commands case "lc0-config": if (CeresEngine?.Search != null) { string netID = EvaluatorDef.Nets[0].Net.NetworkID; INNWeightsFileInfo netDef = NNWeightsFiles.LookupNetworkFile(netID); (string exe, string options) = LC0EngineConfigured.GetLC0EngineOptions(null, null, CeresEngine.Search.Manager.Context.EvaluatorDef, netDef, false, false); UCIWriteLine("info string " + exe + " " + options); } else { UCIWriteLine("info string No search manager created"); } break; case "dump-params": if (CeresEngine?.Search != null) { CeresEngine?.Search.Manager.DumpParams(); } else { UCIWriteLine("info string No search manager created"); } break; case "dump-processor": HardwareManager.DumpProcessorInfo(); break; case "dump-time": if (CeresEngine?.Search != null) { CeresEngine?.Search.Manager.DumpTimeInfo(OutStream); } else { UCIWriteLine("info string No search manager created"); } break; case "dump-store": if (CeresEngine?.Search != null) { using (new SearchContextExecutionBlock(CeresEngine.Search.Manager.Context)) CeresEngine.Search.Manager.Context.Tree.Store.Dump(true); } else { UCIWriteLine("info string No search manager created"); } break; case "dump-move-stats": if (CeresEngine?.Search != null) { OutputVerboseMoveStats(CeresEngine.Search.SearchRootNode); } else { UCIWriteLine("info string No search manager created"); } break; case "dump-pv": DumpPV(false); break; case "dump-pv-detail": DumpPV(true); break; case "dump-nvidia": NVML.DumpInfo(); break; case "show-tree-plot": if (CeresEngine?.Search != null) { using (new SearchContextExecutionBlock(CeresEngine.Search.Manager.Context)) { TreePlot.Show(CeresEngine.Search.Manager.Context.Root.Ref); } } else { UCIWriteLine("info string No search manager created"); } break; case string c when c.StartsWith("save-tree-plot"): if (CeresEngine?.Search != null) { string[] parts = command.Split(" "); if (parts.Length == 2) { string fileName = parts[1]; using (new SearchContextExecutionBlock(CeresEngine.Search.Manager.Context)) { TreePlot.Save(CeresEngine.Search.Manager.Context.Root.Ref, fileName); } } else if (parts.Length == 1) { UCIWriteLine("Filename was not provided"); } else { UCIWriteLine("Filename cannot contain spaces"); } } else { UCIWriteLine("info string No search manager created"); } break; case "waitdone": // proprietary verb used for test driver taskSearchCurrentlyExecuting?.Wait(); break; default: UCIWriteLine($"error Unknown command: {command}"); break; } } }
static void DumpNodeStr(PositionWithHistory priorMoves, MCTSNode node, int depth, int countTimesSeen, bool fullDetail) { node.Context.Tree.Annotate(node); char extraChar = ' '; if (node.Terminal == GameResult.Checkmate) { extraChar = 'C'; } else if (node.Terminal == GameResult.Draw) { extraChar = 'D'; } else if (countTimesSeen > 1) { extraChar = countTimesSeen > 9 ? '9' : countTimesSeen.ToString()[0]; } float multiplier = depth % 2 == 0 ? 1.0f : -1.0f; float pctOfVisits = node.IsRoot ? 100.0f : (100.0f * node.N / node.Parent.N); MCTSNode bestMove = null; // TODO: someday show this too MCTSNode nextBestMove = null; if (!node.IsRoot) { MCTSNode[] parentsChildrenSortedQ = node.ChildrenSorted(innerNode => - multiplier * (float)innerNode.Q); if (parentsChildrenSortedQ.Length > 0) { bestMove = parentsChildrenSortedQ[0]; } // if (parentsChildrenSortedQ.Length > 1) nextBestMove = parentsChildrenSortedQ[1]; } // Depth, move Console.Write($"{depth,3}. "); Console.Write(extraChar); Console.Write($" {node.NumPolicyMoves,3} "); Console.Write($"{node.Index,13:N0}"); string san = node.IsRoot ? "" : MGMoveConverter.ToMove(node.Annotation.PriorMoveMG).ToSAN(in node.Parent.Annotation.Pos); // string sanNextBest = node.IsRoot ? "" : MGMoveConverter.ToMove(nextBestMove.Annotation.PriorMoveMG).ToSAN(in node.Parent.Annotation.Pos); if (node.Annotation.Pos.MiscInfo.SideToMove == SideType.White) { Console.Write($" "); Console.Write($"{san,6}"); } else { Console.Write($"{san,6}"); Console.Write($" "); } // float diffBestNextBestQ = 0; // if (nextBestMove != null) diffBestNextBestQ = (float)(bestMove.Q - nextBestMove.Q); // Console.Write($"{ (nextBestMove?.Annotation == null ? "" : nextBestMove.Annotation.PriorMoveMG.ToString()),8}"); // Console.Write($"{diffBestNextBestQ,8:F2}"); Console.Write($"{node.N,13:N0} "); Console.Write($" {pctOfVisits,5:F0}%"); Console.Write($" {100.0 * node.P,6:F2}% "); DumpWithColor(multiplier * node.V, $" {multiplier * node.V,6:F3} ", -0.2f, 0.2f); // DumpWithColor(multiplier * node.VSecondary, $" {multiplier * node.VSecondary,6:F3} ", -0.2f, 0.2f); double q = multiplier * node.Q; DumpWithColor((float)q, $" {q,6:F3} ", -0.2f, 0.2f); // float qStdDev = MathF.Sqrt(node.Ref.VVariance); // if (float.IsNaN(qStdDev)) // Console.WriteLine("found negative var"); // Console.Write($" +/-{qStdDev,5:F2} "); Console.Write($" {node.WinP,5:F2}/{node.DrawP,5:F2}/{node.LossP,5:F2} "); Console.Write($" {node.WAvg,5:F2}/{node.DAvg,5:F2}/{node.LAvg,5:F2} "); // Console.Write($" {node.Ref.QUpdatesWtdAvg,5:F2} "); // Console.Write($" +/-:{MathF.Sqrt(node.Ref.QUpdatesWtdVariance),5:F2} "); // Console.Write($" {node.Ref.TrendBonusToP,5:F2} "); Console.Write($" {node.MPosition,3:F0} "); Console.Write($" {node.MAvg,3:F0} "); if (fullDetail) { int numPieces = node.Annotation.Pos.PieceCount; // Console.Write($" {PosStr(node.Annotation.Pos)} "); Console.Write($" {node.Annotation.Pos.FEN}"); } Console.WriteLine(); }