/// <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> /// 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(); } // Run the analysis LC0VerboseMoveStats lc0Analysis = LC0Engine.AnalyzePositionFromFENAndMoves(curPositionAndMoves.FENAndMovesString, 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> /// Experimental sample code of runnings suites via the API. /// </summary> public static void RunSuiteTest() { const int PARALLELISM = 1; string deviceSuffix = PARALLELISM > 1 ? ":POOLED" : ""; NNEvaluatorDef evalDef1 = NNEvaluatorDefFactory.FromSpecification("LC0:j92-280", $"GPU:1{deviceSuffix}"); NNEvaluatorDef evalDef2 = NNEvaluatorDefFactory.FromSpecification("LC0:66733", $"GPU:1{deviceSuffix}"); SearchLimit limit = SearchLimit.NodesPerMove(10_000); string[] extraUCI = new string[] { "setoption name Contempt value 5000" }; GameEngineDef ged1 = new GameEngineDefCeres("Ceres1", evalDef1); GameEngineDef ged2 = new GameEngineDefCeres("Ceres1", evalDef2); GameEngineUCISpec geSF = new GameEngineUCISpec("SF12", @"\\synology\dev\chess\engines\stockfish_20090216_x64_avx2.exe", 32, 2048, CeresUserSettingsManager.Settings.DirTablebases, uciSetOptionCommands: extraUCI); EnginePlayerDef ceresEngineDef1 = new EnginePlayerDef(ged1, limit); EnginePlayerDef ceresEngineDef2 = new EnginePlayerDef(ged2, limit); GameEngineDefUCI sf12EngineDef = new GameEngineDefUCI("SF12", geSF); EnginePlayerDef sfEngine = new EnginePlayerDef(sf12EngineDef, limit * 875); SuiteTestDef def = new SuiteTestDef("Test1", @"\\synology\dev\chess\data\epd\ERET_VESELY203.epd", ceresEngineDef1, ceresEngineDef2, sfEngine); def.MaxNumPositions = 1500; SuiteTestRunner ser = new SuiteTestRunner(def); ser.Run(PARALLELISM, true); }
/// <summary> /// Actually runs a search with specified limits. /// </summary> /// <param name="searchLimit"></param> /// <returns></returns> private GameEngineSearchResultCeres RunSearch(SearchLimit searchLimit) { DateTime lastInfoUpdate = DateTime.Now; int numUpdatesSent = 0; MCTSManager.MCTSProgressCallback callback = (manager) => { curManager = manager; DateTime now = DateTime.Now; float timeSinceLastUpdate = (float)(now - lastInfoUpdate).TotalSeconds; bool isFirstUpdate = numUpdatesSent == 0; float UPDATE_INTERVAL_SECONDS = isFirstUpdate ? 0.1f : 0.5f; if (curManager != null && timeSinceLastUpdate > UPDATE_INTERVAL_SECONDS && curManager.Root.N > 0) { Send(UCIInfoString(curManager)); numUpdatesSent++; lastInfoUpdate = now; } }; GameEngineCeresInProcess.ProgressCallback callbackPlain = obj => callback((MCTSManager)obj); // use this? movesSinceNewGame // Search from this position (possibly with tree reuse) GameEngineSearchResultCeres result = CeresEngine.Search(curPositionAndMoves, searchLimit, gameMoveHistory, callbackPlain) as GameEngineSearchResultCeres; GameMoveStat moveStat = new GameMoveStat(gameMoveHistory.Count, curPositionAndMoves.FinalPosition.MiscInfo.SideToMove, result.ScoreQ, result.ScoreCentipawns, float.NaN, //engine1.CumulativeSearchTimeSeconds, curPositionAndMoves.FinalPosition.PieceCount, result.MAvg, result.FinalN, result.FinalN - result.StartingN, searchLimit, (float)result.TimingStats.ElapsedTimeSecs); gameMoveHistory.Add(moveStat); if (SearchFinishedEvent != null) { SearchFinishedEvent(result.Search.Manager); } // Send the final info string (unless this was an instamove). Send(UCIInfoString(result.Search.Manager, result.Search.BestMoveRoot)); // Send the best move Send("bestmove " + result.Search.BestMove.MoveStr(MGMoveNotationStyle.LC0Coordinate)); if (debug) { Send("info string " + result.Search.BestMoveRoot.BestMoveInfo(false)); } return(result); }
/// <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> /// Launches the search with specified limit. /// </summary> /// <param name="searchLimit"></param> /// <param name="progressCallback"></param> /// <param name="nnRemoteEvaluatorExtraSuffix"></param> /// <returns></returns> internal (TimingStats, MCTSNode) DoSearch(SearchLimit searchLimit, MCTSProgressCallback progressCallback) { CheckMemoryExhaustion(); ThreadSearchContext = this.Context; Context.ProgressCallback = progressCallback; TimingStats stats = new TimingStats(); int numMCTSNodesProcessedTotal = 0; using (new TimingBlock($"MCTS SEARCH {searchLimit}", stats, TimingBlock.LoggingType.None)) { flow = new MCTSSearchFlow(this, Context); int batchNum = 0; int hardLimitNumNodes = -1; bool shouldProcess = true; if (searchLimit.Type == SearchLimitType.NodesPerMove) { if (Root.N >= searchLimit.Value) { shouldProcess = false; } else { hardLimitNumNodes = (int)searchLimit.Value - Root.N; } } StartTimeFirstVisit = DateTime.Now; if (shouldProcess) { flow.ProcessDirectOverlapped(this, hardLimitNumNodes, batchNum, null); } batchNum++; } // Make sure nothing was left in flight after the search if ((Root.NInFlight != 0 || Root.NInFlight2 != 0) && !haveWarned) { Console.WriteLine($"Internal error: search ended with N={Root.N} NInFlight={Root.NInFlight} NInFlight2={Root.NInFlight2} {Root}"); haveWarned = true; } // Possibly validate tree integrity. if (MCTSDiagnostics.VerifyTreeIntegrityAtSearchEnd) { using (new SearchContextExecutionBlock(Context)) { Context.Tree.Store.Validate(); } } return(stats, Root); }
/// <summary> /// Sets the search limit to be applied to the iterated search. /// </summary> /// <param name="overallLimit"></param> public void SetForSearchLimit(SearchLimit overallLimit) { foreach (IteratedMCTSStepDef step in StepDefs) { step.SetForOverallLimit(overallLimit); } }
/// <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> /// /// </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 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="fen">a FEN</param> /// <param name="nodes"></param> /// <returns></returns> public LC0VerboseMoveStats AnalyzePositionFromFENAndMoves(string startFEN, string movesStr, string endFEN, SearchLimit searchLimit) { Position positionEnd = Position.FromFEN(endFEN); List <LC0VerboseMoveStat> moves = new List <LC0VerboseMoveStat>(); UCISearchInfo searchInfo; int searchValueMilliseconds = (int)((float)searchLimit.Value * 1000.0f); switch (searchLimit.Type) { case SearchLimitType.NodesPerMove: searchInfo = Runner.EvalPositionToNodes(startFEN, movesStr, (int)searchLimit.Value); break; case SearchLimitType.SecondsPerMove: searchInfo = Runner.EvalPositionToMovetime(startFEN, movesStr, searchValueMilliseconds); break; case SearchLimitType.NodesForAllMoves: throw new Exception("NodesForAllMoves not supported for Leela Chess Zero"); case SearchLimitType.SecondsForAllMoves: bool weAreWhite = positionEnd.MiscInfo.SideToMove == SideType.White; searchInfo = Runner.EvalPositionRemainingTime(startFEN, movesStr, 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(positionEnd, 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));
/// <summary> /// Resturns a SeachLimit by parsing a string limit specification. /// </summary> /// <param name="specificationString"></param> /// <returns></returns> public static SearchLimit Parse(string specificationString) { SearchLimit limit = TryParse(specificationString, out string errorString); if (limit == null) { throw new Exception($"Error parsing SearchLimit specification string {specificationString}"); } else { return(limit); } }
/// <summary> /// Resturns a SeachLimit by parsing a string limit specification. /// </summary> /// <param name="specificationString"></param> /// <returns></returns> public static SearchLimit Parse(string specificationString) { SearchLimit limit = TryParse(specificationString, out string errorString); if (limit == null) { throw new Exception($"Error parsing SearchLimit specification string {specificationString}. " + "Expecting a number followed by NM, NG, SM or SG (nodes/seconds per move/game)."); } else { return(limit); } }
/// <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); }
/// <summary> /// Constructor. /// </summary> /// <param name="nnEvaluatorDef"></param> /// <param name="paramsSearch"></param> /// <param name="paramsSelect"></param> /// <param name="searchLimit"></param> public ParamsSearchExecutionChooser(NNEvaluatorDef nnEvaluatorDef, ParamsSearch paramsSearch, ParamsSelect paramsSelect, SearchLimit searchLimit) { // Make sure params arguments look initialized if (nnEvaluatorDef == null) { throw new ArgumentNullException(nameof(nnEvaluatorDef)); } NNEvaluatorDef = nnEvaluatorDef; ParamsSearch = paramsSearch; ParamsSelect = paramsSelect; SearchLimit = searchLimit with { }; }
/// <summary> /// Parses a specification of search time in UCI format into an equivalent SearchLimit. /// Returns null if parsing failed. /// </summary> /// <param name="command"></param> /// <returns></returns> private SearchLimit GetSearchLimit(string command) { bool weAreWhite = curPositionAndMoves.FinalPosition.MiscInfo.SideToMove == SideType.White; UCIGoCommandParsed goInfo = new UCIGoCommandParsed(command, weAreWhite); if (!goInfo.IsValid) { return(null); } if (goInfo.Nodes.HasValue) { return(SearchLimit.NodesPerMove(goInfo.Nodes.Value)); } else if (goInfo.MoveTime.HasValue) { return(SearchLimit.SecondsPerMove(goInfo.MoveTime.Value / 1000.0f)); } else if (goInfo.Infinite) { // TODO: determine a reasonable maximum value based on memory in computer return(SearchLimit.NodesPerMove(MCTSNodeStore.MAX_NODES)); } else if (goInfo.TimeOurs.HasValue) { float increment = 0; if (goInfo.IncrementOurs.HasValue) { increment = goInfo.IncrementOurs.Value / 1000.0f; } int?movesToGo = null; if (goInfo.MovesToGo.HasValue) { movesToGo = goInfo.MovesToGo.Value; } return(SearchLimit.SecondsForAllMoves(goInfo.TimeOurs.Value / 1000.0f, increment, movesToGo, true)); } else { Console.WriteLine($"Unsupported time control in UCI go command {command}"); return(null); } }
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)); }
private void Search() { int seed = 2; //This is a good seed to show the crash /* Assigning first tools */ DecisionBuilder myToolAssignmentPhase = new RandomSelectToolHeuristic(this, seed); /* Ranking of the tools */ DecisionBuilder sequencingPhase = solver.MakePhase(allToolSequences, Solver.SEQUENCE_DEFAULT); /* Then fixing time of tasks as early as possible */ DecisionBuilder timingPhase = solver.MakePhase( makespan, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); /* Overall phase */ DecisionBuilder mainPhase = solver.Compose(myToolAssignmentPhase, sequencingPhase, timingPhase); /* Logging */ const int logFrequency = 1000000; SearchMonitor searchLog = solver.MakeSearchLog(logFrequency, objective); /* Restarts */ SearchMonitor searchRestart = solver.MakeLubyRestart(100); /* Search Limit in ms */ SearchLimit limit = solver.MakeTimeLimit(180 * 1000); /* Collecting best solution */ SolutionCollector collector = solver.MakeLastSolutionCollector(); collector.AddObjective(makespan); //collector.Add( pile.ToArray() ); solver.NewSearch(mainPhase, searchLog, searchRestart, objective, limit); while (solver.NextSolution()) { Console.WriteLine("MAKESPAN: " + makespan.Value()); } }
public static void TestSF(int index, bool gitVersion) { NNEvaluatorDef evalDef1 = NNEvaluatorDefFactory.FromSpecification("LC0:j94-100", "GPU:" + index); GameEngineDefCeres engineDefCeres1 = new GameEngineDefCeres("CeresInProc", evalDef1, new ParamsSearch(), null, new ParamsSelect(), null, "CeresSF.log.txt"); SearchLimit limitCeres = SearchLimit.SecondsForAllMoves(60, 1.25f) * 0.15f; SearchLimit limitSF = limitCeres * 1.5f; GameEngineDef engineDefCeresUCIGit = new GameEngineDefCeresUCI("CeresUCIGit", evalDef1, overrideEXE: @"C:\ceres\releases\v0.88\ceres.exe"); EnginePlayerDef playerCeres = new EnginePlayerDef(gitVersion ? engineDefCeresUCIGit : engineDefCeres1, limitCeres); EnginePlayerDef playerSF = new EnginePlayerDef(engineDefStockfish13, limitSF); TournamentDef def = new TournamentDef("TOURN", playerCeres, playerSF); def.OpeningsFileName = "TCEC1819.pgn"; //def.NumGamePairs = 10; def.ShowGameMoves = false; TournamentManager runner = new TournamentManager(def, 1); TournamentResultStats results; TimingStats stats = new TimingStats(); using (new TimingBlock(stats, TimingBlock.LoggingType.None)) { results = runner.RunTournament(); } Console.WriteLine(); Console.WriteLine($"Tournament completed in {stats.ElapsedTimeSecs,8:F2} seconds."); Console.WriteLine(playerCeres + " " + results.GameOutcomesString); }
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); }
// We don't need helper functions here // Csharp syntax is easier than C++ syntax! private static void CPisFun(int kBase, int time_limit_param, bool print) { // Use some profiling and change the default parameters of the solver SolverParameters solver_params = new SolverParameters(); // Change the profile level solver_params.profile_level = SolverParameters.NORMAL_PROFILING; // Constraint Programming engine Solver solver = new Solver("CP is fun!", solver_params); // Decision variables IntVar c = solver.MakeIntVar(1, kBase - 1, "C"); IntVar p = solver.MakeIntVar(0, kBase - 1, "P"); IntVar i = solver.MakeIntVar(1, kBase - 1, "I"); IntVar s = solver.MakeIntVar(0, kBase - 1, "S"); IntVar f = solver.MakeIntVar(1, kBase - 1, "F"); IntVar u = solver.MakeIntVar(0, kBase - 1, "U"); IntVar n = solver.MakeIntVar(0, kBase - 1, "N"); IntVar t = solver.MakeIntVar(1, kBase - 1, "T"); IntVar r = solver.MakeIntVar(0, kBase - 1, "R"); IntVar e = solver.MakeIntVar(0, kBase - 1, "E"); // We need to group variables in a vector to be able to use // the global constraint AllDifferent IntVar[] letters = new IntVar[] { c, p, i, s, f, u, n, t, r, e }; // Check if we have enough digits if (kBase < letters.Length) { throw new Exception("kBase < letters.Length"); } // Constraints solver.Add(letters.AllDifferent()); // CP + IS + FUN = TRUE solver.Add(p + s + n + kBase * (c + i + u) + kBase * kBase * f == e + kBase * u + kBase * kBase * r + kBase * kBase * kBase * t); SolutionCollector all_solutions = solver.MakeAllSolutionCollector(); // Add the interesting variables to the SolutionCollector all_solutions.Add(letters); // Decision Builder: hot to scour the search tree DecisionBuilder db = solver.MakePhase(letters, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); // Add some time limit SearchLimit time_limit = solver.MakeTimeLimit(time_limit_param); solver.Solve(db, all_solutions, time_limit); // Retrieve the solutions int numberSolutions = all_solutions.SolutionCount(); Console.WriteLine("Number of solutions: " + numberSolutions); if (print) { for (int index = 0; index < numberSolutions; ++index) { Console.Write("C=" + all_solutions.Value(index, c)); Console.Write(" P=" + all_solutions.Value(index, p)); Console.Write(" I=" + all_solutions.Value(index, i)); Console.Write(" S=" + all_solutions.Value(index, s)); Console.Write(" F=" + all_solutions.Value(index, f)); Console.Write(" U=" + all_solutions.Value(index, u)); Console.Write(" N=" + all_solutions.Value(index, n)); Console.Write(" T=" + all_solutions.Value(index, t)); Console.Write(" R=" + all_solutions.Value(index, r)); Console.Write(" E=" + all_solutions.Value(index, e)); Console.WriteLine(); } } // Save profile in file solver.ExportProfilingOverview("profile.txt"); }
/// <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,
/// <summary> /// Test code. Currently configured for 703810 using 2A100 versus LC0. /// </summary> public static void Test() { string ETHERAL_EXE = @"\\synology\dev\chess\engines\Ethereal12.75-x64-popcnt-avx2.exe"; string SF11_EXE = @"\\synology\dev\chess\engines\stockfish_11_x64_bmi2.exe"; string SF12_EXE = @"\\synology\dev\chess\engines\stockfish_20090216_x64_avx2.exe"; GameEngineUCISpec specEthereal = new GameEngineUCISpec("Ethereal12", ETHERAL_EXE); GameEngineUCISpec specSF = new GameEngineUCISpec("SF12", SF12_EXE); GameEngineUCISpec specLC0 = new GameEngineUCISpec("LC0", "lc0.exe"); // 66511 //NNEvaluatorDef def1 = NNEvaluatorDefFactory.SingleNet("j92-280", NNEvaluatorType.LC0Dll,1); // NNEvaluatorDef def0 = NNEvaluatorDefFactory.FromSpecification("LC0:j92-280", "GPU:1:POOLED");// POOLED");//:POOLED"); // NNEvaluatorDef def1 = NNEvaluatorDefFactory.FromSpecification("LC0:66666", "GPU:1:POOLED");// POOLED");//:POOLED"); NNEvaluatorDef evalDef1 = NNEvaluatorDefFactory.FromSpecification("LC0:703810", "GPU:0,1"); // POOLED");//:POOLED"); NNEvaluatorDef evalDef2 = NNEvaluatorDefFactory.FromSpecification("LC0:703810", "GPU:0,1"); // POOLED");//:POOLED"); // sv5300 j104.0-10000 // def1.MakePersistent(); //def1.PersistentID = "PERSIST"; // NNEvaluatorDef def2 = NNEvaluatorDefFactory.FromSpecification("LC0:66581", "GPU:3");//:POOLED"); // SearchLimit slLC0 = SearchLimit.NodesPerMove(10_000); // SearchLimit slEthereal = slLC0 * 875; // SearchLimit slSF = slLC0 * 875; //specEthereal = specSF; string[] extraUCI = null;// new string[] {"setoption name Contempt value 5000" }; const int NUM_THREADS = 32; const int HASH_SIZE_MB = 2048; string TB_PATH = CeresUserSettingsManager.Settings.DirTablebases; SearchLimit limit = SearchLimit.NodesPerMove(1_000); //limit = SearchLimit.SecondsForAllMoves(60); //limit = SearchLimit.SecondsForAllMoves(60, 1f); limit = SearchLimit.SecondsForAllMoves(120, 0.5f); GameEngineDefCeres engineDefCeres1 = new GameEngineDefCeres("Ceres1", evalDef1, new ParamsSearch(), null, new ParamsSelect(), null); GameEngineDefCeres engineDefCeres2 = new GameEngineDefCeres("Ceres2", evalDef2, new ParamsSearch(), null, new ParamsSelect(), null); bool forceDisableSmartPruning = limit.IsNodesLimit; if (forceDisableSmartPruning) { engineDefCeres1.SearchParams.FutilityPruningStopSearchEnabled = false; engineDefCeres2.SearchParams.FutilityPruningStopSearchEnabled = false; } GameEngineDef engineDefEthereal = new GameEngineDefUCI("Etheral", new GameEngineUCISpec("Etheral", ETHERAL_EXE, NUM_THREADS, HASH_SIZE_MB, TB_PATH, uciSetOptionCommands: extraUCI)); GameEngineDef engineDefStockfish11 = new GameEngineDefUCI("SF11", new GameEngineUCISpec("SF11", SF11_EXE, NUM_THREADS, HASH_SIZE_MB, TB_PATH, uciSetOptionCommands: extraUCI)); GameEngineDef engineDefStockfish12 = new GameEngineDefUCI("SF12", new GameEngineUCISpec("SF12", SF12_EXE, NUM_THREADS, HASH_SIZE_MB, TB_PATH, uciSetOptionCommands: extraUCI)); //GameEngineDef engineDefCeresUCI = new GameEngineDefUCI("CeresUCI", new GameEngineUCISpec("CeresUCI", @"c:\dev\ceres\artifacts\release\net5.0\ceres.exe")); GameEngineDef engineDefCeresUCI = new GameEngineDefCeresUCI("CeresUCI", evalDef1, overrideEXE: @"c:\dev\ceres\artifacts\release\net5.0\ceres.exe"); GameEngineDefLC0 engineDefLC1 = new GameEngineDefLC0("LC0_0", evalDef1, forceDisableSmartPruning, null, null); GameEngineDefLC0 engineDefLC2 = new GameEngineDefLC0("LC0_1", evalDef2, forceDisableSmartPruning, null, null); EnginePlayerDef playerCeres1UCI = new EnginePlayerDef(engineDefCeresUCI, limit); EnginePlayerDef playerCeres2UCI = new EnginePlayerDef(engineDefCeresUCI, limit); EnginePlayerDef playerCeres1 = new EnginePlayerDef(engineDefCeres1, limit); EnginePlayerDef playerCeres2 = new EnginePlayerDef(engineDefCeres2, limit); EnginePlayerDef playerEthereal = new EnginePlayerDef(engineDefEthereal, limit); EnginePlayerDef playerStockfish11 = new EnginePlayerDef(engineDefStockfish11, limit); EnginePlayerDef playerStockfish12 = new EnginePlayerDef(engineDefStockfish12, limit); EnginePlayerDef playerLC0 = new EnginePlayerDef(engineDefLC1, limit); EnginePlayerDef playerLC1 = new EnginePlayerDef(engineDefLC2, limit); // def.SearchLimitEngine1 = def.SearchLimitEngine2 = SearchLimit.SecondsForAllMoves(15, 0.25f); // def.SearchLimitEngine2 = def.SearchLimitEngine2 = SearchLimit.SecondsForAllMoves(15, 0.25f); //(playerCeres1.EngineDef as GameEngineDefCeres).SearchParams.DrawByRepetitionLookbackPlies = 40; if (false) { // =============================================================================== SuiteTestDef suiteDef = new SuiteTestDef("Suite", @"\\synology\dev\chess\data\epd\endgame2.epd", playerCeres1, playerCeres2, null); // suiteDef.MaxNumPositions = 100; SuiteTestRunner suiteRunner = new SuiteTestRunner(suiteDef); suiteRunner.Run(1, true, false); return; // =============================================================================== } // engineDefCeres2.SearchParams.TwofoldDrawEnabled = false; //engineDefCeres1.SearchParams.TreeReuseEnabled = false; //engineDefCeres2.SearchParams.TreeReuseEnabled = false; //engineDefCeres1.SearchParams.FutilityPruningStopSearchEnabled= false; //engineDefCeres2.SearchParams.FutilityPruningStopSearchEnabled= false; //engineDefLC0.SearchParamsEmulate.FutilityPruningStopSearchEnabled= false; //TournamentDef def = new TournamentDef("TOURN", playerCeres1, playerLC0); TournamentDef def = new TournamentDef("TOURN", playerCeres1, playerLC0); def.NumGamePairs = 50; // def.ShowGameMoves = false; // def.OpeningsFileName = @"HERT_2017\Hert500.pgn"; // def.StartingFEN = "1q6/2n4k/1r1p1pp1/RP1P2p1/2Q1P1P1/2N4P/3K4/8 b - - 8 71"; // def.OpeningsFileName = @"\\synology\dev\chess\data\openings\Drawkiller_500pos_reordered.pgn";// def.OpeningsFileName = "TCEC19_NoomenSelect.pgn"; // def.OpeningsFileName = "TCEC1819.pgn"; // def.AdjudicationThresholdCentipawns = 500; // def.AdjudicationThresholdNumMoves = 3; const int CONCURRENCY = 1;// 15; TournamentManager runner = new TournamentManager(def, CONCURRENCY); TournamentResultStats results; //UCIEngineProcess.VERBOSE = true; TimingStats stats = new TimingStats(); using (new TimingBlock(stats, TimingBlock.LoggingType.None)) { results = runner.RunTournament(); } Console.WriteLine(); Console.WriteLine($"Tournament completed in {stats.ElapsedTimeSecs,8:F2} seconds."); Console.WriteLine(results.GameOutcomesString); }
/// <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 }
public static void Solve(FlexibleJobShopData data, bool chatty = false) { // Compute horizon var horizon = data.JobList.Sum( j => j.TaskList.Sum( t => t.Durations.Max())); // ----- Create all intervals and vars ----- /* * // Use some profiling and change the default parameters of the solver * SolverParameters solverParams = new SolverParameters(); * // Change the profile level * solverParams.profile_level = SolverParameters.NORMAL_PROFILING; * // Constraint programming engine * Solver solver = new Solver("JobShop", solverParams); */ // Constraint programming engine Solver solver = new Solver($"FlexibleJobShop: {data.Name}"); // Initialize dictionaries to hold references to task intervals var tasksByJobId = new Dictionary <uint, List <TaskAlternative> >(); foreach (var job in data.JobList) { tasksByJobId[job.Id] = new List <TaskAlternative>(job.TaskList.Count); } var tasksByMachineId = new Dictionary <uint, IntervalVarVector>(); foreach (var machine in data.MachineList) { tasksByMachineId[machine.Id] = new IntervalVarVector(); } // Creates all individual interval variables and collect in dictionaries foreach (var job in data.JobList) { foreach (var task in job.TaskList) { var alternative = new TaskAlternative(job.Id); tasksByJobId[job.Id].Add(alternative); var activeVariables = new IntVarVector(); var hasAlternatives = task.AlternativesCount > 1; for (int alt = 0; alt < task.AlternativesCount; alt++) { var machine = task.Machines[alt]; var duration = task.Durations[alt]; var name = $"{task.Name}; Alternative {alt}: {machine.Name}, Duration {duration}"; IntervalVar interval = solver.MakeFixedDurationIntervalVar(0, horizon, duration, hasAlternatives, name); alternative.Add(interval); tasksByMachineId[machine.Id].Add(interval); if (hasAlternatives) { activeVariables.Add(interval.PerformedExpr().Var()); } } alternative.AlternativeVar = solver.MakeIntVar(0, task.AlternativesCount - 1, task.Name); if (hasAlternatives) { solver.Add(solver.MakeMapDomain(alternative.AlternativeVar, activeVariables)); } } } // ----- Create model ----- // Create precedences inside jobs foreach (var taskAltList in tasksByJobId.Values) { for (int i = 0; i < taskAltList.Count - 1; i++) { TaskAlternative currentTaskAlt = taskAltList[i]; TaskAlternative nextTaskAlt = taskAltList[i + 1]; foreach (var alt1 in currentTaskAlt) { foreach (var alt2 in nextTaskAlt) { solver.Add(solver.MakeIntervalVarRelation( alt2, Solver.STARTS_AFTER_END, alt1)); } } } } // Collect alternative variables. IntVarVector alternativeVariableVec = new IntVarVector(); foreach (var taskAltList in tasksByJobId.Values) { foreach (var taskAlt in taskAltList) { if (!taskAlt.AlternativeVar.Bound()) { alternativeVariableVec.Add(taskAlt.AlternativeVar); } } } // Add disjunctive constraints on unary resources, and create // sequence variables. A sequence variable is a dedicated variable // whose job is to sequence interval variables. SequenceVarVector allSequences = new SequenceVarVector(); foreach (var machine in data.MachineList) { DisjunctiveConstraint disjCt = solver.MakeDisjunctiveConstraint( tasksByMachineId[machine.Id], machine.Name); solver.Add(disjCt); allSequences.Add(disjCt.SequenceVar()); } // Create array of end_times of jobs IntVarVector endsVec = new IntVarVector(); foreach (var taskAltList in tasksByJobId.Values) { TaskAlternative lastTaskAlt = taskAltList.Last(); foreach (var alt in lastTaskAlt) { endsVec.Add(alt.SafeEndExpr(-1).Var()); } } // Objective: minimize the makespan (maximum end times of all tasks) // of the problem. IntVar objectiveVar = solver.MakeMax(endsVec).Var(); OptimizeVar objectiveMonitor = solver.MakeMinimize(objectiveVar, 1); // ----- Search monitors and decision builder ----- // This decision builder will assign all alternative variables. DecisionBuilder alternativePhase = solver.MakePhase(alternativeVariableVec, Solver.CHOOSE_MIN_SIZE, Solver.ASSIGN_MIN_VALUE); // This decision builder will rank all tasks on all machines. DecisionBuilder sequencePhase = solver.MakePhase(allSequences, Solver.SEQUENCE_DEFAULT); // After the ranking of tasks, the schedule is still loose and any // task can be postponed at will. But, because the problem is now a PERT // (http://en.wikipedia.org/wiki/Program_Evaluation_and_Review_Technique), // we can schedule each task at its earliest start time. This is // conveniently done by fixing the objective variable to its // minimum value. DecisionBuilder objectivePhase = solver.MakePhase(objectiveVar, Solver.CHOOSE_FIRST_UNBOUND, Solver.ASSIGN_MIN_VALUE); // The main decision builder (ranks all tasks, then fixes the // objective_variable). DecisionBuilder mainPhase = solver.Compose(alternativePhase, sequencePhase, objectivePhase); // Search log const int kLogFrequency = 1000000; SearchMonitor searchLog = solver.MakeSearchLog(kLogFrequency, objectiveMonitor); const long FLAGS_time_limit_in_ms = 1000 * 60 * 20; SearchLimit limit = null; if (FLAGS_time_limit_in_ms > 0) { limit = solver.MakeTimeLimit(FLAGS_time_limit_in_ms); } SolutionCollector collector = solver.MakeLastSolutionCollector(); collector.AddObjective(objectiveVar); collector.Add(alternativeVariableVec); collector.Add(allSequences); foreach (var taskVec in tasksByMachineId.Values) { foreach (var task in taskVec) { collector.Add(task.StartExpr().Var()); } } // ----- Search ----- bool solutionFound = solver.Solve(mainPhase, searchLog, objectiveMonitor, limit, collector); if (solutionFound) { // The index of the solution from the collector const int SOLUTION_INDEX = 0; Assignment solution = collector.Solution(SOLUTION_INDEX); Console.WriteLine(); uint machineIdx = 0; foreach (var seq in allSequences) { machineIdx++; var taskSeq = collector.ForwardSequence(SOLUTION_INDEX, seq); Console.WriteLine($"{seq.Name()}:"); Console.WriteLine(" Tasks: " + string.Join(", ", taskSeq)); //foreach (var taskIndex in storedSequence) //{ // IntervalVar task = sequence.Interval(taskIndex); // long startMin = solution.StartMin(task); // long startMax = solution.StartMax(task); // if (startMin == startMax) // { // Console.WriteLine($"Task {task.Name()} starts at {startMin}."); // } // else // { // Console.WriteLine($"Task {task.Name()} starts between {startMin} and {startMax}."); // } //} Console.WriteLine(); Console.Write(" Starting times:"); foreach (var s in taskSeq) { Console.Write(" " + collector.Value(0, tasksByMachineId[machineIdx][s].StartExpr().Var()).ToString()); } Console.WriteLine(); } //var x = tasksByMachineId[1][0].StartExpr().Var(); //var xValStr = collector.Value(0, x).ToString(); Console.WriteLine("objective function value = " + solution.ObjectiveValue()); //TEMP } // Save profile in file //solver.ExportProfilingOverview("profile.txt"); // Done solver.EndSearch(); }
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(); } }
public SuiteTestResult Run(int numConcurrentSuiteThreads = 1, bool outputDetail = true, bool saveCacheWhenDone = true) { // Tree reuse is no help, indicate that we won't need it Def.Engine1Def.SearchParams.TreeReuseEnabled = false; if (Def.Engine2Def != null) { Def.Engine2Def.SearchParams.TreeReuseEnabled = false; } // Disable dump for now, the execution parameters are modified // for the warmup which is confusing because different parameters // will be chosen for the actual search. //DumpParams(Def.Output, true); // Create evaluators evaluatorSet1 = new NNEvaluatorSet(Def.Engine1Def.EvaluatorDef); if (Def.Engine2Def != null) { evaluatorSet2 = new NNEvaluatorSet(Def.Engine2Def.EvaluatorDef); } numConcurrentSuiteThreads = numConcurrentSuiteThreads; int timerFiredCount = 0; // TODO: add path automatically List <EPDEntry> epds = EPDEntry.EPDEntriesInEPDFile(Def.EPDFileName, int.MaxValue); if (Def.MaxNumPositions == 0) { Def.MaxNumPositions = epds.Count; } Def.Output.WriteLine(); Def.Output.WriteLine("C1 = " + Def.Engine1Def.EvaluatorDef); if (Def.RunCeres2Engine) { Def.Output.WriteLine("C2 = " + Def.Engine2Def.EvaluatorDef); } if (Def.ExternalEngineDef != null) { Def.Output.WriteLine("EX = " + Def.ExternalEngineDef.EngineDef); } #if NOT // To make up for the fact that LZ0 "cheats" by sometimes running over specified number of nodes // (she seems to always fill the batch even if reached limit), add half a batch extra for Ceres as compensation if (searchLimitCeres1.Type == SearchLimit.LimitType.NodesPerMove) { searchLimitCeres1 = new SearchLimit(searchLimit.Type, searchLimit.Value + paramsSearch1.BATCH_SIZE_PRIMARY / 2); searchLimitCeres2 = new SearchLimit(searchLimit.Type, searchLimit.Value + paramsSearch2.BATCH_SIZE_PRIMARY / 2); } #endif //Def.Output.WriteLine($"MAX_CERES_GAME_THREADS {numConcurrentCeresGames} MAX_LEELA_GAME_THREADS {MAX_LEELA_GAME_THREADS}"); // Turn of position reuse if evaluators produce different results if (Def.RunCeres2Engine && !Def.Engine1Def.EvaluatorDef.NetEvaluationsIdentical(Def.Engine2Def.EvaluatorDef)) { Def.Engine1Def.SearchParams.ReusePositionEvaluationsFromOtherTree = false; Def.Engine2Def.SearchParams.ReusePositionEvaluationsFromOtherTree = false; } if (Def.RunCeres2Engine && (Def.Engine1Def.SearchParams.ReusePositionEvaluationsFromOtherTree || Def.Engine2Def.SearchParams.ReusePositionEvaluationsFromOtherTree)) { Console.ForegroundColor = ConsoleColor.Cyan; Console.WriteLine("\r\nWARNING: REUSE_POSITION_EVALUATIONS_FROM_OTHER_TREE is turned on for one or both evaluators\r\n" + "(alternating between the two evaluators). This may cause slight differences in search behavior and speed.\r\n"); Console.ForegroundColor = ConsoleColor.White; } Def.Output.WriteLine(); if (Def.MaxNumPositions > epds.Count) { Def.MaxNumPositions = epds.Count; } epds = epds.GetRange(Def.FirstTestPosition, Def.MaxNumPositions); int numExternalGameProcesses = 1; numConcurrentSuiteThreads = Math.Min(Def.MaxNumPositions, numConcurrentSuiteThreads); if (numConcurrentSuiteThreads > 1) { bool evaluator1NonPooled = Def.Engine1Def.EvaluatorDef != null && Def.Engine1Def.EvaluatorDef.DeviceCombo != Chess.NNEvaluators.Defs.NNEvaluatorDeviceComboType.Pooled; bool evaluator2NonPooled = Def.Engine2Def.EvaluatorDef != null && Def.Engine2Def.EvaluatorDef.DeviceCombo != Chess.NNEvaluators.Defs.NNEvaluatorDeviceComboType.Pooled; if (evaluator1NonPooled || evaluator2NonPooled) { throw new Exception("Must use POOLED neural network evaluator when running suites with parallelism");; } if (Def.ExternalEngineDef != null) { // For safety (to not overflow main or GPU memory) we limit number of LC0 processes. const int MAX_LC0_PROCESSES = 4; numExternalGameProcesses = Math.Min(MAX_LC0_PROCESSES, numConcurrentSuiteThreads); } } bool leelaVerboseMovesStats = true;//xxx Def.NumTestPos == 1; Func <object> makeExternalEngine = null; if (Def.ExternalEngineDef != null) { if (Def.ExternalEngineDef.EngineDef is GameEngineDefLC0) { bool forceDisableSmartPruning = (Def.ExternalEngineDef.EngineDef as GameEngineDefLC0).ForceDisableSmartPruning; makeExternalEngine = () => { LC0Engine engine = LC0EngineConfigured.GetLC0Engine(null, null, Def.Engine1Def.EvaluatorDef, NNWeightsFiles.LookupNetworkFile(Def.Engine1Def.EvaluatorDef.Nets[0].Net.NetworkID), true, false, leelaVerboseMovesStats, forceDisableSmartPruning); // WARMUP engine.AnalyzePositionFromFEN(Position.StartPosition.FEN, null, SearchLimit.NodesPerMove(1)); return(engine); }; } else { bool resetMovesBetweenMoves = !Def.Engine2Def.SearchParams.TreeReuseEnabled; bool enableTranpsositions = Def.Engine2Def.SearchParams.Execution.TranspositionMode != TranspositionMode.None; bool enableTablebases = Def.Engine2Def.SearchParams.EnableTablebases; makeExternalEngine = () => Def.ExternalEngineDef.EngineDef.CreateEngine(); } } // Don't create too many non_Ceres threads since each one will consume seaprate GPU memory or threads int maxLeelaThreads = Math.Min(numExternalGameProcesses, numConcurrentSuiteThreads); ObjectPool <object> externalEnginePool = new ObjectPool <object>(makeExternalEngine, maxLeelaThreads); using (new TimingBlock("EPDS")) { Parallel.For(0, epds.Count, new ParallelOptions() { MaxDegreeOfParallelism = numConcurrentSuiteThreads }, delegate(int gameNum) { try { EPDEntry epd = epds[gameNum]; // Skip positions which are already draws if (epd.Position.CheckDrawBasedOnMaterial == Position.PositionDrawStatus.DrawByInsufficientMaterial) { return; } // TODO: also do this for checkmate? ProcessEPD(gameNum, epds[gameNum], outputDetail, externalEnginePool); } catch (Exception exc) { Def.Output.WriteLine("Error in ProcessEPD " + exc); throw exc; } }); } Def.Output.WriteLine(); Def.Output.WriteLine(); if (Def.ExternalEngineDef != null) { Def.Output.WriteLine($"Total {Def.ExternalEngineDef.ID} Time {totalTimeOther,6:F2}"); } Def.Output.WriteLine($"Total C1 Time {totalTimeCeres1,6:F2}"); if (Def.CeresEngine2Def != null) { Def.Output.WriteLine($"Total C2 Time {totalTimeCeres2,6:F2}"); } Def.Output.WriteLine(); if (Def.ExternalEngineDef != null) { Def.Output.WriteLine($"Avg {Def.ExternalEngineDef.ID} pos/sec {totalNodesOther / totalTimeOther,8:F2}"); } Def.Output.WriteLine($"Avg Ceres pos/sec {totalNodes1 / totalTimeCeres1,8:F2}"); if (Def.CeresEngine2Def != null) { Def.Output.WriteLine($"Avg Ceres2 pos/sec {totalNodes2 / totalTimeCeres2,8:F2}"); } Def.Output.WriteLine(); Def.Output.WriteLine(); EngineCeres1.Dispose(); EngineCeres2?.Dispose(); EngineExternal?.Dispose(); externalEnginePool.Shutdown(engineObj => (engineObj as LC0Engine).Dispose()); evaluatorSet1.Dispose(); evaluatorSet2?.Dispose(); return(new SuiteTestResult(Def) { AvgScore1 = (float)accCeres1 / numSearches, AvgScore2 = (float)accCeres2 / numSearches, AvgWScore1 = (float)accWCeres1 / numSearches, AvgWScore2 = (float)accWCeres2 / numSearches, AvgScoreLC0 = (float)avgOther / numSearches, TotalRuntimeLC0 = totalTimeOther, TotalRuntime1 = totalTimeCeres1, TotalRuntime2 = totalTimeCeres2, TotalNodesLC0 = totalNodesOther, TotalNodes1 = totalNodes1, TotalNodes2 = totalNodes2 }); }
/// <summary> /// Constructor. /// </summary> /// <param name="store"></param> /// <param name="nnParams"></param> /// <param name="searchParams"></param> /// <param name="childSelectParams"></param> /// <param name="priorMoves">if null, the prior moves are taken from the passed store</param> /// <param name="searchLimit"></param> public MCTSManager(MCTSNodeStore store, MCTSIterator reuseOtherContextForEvaluatedNodes, PositionEvalCache reusePositionCache, TranspositionRootsDict reuseTranspositionRoots, NNEvaluatorSet nnEvaluators, ParamsSearch searchParams, ParamsSelect childSelectParams, SearchLimit searchLimit, ParamsSearchExecutionModifier paramsSearchExecutionPostprocessor, IManagerGameLimit timeManager, DateTime startTime, MCTSManager priorManager, List <GameMoveStat> gameMoveHistory, bool isFirstMoveOfGame) { StartTimeThisSearch = startTime; RootNWhenSearchStarted = store.Nodes.nodes[store.RootIndex.Index].N; ParamsSearchExecutionPostprocessor = paramsSearchExecutionPostprocessor; IsFirstMoveOfGame = isFirstMoveOfGame; PriorMoveStats = new List <GameMoveStat>(); // Make our own copy of move history. if (gameMoveHistory != null) { PriorMoveStats.AddRange(gameMoveHistory); } // Possibly convert time limit per game into time for this move. if (searchLimit.IsPerGameLimit) { SearchLimitType type = searchLimit.Type == SearchLimitType.SecondsForAllMoves ? SearchLimitType.SecondsPerMove : SearchLimitType.NodesPerMove; float rootQ = priorManager == null ? float.NaN : (float)store.RootNode.Q; ManagerGameLimitInputs timeManagerInputs = new(store.Nodes.PriorMoves.FinalPosition, searchParams, PriorMoveStats, type, store.RootNode.N, rootQ, searchLimit.Value, searchLimit.ValueIncrement, float.NaN, float.NaN, maxMovesToGo : searchLimit.MaxMovesToGo, isFirstMoveOfGame : isFirstMoveOfGame); ManagerGameLimitOutputs timeManagerOutputs = timeManager.ComputeMoveAllocation(timeManagerInputs); SearchLimit = timeManagerOutputs.LimitTarget; } else { SearchLimit = searchLimit; } // Possibly autoselect new optimal parameters ParamsSearchExecutionChooser paramsChooser = new ParamsSearchExecutionChooser(nnEvaluators.EvaluatorDef, searchParams, childSelectParams, searchLimit); // TODO: technically this is overwriting the params belonging to the prior search, that's ugly (but won't actually cause a problem) paramsChooser.ChooseOptimal(searchLimit.EstNumNodes(50_000), paramsSearchExecutionPostprocessor); // TODO: make 50_000 smarter int estNumNodes = EstimatedNumSearchNodesForEvaluator(searchLimit, nnEvaluators); // Adjust the nodes estimate if we are continuing an existing search if (searchLimit.Type == SearchLimitType.NodesPerMove && RootNWhenSearchStarted > 0) { estNumNodes = Math.Max(0, estNumNodes - RootNWhenSearchStarted); } Context = new MCTSIterator(store, reuseOtherContextForEvaluatedNodes, reusePositionCache, reuseTranspositionRoots, nnEvaluators, searchParams, childSelectParams, searchLimit, estNumNodes); ThreadSearchContext = Context; TerminationManager = new MCTSFutilityPruning(this, Context); LimitManager = timeManager; CeresEnvironment.LogInfo("MCTS", "Init", $"SearchManager created for store {store}", InstanceID); }
/// <summary> /// Parses a specification of search time in UCI format into an equivalent SearchLimit. /// Returns null if parsing failed. /// </summary> /// <param name="command"></param> /// <returns></returns> private SearchLimit GetSearchLimit(string command) { SearchLimit searchLimit; bool weAreWhite = curPositionAndMoves.FinalPosition.MiscInfo.SideToMove == SideType.White; UCIGoCommandParsed goInfo = new UCIGoCommandParsed(command, weAreWhite); if (!goInfo.IsValid) { return(null); } if (goInfo.Nodes.HasValue) { searchLimit = SearchLimit.NodesPerMove(goInfo.Nodes.Value); } else if (goInfo.MoveTime.HasValue) { searchLimit = SearchLimit.SecondsPerMove(goInfo.MoveTime.Value / 1000.0f); } else if (goInfo.Infinite) { searchLimit = SearchLimit.NodesPerMove(MCTSNodeStore.MAX_NODES); } else if (goInfo.TimeOurs.HasValue) { float increment = 0; if (goInfo.IncrementOurs.HasValue) { increment = goInfo.IncrementOurs.Value / 1000.0f; } int?movesToGo = null; if (goInfo.MovesToGo.HasValue) { movesToGo = goInfo.MovesToGo.Value; } searchLimit = SearchLimit.SecondsForAllMoves(goInfo.TimeOurs.Value / 1000.0f, increment, movesToGo, true); } else if (goInfo.NodesOurs.HasValue) { float increment = 0; if (goInfo.IncrementOurs.HasValue) { increment = goInfo.IncrementOurs.Value; } int?movesToGo = null; if (goInfo.MovesToGo.HasValue) { movesToGo = goInfo.MovesToGo.Value; } searchLimit = SearchLimit.NodesForAllMoves(goInfo.NodesOurs.Value, (int)increment, movesToGo, true); } else { UCIWriteLine($"Unsupported time control in UCI go command {command}"); return(null); } // Add on possible search moves restriction. return(searchLimit with { SearchMoves = goInfo.SearchMoves }); }