Speed up thread pool startup (bench 3475935)

This commit is contained in:
2025-04-02 11:26:42 +02:00
parent 5b1b797673
commit 8b6f310f54
4 changed files with 96 additions and 62 deletions

View File

@@ -1,20 +1,35 @@
FROM nimlang/nim:2.2.0-ubuntu-regular
# By @agethereal. Thanks Andy!
FROM ubuntu:24.04
RUN apt update && apt-get -y install git clang llvm lld git-lfs
# Set non-interactive mode to avoid prompts
ENV DEBIAN_FRONTEND=noninteractive
# Install dependencies
RUN apt-get update && \
apt-get install -y curl git clang llvm lld build-essential libssl-dev wget && \
rm -rf /var/lib/apt/lists/*
WORKDIR /app
# Install Nim using the official installer
RUN curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y && \
ln -s ~/.nimble/bin/nim /usr/local/bin/nim && \
ln -s ~/.nimble/bin/nimble /usr/local/bin/nimble
COPY . /app/
# Install Git-LFS
RUN wget https://github.com/git-lfs/git-lfs/releases/download/v3.4.0/git-lfs-linux-amd64-v3.4.0.tar.gz && \
tar -xvf git-lfs-linux-amd64-v3.4.0.tar.gz && \
cd git-lfs-3.4.0 && \
./install.sh && \
git lfs install
# Force cache to be thrown away when new commits are pushed
# ------------------------------------------------------------------------------
# Force the cache to break if there have been new commits
ADD https://api.github.com/repos/nocturn9x/heimdall/git/refs/heads/master /.git-hashref
# ------------------------------------------------------------------------------
# The LFS repo is only on my personal gitea. The above cache thing still works because
# GitHub is used as a mirror for the main repo
RUN git clone https://git.nocturn9x.space/heimdall-engine/heimdall --depth 1 && \
cd heimdall && make native
cd heimdall && \
make native
CMD ["heimdall/bin/heimdall"]

View File

@@ -617,6 +617,16 @@ proc isCheckmate*(self: Chessboard): bool {.inline.} =
return moves.len() == 0
proc isCheckmate*(self: var Position): bool {.inline.} =
## Returns whether the game ended with a
## checkmate
if not self.inCheck():
return false
var moves {.noinit.} = newMoveList()
self.generateMoves(moves)
return moves.len() == 0
proc isStalemate*(self: Chessboard): bool {.inline.} =
## Returns whether the game ended with a
## stalemate

View File

@@ -179,6 +179,7 @@ type
# All the heuristic tables and other state
# to be passed to the search manager created
# at worker setup
evalState: EvalState # Creating this from scratch every time is VERY slow
positions: seq[Position]
transpositionTable: ptr TTable
quietHistory: ptr ThreatHistoryTable
@@ -237,68 +238,69 @@ proc newSearchManager*(positions: seq[Position], transpositions: ptr TTable,
result = SearchManager(transpositionTable: transpositions, quietHistory: quietHistory,
captureHistory: captureHistory, killers: killers, counters: counters,
continuationHistory: continuationHistory, parameters: parameters,
state: state, statistics: statistics, workerPool: createWorkerPool())
state: state, statistics: statistics, evalState: evalState)
new(result.board)
if mainWorker:
result.workerPool = createWorkerPool()
result.limiter = newSearchLimiter(result.state, result.statistics)
else:
result.limiter = newDummyLimiter()
result.evalState = evalState
result.state.normalizeScore.store(normalizeScore)
result.state.chess960.store(chess960)
result.state.isMainThread.store(mainWorker)
result.setBoardState(positions)
if mainWorker:
result.setBoardState(positions)
proc workerLoop(self: SearchWorker) {.thread.} =
# Nim gets very mad indeed if we don't do this
while true:
let msg = self.channels.command.recv()
case msg:
of Ping:
self.channels.response.send(Pong)
of Shutdown:
if self.isSetUp.load():
self.isSetUp.store(false)
freeHeapAligned(self.killers)
freeHeapAligned(self.quietHistory)
freeHeapAligned(self.captureHistory)
freeHeapAligned(self.continuationHistory)
freeHeapAligned(self.counters)
self.channels.response.send(Ok)
break
of Reset:
if not self.isSetUp.load():
self.channels.response.send(NotSetUp)
continue
while true:
let msg = self.channels.command.recv()
case msg:
of Ping:
self.channels.response.send(Pong)
of Shutdown:
if self.isSetUp.load():
self.isSetUp.store(false)
freeHeapAligned(self.killers)
freeHeapAligned(self.quietHistory)
freeHeapAligned(self.captureHistory)
freeHeapAligned(self.continuationHistory)
freeHeapAligned(self.counters)
self.channels.response.send(Ok)
break
of Reset:
if not self.isSetUp.load():
self.channels.response.send(NotSetUp)
continue
resetHeuristicTables(self.quietHistory, self.captureHistory, self.killers, self.counters, self.continuationHistory)
self.channels.response.send(Ok)
of Go:
# Start a search
if not self.isSetUp.load():
self.channels.response.send(SetupMissing)
continue
self.channels.response.send(Ok)
discard self.manager.findBestLine(@[], true, false, false, 1)
of Setup:
if self.isSetUp.load():
self.channels.response.send(SetupAlready)
continue
# Allocate on 64-byte boundaries to ensure threads won't have
# overlapping stuff in their cache lines
self.quietHistory = allocHeapAligned(ThreatHistoryTable, 64)
self.continuationHistory = allocHeapAligned(ContinuationHistory, 64)
self.captureHistory = allocHeapAligned(CaptHistTable, 64)
self.killers = allocHeapAligned(KillersTable, 64)
self.counters = allocHeapAligned(CountersTable, 64)
self.isSetUp.store(true)
self.manager = newSearchManager(self.positions, self.transpositionTable,
self.quietHistory, self.captureHistory,
self.killers, self.counters, self.continuationHistory,
self.parameters, false, false)
self.channels.response.send(Ok)
resetHeuristicTables(self.quietHistory, self.captureHistory, self.killers, self.counters, self.continuationHistory)
self.channels.response.send(Ok)
of Go:
# Start a search
if not self.isSetUp.load():
self.channels.response.send(SetupMissing)
continue
self.channels.response.send(Ok)
discard self.manager.findBestLine(@[], true, false, false, 1)
of Setup:
if self.isSetUp.load():
self.channels.response.send(SetupAlready)
continue
# Allocate on 64-byte boundaries to ensure threads won't have
# overlapping stuff in their cache lines
self.quietHistory = allocHeapAligned(ThreatHistoryTable, 64)
self.continuationHistory = allocHeapAligned(ContinuationHistory, 64)
self.captureHistory = allocHeapAligned(CaptHistTable, 64)
self.killers = allocHeapAligned(KillersTable, 64)
self.counters = allocHeapAligned(CountersTable, 64)
self.isSetUp.store(true)
self.manager = newSearchManager(self.positions, self.transpositionTable,
self.quietHistory, self.captureHistory,
self.killers, self.counters, self.continuationHistory,
self.parameters, false, false, self.evalState)
self.channels.response.send(Ok)
proc cmd(self: SearchWorker, cmd: WorkerCommand, expected: WorkerResponse = Ok) {.inline.} =
@@ -364,6 +366,7 @@ proc setupWorkers(self: var SearchManager) {.inline.} =
var worker = self.workerPool.workers[i]
# This is the only stuff that we pass from the outside
worker.positions = self.board.positions.deepCopy()
worker.evalState = self.evalState.deepCopy()
worker.parameters = self.parameters
worker.transpositionTable = self.transpositionTable
# Keep track of worker statistics
@@ -422,8 +425,9 @@ proc setBoardState*(self: SearchManager, state: seq[Position]) {.gcsafe.} =
for position in state:
self.board.positions.add(position.clone())
self.evalState.init(self.board)
for worker in self.workerPool.workers:
worker.manager.setBoardState(state)
if self.state.isMainThread.load():
for worker in self.workerPool.workers:
worker.manager.setBoardState(state)
func getCurrentPosition*(self: SearchManager): lent Position {.inline.} =
@@ -455,9 +459,10 @@ func stop*(self: SearchManager) {.inline.} =
if not self.isSearching():
return
self.state.stop.store(true)
# Stop all worker threads
for child in self.workerPool.workers:
child.manager.stop()
if self.state.isMainThread.load():
# Stop all worker threads
for child in self.workerPool.workers:
child.manager.stop()
func isKillerMove(self: SearchManager, move: Move, ply: int): bool {.inline.} =

View File

@@ -633,6 +633,10 @@ proc startUCISession* =
doAssert searchWorker.channels.send.recv() == SearchComplete
echo "info string premium membership is required to send go during search. Please check out https://n9x.co/heimdall-premium for details"
continue
if session.history[^1].isCheckmate():
echo "info string position is mated"
echo "bestmove 0000"
continue
# Start the clock as soon as possible to account
# for startup delays in our time management
session.searcher.startClock()