From d86b32f27442989e7aa56f41e3eff0a515372198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 4 Nov 2024 01:38:45 +0100 Subject: [PATCH 01/12] Move time management calculations and UCI best move printing from Engine to Searcher --- src/Lynx.Cli/Program.cs | 2 +- src/Lynx/Bench.cs | 9 +- src/Lynx/Engine.cs | 120 ++++-------------- src/Lynx/Model/SearchConstraints.cs | 19 +++ src/Lynx/Searcher.cs | 23 +++- src/Lynx/TimeManager.cs | 99 +++++++++++++++ .../UCI/Commands/Engine/BestMoveCommand.cs | 8 +- 7 files changed, 176 insertions(+), 104 deletions(-) create mode 100644 src/Lynx/Model/SearchConstraints.cs create mode 100644 src/Lynx/TimeManager.cs diff --git a/src/Lynx.Cli/Program.cs b/src/Lynx.Cli/Program.cs index 1e5ef0bee..cfbb4fc8d 100644 --- a/src/Lynx.Cli/Program.cs +++ b/src/Lynx.Cli/Program.cs @@ -38,7 +38,7 @@ var tasks = new List { Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), - Task.Run(() => new Searcher(uciChannel, engine).Run(cancellationToken)), + Task.Run(() => new Searcher(uciChannel, engineChannel, engine).Run(cancellationToken)), Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), uciChannel.Reader.Completion, engineChannel.Reader.Completion diff --git a/src/Lynx/Bench.cs b/src/Lynx/Bench.cs index df3044973..0aadf15a3 100644 --- a/src/Lynx/Bench.cs +++ b/src/Lynx/Bench.cs @@ -1,4 +1,5 @@ -using System.Diagnostics; +using Lynx.UCI.Commands.GUI; +using System.Diagnostics; namespace Lynx; @@ -116,7 +117,11 @@ public partial class Engine AdjustPosition($"position fen {fen}"); stopwatch.Restart(); - var result = BestMove(new($"go depth {depth}")); + var goCommand = new GoCommand($"go depth {depth}"); + var searchConstraints = TimeManager.CalculateTimeManagement(Game, goCommand); + + var result = BestMove(goCommand, in searchConstraints); + var elapsedSeconds = Utils.CalculateElapsedSeconds(_stopWatch); totalSeconds += elapsedSeconds; totalNodes += result.Nodes; diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index d84b916ff..97ab1405a 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -37,8 +37,6 @@ public sealed partial class Engine private bool _isNewGameComing; #pragma warning restore IDE0052, CS0414 // Remove unread private members - private Move? _moveToPonder; - public double AverageDepth { get; private set; } public RegisterCommand? Registration { get; set; } @@ -106,10 +104,14 @@ private void WarmupEngine() var sw = Stopwatch.StartNew(); InitializeStaticClasses(); + const string goWarmupCommand = "go depth 10"; // ~300 ms + var command = new GoCommand(goWarmupCommand); AdjustPosition(Constants.SuperLongPositionCommand); - BestMove(new(goWarmupCommand)); + + var searchConstrains = TimeManager.CalculateTimeManagement(Game, command); + BestMove(command, searchConstrains); Bench(2); @@ -182,89 +184,33 @@ public void PonderHit() StopSearching(); } + /// + /// Uses internally + /// + /// + /// public SearchResult BestMove(GoCommand goCommand) { - bool isPondering = goCommand.Ponder; + var searchConstraints = TimeManager.CalculateTimeManagement(Game, goCommand); + return BestMove(goCommand, in searchConstraints); + } + + public SearchResult BestMove(GoCommand goCommand, in SearchConstraints searchConstrains) + { _searchCancellationTokenSource = new(); _absoluteSearchCancellationTokenSource = new(); - int maxDepth = -1; - int hardLimitTimeBound; - int softLimitTimeBound = int.MaxValue; - - double millisecondsLeft; - int millisecondsIncrement; - if (Game.CurrentPosition.Side == Side.White) - { - millisecondsLeft = goCommand.WhiteTime; - millisecondsIncrement = goCommand.WhiteIncrement; - } - else - { - millisecondsLeft = goCommand.BlackTime; - millisecondsIncrement = goCommand.BlackIncrement; - } - - // Inspired by Alexandria: time overhead to avoid timing out in the engine-gui communication process - const int engineGuiCommunicationTimeOverhead = 50; - - if (!isPondering) - { - if (goCommand.WhiteTime != 0 || goCommand.BlackTime != 0) // Cutechess sometimes sends negative wtime/btime - { - const int minSearchTime = 50; - - var movesDivisor = goCommand.MovesToGo == 0 - ? ExpectedMovesLeft(Game.PositionHashHistoryLength()) * 3 / 2 - : goCommand.MovesToGo; - - millisecondsLeft -= engineGuiCommunicationTimeOverhead; - millisecondsLeft = Math.Clamp(millisecondsLeft, minSearchTime, int.MaxValue); // Avoiding 0/negative values - - hardLimitTimeBound = (int)(millisecondsLeft * Configuration.EngineSettings.HardTimeBoundMultiplier); - - var softLimitBase = (millisecondsLeft / movesDivisor) + (millisecondsIncrement * Configuration.EngineSettings.SoftTimeBaseIncrementMultiplier); - softLimitTimeBound = Math.Min(hardLimitTimeBound, (int)(softLimitBase * Configuration.EngineSettings.SoftTimeBoundMultiplier)); - - _logger.Info("Soft time bound: {0}s", 0.001 * softLimitTimeBound); - _logger.Info("Hard time bound: {0}s", 0.001 * hardLimitTimeBound); - - _searchCancellationTokenSource.CancelAfter(hardLimitTimeBound); - } - else if (goCommand.MoveTime > 0) - { - softLimitTimeBound = hardLimitTimeBound = goCommand.MoveTime - engineGuiCommunicationTimeOverhead; - _logger.Info("Time to move: {0}s", 0.001 * hardLimitTimeBound); - - _searchCancellationTokenSource.CancelAfter(hardLimitTimeBound); - } - else if (goCommand.Depth > 0) - { - maxDepth = goCommand.Depth > Constants.AbsoluteMaxDepth ? Constants.AbsoluteMaxDepth : goCommand.Depth; - } - else if (goCommand.Infinite) - { - maxDepth = Configuration.EngineSettings.MaxDepth; - _logger.Info("Infinite search (depth {0})", maxDepth); - } - else - { - maxDepth = DefaultMaxDepth; - _logger.Warn("Unexpected or unsupported go command"); - } - } - else + if (searchConstrains.HardLimitTimeBound != SearchConstraints.DefaultHardLimitTimeBound) { - maxDepth = Configuration.EngineSettings.MaxDepth; - _logger.Info("Pondering search (depth {0})", maxDepth); + _searchCancellationTokenSource.CancelAfter(searchConstrains.HardLimitTimeBound); } - SearchResult resultToReturn = IDDFS(maxDepth, softLimitTimeBound); + SearchResult resultToReturn = IDDFS(searchConstrains.MaxDepth, searchConstrains.SoftLimitTimeBound); //SearchResult resultToReturn = await SearchBestMove(maxDepth, decisionTime); Game.ResetCurrentPositionToBeforeSearchState(); - if (!isPondering + if (!goCommand.Ponder && resultToReturn.BestMove != default && !_absoluteSearchCancellationTokenSource.IsCancellationRequested) { @@ -277,21 +223,6 @@ public SearchResult BestMove(GoCommand goCommand) return resultToReturn; } - /// - /// Straight from expositor's author paper, https://expositor.dev/pdf/movetime.pdf - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private static int ExpectedMovesLeft(int plies_played) - { - double p = (double)(plies_played); - - return (int)Math.Round( - (59.3 + ((72830.0 - (p * 2330.0)) / ((p * p) + (p * 10.0) + 2644.0))) // Plies remaining - / 2.0); // Full moves remaining - } - #pragma warning disable S1144 // Unused private types or members should be removed - wanna keep this around private async ValueTask SearchBestMove(int maxDepth, int softLimitTimeBound) #pragma warning restore S1144 // Unused private types or members should be removed @@ -336,7 +267,7 @@ private async ValueTask SearchBestMove(int maxDepth, int softLimit return tbResult ?? searchResult!; } - public void Search(GoCommand goCommand) + public SearchResult? Search(GoCommand goCommand, in SearchConstraints searchConstraints) { if (_isSearching) { @@ -349,7 +280,7 @@ public void Search(GoCommand goCommand) try { _isPondering = goCommand.Ponder; - var searchResult = BestMove(goCommand); + var searchResult = BestMove(goCommand, in searchConstraints); if (_isPondering) { @@ -367,17 +298,16 @@ public void Search(GoCommand goCommand) _isPondering = false; goCommand.DisablePonder(); - searchResult = BestMove(goCommand); + searchResult = BestMove(goCommand, in searchConstraints); } } - // We print best move even in case of go ponder + stop, and IDEs are expected to ignore it - _moveToPonder = searchResult.Moves.Length >= 2 ? searchResult.Moves[1] : null; - _engineWriter.TryWrite(new BestMoveCommand(searchResult.BestMove, _moveToPonder)); + return searchResult; } catch (Exception e) { _logger.Fatal(e, "Error in {0} while calculating BestMove", nameof(Search)); + return null; } finally { diff --git a/src/Lynx/Model/SearchConstraints.cs b/src/Lynx/Model/SearchConstraints.cs new file mode 100644 index 000000000..db065ff0f --- /dev/null +++ b/src/Lynx/Model/SearchConstraints.cs @@ -0,0 +1,19 @@ +namespace Lynx.Model; + +public readonly struct SearchConstraints +{ + public const int DefaultHardLimitTimeBound = int.MaxValue; + + public readonly int HardLimitTimeBound; + + public readonly int SoftLimitTimeBound; + + public readonly int MaxDepth; + + public SearchConstraints(int hardLimitTimeBound, int softLimitTimeBound, int maxDepth) + { + HardLimitTimeBound = hardLimitTimeBound; + SoftLimitTimeBound = softLimitTimeBound; + MaxDepth = maxDepth; + } +} diff --git a/src/Lynx/Searcher.cs b/src/Lynx/Searcher.cs index 269d8bfd0..1ab9bde0f 100644 --- a/src/Lynx/Searcher.cs +++ b/src/Lynx/Searcher.cs @@ -1,4 +1,6 @@ -using Lynx.UCI.Commands.GUI; +using Lynx.Model; +using Lynx.UCI.Commands.Engine; +using Lynx.UCI.Commands.GUI; using NLog; using System.Threading.Channels; @@ -7,12 +9,14 @@ namespace Lynx; public sealed class Searcher { private readonly ChannelReader _uciReader; + private readonly ChannelWriter _engineWriter; private readonly Engine _engine; private readonly Logger _logger; - public Searcher(ChannelReader uciReader, Engine engine) + public Searcher(ChannelReader uciReader, ChannelWriter engineWriter, Engine engine) { _uciReader = uciReader; + _engineWriter = engineWriter; _engine = engine; _logger = LogManager.GetCurrentClassLogger(); } @@ -27,7 +31,7 @@ public async Task Run(CancellationToken cancellationToken) { if (_uciReader.TryRead(out var rawCommand)) { - _engine.Search(new GoCommand(rawCommand)); + OnGoCommand(new GoCommand(rawCommand)); } } catch (Exception e) @@ -45,4 +49,17 @@ public async Task Run(CancellationToken cancellationToken) _logger.Info("Finishing {0}", nameof(Searcher)); } } + + private void OnGoCommand(GoCommand goCommand) + { + var searchConstraints = TimeManager.CalculateTimeManagement(_engine.Game, goCommand); + + var searchResult = _engine.Search(goCommand, searchConstraints); + + if (searchResult is not null) + { + // We always print best move, even in case of go ponder + stop, in which case IDEs are expected to ignore it + _engineWriter.TryWrite(new BestMoveCommand(searchResult)); + } + } } diff --git a/src/Lynx/TimeManager.cs b/src/Lynx/TimeManager.cs new file mode 100644 index 000000000..908742dcf --- /dev/null +++ b/src/Lynx/TimeManager.cs @@ -0,0 +1,99 @@ +using Lynx.Model; +using Lynx.UCI.Commands.GUI; +using NLog; +using System.Runtime.CompilerServices; + +namespace Lynx; +public static class TimeManager +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public static SearchConstraints CalculateTimeManagement(Game game, GoCommand goCommand) + { + bool isPondering = goCommand.Ponder; + + int maxDepth = -1; + int hardLimitTimeBound = SearchConstraints.DefaultHardLimitTimeBound; + int softLimitTimeBound = int.MaxValue; + + double millisecondsLeft; + int millisecondsIncrement; + if (game.CurrentPosition.Side == Side.White) + { + millisecondsLeft = goCommand.WhiteTime; + millisecondsIncrement = goCommand.WhiteIncrement; + } + else + { + millisecondsLeft = goCommand.BlackTime; + millisecondsIncrement = goCommand.BlackIncrement; + } + + // Inspired by Alexandria: time overhead to avoid timing out in the engine-gui communication process + const int engineGuiCommunicationTimeOverhead = 50; + + if (!isPondering) + { + if (goCommand.WhiteTime != 0 || goCommand.BlackTime != 0) // Cutechess sometimes sends negative wtime/btime + { + const int minSearchTime = 50; + + var movesDivisor = goCommand.MovesToGo == 0 + ? ExpectedMovesLeft(game.PositionHashHistoryLength()) * 3 / 2 + : goCommand.MovesToGo; + + millisecondsLeft -= engineGuiCommunicationTimeOverhead; + millisecondsLeft = Math.Clamp(millisecondsLeft, minSearchTime, int.MaxValue); // Avoiding 0/negative values + + hardLimitTimeBound = (int)(millisecondsLeft * Configuration.EngineSettings.HardTimeBoundMultiplier); + + var softLimitBase = (millisecondsLeft / movesDivisor) + (millisecondsIncrement * Configuration.EngineSettings.SoftTimeBaseIncrementMultiplier); + softLimitTimeBound = Math.Min(hardLimitTimeBound, (int)(softLimitBase * Configuration.EngineSettings.SoftTimeBoundMultiplier)); + + _logger.Info("Soft time bound: {0}s", 0.001 * softLimitTimeBound); + _logger.Info("Hard time bound: {0}s", 0.001 * hardLimitTimeBound); + } + else if (goCommand.MoveTime > 0) + { + softLimitTimeBound = hardLimitTimeBound = goCommand.MoveTime - engineGuiCommunicationTimeOverhead; + _logger.Info("Time to move: {0}s", 0.001 * hardLimitTimeBound); + } + else if (goCommand.Depth > 0) + { + maxDepth = goCommand.Depth > Constants.AbsoluteMaxDepth ? Constants.AbsoluteMaxDepth : goCommand.Depth; + } + else if (goCommand.Infinite) + { + maxDepth = Configuration.EngineSettings.MaxDepth; + _logger.Info("Infinite search (depth {0})", maxDepth); + } + else + { + maxDepth = Engine.DefaultMaxDepth; + _logger.Warn("Unexpected or unsupported go command"); + } + } + else + { + maxDepth = Configuration.EngineSettings.MaxDepth; + _logger.Info("Pondering search (depth {0})", maxDepth); + } + + return new(hardLimitTimeBound, softLimitTimeBound, maxDepth); + } + + /// + /// Straight from expositor's author paper, https://expositor.dev/pdf/movetime.pdf + /// + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static int ExpectedMovesLeft(int plies_played) + { + double p = (double)(plies_played); + + return (int)Math.Round( + (59.3 + ((72830.0 - (p * 2330.0)) / ((p * p) + (p * 10.0) + 2644.0))) // Plies remaining + / 2.0); // Full moves remaining + } +} diff --git a/src/Lynx/UCI/Commands/Engine/BestMoveCommand.cs b/src/Lynx/UCI/Commands/Engine/BestMoveCommand.cs index 926e8b8ea..c669771ec 100644 --- a/src/Lynx/UCI/Commands/Engine/BestMoveCommand.cs +++ b/src/Lynx/UCI/Commands/Engine/BestMoveCommand.cs @@ -18,10 +18,12 @@ public sealed class BestMoveCommand : IEngineBaseCommand private readonly Move _move; private readonly Move? _moveToPonder; - public BestMoveCommand(Move move, Move? moveToPonder = null) + public BestMoveCommand(SearchResult searchResult) { - _move = move; - _moveToPonder = moveToPonder; + _move = searchResult.BestMove; + + // We alwaus try to print ponder move, regardless of ponder on/off + _moveToPonder = searchResult.Moves.Length >= 2 ? searchResult.Moves[1] : null; } public override string ToString() From 240179de6a39bf618feb9fa90c333bf1f266f042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 4 Nov 2024 16:04:32 +0100 Subject: [PATCH 02/12] Add missing `in` --- src/Lynx/Engine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 97ab1405a..bd66234d9 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -111,7 +111,7 @@ private void WarmupEngine() AdjustPosition(Constants.SuperLongPositionCommand); var searchConstrains = TimeManager.CalculateTimeManagement(Game, command); - BestMove(command, searchConstrains); + BestMove(command, in searchConstrains); Bench(2); From 73541f690a56873c58b754b7f8050b20d65d6717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 4 Nov 2024 20:03:53 +0100 Subject: [PATCH 03/12] Move TT outside of Engine, and wrap the actual array in a class instantiated at top level --- Directory.Build.props | 1 - src/Lynx.Benchmark/UCI_Benchmark.cs | 3 +- src/Lynx.Cli/Program.cs | 6 +- src/Lynx.Dev/Program.cs | 15 +- src/Lynx/Engine.cs | 27 +- src/Lynx/Model/TranspositionTable.cs | 233 +++++++----------- src/Lynx/Model/TranspositionTableElement.cs | 77 ++++++ src/Lynx/Search/Helpers.cs | 4 +- src/Lynx/Search/IDDFS.cs | 5 +- src/Lynx/Search/NegaMax.cs | 18 +- src/Lynx/Search/OnlineTablebase.cs | 2 +- src/Lynx/Searcher.cs | 4 +- tests/Lynx.Test/EvaluationConstantsTest.cs | 8 +- .../Lynx.Test/Model/TranspositionTableTest.cs | 12 +- 14 files changed, 216 insertions(+), 199 deletions(-) create mode 100644 src/Lynx/Model/TranspositionTableElement.cs diff --git a/Directory.Build.props b/Directory.Build.props index 6b023ad0c..f6dc60b67 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,7 +19,6 @@ - diff --git a/src/Lynx.Benchmark/UCI_Benchmark.cs b/src/Lynx.Benchmark/UCI_Benchmark.cs index 593ee7a41..292ee2ef8 100644 --- a/src/Lynx.Benchmark/UCI_Benchmark.cs +++ b/src/Lynx.Benchmark/UCI_Benchmark.cs @@ -33,6 +33,7 @@ */ using BenchmarkDotNet.Attributes; +using Lynx.Model; using System.Threading.Channels; namespace Lynx.Benchmark; @@ -44,7 +45,7 @@ public class UCI_Benchmark : BaseBenchmark [Benchmark] public (ulong, ulong) Bench_DefaultDepth() { - var engine = new Engine(_channel.Writer); + var engine = new Engine(_channel.Writer, new TranspositionTable()); return engine.Bench(Configuration.EngineSettings.BenchDepth); } } diff --git a/src/Lynx.Cli/Program.cs b/src/Lynx.Cli/Program.cs index cfbb4fc8d..0ba205963 100644 --- a/src/Lynx.Cli/Program.cs +++ b/src/Lynx.Cli/Program.cs @@ -1,5 +1,6 @@ using Lynx; using Lynx.Cli; +using Lynx.Model; using Lynx.UCI.Commands.Engine; using Microsoft.Extensions.Configuration; using NLog; @@ -32,13 +33,14 @@ using CancellationTokenSource source = new(); CancellationToken cancellationToken = source.Token; -var engine = new Engine(engineChannel); +var tt = new TranspositionTable(); +var engine = new Engine(engineChannel, tt); var uciHandler = new UCIHandler(uciChannel, engineChannel, engine); var tasks = new List { Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), - Task.Run(() => new Searcher(uciChannel, engineChannel, engine).Run(cancellationToken)), + Task.Run(() => new Searcher(uciChannel, engineChannel, engine, tt).Run(cancellationToken)), Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), uciChannel.Reader.Completion, engineChannel.Reader.Completion diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 7488131b2..b677b01a3 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -703,7 +703,7 @@ static void _54_ScoreMove() var position = new Position(KillerPosition); position.Print(); - var engine = new Engine(Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = false })); + var engine = new Engine(Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = false }), new TranspositionTable()); engine.SetGame(new(position.FEN())); foreach (var move in MoveGenerator.GenerateAllMoves(position, capturesOnly: true)) { @@ -1050,13 +1050,13 @@ static void TranspositionTable() static void TesSize(int size) { Console.WriteLine("Hash: {0} MB", size); - var length = TranspositionTableExtensions.CalculateLength(size); + var length = Lynx.Model.TranspositionTable.CalculateLength(size); var lengthMb = length / 1024 / 1024; Console.WriteLine("TT memory: {0} MB", lengthMb * Marshal.SizeOf(typeof(TranspositionTableElement))); Console.WriteLine("TT array length: {0}MB, (0x{1}, {2} items)", lengthMb, length.ToString("X"), length); - Console.WriteLine("TT mask: 0x{0} ({1})\n", (length - 1).ToString("X"), Convert.ToString((length - 1), 2)); + Console.WriteLine("TT mask: 0x{0} ({1})\n", (length - 1).ToString("X"), Convert.ToString(length - 1, 2)); } Console.WriteLine($"{nameof(TranspositionTableElement)} size: {Marshal.SizeOf(typeof(TranspositionTableElement))} bytes\n"); @@ -1071,8 +1071,9 @@ static void TesSize(int size) TesSize(512); TesSize(1024); - var ttLength = TranspositionTableExtensions.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); - var transpositionTable = new TranspositionTableElement[ttLength]; + var ttLength = Lynx.Model.TranspositionTable.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); + var transpositionTable = new TranspositionTable(); + var position = new Position(Constants.InitialPositionFEN); position.Print(); Console.WriteLine($"Hash: {position.UniqueIdentifier}"); @@ -1080,10 +1081,10 @@ static void TesSize(int size) var hashKey = position.UniqueIdentifier % 0x400000; Console.WriteLine(hashKey); - var hashKey2 = TranspositionTableExtensions.CalculateTTIndex(position.UniqueIdentifier, ttLength); + var hashKey2 = transpositionTable.CalculateTTIndex(position.UniqueIdentifier); Console.WriteLine(hashKey2); - Array.Clear(transpositionTable); + transpositionTable.ResetTT(); //transpositionTable.RecordHash(position, depth: 3, maxDepth: 5, move: 1234, eval: +5, nodeType: NodeType.Alpha); //var entry = transpositionTable.ProbeHash(position, maxDepth: 5, depth: 3, alpha: 1, beta: 2); diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index bd66234d9..cdef85246 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -14,6 +14,7 @@ public sealed partial class Engine private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly ChannelWriter _engineWriter; + private readonly TranspositionTable _ttWraper; private bool _isSearching; @@ -48,7 +49,9 @@ public sealed partial class Engine private CancellationTokenSource _searchCancellationTokenSource; private CancellationTokenSource _absoluteSearchCancellationTokenSource; - public Engine(ChannelWriter engineWriter) + public Engine(ChannelWriter engineWriter) : this(engineWriter, new()) { } + + public Engine(ChannelWriter engineWriter, TranspositionTable tt) { AverageDepth = 0; Game = new Game(Constants.InitialPositionFEN); @@ -56,7 +59,7 @@ public Engine(ChannelWriter engineWriter) _searchCancellationTokenSource = new(); _absoluteSearchCancellationTokenSource = new(); _engineWriter = engineWriter; - + _ttWraper = tt; // Update ResetEngine() after any changes here _quietHistory = new int[12][]; @@ -71,8 +74,6 @@ public Engine(ChannelWriter engineWriter) _killerMoves[i] = new Move[3]; } - AllocateTT(); - #if !DEBUG // Temporary channel so that no output is generated _engineWriter = Channel.CreateUnbounded(new UnboundedChannelOptions() { SingleReader = true, SingleWriter = false }).Writer; @@ -89,14 +90,6 @@ public Engine(ChannelWriter engineWriter) #pragma warning restore S1215 // "GC.Collect" should not be called } - private void AllocateTT() - { - _currentTranspositionTableSize = Configuration.EngineSettings.TranspositionTableSize; - - var ttLength = TranspositionTableExtensions.CalculateLength(_currentTranspositionTableSize); - _tt = GC.AllocateArray(ttLength, pinned: true); - } - #pragma warning disable S1144 // Unused private types or members should be removed - used in Release mode private void WarmupEngine() { @@ -122,15 +115,7 @@ private void WarmupEngine() private void ResetEngine() { - if (_currentTranspositionTableSize == Configuration.EngineSettings.TranspositionTableSize) - { - Array.Clear(_tt); - } - else - { - _logger.Info("Resizing TT ({CurrentSize} MB -> {NewSize} MB)", _currentTranspositionTableSize, Configuration.EngineSettings.TranspositionTableSize); - AllocateTT(); - } + _ttWraper.ResetTT(); // Clear histories for (int i = 0; i < 12; ++i) diff --git a/src/Lynx/Model/TranspositionTable.cs b/src/Lynx/Model/TranspositionTable.cs index adc466831..265304d97 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -4,119 +4,53 @@ using System.Runtime.InteropServices; namespace Lynx.Model; - -public enum NodeType : byte -{ - Unknown, // Making it 0 instead of -1 because of default struct initialization - Exact, - Alpha, - Beta -} - -public struct TranspositionTableElement +public class TranspositionTable { - private ushort _key; - - private ShortMove _move; - - private short _score; - - private short _staticEval; - - private byte _depth; - - private NodeType _type; - - /// - /// 16 MSB of Position's Zobrist key - /// - public readonly ushort Key => _key; - - /// - /// Best move found in the position. 0 if the search failed low (score <= alpha) - /// - public readonly ShortMove Move => _move; - - /// - /// Position's score - /// - public readonly int Score => _score; - - /// - /// Position's static evaluation - /// - public readonly int StaticEval => _staticEval; + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - /// - /// How deep the recorded search went. For us this numberis targetDepth - ply - /// - public readonly int Depth => _depth; + private int _currentTranspositionTableSize; - /// - /// Node (position) type: - /// : == , - /// : <= , - /// : >= - /// - public readonly NodeType Type => _type; + TranspositionTableElement[] _tt = []; - /// - /// Struct size in bytes - /// - public static ulong Size => (ulong)Marshal.SizeOf(typeof(TranspositionTableElement)); + public TranspositionTableElement[] TT { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _tt; } - public void Update(ulong key, int score, int staticEval, int depth, NodeType nodeType, Move? move) + public TranspositionTable() { - _key = (ushort)key; - _score = (short)score; - _staticEval = (short)staticEval; - _depth = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref depth, 1))[0]; - _type = nodeType; - _move = move != null ? (ShortMove)move : Move; // Suggested by cj5716 instead of 0. https://github.com/lynx-chess/Lynx/pull/462 + InitializeTT(); } -} - -public static class TranspositionTableExtensions -{ - private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public static int CalculateLength(int size) + private void InitializeTT() { - var ttEntrySize = TranspositionTableElement.Size; + _currentTranspositionTableSize = Configuration.EngineSettings.TranspositionTableSize; - ulong sizeBytes = (ulong)size * 1024ul * 1024ul; - ulong ttLength = sizeBytes / ttEntrySize; - var ttLengthMb = (double)ttLength / 1024 / 1024; + var ttLength = CalculateLength(_currentTranspositionTableSize); + _tt = GC.AllocateArray(ttLength, pinned: true); + } - if (ttLength > (ulong)Array.MaxLength) + public void ResetTT() + { + if (_currentTranspositionTableSize == Configuration.EngineSettings.TranspositionTableSize) { - throw new ArgumentException($"Invalid transpositon table (Hash) size: {ttLengthMb}Mb, {ttLength} values (> Array.MaxLength, {Array.MaxLength})"); + Array.Clear(TT); + } + else + { + _logger.Info("Resizing TT ({CurrentSize} MB -> {NewSize} MB)", _currentTranspositionTableSize, Configuration.EngineSettings.TranspositionTableSize); + InitializeTT(); } - - var mask = ttLength - 1; - - _logger.Info("Hash value:\t{0} MB", size); - _logger.Info("TT memory:\t{0} MB", (ttLengthMb * ttEntrySize).ToString("F")); - _logger.Info("TT length:\t{0} items", ttLength); - _logger.Info("TT entry:\t{0} bytes", ttEntrySize); - _logger.Info("TT mask:\t{0}", mask.ToString("X")); - - return (int)ttLength; } /// /// 'Fixed-point multiplication trick', see https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ /// /// - /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static ulong CalculateTTIndex(ulong positionUniqueIdentifier, int ttLength) => (ulong)(((UInt128)positionUniqueIdentifier * (UInt128)ttLength) >> 64); + public ulong CalculateTTIndex(ulong positionUniqueIdentifier) => (ulong)(((UInt128)positionUniqueIdentifier * (UInt128)_tt.Length) >> 64); /// /// Checks the transposition table and, if there's a eval value that can be deducted from it of there's a previously recorded , it's returned. is returned otherwise /// - /// /// /// /// Ply @@ -124,10 +58,10 @@ public static int CalculateLength(int size) /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static (int Score, ShortMove BestMove, NodeType NodeType, int RawScore, int StaticEval) ProbeHash(this TranspositionTable tt, Position position, int depth, int ply, int alpha, int beta) + public (int Score, ShortMove BestMove, NodeType NodeType, int RawScore, int StaticEval) ProbeHash(Position position, int depth, int ply, int alpha, int beta) { - var ttIndex = CalculateTTIndex(position.UniqueIdentifier, tt.Length); - ref var entry = ref tt[ttIndex]; + var ttIndex = CalculateTTIndex(position.UniqueIdentifier); + ref var entry = ref _tt[ttIndex]; if ((ushort)position.UniqueIdentifier != entry.Key) { @@ -158,7 +92,6 @@ public static (int Score, ShortMove BestMove, NodeType NodeType, int RawScore, i /// /// Adds a to the transposition tabke /// - /// /// /// /// Ply @@ -166,10 +99,10 @@ public static (int Score, ShortMove BestMove, NodeType NodeType, int RawScore, i /// /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static void RecordHash(this TranspositionTable tt, Position position, int staticEval, int depth, int ply, int score, NodeType nodeType, Move? move = null) + public void RecordHash(Position position, int staticEval, int depth, int ply, int score, NodeType nodeType, Move? move = null) { - var ttIndex = CalculateTTIndex(position.UniqueIdentifier, tt.Length); - ref var entry = ref tt[ttIndex]; + var ttIndex = CalculateTTIndex(position.UniqueIdentifier); + ref var entry = ref _tt[ttIndex]; //if (entry.Key != default && entry.Key != position.UniqueIdentifier) //{ @@ -194,6 +127,59 @@ public static void RecordHash(this TranspositionTable tt, Position position, int entry.Update(position.UniqueIdentifier, recalculatedScore, staticEval, depth, nodeType, move); } + /// + /// Exact TT occupancy per mill + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int HashfullPermill() => _tt.Length > 0 + ? (int)(1000L * PopulatedItemsCount() / _tt.LongLength) + : 0; + + /// + /// Orders of magnitude faster than + /// + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int HashfullPermillApprox() + { + int items = 0; + for (int i = 0; i < 1000; ++i) + { + if (_tt[i].Key != default) + { + ++items; + } + } + + //Console.WriteLine($"Real: {HashfullPermill(transpositionTable)}, estimated: {items}"); + return items; + } + + internal static int CalculateLength(int size) + { + var ttEntrySize = TranspositionTableElement.Size; + + ulong sizeBytes = (ulong)size * 1024ul * 1024ul; + ulong ttLength = sizeBytes / ttEntrySize; + var ttLengthMb = (double)ttLength / 1024 / 1024; + + if (ttLength > (ulong)Array.MaxLength) + { + throw new ArgumentException($"Invalid transpositon table (Hash) size: {ttLengthMb}Mb, {ttLength} values (> Array.MaxLength, {Array.MaxLength})"); + } + + var mask = ttLength - 1; + + _logger.Info("Hash value:\t{0} MB", size); + _logger.Info("TT memory:\t{0} MB", (ttLengthMb * ttEntrySize).ToString("F")); + _logger.Info("TT length:\t{0} items", ttLength); + _logger.Info("TT entry:\t{0} bytes", ttEntrySize); + _logger.Info("TT mask:\t{0}", mask.ToString("X")); + + return (int)ttLength; + } + /// /// If playing side is giving checkmate, decrease checkmate score (increase n in checkmate in n moves) due to being searching at a given depth already when this position is found. /// The opposite if the playing side is getting checkmated. @@ -218,12 +204,12 @@ internal static int RecalculateMateScores(int score, int ply) } [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int PopulatedItemsCount(this TranspositionTable transpositionTable) + private int PopulatedItemsCount() { int items = 0; - for (int i = 0; i < transpositionTable.Length; ++i) + for (int i = 0; i < _tt.Length; ++i) { - if (transpositionTable[i].Key != default) + if (_tt[i].Key != default) { ++items; } @@ -232,64 +218,33 @@ public static int PopulatedItemsCount(this TranspositionTable transpositionTable return items; } - /// - /// Exact TT occupancy per mill - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int HashfullPermill(this TranspositionTable transpositionTable) => transpositionTable.Length > 0 - ? (int)(1000L * transpositionTable.PopulatedItemsCount() / transpositionTable.LongLength) - : 0; - - /// - /// Orders of magnitude faster than - /// - /// - /// - [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int HashfullPermillApprox(this TranspositionTable transpositionTable) - { - int items = 0; - for (int i = 0; i < 1000; ++i) - { - if (transpositionTable[i].Key != default) - { - ++items; - } - } - - //Console.WriteLine($"Real: {HashfullPermill(transpositionTable)}, estimated: {items}"); - return items; - } - [Conditional("DEBUG")] - internal static void Stats(this TranspositionTable transpositionTable) + private void Stats() { int items = 0; - for (int i = 0; i < transpositionTable.Length; ++i) + for (int i = 0; i < _tt.Length; ++i) { - if (transpositionTable[i].Key != default) + if (_tt[i].Key != default) { ++items; } } _logger.Info("TT Occupancy:\t{0}% ({1}MB)", - 100 * transpositionTable.PopulatedItemsCount() / transpositionTable.Length, - transpositionTable.Length * Marshal.SizeOf(typeof(TranspositionTableElement)) / 1024 / 1024); + 100 * PopulatedItemsCount() / _tt.Length, + _tt.Length * Marshal.SizeOf(typeof(TranspositionTableElement)) / 1024 / 1024); } [Conditional("DEBUG")] - internal static void Print(this TranspositionTable transpositionTable) + private void Print() { Console.WriteLine("Transposition table content:"); - for (int i = 0; i < transpositionTable.Length; ++i) + for (int i = 0; i < _tt.Length; ++i) { - if (transpositionTable[i].Key != default) + if (_tt[i].Key != default) { - Console.WriteLine($"{i}: Key = {transpositionTable[i].Key}, Depth: {transpositionTable[i].Depth}, Score: {transpositionTable[i].Score}, Move: {(transpositionTable[i].Move != 0 ? ((Move)transpositionTable[i].Move).UCIString() : "-")} {transpositionTable[i].Type}"); + Console.WriteLine($"{i}: Key = {_tt[i].Key}, Depth: {_tt[i].Depth}, Score: {_tt[i].Score}, Move: {(_tt[i].Move != 0 ? ((Move)_tt[i].Move).UCIString() : "-")} {_tt[i].Type}"); } } Console.WriteLine(""); } -} \ No newline at end of file +} diff --git a/src/Lynx/Model/TranspositionTableElement.cs b/src/Lynx/Model/TranspositionTableElement.cs new file mode 100644 index 000000000..cf86d3adb --- /dev/null +++ b/src/Lynx/Model/TranspositionTableElement.cs @@ -0,0 +1,77 @@ +using NLog; +using System.Diagnostics; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Lynx.Model; + +public enum NodeType : byte +{ + Unknown, // Making it 0 instead of -1 because of default struct initialization + Exact, + Alpha, + Beta +} + +public struct TranspositionTableElement +{ + private ushort _key; + + private ShortMove _move; + + private short _score; + + private short _staticEval; + + private byte _depth; + + private NodeType _type; + + /// + /// 16 MSB of Position's Zobrist key + /// + public readonly ushort Key => _key; + + /// + /// Best move found in the position. 0 if the search failed low (score <= alpha) + /// + public readonly ShortMove Move => _move; + + /// + /// Position's score + /// + public readonly int Score => _score; + + /// + /// Position's static evaluation + /// + public readonly int StaticEval => _staticEval; + + /// + /// How deep the recorded search went. For us this numberis targetDepth - ply + /// + public readonly int Depth => _depth; + + /// + /// Node (position) type: + /// : == , + /// : <= , + /// : >= + /// + public readonly NodeType Type => _type; + + /// + /// Struct size in bytes + /// + public static ulong Size => (ulong)Marshal.SizeOf(typeof(TranspositionTableElement)); + + public void Update(ulong key, int score, int staticEval, int depth, NodeType nodeType, Move? move) + { + _key = (ushort)key; + _score = (short)score; + _staticEval = (short)staticEval; + _depth = MemoryMarshal.AsBytes(MemoryMarshal.CreateSpan(ref depth, 1))[0]; + _type = nodeType; + _move = move != null ? (ShortMove)move : Move; // Suggested by cj5716 instead of 0. https://github.com/lynx-chess/Lynx/pull/462 + } +} diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index 8c150770a..ba8c3b7d7 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -13,14 +13,14 @@ private void PrefetchTTEntry() { if (Sse.IsSupported) { - var index = TranspositionTableExtensions.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier, _tt.Length); + var index = _ttWraper.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier); unsafe { // Since _tt is a pinned array // This is no-op pinning as it does not influence the GC compaction // https://tooslowexception.com/pinned-object-heap-in-net-5/ - fixed (TranspositionTableElement* ttPtr = _tt) + fixed (TranspositionTableElement* ttPtr = _ttWraper.TT) { Sse.Prefetch0(ttPtr + index); } diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index 97bb9b759..6d21fc7a9 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -42,9 +42,6 @@ public sealed partial class Engine private readonly int[] _maxDepthReached = GC.AllocateArray(Configuration.EngineSettings.MaxDepth + Constants.ArrayDepthMargin, pinned: true); - private int _currentTranspositionTableSize; - private TranspositionTable _tt = null!; - private ulong _nodes; private SearchResult? _previousSearchResult; @@ -355,7 +352,7 @@ private SearchResult GenerateFinalSearchResult(SearchResult? lastSearchResult, finalSearchResult.Nodes = _nodes; finalSearchResult.Time = Utils.CalculateUCITime(elapsedSeconds); finalSearchResult.NodesPerSecond = Utils.CalculateNps(_nodes, elapsedSeconds); - finalSearchResult.HashfullPermill = _tt.HashfullPermillApprox(); + finalSearchResult.HashfullPermill = _ttWraper.HashfullPermillApprox(); if (Configuration.EngineSettings.ShowWDL) { finalSearchResult.WDL = WDL.WDLModel(bestScore, depth); diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 0510bf141..03d8b286f 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -47,7 +47,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM if (!isRoot) { - (ttScore, ttBestMove, ttElementType, ttRawScore, ttStaticEval) = _tt.ProbeHash(position, depth, ply, alpha, beta); + (ttScore, ttBestMove, ttElementType, ttRawScore, ttStaticEval) = _ttWraper.ProbeHash(position, depth, ply, alpha, beta); // TT cutoffs if (!pvNode && ttScore != EvaluationConstants.NoHashEntry) @@ -92,7 +92,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM } var finalPositionEvaluation = Position.EvaluateFinalPosition(ply, isInCheck); - _tt.RecordHash(position, finalPositionEvaluation, depth, ply, finalPositionEvaluation, NodeType.Exact); + _ttWraper.RecordHash(position, finalPositionEvaluation, depth, ply, finalPositionEvaluation, NodeType.Exact); return finalPositionEvaluation; } else if (!pvNode) @@ -441,7 +441,7 @@ void RevertMove() UpdateMoveOrderingHeuristicsOnQuietBetaCutoff(historyDepth, ply, visitedMoves, visitedMovesCounter, move, isRoot); } - _tt.RecordHash(position, staticEval, depth, ply, bestScore, NodeType.Beta, bestMove); + _ttWraper.RecordHash(position, staticEval, depth, ply, bestScore, NodeType.Beta, bestMove); return bestScore; } @@ -455,12 +455,12 @@ void RevertMove() Debug.Assert(bestMove is null); var finalEval = Position.EvaluateFinalPosition(ply, isInCheck); - _tt.RecordHash(position, staticEval, depth, ply, finalEval, NodeType.Exact); + _ttWraper.RecordHash(position, staticEval, depth, ply, finalEval, NodeType.Exact); return finalEval; } - _tt.RecordHash(position, staticEval, depth, ply, bestScore, nodeType, bestMove); + _ttWraper.RecordHash(position, staticEval, depth, ply, bestScore, nodeType, bestMove); // Node fails low return bestScore; @@ -497,7 +497,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta) var nextPvIndex = PVTable.Indexes[ply + 1]; _pVTable[pvIndex] = _defaultMove; // Nulling the first value before any returns - var ttProbeResult = _tt.ProbeHash(position, 0, ply, alpha, beta); + var ttProbeResult = _ttWraper.ProbeHash(position, 0, ply, alpha, beta); if (ttProbeResult.Score != EvaluationConstants.NoHashEntry) { return ttProbeResult.Score; @@ -597,7 +597,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta) { PrintMessage($"Pruning: {move} is enough to discard this line"); - _tt.RecordHash(position, staticEval, 0, ply, bestScore, NodeType.Beta, bestMove); + _ttWraper.RecordHash(position, staticEval, 0, ply, bestScore, NodeType.Beta, bestMove); return bestScore; // The refutation doesn't matter, since it'll be pruned } @@ -622,12 +622,12 @@ public int QuiescenceSearch(int ply, int alpha, int beta) Debug.Assert(bestMove is null); var finalEval = Position.EvaluateFinalPosition(ply, position.IsInCheck()); - _tt.RecordHash(position, staticEval, 0, ply, finalEval, NodeType.Exact); + _ttWraper.RecordHash(position, staticEval, 0, ply, finalEval, NodeType.Exact); return finalEval; } - _tt.RecordHash(position, staticEval, 0, ply, bestScore, nodeType, bestMove); + _ttWraper.RecordHash(position, staticEval, 0, ply, bestScore, nodeType, bestMove); return bestScore; } diff --git a/src/Lynx/Search/OnlineTablebase.cs b/src/Lynx/Search/OnlineTablebase.cs index 51ff75f2e..1e795dc46 100644 --- a/src/Lynx/Search/OnlineTablebase.cs +++ b/src/Lynx/Search/OnlineTablebase.cs @@ -22,7 +22,7 @@ public sealed partial class Engine Nodes = 666, // In case some guis proritize the info command with biggest depth Time = Utils.CalculateUCITime(elapsedSeconds), NodesPerSecond = 0, - HashfullPermill = _tt.HashfullPermillApprox(), + HashfullPermill = _ttWraper.HashfullPermillApprox(), WDL = WDL.WDLModel( (int)Math.CopySign( EvaluationConstants.PositiveCheckmateDetectionLimit + (EvaluationConstants.CheckmateDepthFactor * tablebaseResult.MateScore), diff --git a/src/Lynx/Searcher.cs b/src/Lynx/Searcher.cs index 1ab9bde0f..c01e88b7d 100644 --- a/src/Lynx/Searcher.cs +++ b/src/Lynx/Searcher.cs @@ -12,12 +12,14 @@ public sealed class Searcher private readonly ChannelWriter _engineWriter; private readonly Engine _engine; private readonly Logger _logger; + private readonly TranspositionTable _tt; - public Searcher(ChannelReader uciReader, ChannelWriter engineWriter, Engine engine) + public Searcher(ChannelReader uciReader, ChannelWriter engineWriter, Engine engine, TranspositionTable tt) { _uciReader = uciReader; _engineWriter = engineWriter; _engine = engine; + _tt = tt; _logger = LogManager.GetCurrentClassLogger(); } diff --git a/tests/Lynx.Test/EvaluationConstantsTest.cs b/tests/Lynx.Test/EvaluationConstantsTest.cs index d516e7101..e8341613a 100644 --- a/tests/Lynx.Test/EvaluationConstantsTest.cs +++ b/tests/Lynx.Test/EvaluationConstantsTest.cs @@ -48,8 +48,8 @@ public void MaxEvalTest() { Assert.Greater(MaxEval, PositiveCheckmateDetectionLimit + ((Constants.AbsoluteMaxDepth + 10) * CheckmateDepthFactor)); Assert.Greater(MaxEval, CheckMateBaseEvaluation + ((Constants.AbsoluteMaxDepth + 10) * CheckmateDepthFactor)); - Assert.Greater(MaxEval, TranspositionTableExtensions.RecalculateMateScores(CheckMateBaseEvaluation, Constants.AbsoluteMaxDepth)); - Assert.Greater(MaxEval, TranspositionTableExtensions.RecalculateMateScores(CheckMateBaseEvaluation, -Constants.AbsoluteMaxDepth)); + Assert.Greater(MaxEval, TranspositionTable.RecalculateMateScores(CheckMateBaseEvaluation, Constants.AbsoluteMaxDepth)); + Assert.Greater(MaxEval, TranspositionTable.RecalculateMateScores(CheckMateBaseEvaluation, -Constants.AbsoluteMaxDepth)); Assert.Less(MaxEval, short.MaxValue); } @@ -58,8 +58,8 @@ public void MinEvalTest() { Assert.Less(MinEval, NegativeCheckmateDetectionLimit - ((Constants.AbsoluteMaxDepth + 10) * CheckmateDepthFactor)); Assert.Less(MinEval, -CheckMateBaseEvaluation - ((Constants.AbsoluteMaxDepth + 10) * CheckmateDepthFactor)); - Assert.Less(MinEval, TranspositionTableExtensions.RecalculateMateScores(-CheckMateBaseEvaluation, Constants.AbsoluteMaxDepth)); - Assert.Less(MinEval, TranspositionTableExtensions.RecalculateMateScores(-CheckMateBaseEvaluation, -Constants.AbsoluteMaxDepth)); + Assert.Less(MinEval, TranspositionTable.RecalculateMateScores(-CheckMateBaseEvaluation, Constants.AbsoluteMaxDepth)); + Assert.Less(MinEval, TranspositionTable.RecalculateMateScores(-CheckMateBaseEvaluation, -Constants.AbsoluteMaxDepth)); Assert.Greater(MinEval, short.MinValue); } diff --git a/tests/Lynx.Test/Model/TranspositionTableTest.cs b/tests/Lynx.Test/Model/TranspositionTableTest.cs index edabc3ca0..c3b1bc22c 100644 --- a/tests/Lynx.Test/Model/TranspositionTableTest.cs +++ b/tests/Lynx.Test/Model/TranspositionTableTest.cs @@ -21,7 +21,7 @@ public class TranspositionTableTests [TestCase(-CheckMateBaseEvaluation + (2 * CheckmateDepthFactor), 4, -CheckMateBaseEvaluation + (6 * CheckmateDepthFactor))] public void RecalculateMateScores(int evaluation, int depth, int expectedEvaluation) { - Assert.AreEqual(expectedEvaluation, TranspositionTableExtensions.RecalculateMateScores(evaluation, depth)); + Assert.AreEqual(expectedEvaluation, TranspositionTable.RecalculateMateScores(evaluation, depth)); } [TestCase(+19, NodeType.Alpha, +20, +30, +19)] @@ -31,8 +31,8 @@ public void RecalculateMateScores(int evaluation, int depth, int expectedEvaluat public void RecordHash_ProbeHash(int recordedEval, NodeType recordNodeType, int probeAlpha, int probeBeta, int expectedProbeEval) { var position = new Position(Constants.InitialPositionFEN); - var ttLength = TranspositionTableExtensions.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); - var transpositionTable = new TranspositionTableElement[ttLength]; + var transpositionTable = new TranspositionTable(); + var staticEval = position.StaticEvaluation().Score; transpositionTable.RecordHash(position, staticEval, depth: 5, ply: 3, score: recordedEval, nodeType: recordNodeType, move: 1234); @@ -48,8 +48,7 @@ public void RecordHash_ProbeHash_CheckmateSameDepth(int recordedEval) { const int sharedDepth = 5; var position = new Position(Constants.InitialPositionFEN); - var ttLength = TranspositionTableExtensions.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); - var transpositionTable = new TranspositionTableElement[ttLength]; + var transpositionTable = new TranspositionTable(); transpositionTable.RecordHash(position, recordedEval, depth: 10, ply: sharedDepth, score: recordedEval, nodeType: NodeType.Exact, move: 1234); @@ -65,8 +64,7 @@ public void RecordHash_ProbeHash_CheckmateSameDepth(int recordedEval) public void RecordHash_ProbeHash_CheckmateDifferentDepth(int recordedEval, int recordedDeph, int probeDepth, int expectedProbeEval) { var position = new Position(Constants.InitialPositionFEN); - var ttLength = TranspositionTableExtensions.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); - var transpositionTable = new TranspositionTableElement[ttLength]; + var transpositionTable = new TranspositionTable(); transpositionTable.RecordHash(position, recordedEval, depth: 10, ply: recordedDeph, score: recordedEval, nodeType: NodeType.Exact, move: 1234); From 73c5abecfbaa4ad2a9c34db40cb9d06093e3b9c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 4 Nov 2024 20:09:25 +0100 Subject: [PATCH 04/12] _ttWraper -> _tt --- src/Lynx/Engine.cs | 6 +++--- src/Lynx/Search/Helpers.cs | 4 ++-- src/Lynx/Search/IDDFS.cs | 2 +- src/Lynx/Search/NegaMax.cs | 18 +++++++++--------- src/Lynx/Search/OnlineTablebase.cs | 2 +- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index cdef85246..15f07d6d8 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -14,7 +14,7 @@ public sealed partial class Engine private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly ChannelWriter _engineWriter; - private readonly TranspositionTable _ttWraper; + private readonly TranspositionTable _tt; private bool _isSearching; @@ -59,7 +59,7 @@ public Engine(ChannelWriter engineWriter, TranspositionTable tt) _searchCancellationTokenSource = new(); _absoluteSearchCancellationTokenSource = new(); _engineWriter = engineWriter; - _ttWraper = tt; + _tt = tt; // Update ResetEngine() after any changes here _quietHistory = new int[12][]; @@ -115,7 +115,7 @@ private void WarmupEngine() private void ResetEngine() { - _ttWraper.ResetTT(); + _tt.ResetTT(); // Clear histories for (int i = 0; i < 12; ++i) diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index ba8c3b7d7..ba286da63 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -13,14 +13,14 @@ private void PrefetchTTEntry() { if (Sse.IsSupported) { - var index = _ttWraper.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier); + var index = _tt.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier); unsafe { // Since _tt is a pinned array // This is no-op pinning as it does not influence the GC compaction // https://tooslowexception.com/pinned-object-heap-in-net-5/ - fixed (TranspositionTableElement* ttPtr = _ttWraper.TT) + fixed (TranspositionTableElement* ttPtr = _tt.TT) { Sse.Prefetch0(ttPtr + index); } diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index 6d21fc7a9..5de50d03f 100644 --- a/src/Lynx/Search/IDDFS.cs +++ b/src/Lynx/Search/IDDFS.cs @@ -352,7 +352,7 @@ private SearchResult GenerateFinalSearchResult(SearchResult? lastSearchResult, finalSearchResult.Nodes = _nodes; finalSearchResult.Time = Utils.CalculateUCITime(elapsedSeconds); finalSearchResult.NodesPerSecond = Utils.CalculateNps(_nodes, elapsedSeconds); - finalSearchResult.HashfullPermill = _ttWraper.HashfullPermillApprox(); + finalSearchResult.HashfullPermill = _tt.HashfullPermillApprox(); if (Configuration.EngineSettings.ShowWDL) { finalSearchResult.WDL = WDL.WDLModel(bestScore, depth); diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 03d8b286f..0510bf141 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -47,7 +47,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM if (!isRoot) { - (ttScore, ttBestMove, ttElementType, ttRawScore, ttStaticEval) = _ttWraper.ProbeHash(position, depth, ply, alpha, beta); + (ttScore, ttBestMove, ttElementType, ttRawScore, ttStaticEval) = _tt.ProbeHash(position, depth, ply, alpha, beta); // TT cutoffs if (!pvNode && ttScore != EvaluationConstants.NoHashEntry) @@ -92,7 +92,7 @@ private int NegaMax(int depth, int ply, int alpha, int beta, bool parentWasNullM } var finalPositionEvaluation = Position.EvaluateFinalPosition(ply, isInCheck); - _ttWraper.RecordHash(position, finalPositionEvaluation, depth, ply, finalPositionEvaluation, NodeType.Exact); + _tt.RecordHash(position, finalPositionEvaluation, depth, ply, finalPositionEvaluation, NodeType.Exact); return finalPositionEvaluation; } else if (!pvNode) @@ -441,7 +441,7 @@ void RevertMove() UpdateMoveOrderingHeuristicsOnQuietBetaCutoff(historyDepth, ply, visitedMoves, visitedMovesCounter, move, isRoot); } - _ttWraper.RecordHash(position, staticEval, depth, ply, bestScore, NodeType.Beta, bestMove); + _tt.RecordHash(position, staticEval, depth, ply, bestScore, NodeType.Beta, bestMove); return bestScore; } @@ -455,12 +455,12 @@ void RevertMove() Debug.Assert(bestMove is null); var finalEval = Position.EvaluateFinalPosition(ply, isInCheck); - _ttWraper.RecordHash(position, staticEval, depth, ply, finalEval, NodeType.Exact); + _tt.RecordHash(position, staticEval, depth, ply, finalEval, NodeType.Exact); return finalEval; } - _ttWraper.RecordHash(position, staticEval, depth, ply, bestScore, nodeType, bestMove); + _tt.RecordHash(position, staticEval, depth, ply, bestScore, nodeType, bestMove); // Node fails low return bestScore; @@ -497,7 +497,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta) var nextPvIndex = PVTable.Indexes[ply + 1]; _pVTable[pvIndex] = _defaultMove; // Nulling the first value before any returns - var ttProbeResult = _ttWraper.ProbeHash(position, 0, ply, alpha, beta); + var ttProbeResult = _tt.ProbeHash(position, 0, ply, alpha, beta); if (ttProbeResult.Score != EvaluationConstants.NoHashEntry) { return ttProbeResult.Score; @@ -597,7 +597,7 @@ public int QuiescenceSearch(int ply, int alpha, int beta) { PrintMessage($"Pruning: {move} is enough to discard this line"); - _ttWraper.RecordHash(position, staticEval, 0, ply, bestScore, NodeType.Beta, bestMove); + _tt.RecordHash(position, staticEval, 0, ply, bestScore, NodeType.Beta, bestMove); return bestScore; // The refutation doesn't matter, since it'll be pruned } @@ -622,12 +622,12 @@ public int QuiescenceSearch(int ply, int alpha, int beta) Debug.Assert(bestMove is null); var finalEval = Position.EvaluateFinalPosition(ply, position.IsInCheck()); - _ttWraper.RecordHash(position, staticEval, 0, ply, finalEval, NodeType.Exact); + _tt.RecordHash(position, staticEval, 0, ply, finalEval, NodeType.Exact); return finalEval; } - _ttWraper.RecordHash(position, staticEval, 0, ply, bestScore, nodeType, bestMove); + _tt.RecordHash(position, staticEval, 0, ply, bestScore, nodeType, bestMove); return bestScore; } diff --git a/src/Lynx/Search/OnlineTablebase.cs b/src/Lynx/Search/OnlineTablebase.cs index 1e795dc46..51ff75f2e 100644 --- a/src/Lynx/Search/OnlineTablebase.cs +++ b/src/Lynx/Search/OnlineTablebase.cs @@ -22,7 +22,7 @@ public sealed partial class Engine Nodes = 666, // In case some guis proritize the info command with biggest depth Time = Utils.CalculateUCITime(elapsedSeconds), NodesPerSecond = 0, - HashfullPermill = _ttWraper.HashfullPermillApprox(), + HashfullPermill = _tt.HashfullPermillApprox(), WDL = WDL.WDLModel( (int)Math.CopySign( EvaluationConstants.PositiveCheckmateDetectionLimit + (EvaluationConstants.CheckmateDepthFactor * tablebaseResult.MateScore), From 684f00f360860ae7c4ba0bc48cbcc58768b941a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 4 Nov 2024 20:16:33 +0100 Subject: [PATCH 05/12] Renames --- src/Lynx.Dev/Program.cs | 2 +- src/Lynx/Engine.cs | 2 +- src/Lynx/Model/TranspositionTable.cs | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index b677b01a3..627d1fc55 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -1084,7 +1084,7 @@ static void TesSize(int size) var hashKey2 = transpositionTable.CalculateTTIndex(position.UniqueIdentifier); Console.WriteLine(hashKey2); - transpositionTable.ResetTT(); + transpositionTable.Reset(); //transpositionTable.RecordHash(position, depth: 3, maxDepth: 5, move: 1234, eval: +5, nodeType: NodeType.Alpha); //var entry = transpositionTable.ProbeHash(position, maxDepth: 5, depth: 3, alpha: 1, beta: 2); diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 15f07d6d8..76cfe304e 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -115,7 +115,7 @@ private void WarmupEngine() private void ResetEngine() { - _tt.ResetTT(); + _tt.Reset(); // Clear histories for (int i = 0; i < 12; ++i) diff --git a/src/Lynx/Model/TranspositionTable.cs b/src/Lynx/Model/TranspositionTable.cs index 265304d97..d1300c9c6 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -16,10 +16,10 @@ public class TranspositionTable public TranspositionTable() { - InitializeTT(); + Allocate(); } - private void InitializeTT() + private void Allocate() { _currentTranspositionTableSize = Configuration.EngineSettings.TranspositionTableSize; @@ -27,7 +27,7 @@ private void InitializeTT() _tt = GC.AllocateArray(ttLength, pinned: true); } - public void ResetTT() + public void Reset() { if (_currentTranspositionTableSize == Configuration.EngineSettings.TranspositionTableSize) { @@ -36,7 +36,7 @@ public void ResetTT() else { _logger.Info("Resizing TT ({CurrentSize} MB -> {NewSize} MB)", _currentTranspositionTableSize, Configuration.EngineSettings.TranspositionTableSize); - InitializeTT(); + Allocate(); } } From 2add9fbc5f58063a86ffa9a56d024636aa9b8621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sat, 9 Nov 2024 02:50:21 +0100 Subject: [PATCH 06/12] Move Lynx init logic to Runner --- src/Lynx.Cli/Program.cs | 54 +------------------------------- src/Lynx.Cli/Runner.cs | 69 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 53 deletions(-) create mode 100644 src/Lynx.Cli/Runner.cs diff --git a/src/Lynx.Cli/Program.cs b/src/Lynx.Cli/Program.cs index 0ba205963..cf5cc5d85 100644 --- a/src/Lynx.Cli/Program.cs +++ b/src/Lynx.Cli/Program.cs @@ -1,11 +1,8 @@ using Lynx; using Lynx.Cli; -using Lynx.Model; -using Lynx.UCI.Commands.Engine; using Microsoft.Extensions.Configuration; using NLog; using NLog.Extensions.Logging; -using System.Threading.Channels; #if DEBUG Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "Development"); @@ -27,55 +24,6 @@ LogManager.Configuration = new NLogLoggingConfiguration(config.GetSection("NLog")); } -var uciChannel = Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = true, FullMode = BoundedChannelFullMode.Wait }); -var engineChannel = Channel.CreateBounded(new BoundedChannelOptions(2 * Configuration.EngineSettings.MaxDepth) { SingleReader = true, SingleWriter = false, FullMode = BoundedChannelFullMode.DropOldest }); - -using CancellationTokenSource source = new(); -CancellationToken cancellationToken = source.Token; - -var tt = new TranspositionTable(); -var engine = new Engine(engineChannel, tt); -var uciHandler = new UCIHandler(uciChannel, engineChannel, engine); - -var tasks = new List -{ - Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), - Task.Run(() => new Searcher(uciChannel, engineChannel, engine, tt).Run(cancellationToken)), - Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), - uciChannel.Reader.Completion, - engineChannel.Reader.Completion -}; - -try -{ - Console.WriteLine($"{IdCommand.EngineName} {IdCommand.GetVersion()} by {IdCommand.EngineAuthor}"); - await Task.WhenAny(tasks); -} -catch (AggregateException ae) -{ - foreach (var e in ae.InnerExceptions) - { - if (e is TaskCanceledException taskCanceledException) - { - Console.WriteLine("Cancellation requested: {0}", taskCanceledException.Message); - } - else - { - Console.WriteLine("Exception: {0}", e.GetType().Name); - } - } -} -catch (Exception e) -{ - Console.WriteLine("Unexpected exception"); - Console.WriteLine(e.Message); -} -finally -{ - engineChannel.Writer.TryComplete(); - uciChannel.Writer.TryComplete(); - //source.Cancel(); - LogManager.Shutdown(); // Flush and close down internal threads and timers -} +await Runner.Run(args); Thread.Sleep(2_000); diff --git a/src/Lynx.Cli/Runner.cs b/src/Lynx.Cli/Runner.cs new file mode 100644 index 000000000..b2dca6b29 --- /dev/null +++ b/src/Lynx.Cli/Runner.cs @@ -0,0 +1,69 @@ +using Lynx.Model; +using Lynx.UCI.Commands.Engine; +using NLog; +using System.Threading.Channels; + +namespace Lynx.Cli; + +public static class Runner +{ + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); + + public static async Task Run(params string[] args) + { + var uciChannel = Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = true, FullMode = BoundedChannelFullMode.Wait }); + var engineChannel = Channel.CreateBounded(new BoundedChannelOptions(2 * Configuration.EngineSettings.MaxDepth) { SingleReader = true, SingleWriter = false, FullMode = BoundedChannelFullMode.DropOldest }); + + using CancellationTokenSource source = new(); + CancellationToken cancellationToken = source.Token; + + var tt = new TranspositionTable(); + var engine = new Engine(engineChannel, tt); + var uciHandler = new UCIHandler(uciChannel, engineChannel, engine); + + var tasks = new List + { + Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), + Task.Run(() => new Searcher(uciChannel, engineChannel, engine, tt).Run(cancellationToken)), + Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), + uciChannel.Reader.Completion, + engineChannel.Reader.Completion + }; + + try + { + Console.WriteLine($"{IdCommand.EngineName} {IdCommand.GetVersion()} by {IdCommand.EngineAuthor}"); + await Task.WhenAny(tasks); + } + catch (AggregateException ae) + { + foreach (var e in ae.InnerExceptions) + { + if (e is TaskCanceledException taskCanceledException) + { + Console.WriteLine("Cancellation requested exception: {0}", taskCanceledException.Message); + _logger.Fatal(ae, "Cancellation requested exception: {0}", taskCanceledException.Message); + } + else + { + Console.WriteLine("Exception {0}: {1}", e.GetType().Name, e.Message); + _logger.Fatal(ae, "Exception {0}: {1}", e.GetType().Name, e.Message); + } + } + } + catch (Exception e) + { + Console.WriteLine("Unexpected exception"); + Console.WriteLine(e.Message); + + _logger.Fatal(e, "Unexpected exception: {Exception}", e.Message); + } + finally + { + engineChannel.Writer.TryComplete(); + uciChannel.Writer.TryComplete(); + //source.Cancel(); + LogManager.Shutdown(); // Flush and close down internal threads and timers + } + } +} From 8653b29b595c344759fbbd1898ec7bfad36a3b01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Sun, 10 Nov 2024 00:03:46 +0100 Subject: [PATCH 07/12] Allow the searcher to initialize the engine --- src/Lynx.Cli/Runner.cs | 10 ++++------ src/Lynx/Engine.cs | 1 - src/Lynx/Searcher.cs | 14 ++++++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/Lynx.Cli/Runner.cs b/src/Lynx.Cli/Runner.cs index b2dca6b29..b3ebee4f4 100644 --- a/src/Lynx.Cli/Runner.cs +++ b/src/Lynx.Cli/Runner.cs @@ -1,5 +1,4 @@ -using Lynx.Model; -using Lynx.UCI.Commands.Engine; +using Lynx.UCI.Commands.Engine; using NLog; using System.Threading.Channels; @@ -17,14 +16,13 @@ public static async Task Run(params string[] args) using CancellationTokenSource source = new(); CancellationToken cancellationToken = source.Token; - var tt = new TranspositionTable(); - var engine = new Engine(engineChannel, tt); - var uciHandler = new UCIHandler(uciChannel, engineChannel, engine); + var searcher = new Searcher(uciChannel, engineChannel); + var uciHandler = new UCIHandler(uciChannel, engineChannel, searcher.Engine); var tasks = new List { Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), - Task.Run(() => new Searcher(uciChannel, engineChannel, engine, tt).Run(cancellationToken)), + Task.Run(() => searcher.Run(cancellationToken)), Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), uciChannel.Reader.Completion, engineChannel.Reader.Completion diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index 76cfe304e..d4618ebdf 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -1,5 +1,4 @@ using Lynx.Model; -using Lynx.UCI.Commands.Engine; using Lynx.UCI.Commands.GUI; using NLog; using System.Diagnostics; diff --git a/src/Lynx/Searcher.cs b/src/Lynx/Searcher.cs index c01e88b7d..444df0d99 100644 --- a/src/Lynx/Searcher.cs +++ b/src/Lynx/Searcher.cs @@ -10,19 +10,21 @@ public sealed class Searcher { private readonly ChannelReader _uciReader; private readonly ChannelWriter _engineWriter; - private readonly Engine _engine; private readonly Logger _logger; private readonly TranspositionTable _tt; - public Searcher(ChannelReader uciReader, ChannelWriter engineWriter, Engine engine, TranspositionTable tt) + public Searcher(ChannelReader uciReader, ChannelWriter engineWriter) { _uciReader = uciReader; _engineWriter = engineWriter; - _engine = engine; - _tt = tt; + + _tt = new TranspositionTable(); _logger = LogManager.GetCurrentClassLogger(); + Engine = new Engine(_engineWriter, _tt); } + public Engine Engine { get; } + public async Task Run(CancellationToken cancellationToken) { try @@ -54,9 +56,9 @@ public async Task Run(CancellationToken cancellationToken) private void OnGoCommand(GoCommand goCommand) { - var searchConstraints = TimeManager.CalculateTimeManagement(_engine.Game, goCommand); + var searchConstraints = TimeManager.CalculateTimeManagement(Engine.Game, goCommand); - var searchResult = _engine.Search(goCommand, searchConstraints); + var searchResult = Engine.Search(goCommand, searchConstraints); if (searchResult is not null) { From 684800af6d12e7a888cd35726355a87c0505e012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Mon, 11 Nov 2024 17:55:04 +0100 Subject: [PATCH 08/12] Move `PrefetchTTEntry()` from Helpers to TranspositionTable --- src/Lynx/Model/TranspositionTable.cs | 27 +++++++++++++++++++++++---- src/Lynx/Search/Helpers.cs | 20 -------------------- src/Lynx/Search/NegaMax.cs | 4 ++-- 3 files changed, 25 insertions(+), 26 deletions(-) diff --git a/src/Lynx/Model/TranspositionTable.cs b/src/Lynx/Model/TranspositionTable.cs index d1300c9c6..e9486d276 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -2,6 +2,7 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; namespace Lynx.Model; public class TranspositionTable @@ -10,9 +11,7 @@ public class TranspositionTable private int _currentTranspositionTableSize; - TranspositionTableElement[] _tt = []; - - public TranspositionTableElement[] TT { [MethodImpl(MethodImplOptions.AggressiveInlining)] get => _tt; } + private TranspositionTableElement[] _tt = []; public TranspositionTable() { @@ -31,7 +30,7 @@ public void Reset() { if (_currentTranspositionTableSize == Configuration.EngineSettings.TranspositionTableSize) { - Array.Clear(TT); + Array.Clear(_tt); } else { @@ -40,6 +39,26 @@ public void Reset() } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrefetchTTEntry(Position position) + { + if (Sse.IsSupported) + { + var index = CalculateTTIndex(position.UniqueIdentifier); + + unsafe + { + // Since _tt is a pinned array + // This is no-op pinning as it does not influence the GC compaction + // https://tooslowexception.com/pinned-object-heap-in-net-5/ + fixed (TranspositionTableElement* ttPtr = _tt) + { + Sse.Prefetch0(ttPtr + index); + } + } + } + } + /// /// 'Fixed-point multiplication trick', see https://lemire.me/blog/2016/06/27/a-fast-alternative-to-the-modulo-reduction/ /// diff --git a/src/Lynx/Search/Helpers.cs b/src/Lynx/Search/Helpers.cs index ba286da63..9e09fc4c7 100644 --- a/src/Lynx/Search/Helpers.cs +++ b/src/Lynx/Search/Helpers.cs @@ -8,26 +8,6 @@ namespace Lynx; public sealed partial class Engine { - [MethodImpl(MethodImplOptions.AggressiveInlining)] - private void PrefetchTTEntry() - { - if (Sse.IsSupported) - { - var index = _tt.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier); - - unsafe - { - // Since _tt is a pinned array - // This is no-op pinning as it does not influence the GC compaction - // https://tooslowexception.com/pinned-object-heap-in-net-5/ - fixed (TranspositionTableElement* ttPtr = _tt.TT) - { - Sse.Prefetch0(ttPtr + index); - } - } - } - } - #pragma warning disable RCS1226 // Add paragraph to documentation comment #pragma warning disable RCS1243 // Duplicate word in a comment /// diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index 8808cf7bf..827ac3e99 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -288,7 +288,7 @@ void RevertMove() } else if (visitedMovesCounter == 0) { - PrefetchTTEntry(); + _tt.PrefetchTTEntry(position); #pragma warning disable S2234 // Arguments should be passed in the same order as the method parameters score = -NegaMax(depth - 1, ply + 1, -beta, -alpha); #pragma warning restore S2234 // Arguments should be passed in the same order as the method parameters @@ -335,7 +335,7 @@ void RevertMove() } } - PrefetchTTEntry(); + _tt.PrefetchTTEntry(position); int reduction = 0; From 9ba9b5b9a699c051d3e7686e9d1f64c0d872c198 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Tue, 19 Nov 2024 23:48:57 +0100 Subject: [PATCH 09/12] Fix merge --- src/Lynx/Lynx.csproj | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Lynx/Lynx.csproj b/src/Lynx/Lynx.csproj index 72850ad82..de023e694 100644 --- a/src/Lynx/Lynx.csproj +++ b/src/Lynx/Lynx.csproj @@ -43,10 +43,6 @@ - - - - true Generated From b92611e3e69abbf2582865b96145be0ce99fdf55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Tue, 19 Nov 2024 23:51:11 +0100 Subject: [PATCH 10/12] Revert unnecessary changes --- src/Lynx.Benchmark/UCI_Benchmark.cs | 3 +-- src/Lynx.Dev/Program.cs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Lynx.Benchmark/UCI_Benchmark.cs b/src/Lynx.Benchmark/UCI_Benchmark.cs index 292ee2ef8..593ee7a41 100644 --- a/src/Lynx.Benchmark/UCI_Benchmark.cs +++ b/src/Lynx.Benchmark/UCI_Benchmark.cs @@ -33,7 +33,6 @@ */ using BenchmarkDotNet.Attributes; -using Lynx.Model; using System.Threading.Channels; namespace Lynx.Benchmark; @@ -45,7 +44,7 @@ public class UCI_Benchmark : BaseBenchmark [Benchmark] public (ulong, ulong) Bench_DefaultDepth() { - var engine = new Engine(_channel.Writer, new TranspositionTable()); + var engine = new Engine(_channel.Writer); return engine.Bench(Configuration.EngineSettings.BenchDepth); } } diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 627d1fc55..15f692ce6 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -703,7 +703,7 @@ static void _54_ScoreMove() var position = new Position(KillerPosition); position.Print(); - var engine = new Engine(Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = false }), new TranspositionTable()); + var engine = new Engine(Channel.CreateBounded(new BoundedChannelOptions(100) { SingleReader = true, SingleWriter = false })); engine.SetGame(new(position.FEN())); foreach (var move in MoveGenerator.GenerateAllMoves(position, capturesOnly: true)) { From 682007786c02bf94fe1a0790b078395b7c423688 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Tue, 19 Nov 2024 23:52:52 +0100 Subject: [PATCH 11/12] Avoid name conflict in Program dev --- src/Lynx.Dev/Program.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 15f692ce6..2bfed3ebf 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -49,7 +49,7 @@ //FileAndRankMasks(); //EnhancedPawnEvaluation(); //RookEvaluation(); -//TranspositionTable(); +//TranspositionTableMethod(); //UnmakeMove(); //PieceSquareTables(); //NewMasks(); @@ -1045,12 +1045,12 @@ static void RookEvaluation() Console.WriteLine(eval); } -static void TranspositionTable() +static void TranspositionTableMethod() { static void TesSize(int size) { Console.WriteLine("Hash: {0} MB", size); - var length = Lynx.Model.TranspositionTable.CalculateLength(size); + var length = TranspositionTable.CalculateLength(size); var lengthMb = length / 1024 / 1024; @@ -1071,7 +1071,7 @@ static void TesSize(int size) TesSize(512); TesSize(1024); - var ttLength = Lynx.Model.TranspositionTable.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); + var ttLength = TranspositionTable.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); var transpositionTable = new TranspositionTable(); var position = new Position(Constants.InitialPositionFEN); From 883440f076c4a5cdc15b9ebf695b0038f94ba6a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eduardo=20C=C3=A1ceres?= Date: Thu, 21 Nov 2024 01:43:48 +0100 Subject: [PATCH 12/12] Pass Searcher instead of Engine to UCIHandler, and don't expose Engine in Searcher --- src/Lynx.Cli/Runner.cs | 8 ++-- src/Lynx.Dev/Program.cs | 4 +- src/Lynx/Engine.cs | 2 - src/Lynx/Model/Game.cs | 2 + src/Lynx/Model/TranspositionTable.cs | 2 +- src/Lynx/Searcher.cs | 59 ++++++++++++++++++++++++---- src/Lynx/UCIHandler.cs | 51 ++++++++---------------- 7 files changed, 79 insertions(+), 49 deletions(-) diff --git a/src/Lynx.Cli/Runner.cs b/src/Lynx.Cli/Runner.cs index b3ebee4f4..edd18d964 100644 --- a/src/Lynx.Cli/Runner.cs +++ b/src/Lynx.Cli/Runner.cs @@ -17,13 +17,15 @@ public static async Task Run(params string[] args) CancellationToken cancellationToken = source.Token; var searcher = new Searcher(uciChannel, engineChannel); - var uciHandler = new UCIHandler(uciChannel, engineChannel, searcher.Engine); + var uciHandler = new UCIHandler(uciChannel, engineChannel, searcher); + var writer = new Writer(engineChannel); + var listener = new Listener(uciHandler); var tasks = new List { - Task.Run(() => new Writer(engineChannel).Run(cancellationToken)), + Task.Run(() => writer.Run(cancellationToken)), Task.Run(() => searcher.Run(cancellationToken)), - Task.Run(() => new Listener(uciHandler).Run(cancellationToken, args)), + Task.Run(() => listener.Run(cancellationToken, args)), uciChannel.Reader.Completion, engineChannel.Reader.Completion }; diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 21c65afcb..7c8befdb8 100644 --- a/src/Lynx.Dev/Program.cs +++ b/src/Lynx.Dev/Program.cs @@ -1047,12 +1047,12 @@ static void TesSize(int size) var lengthMb = length / 1024 / 1024; - Console.WriteLine("TT memory: {0} MB", lengthMb * Marshal.SizeOf(typeof(TranspositionTableElement))); + Console.WriteLine("TT memory: {0} MB", lengthMb * Marshal.SizeOf()); Console.WriteLine("TT array length: {0}MB, (0x{1}, {2} items)", lengthMb, length.ToString("X"), length); Console.WriteLine("TT mask: 0x{0} ({1})\n", (length - 1).ToString("X"), Convert.ToString(length - 1, 2)); } - Console.WriteLine($"{nameof(TranspositionTableElement)} size: {Marshal.SizeOf(typeof(TranspositionTableElement))} bytes\n"); + Console.WriteLine($"{nameof(TranspositionTableElement)} size: {Marshal.SizeOf()} bytes\n"); TesSize(2); TesSize(4); diff --git a/src/Lynx/Engine.cs b/src/Lynx/Engine.cs index d4618ebdf..c9a0247c7 100644 --- a/src/Lynx/Engine.cs +++ b/src/Lynx/Engine.cs @@ -39,8 +39,6 @@ public sealed partial class Engine public double AverageDepth { get; private set; } - public RegisterCommand? Registration { get; set; } - public Game Game { get; private set; } public bool PendingConfirmation { get; set; } diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 7e4e8c645..26e87a082 100644 --- a/src/Lynx/Model/Game.cs +++ b/src/Lynx/Model/Game.cs @@ -29,6 +29,8 @@ public sealed class Game : IDisposable public Position PositionBeforeLastSearch { get; private set; } + public string FEN => CurrentPosition.FEN(HalfMovesWithoutCaptureOrPawnMove); + public Game(ReadOnlySpan fen) : this(fen, [], [], []) { } diff --git a/src/Lynx/Model/TranspositionTable.cs b/src/Lynx/Model/TranspositionTable.cs index e9486d276..e8f719431 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -250,7 +250,7 @@ private void Stats() } _logger.Info("TT Occupancy:\t{0}% ({1}MB)", 100 * PopulatedItemsCount() / _tt.Length, - _tt.Length * Marshal.SizeOf(typeof(TranspositionTableElement)) / 1024 / 1024); + _tt.Length * Marshal.SizeOf() / 1024 / 1024); } [Conditional("DEBUG")] diff --git a/src/Lynx/Searcher.cs b/src/Lynx/Searcher.cs index c5cc103e7..e0735ac87 100644 --- a/src/Lynx/Searcher.cs +++ b/src/Lynx/Searcher.cs @@ -10,23 +10,25 @@ public sealed class Searcher { private readonly ChannelReader _uciReader; private readonly ChannelWriter _engineWriter; - private readonly Engine _engine; private readonly Logger _logger; - private readonly TranspositionTable _tt; - public Searcher(ChannelReader uciReader, ChannelWriter engineWriter, Engine engine) + private readonly Engine _engine; + + public Position CurrentPosition => _engine.Game.CurrentPosition; + + public string FEN => _engine.Game.FEN; + + public Searcher(ChannelReader uciReader, ChannelWriter engineWriter) { _uciReader = uciReader; _engineWriter = engineWriter; - _tt = new TranspositionTable(); - _engine = engine; + TranspositionTable _ttWrapper = new(); + _engine = new Engine(_engineWriter, _ttWrapper); _logger = LogManager.GetCurrentClassLogger(); } - public Engine Engine => _engine; - public async Task Run(CancellationToken cancellationToken) { try @@ -56,6 +58,8 @@ public async Task Run(CancellationToken cancellationToken) } } + public void PrintCurrentPosition() => _engine.Game.CurrentPosition.Print(); + private void OnGoCommand(GoCommand goCommand) { var searchConstraints = TimeManager.CalculateTimeManagement(_engine.Game, goCommand); @@ -68,4 +72,45 @@ private void OnGoCommand(GoCommand goCommand) _engineWriter.TryWrite(new BestMoveCommand(searchResult)); } } + + public void AdjustPosition(ReadOnlySpan command) + { + _engine.AdjustPosition(command); + } + + public void StopSearching() + { + _engine.StopSearching(); + } + + public void PonderHit() + { + _engine.PonderHit(); + } + + public void NewGame() + { + var averageDepth = _engine.AverageDepth; + if (averageDepth > 0 && averageDepth < int.MaxValue) + { + _logger.Info("Average depth: {0}", averageDepth); + } + + _engine.NewGame(); + } + + public void Quit() + { + var averageDepth = _engine.AverageDepth; + if (averageDepth > 0 && averageDepth < int.MaxValue) + { + _logger.Info("Average depth: {0}", averageDepth); + } + } + + public async ValueTask RunBench(int depth) + { + var results = _engine.Bench(depth); + await _engine.PrintBenchResults(results); + } } diff --git a/src/Lynx/UCIHandler.cs b/src/Lynx/UCIHandler.cs index 94661d53c..ba631689c 100644 --- a/src/Lynx/UCIHandler.cs +++ b/src/Lynx/UCIHandler.cs @@ -15,15 +15,15 @@ public sealed class UCIHandler private readonly Channel _uciToEngine; private readonly Channel _engineToUci; - private readonly Engine _engine; + private readonly Searcher _searcher; private readonly Logger _logger; - public UCIHandler(Channel uciToEngine, Channel engineToUci, Engine engine) + public UCIHandler(Channel uciToEngine, Channel engineToUci, Searcher searcher) { _uciToEngine = uciToEngine; _engineToUci = engineToUci; - _engine = engine; + _searcher = searcher; _logger = LogManager.GetCurrentClassLogger(); } @@ -66,9 +66,6 @@ static ReadOnlySpan ExtractCommandItems(string rawCommand) case QuitCommand.Id: HandleQuit(); return; - case RegisterCommand.Id: - HandleRegister(rawCommand); - break; case SetOptionCommand.Id: HandleSetOption(rawCommand); break; @@ -133,18 +130,18 @@ private void HandlePosition(ReadOnlySpan command) { #if DEBUG var sw = Stopwatch.StartNew(); - _engine.Game.CurrentPosition.Print(); + _searcher.PrintCurrentPosition(); #endif - _engine.AdjustPosition(command); + _searcher.AdjustPosition(command); #if DEBUG - _engine.Game.CurrentPosition.Print(); + _searcher.PrintCurrentPosition(); _logger.Info("Position parsing took {0}ms", sw.ElapsedMilliseconds); #endif } - private void HandleStop() => _engine.StopSearching(); + private void HandleStop() => _searcher.StopSearching(); private async Task HandleUCI(CancellationToken cancellationToken) { @@ -165,7 +162,7 @@ private void HandlePonderHit() { if (Configuration.EngineSettings.IsPonder) { - _engine.PonderHit(); + _searcher.PonderHit(); } else { @@ -536,33 +533,24 @@ private void HandleSetOption(ReadOnlySpan command) private void HandleNewGame() { - if (_engine.AverageDepth > 0 && _engine.AverageDepth < int.MaxValue) - { - _logger.Info("Average depth: {0}", _engine.AverageDepth); - } - _engine.NewGame(); + _searcher.NewGame(); } private static void HandleDebug(ReadOnlySpan command) => Configuration.IsDebug = DebugCommand.Parse(command); private void HandleQuit() { - if (_engine.AverageDepth > 0 && _engine.AverageDepth < int.MaxValue) - { - _logger.Info("Average depth: {0}", _engine.AverageDepth); - } + _searcher.Quit(); _engineToUci.Writer.Complete(); } - private void HandleRegister(ReadOnlySpan rawCommand) => _engine.Registration = new RegisterCommand(rawCommand); - private void HandlePerft(string rawCommand) { var items = rawCommand.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (items.Length >= 2 && int.TryParse(items[1], out int depth) && depth >= 1) { - Perft.RunPerft(_engine.Game.CurrentPosition, depth, str => _engineToUci.Writer.TryWrite(str)); + Perft.RunPerft(_searcher.CurrentPosition, depth, str => _engineToUci.Writer.TryWrite(str)); } } @@ -572,7 +560,7 @@ private void HandleDivide(string rawCommand) if (items.Length >= 2 && int.TryParse(items[1], out int depth) && depth >= 1) { - Perft.RunDivide(_engine.Game.CurrentPosition, depth, str => _engineToUci.Writer.TryWrite(str)); + Perft.RunDivide(_searcher.CurrentPosition, depth, str => _engineToUci.Writer.TryWrite(str)); } } @@ -584,8 +572,8 @@ private async ValueTask HandleBench(string rawCommand) { depth = Configuration.EngineSettings.BenchDepth; } - var results = _engine.Bench(depth); - await _engine.PrintBenchResults(results); + + await _searcher.RunBench(depth); } private async ValueTask HandleSettings() @@ -665,18 +653,13 @@ private async ValueTask HandleStaticEval(string rawCommand, CancellationToken ca private async Task HandleEval(CancellationToken cancellationToken) { - var score = WDL.NormalizeScore(_engine.Game.CurrentPosition.StaticEvaluation().Score); - - await _engineToUci.Writer.WriteAsync(score, cancellationToken); + var normalizedScore = WDL.NormalizeScore(_searcher.CurrentPosition.StaticEvaluation().Score); + await _engineToUci.Writer.WriteAsync(normalizedScore, cancellationToken); } private async Task HandleFEN(CancellationToken cancellationToken) { - const string fullMoveCounterString = " 1"; - - var fen = _engine.Game.CurrentPosition.FEN()[..^3] + _engine.Game.HalfMovesWithoutCaptureOrPawnMove.ToString() + fullMoveCounterString; - - await _engineToUci.Writer.WriteAsync(fen, cancellationToken); + await _engineToUci.Writer.WriteAsync(_searcher.FEN, cancellationToken); } private async ValueTask HandleOpenBenchSPSA(CancellationToken cancellationToken)