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 --cc:clang
-o:"bin/nimfish" -o:"bin/nimfish"
-d:danger -d:debug
--passL:"-flto -lmimalloc" --passL:"-flto -lmimalloc"
--passC:"-flto -march=native -mtune=native" --passC:"-flto -march=native -mtune=native"
-d:useMalloc -d:useMalloc

View File

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

View File

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