Skip to content

Commit

Permalink
Add ponder support (#772)
Browse files Browse the repository at this point in the history
In case of ponderhit, the search is restarted with usual time limitations
  • Loading branch information
eduherminio authored May 27, 2024
1 parent 439e760 commit 8b0cef2
Show file tree
Hide file tree
Showing 7 changed files with 108 additions and 70 deletions.
1 change: 1 addition & 0 deletions src/Lynx.Cli/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"UseOnlineTablebaseInSearch": false,
"OnlineTablebaseMaxSupportedPieces": 7,
"ShowWDL": false,
"IsPonder": false,
"SPSA_OB_R_end": 0.02,

"HardTimeBoundMultiplier": 0.52,
Expand Down
19 changes: 2 additions & 17 deletions src/Lynx/Configuration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ public static class Configuration
#pragma warning disable IDE1006 // Naming Styles
private static int _UCI_AnalyseMode = 0;
#pragma warning restore IDE1006 // Naming Styles
private static int _ponder = 0;

public static bool IsDebug
{
Expand Down Expand Up @@ -46,22 +45,6 @@ public static bool UCI_AnalyseMode
}
}

public static bool IsPonder
{
get => Interlocked.CompareExchange(ref _ponder, 1, 1) == 1;
set
{
if (value)
{
Interlocked.CompareExchange(ref _ponder, 1, 0);
}
else
{
Interlocked.CompareExchange(ref _ponder, 0, 1);
}
}
}

