feat(*): Add exported Linux binary training path for faster parallel instances

This commit is contained in:
Josh Creek
2026-07-28 20:56:14 +01:00
parent 4408ec3ecd
commit 01dbfc7ede
10 changed files with 179 additions and 11 deletions
+24 -8
View File
@@ -19,10 +19,27 @@ TRAINING_SCENE = "res://scenes/training.tscn"
class CosmicClashEnv(GodotEnv):
"""GodotEnv that launches `godot --path Game res://scenes/training.tscn`."""
"""GodotEnv that launches either the project from source or an exported binary.
# env_path is a Godot binary, not an exported game: skip the suffix and
# platform checks stock GodotEnv applies to exported executables.
Source mode (default): `godot --path Game res://scenes/training.tscn` — the
positional scene argument overrides the project's normal main scene.
Exported mode (`exported=True`): `env_path` is a pre-built game executable
(see training/export_linux.sh, "Linux Training" preset) — no `--path` and
no scene override needed or possible: official Godot export templates have
path/scene overrides compiled out (`--scene`/a positional scene argument
hard-aborts with "compiled without support for path overrides"), so the
binary instead boots straight into training.tscn on its own via
project.godot's `run/main_scene.training` feature-tag override, activated
by that preset's `custom_features="training"`.
"""
def __init__(self, *args, exported: bool = False, **kwargs):
self.exported = exported
super().__init__(*args, **kwargs)
# env_path is a Godot binary (or, in exported mode, a game executable we
# built ourselves), not a stock godot_rl exported-project path: skip the
# suffix and platform checks stock GodotEnv applies to those.
def _set_platform_suffix(self, env_path: str) -> str:
return env_path
@@ -32,11 +49,10 @@ class CosmicClashEnv(GodotEnv):
def _launch_env(self, env_path, port, show_window, framerate, seed, action_repeat, speedup, **kwargs):
# sync.gd reads --key=value pairs from the raw command line; they must
# NOT go after a `--` separator or OS.get_cmdline_args() drops them.
cmd = [
env_path,
"--path",
str(GAME_DIR),
TRAINING_SCENE,
cmd = [env_path]
if not self.exported:
cmd += ["--path", str(GAME_DIR), TRAINING_SCENE]
cmd += [
f"--port={port}",
f"--env_seed={seed}",
]
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Build the exported Linux training binary from the "Linux Training" preset in
# Game/export_presets.cfg. That preset's custom_features="training" activates
# project.godot's `run/main_scene.training` override, so the resulting binary
# boots straight into training.tscn on its own — official Godot export
# templates have path/scene overrides compiled out, so this can't be done at
# launch time via --scene. Re-run any time training-relevant scripts/scenes
# change — an exported binary is a snapshot, not a live view of the source.
# See TRAINING_LINUX.md.
set -euo pipefail
cd "$(dirname "$0")"
GODOT_VERSION="4.7.1"
GODOT_BIN="${GODOT_BIN:-$HOME/ai-training/godot/Godot_v${GODOT_VERSION}-stable_linux.x86_64}"
OUT="build/CosmicClash.x86_64"
mkdir -p build
"$GODOT_BIN" --headless --path ../Game --export-release "Linux Training" "$(pwd)/$OUT"
chmod +x "$OUT"
echo "Exported: training/$OUT"
+19 -1
View File
@@ -16,10 +16,28 @@ shift
# Train on the latest code and checkpoints from any machine
git pull --rebase
# Rebuild the exported training binary against the code we just pulled, so a
# --exported-binary run never trains on a stale snapshot. Only when the
# caller has already opted into the exported-binary flow (training/build/
# exists from a prior export) — a source-run caller pays no extra latency.
# Fails the whole run rather than silently falling through to train.py
# against a stale or partially-rebuilt binary (export_linux.sh sets -e, but
# that only exits *it*, not this script).
EXTRA_ARGS=()
if [ -d build ]; then
./export_linux.sh || { echo "export_linux.sh failed — aborting" >&2; exit 1; }
# Standing entry points (next_run.sh, curriculum.py) don't know about the
# exported binary and never pass --exported-binary themselves; default it
# here so opting in (by ever running export_linux.sh once) actually gets
# used, not just kept up to date. An explicit --exported-binary in "$@"
# still wins (argparse: later occurrence overrides).
EXTRA_ARGS=(--exported-binary build/CosmicClash.x86_64)
fi
# Let Ctrl-C stop train.py without killing this script, so the export and
# commit below still run
trap ':' INT
.venv/bin/python train.py --experiment "$EXP" "$@"
.venv/bin/python train.py --experiment "$EXP" "${EXTRA_ARGS[@]}" "$@"
trap - INT
if [ ! -f "checkpoints/$EXP/final.zip" ]; then
+18
View File
@@ -20,6 +20,24 @@ if [ ! -x "$GODOT_BIN" ]; then
fi
echo "Godot: $GODOT_BIN"
# Export templates — needed to build the exported training binary
# (export_linux.sh); the editor binary alone can't produce release exports.
TEMPLATES_DIR="$HOME/.local/share/godot/export_templates/${GODOT_VERSION}.stable"
if [ ! -d "$TEMPLATES_DIR" ]; then
echo "Downloading Godot $GODOT_VERSION export templates to $TEMPLATES_DIR"
TMP_DL="$(mktemp -d)"
wget -q "https://github.com/godotengine/godot/releases/download/${GODOT_VERSION}-stable/Godot_v${GODOT_VERSION}-stable_export_templates.tpz" \
-O "$TMP_DL/templates.tpz"
unzip -o -q "$TMP_DL/templates.tpz" -d "$TMP_DL"
mkdir -p "$(dirname "$TEMPLATES_DIR")"
# Single mv creates $TEMPLATES_DIR only on full success — if this script is
# interrupted any earlier, the guard above correctly sees "not installed"
# and retries, instead of finding a half-populated dir and skipping.
mv "$TMP_DL/templates" "$TEMPLATES_DIR"
rm -rf "$TMP_DL"
fi
echo "Export templates: $TEMPLATES_DIR"
# Python env
[ -d .venv ] || python3 -m venv .venv
.venv/bin/pip install -q -r requirements.txt
+10 -1
View File
@@ -32,6 +32,13 @@ def parse_args():
default=os.environ.get("GODOT_BIN", DEFAULT_GODOT_MACOS),
help="Path to the Godot binary (or set GODOT_BIN)",
)
parser.add_argument(
"--exported-binary",
default=None,
help="Path to a pre-built game executable (see export_linux.sh) instead of running the "
"project from source — skips per-instance script/resource import for faster parallel "
"startup. Overrides --godot_bin when set.",
)
parser.add_argument("--experiment", default="default", help="Run name for logs/checkpoints")
parser.add_argument("--timesteps", type=int, default=200_000)
parser.add_argument("--n-parallel", type=int, default=2, help="Parallel Godot instances (2 agents each)")
@@ -146,8 +153,10 @@ def main():
wandb.init(project="cosmic-clash-rl", name=args.experiment, sync_tensorboard=True)
exported_binary = args.exported_binary or None
env = CosmicClashVecEnv(
godot_bin=args.godot_bin,
godot_bin=exported_binary or args.godot_bin,
exported=exported_binary is not None,
n_parallel=args.n_parallel,
seed=args.seed,
port=args.port,