From 0233c8ddba42f29e16946284e68caa518177d195 Mon Sep 17 00:00:00 2001 From: Pim Bax Date: Thu, 7 Nov 2024 19:35:13 +0100 Subject: [PATCH] Implement parallelization for ParameterSearch Due to the way PS is implemented currently, it's not possible to parallelize the individual evaluations within each run, which would be far superior time improvement compared to just running all runs in parallel. This is because the individual evaluations are all overseen by the NTBEA library, which controls the loop, and has no parallelized `fitness` loop function, nor can it be @Overwritten (since it's package-private). However, the individual runs can be made parallel: - NTBEA objects have a copy function, to ensure they do not interfere with eachother; NTBEA runs are executed on a copy of the main NTBEA object. - Non-multithreaded runs work the same, but instead just pass on `this`. - After parallel runs are completed, some final tallying of the scores is done in order to get all data in the right place Still a big TODO: Round Robin tournaments should have the exhaustive self play converted to an iterative version, instead of a recursive one. Without iterative version, it is significantly harder to parallelize (or perhaps impossible; I don't want to know). After that is done, I think the most important parts of the software has been parallelized. --- json/players/gameSpecific/TicTacToe.json | 4 +- src/main/java/core/Game.java | 1 - src/main/java/evaluation/RunArg.java | 2 +- .../evaluation/optimisation/MultiNTBEA.java | 4 + .../java/evaluation/optimisation/NTBEA.java | 77 ++++++++++++++++--- .../optimisation/NTBEAParameters.java | 2 + .../tournaments/RoundRobinTournament.java | 2 +- 7 files changed, 77 insertions(+), 15 deletions(-) diff --git a/json/players/gameSpecific/TicTacToe.json b/json/players/gameSpecific/TicTacToe.json index a62ed7813..f534f498e 100644 --- a/json/players/gameSpecific/TicTacToe.json +++ b/json/players/gameSpecific/TicTacToe.json @@ -2,11 +2,11 @@ "budgetType": "BUDGET_TIME", "rolloutLength": 30, "opponentTreePolicy": "OneTree", - "MASTGamma": 0, + "MASTGamma": 0.0, "heuristic": { "class": "players.heuristics.WinOnlyHeuristic" }, - "K": 1, + "K": 1.0, "exploreEpsilon": 0.1, "treePolicy": "UCB", "MAST": "Both", diff --git a/src/main/java/core/Game.java b/src/main/java/core/Game.java index 9a73226fe..cff66ba8a 100644 --- a/src/main/java/core/Game.java +++ b/src/main/java/core/Game.java @@ -571,7 +571,6 @@ public AbstractGameState runInstance(LinkedList players, int see } if (debug) System.out.println("Exiting synchronized block in Game"); } - System.out.println("Done playing: " + players); if (firstEnd) { if (gameState.coreGameParameters.verbose) { System.out.println("Ended"); diff --git a/src/main/java/evaluation/RunArg.java b/src/main/java/evaluation/RunArg.java index 63fbc1648..8461566aa 100644 --- a/src/main/java/evaluation/RunArg.java +++ b/src/main/java/evaluation/RunArg.java @@ -123,7 +123,7 @@ public enum RunArg { new Usage[]{Usage.ParameterSearch, Usage.RunGames}), nThreads("The number of threads that can be spawned in order to evaluate games.", 1, - new Usage[]{Usage.RunGames}), + new Usage[]{Usage.ParameterSearch, Usage.RunGames}), neighbourhood("The size of neighbourhood to look at in NTBEA. Default is min(50, |searchSpace|/100) ", 50, new Usage[]{Usage.ParameterSearch}), diff --git a/src/main/java/evaluation/optimisation/MultiNTBEA.java b/src/main/java/evaluation/optimisation/MultiNTBEA.java index d15a8d9ee..8620d2c33 100644 --- a/src/main/java/evaluation/optimisation/MultiNTBEA.java +++ b/src/main/java/evaluation/optimisation/MultiNTBEA.java @@ -132,4 +132,8 @@ private static int manhattan(int[] x, int[] y) { return retValue; } + @Override + public NTBEA copy() { + return new MultiNTBEA(params, game, nPlayers); + } } diff --git a/src/main/java/evaluation/optimisation/NTBEA.java b/src/main/java/evaluation/optimisation/NTBEA.java index 951c29c11..56e386d0c 100644 --- a/src/main/java/evaluation/optimisation/NTBEA.java +++ b/src/main/java/evaluation/optimisation/NTBEA.java @@ -1,20 +1,16 @@ package evaluation.optimisation; import core.AbstractGameState; -import core.AbstractParameters; import core.AbstractPlayer; import core.interfaces.IGameHeuristic; import core.interfaces.IStateHeuristic; import evaluation.RunArg; import evaluation.listeners.IGameListener; -import evaluation.tournaments.AbstractTournament; import evaluation.tournaments.RoundRobinTournament; -import org.apache.commons.math3.util.CombinatoricsUtils; import games.GameType; import ntbea.NTupleBanditEA; import ntbea.NTupleSystem; import org.json.simple.JSONObject; -import players.IAnyTimePlayer; import players.PlayerFactory; import players.heuristics.OrdinalPosition; import players.heuristics.PureScoreHeuristic; @@ -28,6 +24,9 @@ import java.io.FileWriter; import java.io.IOException; import java.util.*; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; import java.util.function.IntToDoubleFunction; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -136,15 +135,53 @@ public void writeAgentJSON(int[] settings, String fileName) { * @return */ public Pair run() { + ExecutorService executor = params.nThreads > 1 ? Executors.newFixedThreadPool(params.nThreads) : null; + // if we're multithreading, we don't want to have different threads interfering with eachother + List clones = new ArrayList<>(); + if (executor == null) { + // no multithreading, so list of clones consists only of the current object + clones.add(this); + } + // Only this loop is parallelized, since the rest is just analysis, or initializing a tournament (which is + // on its own already parallelized) for (currentIteration = 0; currentIteration < params.repeats; currentIteration++) { - runIteration(); - writeAgentJSON(winnerSettings.get(winnerSettings.size() - 1), - params.destDir + File.separator + "Recommended_" + currentIteration + ".json"); + NTBEA clone = executor == null ? this : this.copy(); + if (executor != null) { + clone.currentIteration = currentIteration; // for correct reporting + // run in parallel if allowed + executor.submit(clone::runIteration); + clones.add(clone); + } else { + clone.runIteration(); + } + } + + if (executor != null) { + executor.shutdown(); + try { + // Wait for all tasks to complete; no timeout (infty hours) because this normally also has no timeout + if (!executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS)) { + executor.shutdownNow(); // Force shutdown if tasks are hanging + } + } catch (InterruptedException e) { + executor.shutdownNow(); // Restore interrupted status and shutdown + Thread.currentThread().interrupt(); + } + } + + // After all runs are complete, do some cleaning up and logging + for (NTBEA clone : clones) { + writeAgentJSON(clone.winnerSettings.get(clone.winnerSettings.size() - 1), + params.destDir + File.separator + "Recommended_" + clone.currentIteration + ".json"); + } + if (clones.size() > 1 || clones.get(0) != this) { + // aggregate all data from all clones + collectCloneResults(clones); } - // After all runs are complete, if tournamentGames are specified, then we allow all the - // winners from each iteration to play in a tournament and pick the winner of this tournament + // If tournamentGames are specified, then we allow all the winners from each iteration + // to play in a tournament and pick the winner of this tournament if (params.tournamentGames > 0 && winnersPerRun.get(0) instanceof AbstractPlayer) { activateTournament(); } @@ -158,6 +195,22 @@ public Pair run() { return new Pair<>(params.searchSpace.getAgent(bestResult.b), bestResult.b); } + /** + * Gathers all data from all parallel threads, which have their data stored in a clone of this object + * This ensures all metrics from these clones are incorporated into this object's data + * @param clones the list of clones that have run in separate threads + */ + protected void collectCloneResults(List clones) { + for (NTBEA clone : clones) { + this.elites.addAll(clone.elites); + this.winnersPerRun.addAll(clone.winnersPerRun); + this.winnerSettings.addAll(clone.winnerSettings); + if (clone.bestResult.a.a > this.bestResult.a.a) { + bestResult = clone.bestResult; + } + } + } + protected void activateTournament() { if (!elites.isEmpty()) { // first of all we add the elites into winnerSettings, and winnersPerRun @@ -195,6 +248,7 @@ protected void activateTournament() { config.put(RunArg.budget, params.budget); config.put(RunArg.verbose, false); config.put(RunArg.destDir, params.destDir); + config.put(RunArg.nThreads, params.nThreads); RoundRobinTournament tournament = new RoundRobinTournament(players, game, nPlayers, params.gameParams, config); createListeners().forEach(tournament::addListener); tournament.run(); @@ -384,6 +438,10 @@ private static void logSummary(Pair, int[]> data, NTBEAPara } } + public NTBEA copy() { + return new NTBEA(params, game, nPlayers); + } + private static String valueToString(int paramIndex, int valueIndex, ITPSearchSpace ss) { Object value = ss.value(paramIndex, valueIndex); String valueString = value.toString(); @@ -394,5 +452,4 @@ private static String valueToString(int paramIndex, int valueIndex, ITPSearchSpa } return valueString; } - } diff --git a/src/main/java/evaluation/optimisation/NTBEAParameters.java b/src/main/java/evaluation/optimisation/NTBEAParameters.java index 5bfec6c3d..45d219d90 100644 --- a/src/main/java/evaluation/optimisation/NTBEAParameters.java +++ b/src/main/java/evaluation/optimisation/NTBEAParameters.java @@ -43,6 +43,7 @@ public enum Mode { public ITPSearchSpace searchSpace; public AbstractParameters gameParams; public boolean byTeam; + public int nThreads; public NTBEAParameters(Map args) { this(args, Function.identity()); @@ -67,6 +68,7 @@ public NTBEAParameters(Map args, Function prepro GameType game = GameType.valueOf(args.get(RunArg.game).toString()); gameParams = args.get(RunArg.gameParams).equals("") ? null : AbstractParameters.createFromFile(game, (String) args.get(RunArg.gameParams)); + nThreads = (int) args.get(RunArg.nThreads); mode = Mode.valueOf((String) args.get(RunArg.NTBEAMode)); logFile = "NTBEA.log"; diff --git a/src/main/java/evaluation/tournaments/RoundRobinTournament.java b/src/main/java/evaluation/tournaments/RoundRobinTournament.java index e0a98466f..1d86889a9 100644 --- a/src/main/java/evaluation/tournaments/RoundRobinTournament.java +++ b/src/main/java/evaluation/tournaments/RoundRobinTournament.java @@ -232,7 +232,6 @@ public AbstractPlayer getWinner() { */ public void createAndRunMatchUp(List matchUp) { ExecutorService executor = nThreads > 1 ? Executors.newFixedThreadPool(nThreads) : null; - int nTeams = byTeam ? game.getGameState().getNTeams() : nPlayers; switch (tournamentMode) { case RANDOM: @@ -304,6 +303,7 @@ public void createAndRunMatchUp(List matchUp) { break; case EXHAUSTIVE: case EXHAUSTIVE_SELF_PLAY: + // TODO: Make iterative instead of recursive, to parallelize // in this case we are in exhaustive mode, so we recursively construct all possible combinations of players if (matchUp.size() == nTeams) { evaluateMatchUp(matchUp, gamesPerMatchup, gameSeeds);