Skip to content

Commit

Permalink
Implement parallelization for ParameterSearch
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Pim Bax committed Nov 7, 2024
1 parent 2909cc1 commit 0233c8d
Show file tree
Hide file tree
Showing 7 changed files with 77 additions and 15 deletions.
4 changes: 2 additions & 2 deletions json/players/gameSpecific/TicTacToe.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@
"budgetType": "BUDGET_TIME",
"rolloutLength": 30,
"opponentTreePolicy": "OneTree",
"MASTGamma": 0,
"MASTGamma": 0.0,

This comment has been minimized.

Copy link
@Joeytje50

Joeytje50 Nov 7, 2024

This parameter gave errors due to being an int; it needs to be (convertible) to a double. Same goes for K. Fixed it here to be able to use it as a test case.

"heuristic": {
"class": "players.heuristics.WinOnlyHeuristic"
},
"K": 1,
"K": 1.0,
"exploreEpsilon": 0.1,
"treePolicy": "UCB",
"MAST": "Both",
Expand Down
1 change: 0 additions & 1 deletion src/main/java/core/Game.java
Original file line number Diff line number Diff line change
Expand Up @@ -571,7 +571,6 @@ public AbstractGameState runInstance(LinkedList<AbstractPlayer> 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");
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/evaluation/RunArg.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand Down
4 changes: 4 additions & 0 deletions src/main/java/evaluation/optimisation/MultiNTBEA.java
Original file line number Diff line number Diff line change
Expand Up @@ -132,4 +132,8 @@ private static int manhattan(int[] x, int[] y) {
return retValue;
}

@Override
public NTBEA copy() {
return new MultiNTBEA(params, game, nPlayers);
}
}
77 changes: 67 additions & 10 deletions src/main/java/evaluation/optimisation/NTBEA.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -136,15 +135,53 @@ public void writeAgentJSON(int[] settings, String fileName) {
* @return
*/
public Pair<Object, int[]> 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<NTBEA> 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();
}
Expand All @@ -158,6 +195,22 @@ public Pair<Object, int[]> 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<NTBEA> 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
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -384,6 +438,10 @@ private static void logSummary(Pair<Pair<Double, Double>, 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();
Expand All @@ -394,5 +452,4 @@ private static String valueToString(int paramIndex, int valueIndex, ITPSearchSpa
}
return valueString;
}

}
2 changes: 2 additions & 0 deletions src/main/java/evaluation/optimisation/NTBEAParameters.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ public enum Mode {
public ITPSearchSpace searchSpace;
public AbstractParameters gameParams;
public boolean byTeam;
public int nThreads;

public NTBEAParameters(Map<RunArg, Object> args) {
this(args, Function.identity());
Expand All @@ -67,6 +68,7 @@ public NTBEAParameters(Map<RunArg, Object> args, Function<String, String> 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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,6 @@ public AbstractPlayer getWinner() {
*/
public void createAndRunMatchUp(List<Integer> matchUp) {
ExecutorService executor = nThreads > 1 ? Executors.newFixedThreadPool(nThreads) : null;

int nTeams = byTeam ? game.getGameState().getNTeams() : nPlayers;
switch (tournamentMode) {
case RANDOM:
Expand Down Expand Up @@ -304,6 +303,7 @@ public void createAndRunMatchUp(List<Integer> 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);
Expand Down

0 comments on commit 0233c8d

Please sign in to comment.