public static int Hash
{
get => EngineSettings.TranspositionTableSize;
Expand Down Expand Up @@ -105,6 +88,8 @@ public sealed class EngineSettings

public bool ShowWDL { get; set; } = false;

public bool IsPonder { get; set; } = false;

public double SPSA_OB_R_end { get; set; } = 0.02;

#region Time management
Expand Down
128 changes: 83 additions & 45 deletions src/Lynx/Engine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,31 @@ public sealed partial class Engine
private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
private readonly ChannelWriter<string> _engineWriter;

private bool _isSearching;

/// <summary>
/// Ongoing search is a pondering one and there has been a ponder hit
/// </summary>
private bool _isPonderHit;

/// <summary>
/// Ongoing search is a pondering one
/// </summary>
private bool _isPondering;

#pragma warning disable IDE0052, CS0414, S4487 // Remove unread private members
private bool _isNewGameCommandSupported;
private bool _isNewGameComing;
private bool _isPondering;
#pragma warning restore IDE0052, CS0414 // Remove unread private members

private Move? _moveToPonder;

public double AverageDepth { get; private set; }

public RegisterCommand? Registration { get; set; }

public Game Game { get; private set; }

public bool IsSearching { get; private set; }

public bool PendingConfirmation { get; set; }

private CancellationTokenSource _searchCancellationTokenSource;
Expand Down Expand Up @@ -143,12 +153,14 @@ public void AdjustPosition(ReadOnlySpan<char> rawPositionCommand)

public void PonderHit()
{
Game.MakeMove(_moveToPonder!.Value); // TODO ponder: do we also receive the position command? If so, remove this line
_isPondering = false;
_isPonderHit = true;
StopSearching();
}

public SearchResult BestMove(GoCommand goCommand)
{
bool isPondering = goCommand.Ponder;

_searchCancellationTokenSource = new();
_absoluteSearchCancellationTokenSource = new();

Expand All @@ -172,56 +184,64 @@ public SearchResult BestMove(GoCommand goCommand)
// Inspired by Alexandria: time overhead to avoid timing out in the engine-gui communication process
const int engineGuiCommunicationTimeOverhead = 50;

if (goCommand.WhiteTime != 0 || goCommand.BlackTime != 0) // Cutechess sometimes sends negative wtime/btime
if (!isPondering)
{
const int minSearchTime = 50;

var movesDivisor = goCommand.MovesToGo == 0
? Configuration.EngineSettings.DefaultMovesToGo
: goCommand.MovesToGo;
if (goCommand.WhiteTime != 0 || goCommand.BlackTime != 0) // Cutechess sometimes sends negative wtime/btime
{
const int minSearchTime = 50;

millisecondsLeft -= engineGuiCommunicationTimeOverhead;
millisecondsLeft = Math.Clamp(millisecondsLeft, minSearchTime, int.MaxValue); // Avoiding 0/negative values
var movesDivisor = goCommand.MovesToGo == 0
? Configuration.EngineSettings.DefaultMovesToGo
: goCommand.MovesToGo;

hardLimitTimeBound = (int)(millisecondsLeft * Configuration.EngineSettings.HardTimeBoundMultiplier);
millisecondsLeft -= engineGuiCommunicationTimeOverhead;
millisecondsLeft = Math.Clamp(millisecondsLeft, minSearchTime, int.MaxValue); // Avoiding 0/negative values

var softLimitBase = (millisecondsLeft / movesDivisor) + (millisecondsIncrement * Configuration.EngineSettings.SoftTimeBaseIncrementMultiplier);
softLimitTimeBound = Math.Min(hardLimitTimeBound, (int)(softLimitBase * Configuration.EngineSettings.SoftTimeBoundMultiplier));
hardLimitTimeBound = (int)(millisecondsLeft * Configuration.EngineSettings.HardTimeBoundMultiplier);

_logger.Info("Soft time bound: {0}s", 0.001 * softLimitTimeBound);
_logger.Info("Hard time bound: {0}s", 0.001 * hardLimitTimeBound);
var softLimitBase = (millisecondsLeft / movesDivisor) + (millisecondsIncrement * Configuration.EngineSettings.SoftTimeBaseIncrementMultiplier);
softLimitTimeBound = Math.Min(hardLimitTimeBound, (int)(softLimitBase * Configuration.EngineSettings.SoftTimeBoundMultiplier));

//return default!;
_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.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);
_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
{
maxDepth = DefaultMaxDepth;
_logger.Warn("Unexpected or unsupported go command");
maxDepth = Configuration.EngineSettings.MaxDepth;
_logger.Info("Pondering search (depth {0})", maxDepth);
}

SearchResult resultToReturn = IDDFS(maxDepth, softLimitTimeBound);
//SearchResult resultToReturn = await SearchBestMove(maxDepth, decisionTime);

Game.ResetCurrentPositionToBeforeSearchState();
if (resultToReturn.BestMove != default && !_absoluteSearchCancellationTokenSource.IsCancellationRequested)
if (!isPondering
&& resultToReturn.BestMove != default
&& !_absoluteSearchCancellationTokenSource.IsCancellationRequested)
{
Game.MakeMove(resultToReturn.BestMove);
Game.UpdateInitialPosition();
Expand Down Expand Up @@ -276,19 +296,38 @@ private async ValueTask<SearchResult> SearchBestMove(int maxDepth, int softLimit

public void Search(GoCommand goCommand)
{
if (IsSearching)
if (_isSearching)
{
_logger.Warn("Search already in progress");
}

_isPondering = goCommand.Ponder;
IsSearching = true;
_isSearching = true;

Thread.CurrentThread.Priority = ThreadPriority.Highest;

try
{
_isPondering = goCommand.Ponder;
var searchResult = BestMove(goCommand);

if (_isPondering)
{
// Using either field or local copy for the rest of the method, since goCommand.Ponder could change

// Avoiding the scenario where search finishes early (i.e. mate detected, max depth reached) and results comes
// before a potential ponderhit command
SpinWait.SpinUntil(() => _isPonderHit || _absoluteSearchCancellationTokenSource.IsCancellationRequested);

if (_isPonderHit)
{
_isPonderHit = false;
_isPondering = false;
goCommand.DisablePonder();

searchResult = BestMove(goCommand);
}
}

// We print best move even in case of go pondeer + stop, and IDEs are expected to ignore it
_moveToPonder = searchResult.Moves.Count >= 2 ? searchResult.Moves[1] : null;
_engineWriter.TryWrite(BestMoveCommand.BestMove(searchResult.BestMove, _moveToPonder));
}
Expand All @@ -298,15 +337,14 @@ public void Search(GoCommand goCommand)
}
finally
{
IsSearching = false;
_isSearching = false;
_isPondering = false;
}
// TODO ponder: if ponder, continue with PonderAction, which is searching indefinitely for a move
}

public void StopSearching()
{
_absoluteSearchCancellationTokenSource.Cancel();
IsSearching = false;
}

private void InitializeTT()
Expand Down
7 changes: 6 additions & 1 deletion src/Lynx/Search/IDDFS.cs
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,12 @@ private SearchResult GenerateFinalSearchResult(SearchResult? lastSearchResult,
SearchResult finalSearchResult;
if (lastSearchResult is null)
{
_logger.Warn("Search cancelled at depth 1, choosing first found legal move as best one");
// In the event of a quick ponderhit/stop while pondering because the opponent moved quickly, we don't want no warning triggered here
// when cancelling the pondering search
if (!_isPondering)
{
_logger.Warn("Search cancelled at depth 1, choosing first found legal move as best one");
}
finalSearchResult = new(firstLegalMove, 0, 0, [firstLegalMove], alpha, beta);
}
else
Expand Down
1 change: 1 addition & 0 deletions src/Lynx/UCI/Commands/Engine/OptionCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ public sealed class OptionCommand : EngineBaseCommand
$"option name Hash type spin default {Configuration.EngineSettings.TranspositionTableSize} min {Constants.AbsoluteMinTTSize} max {Constants.AbsoluteMaxTTSize}",
$"option name OnlineTablebaseInRootPositions type check default {Configuration.EngineSettings.UseOnlineTablebaseInRootPositions}",
"option name Threads type spin default 1 min 1 max 1",
$"option name Ponder type check default {Configuration.EngineSettings.IsPonder}",
.. Configuration.GeneralSettings.EnableTuning ? SPSAAttributeHelpers.GenerateOptionStrings() : []
];

Expand Down
9 changes: 7 additions & 2 deletions src/Lynx/UCI/Commands/GUI/GoCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public sealed partial class GoCommand : GUIBaseCommand
public int Depth { get; }
public int MoveTime { get; }
public bool Infinite { get; }
public bool Ponder { get; }
public bool Ponder { get; private set; }

public int Nodes => throw new NotImplementedException();
public int Mate => throw new NotImplementedException();
Expand Down Expand Up @@ -104,7 +104,10 @@ public GoCommand(string command)
Infinite = true;
break;
case "ponder":
Ponder = true;
if (Configuration.EngineSettings.IsPonder)
{
Ponder = true;
}
break;
case "depth":
Depth = int.Parse(group.Value);
Expand All @@ -121,4 +124,6 @@ public GoCommand(string command)
}

public static string Init() => Id;

public void DisablePonder() => Ponder = false;
}
13 changes: 8 additions & 5 deletions src/Lynx/UCIHandler.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
using Lynx.UCI.Commands.GUI;
using NLog;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.Intrinsics.X86;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Channels;

namespace Lynx;
Expand Down Expand Up @@ -161,10 +161,14 @@ private async Task HandleUCI(CancellationToken cancellationToken)

private void HandlePonderHit()
{
if (Configuration.IsPonder)
if (Configuration.EngineSettings.IsPonder)
{
_engine.PonderHit();
}
else
{
_logger.Warn("Unexpected 'ponderhit' command, given pondering is disabled. Ignoring it");
}
}

private void HandleSetOption(ReadOnlySpan<char> command)
Expand All @@ -186,9 +190,8 @@ private void HandleSetOption(ReadOnlySpan<char> command)
{
if (length > 4 && bool.TryParse(command[commandItems[4]], out var value))
{
Configuration.IsPonder = value;
Configuration.EngineSettings.IsPonder = value;
}
_logger.Warn("Ponder not supported yet");
break;
}
case "uci_analysemode":
Expand Down Expand Up @@ -608,7 +611,7 @@ private async Task HandleFEN(CancellationToken cancellationToken)

private async ValueTask HandleOpenBenchSPSA(CancellationToken cancellationToken)
{
foreach(var tunableValue in SPSAAttributeHelpers.GenerateOpenBenchStrings())
foreach (var tunableValue in SPSAAttributeHelpers.GenerateOpenBenchStrings())
{
await SendCommand(tunableValue, cancellationToken);
}
Expand Down

0 comments on commit 8b0cef2

Please sign in to comment.