diff --git a/src/Lynx.Cli/Program.cs b/src/Lynx.Cli/Program.cs index cfbb4fc8..cf5cc5d8 100644 --- a/src/Lynx.Cli/Program.cs +++ b/src/Lynx.Cli/Program.cs @@ -1,10 +1,8 @@ using Lynx; using Lynx.Cli; -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"); @@ -26,54 +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 engine = new Engine(engineChannel); -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 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 00000000..edd18d96 --- /dev/null +++ b/src/Lynx.Cli/Runner.cs @@ -0,0 +1,69 @@ +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 searcher = new Searcher(uciChannel, engineChannel); + var uciHandler = new UCIHandler(uciChannel, engineChannel, searcher); + var writer = new Writer(engineChannel); + var listener = new Listener(uciHandler); + + var tasks = new List + { + Task.Run(() => writer.Run(cancellationToken)), + Task.Run(() => searcher.Run(cancellationToken)), + Task.Run(() => listener.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 + } + } +} diff --git a/src/Lynx.Dev/Program.cs b/src/Lynx.Dev/Program.cs index 9d1510a0..7c8befdb 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(); @@ -1038,21 +1038,21 @@ 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 = TranspositionTableExtensions.CalculateLength(size); + var length = TranspositionTable.CalculateLength(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("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); @@ -1064,8 +1064,9 @@ static void TesSize(int size) TesSize(512); TesSize(1024); - var ttLength = TranspositionTableExtensions.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); - var transpositionTable = new TranspositionTableElement[ttLength]; + var ttLength = TranspositionTable.CalculateLength(Configuration.EngineSettings.TranspositionTableSize); + var transpositionTable = new TranspositionTable(); + var position = new Position(Constants.InitialPositionFEN); position.Print(); Console.WriteLine($"Hash: {position.UniqueIdentifier}"); @@ -1073,10 +1074,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.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 90a860e8..bc67de88 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; @@ -14,6 +13,7 @@ public sealed partial class Engine private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); private readonly ChannelWriter _engineWriter; + private readonly TranspositionTable _tt; private bool _isSearching; @@ -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; } @@ -48,7 +46,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 +56,7 @@ public Engine(ChannelWriter engineWriter) _searchCancellationTokenSource = new(); _absoluteSearchCancellationTokenSource = new(); _engineWriter = engineWriter; - + _tt = tt; // Update ResetEngine() after any changes here _quietHistory = new int[12][]; @@ -71,8 +71,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 +87,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 +112,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(); - } + _tt.Reset(); // Clear histories for (int i = 0; i < 12; ++i) diff --git a/src/Lynx/Lynx.csproj b/src/Lynx/Lynx.csproj index 72850ad8..de023e69 100644 --- a/src/Lynx/Lynx.csproj +++ b/src/Lynx/Lynx.csproj @@ -43,10 +43,6 @@ - - - - true Generated diff --git a/src/Lynx/Model/Game.cs b/src/Lynx/Model/Game.cs index 1c5ae579..48d7478a 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 de0f456b..94656571 100644 --- a/src/Lynx/Model/TranspositionTable.cs +++ b/src/Lynx/Model/TranspositionTable.cs @@ -2,123 +2,78 @@ using System.Diagnostics; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; +using System.Runtime.Intrinsics.X86; 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; - - /// - /// How deep the recorded search went. For us this numberis targetDepth - ply - /// - public readonly int Depth => _depth; + private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - /// - /// Node (position) type: - /// : == , - /// : <= , - /// : >= - /// - public readonly NodeType Type => _type; + private int _currentTranspositionTableSize; - /// - /// Struct size in bytes - /// - public static ulong Size => (ulong)Marshal.SizeOf(typeof(TranspositionTableElement)); + private TranspositionTableElement[] _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 + Allocate(); } -} - -public static class TranspositionTableExtensions -{ - private static readonly Logger _logger = LogManager.GetCurrentClassLogger(); - public static int CalculateLength(int size) + private void Allocate() { - 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 Reset() + { + 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); + Allocate(); + } + } - 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")); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public void PrefetchTTEntry(Position position) + { + if (Sse.IsSupported) + { + var index = CalculateTTIndex(position.UniqueIdentifier); - return (int)ttLength; + 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/ /// [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 [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) { @@ -151,10 +106,10 @@ public static (int Score, ShortMove BestMove, NodeType NodeType, int RawScore, i /// /// Ply [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) //{ @@ -180,94 +135,120 @@ public static void RecordHash(this TranspositionTable tt, Position position, int } /// - /// 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. - /// Logic for when to pass +depth or -depth for the desired effect in https://www.talkchess.com/forum3/viewtopic.php?f=7&t=74411 and https://talkchess.com/forum3/viewtopic.php?p=861852#p861852 + /// Exact TT occupancy per mill /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - internal static int RecalculateMateScores(int score, int ply) - { - if (score > EvaluationConstants.PositiveCheckmateDetectionLimit) - { - return score - (EvaluationConstants.CheckmateDepthFactor * ply); - } - else if (score < EvaluationConstants.NegativeCheckmateDetectionLimit) - { - return score + (EvaluationConstants.CheckmateDepthFactor * ply); - } - - return score; - } + public int HashfullPermill() => _tt.Length > 0 + ? (int)(1000L * PopulatedItemsCount() / _tt.LongLength) + : 0; + /// + /// Orders of magnitude faster than + /// + /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int PopulatedItemsCount(this TranspositionTable transpositionTable) + public int HashfullPermillApprox() { int items = 0; - for (int i = 0; i < transpositionTable.Length; ++i) + for (int i = 0; i < 1000; ++i) { - if (transpositionTable[i].Key != default) + 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; + } + /// - /// Exact TT occupancy per mill + /// 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. + /// Logic for when to pass +depth or -depth for the desired effect in https://www.talkchess.com/forum3/viewtopic.php?f=7&t=74411 and https://talkchess.com/forum3/viewtopic.php?p=861852#p861852 /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int HashfullPermill(this TranspositionTable transpositionTable) => transpositionTable.Length > 0 - ? (int)(1000L * transpositionTable.PopulatedItemsCount() / transpositionTable.LongLength) - : 0; + internal static int RecalculateMateScores(int score, int ply) + { + if (score > EvaluationConstants.PositiveCheckmateDetectionLimit) + { + return score - (EvaluationConstants.CheckmateDepthFactor * ply); + } + else if (score < EvaluationConstants.NegativeCheckmateDetectionLimit) + { + return score + (EvaluationConstants.CheckmateDepthFactor * ply); + } + + return score; + } - /// - /// Orders of magnitude faster than - /// [MethodImpl(MethodImplOptions.AggressiveInlining)] - public static int HashfullPermillApprox(this TranspositionTable transpositionTable) + private int PopulatedItemsCount() { int items = 0; - for (int i = 0; i < 1000; ++i) + for (int i = 0; i < _tt.Length; ++i) { - if (transpositionTable[i].Key != default) + if (_tt[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() / 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 00000000..cf86d3ad --- /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 2ee0c827..bfb7a909 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 = TranspositionTableExtensions.CalculateTTIndex(Game.CurrentPosition.UniqueIdentifier, _tt.Length); - - 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); - } - } - } - } - #pragma warning disable RCS1226 // Add paragraph to documentation comment #pragma warning disable RCS1243 // Duplicate word in a comment /// diff --git a/src/Lynx/Search/IDDFS.cs b/src/Lynx/Search/IDDFS.cs index 077d9e5d..bdf444a8 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; diff --git a/src/Lynx/Search/NegaMax.cs b/src/Lynx/Search/NegaMax.cs index f6f20049..804b714c 100644 --- a/src/Lynx/Search/NegaMax.cs +++ b/src/Lynx/Search/NegaMax.cs @@ -285,7 +285,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 @@ -332,7 +332,7 @@ void RevertMove() } } - PrefetchTTEntry(); + _tt.PrefetchTTEntry(position); int reduction = 0; diff --git a/src/Lynx/Searcher.cs b/src/Lynx/Searcher.cs index 1ab9bde0..e0735ac8 100644 --- a/src/Lynx/Searcher.cs +++ b/src/Lynx/Searcher.cs @@ -10,14 +10,22 @@ public sealed class Searcher { private readonly ChannelReader _uciReader; private readonly ChannelWriter _engineWriter; - private readonly Engine _engine; private readonly Logger _logger; - 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; - _engine = engine; + + TranspositionTable _ttWrapper = new(); + _engine = new Engine(_engineWriter, _ttWrapper); + _logger = LogManager.GetCurrentClassLogger(); } @@ -50,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); @@ -62,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 94661d53..ba631689 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) diff --git a/tests/Lynx.Test/EvaluationConstantsTest.cs b/tests/Lynx.Test/EvaluationConstantsTest.cs index d516e710..e8341613 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 edabc3ca..c3b1bc22 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);