Initial work on killer moves. Add soft tm (gains 41.8 +/- 22.0)

This commit is contained in:
Mattia Giambirtone 2024-05-05 16:07:37 +02:00
parent 651acd26ed
commit 245a5d75e8
3 changed files with 35 additions and 20 deletions

View File

@ -1,6 +1,6 @@
--cc:clang
-o:"bin/nimfish"
-d:danger
-d:debug
--passL:"-flto -lmimalloc"
--passC:"-flto -march=native -mtune=native"
-d:useMalloc

View File

@ -30,6 +30,7 @@ import threading/smartptrs
type
HistoryTable* = array[PieceColor.White..PieceColor.Black, array[Square(0)..Square(63), array[Square(0)..Square(63), Score]]]
KillersTable* = seq[array[2, Move]]
SearchManager* = ref object
## A simple state storage
## for our search
@ -46,15 +47,15 @@ type
searchMoves: seq[Move]
previousBestMove: Move
transpositionTable: SharedPtr[TTable]
# Storage for our history euristic
history: SharedPtr[HistoryTable]
killers: SharedPtr[KillersTable]
proc newSearchManager*(board: Chessboard, transpositions: SharedPtr[TTable], stopFlag, searchFlag: SharedPtr[Atomic[bool]],
history: SharedPtr[HistoryTable]): SearchManager =
history: SharedPtr[HistoryTable], killers: SharedPtr[KillersTable]): SearchManager =
new(result)
result = SearchManager(board: board, bestMoveRoot: nullMove(), transpositionTable: transpositions, stopFlag: stopFlag,
searchFlag: searchFlag, history: history)
searchFlag: searchFlag, history: history, killers: killers)
proc isSearching*(self: SearchManager): bool =
@ -71,19 +72,24 @@ proc stop*(self: SearchManager) =
proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
## Returns an estimated static score for the move
## Returns an estimated static score for the move used
## during move ordering
result = Score(0)
let
sideToMove = self.board.position.sideToMove
nonSideToMove = sideToMove.opposite()
if self.previousBestMove != nullMove() and move == self.previousBestMove:
return highestEval() + 1
let query = self.transpositionTable[].get(self.board.position.zobristKey)
if query.success and query.entry.bestMove != nullMove() and query.entry.bestMove == move:
return highestEval() + 1
if not move.isCapture():
# History euristic bonus
result += self.history[][sideToMove][move.startSquare][move.targetSquare]
if move.isCapture():
# Implementation of MVVLVA: Most Valuable Victim Least Valuable Attacker
# We prioritize moves that capture the most valuable pieces, and as a
@ -91,6 +97,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
# is why we multiply the score of the captured piece by 10, to give
# it priority)
result += 10 * self.board.position.getPieceScore(move.targetSquare) - self.board.position.getPieceScore(move.startSquare)
if move.isPromotion():
# Promotions are a good idea to search first
var piece: Piece
@ -106,6 +113,7 @@ proc getEstimatedMoveScore(self: SearchManager, move: Move): Score =
else:
discard # Unreachable
result += self.board.position.getPieceScore(piece, move.targetSquare)
if self.board.position.getPawnAttacks(move.targetSquare, nonSideToMove) != 0:
# Moving on a square attacked by an enemy pawn is _usually_ a very bad
# idea. Assume the piece is lost and give a malus based on the fact that
@ -342,8 +350,8 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept
# Apparently negative remaining time is a thing. Welp
let
maxSearchTime = max(1, (timeRemaining div 20) + (increment div 2))
softLimit = max(1, (timeRemaining div 30) + (increment div 2))
maxSearchTime = max(1, (timeRemaining div 10) + (increment div 2))
softLimit = max(1, maxSearchTime div 3)
self.bestMoveRoot = nullMove()
result = self.bestMoveRoot
self.maxNodes = maxNodes
@ -357,6 +365,8 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept
self.searchFlag[].store(true)
# Iterative deepening loop
for i in 1..maxDepth:
if self.killers[].len() < i:
self.killers[].add([nullMove(), nullMove()])
# Search the previous best move first
self.previousBestMove = self.bestMoveRoot
self.search(i, 0, lowestEval(), highestEval())
@ -369,17 +379,10 @@ proc findBestMove*(self: SearchManager, timeRemaining, increment: int64, maxDept
# Soft time management: don't start a new search iteration
# if the soft limit has expired, as it is unlikely to complete
# anyway
when defined(softTM):
if self.shouldStop() or getMonoTime() >= self.softLimit:
self.log(i - 1)
break
else:
self.log(i)
if self.shouldStop() or getMonoTime() >= self.softLimit:
self.log(i - 1)
break
else:
if self.shouldStop():
self.log(i - 1)
break
else:
self.log(i)
self.log(i)
self.searchFlag[].store(false)
self.stopFlag[].store(false)

View File

@ -31,13 +31,23 @@ type
UCISession = object
## A UCI session
debug: bool
# Previously reached positions
history: seq[Position]
# The current position
position: Position
# Atomic boolean flag to interrupt the search
stopFlag: SharedPtr[Atomic[bool]]
# Atomic search flag used to know whether a search
# is in progress
searchFlag: SharedPtr[Atomic[bool]]
# Size of the transposition table (in megabytes)
hashTableSize: uint64
# The transposition table
transpositionTable: SharedPtr[TTable]
# Storage for our history heuristic
historyTable: SharedPtr[HistoryTable]
# Storage for our killer move heuristic
killerMoves: SharedPtr[KillersTable]
UCICommandType = enum
## A UCI command type enumeration
@ -305,14 +315,15 @@ proc bestMove(args: tuple[session: UCISession, command: UCICommand]) {.thread.}
## Finds the best move in the current position
setControlCHook(proc () {.noconv.} = quit(0))
# Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread
{.cast(gcsafe).}:
# Yes yes nim sure this isn't gcsafe. Now stfu and spawn a thread
var session = args.session
var board = newChessboard()
board.position = session.position
board.positions = session.history
let command = args.command
var searcher = newSearchManager(board, session.transpositionTable, session.stopFlag, session.searchFlag, session.historyTable)
var searcher = newSearchManager(board, session.transpositionTable, session.stopFlag,
session.searchFlag, session.historyTable, session.killerMoves)
var
timeRemaining = (if session.position.sideToMove == White: command.wtime else: command.btime)
increment = (if session.position.sideToMove == White: command.winc else: command.binc)
@ -339,6 +350,7 @@ proc startUCISession* =
session.searchFlag = newSharedPtr(Atomic[bool])
session.transpositionTable[] = newTranspositionTable(session.hashTableSize * 1024 * 1024)
session.historyTable = newSharedPtr(HistoryTable)
session.killerMoves = newSharedPtr(KillersTable)
session.stopFlag[].store(false)
# Fun fact, nim doesn't collect the memory of thread vars. Another stupid fucking design pitfall
# of nim's AWESOME threading model. Someone is getting a pipebomb in their mailbox about this, mark