Compare commits

...
+75 -4
View File
@@ -25,6 +25,7 @@ import json
import logging import logging
import logging.handlers import logging.handlers
import os import os
import subprocess
import sys import sys
import time import time
import urllib.request import urllib.request
@@ -42,6 +43,9 @@ DEFAULT_CONFIG = {
"time_limit_hours": 11.5, # stop after this many hours (leave buffer for training) "time_limit_hours": 11.5, # stop after this many hours (leave buffer for training)
"pause_between_batches": 10, # seconds to pause between batches "pause_between_batches": 10, # seconds to pause between batches
"auto_train": True, # trigger ML training after all batches "auto_train": True, # trigger ML training after all batches
"auto_update_weights": True, # run update_weights.py after sims
"weights_min_games": 10, # minimum games required for weight update
"weights_lr": 0.05, # learning rate for weight update
"train_epochs": 100, # training epochs "train_epochs": 100, # training epochs
"train_lr": 0.001, # learning rate "train_lr": 0.001, # learning rate
"train_batch_size": 256, # training batch size "train_batch_size": 256, # training batch size
@@ -229,6 +233,52 @@ def wait_for_batch(base_url, batch_id, engine="deepseek", deadline=None):
time.sleep(poll_interval) time.sleep(poll_interval)
def run_update_weights(cfg):
"""
Run ml.scripts.update_weights to nudge learned_weights.json from sim
decision logs. This feeds the deck builder's sim_insights injection.
Returns True on success, False on failure (non-fatal — overnight run
continues regardless).
"""
log.info("")
log.info("── Auto-update sim weights ─────────────────────────────────")
log.info("Running update_weights.py to refresh learned_weights.json...")
cmd = [
sys.executable, "-m", "ml.scripts.update_weights",
"--min-games", str(cfg["weights_min_games"]),
"--lr", str(cfg["weights_lr"]),
]
try:
result = subprocess.run(
cmd,
capture_output=True,
text=True,
timeout=120, # 2 min ceiling — this should complete in seconds
)
if result.returncode == 0:
# Print the last ~800 chars of the weight report to the log
output = (result.stdout or "").strip()
if output:
for line in output.splitlines()[-20:]:
log.info(f" {line}")
log.info("Sim weights updated — deck builder will use new insights on next build.")
return True
else:
log.warning(f"update_weights.py exited with code {result.returncode}")
if result.stderr:
log.warning(result.stderr.strip()[-300:])
return False
except subprocess.TimeoutExpired:
log.warning("update_weights.py timed out after 120s — skipping")
return False
except Exception as e:
log.warning(f"update_weights.py failed: {e}")
return False
def start_training(base_url, epochs, lr, batch_size, patience): def start_training(base_url, epochs, lr, batch_size, patience):
"""Trigger ML training and return immediately.""" """Trigger ML training and return immediately."""
resp = api_post(base_url, "/api/ml/train", { resp = api_post(base_url, "/api/ml/train", {
@@ -364,6 +414,7 @@ def run_overnight(cfg):
else: else:
log.info(f" Mode: Run for {cfg['time_limit_hours']}h, {games_per_batch} games/batch") log.info(f" Mode: Run for {cfg['time_limit_hours']}h, {games_per_batch} games/batch")
log.info(f" Engine: {engine}") log.info(f" Engine: {engine}")
log.info(f" Auto-update weights: {'Yes' if cfg['auto_update_weights'] else 'No'}")
log.info(f" Auto-train after: {'Yes' if cfg['auto_train'] else 'No'}") log.info(f" Auto-train after: {'Yes' if cfg['auto_train'] else 'No'}")
log.info("") log.info("")
@@ -440,6 +491,17 @@ def run_overnight(cfg):
log.info(f" Elapsed time: {str(timedelta(seconds=int(total_elapsed)))}") log.info(f" Elapsed time: {str(timedelta(seconds=int(total_elapsed)))}")
log.info("") log.info("")
# ── Auto-update sim weights ───────────────────────────────
# Runs update_weights.py to nudge learned_weights.json from the
# decision logs produced during this run. This feeds sim_insights
# into the deck builder's Ollama prompts on the next deck build.
# Runs before neural net training so both pipelines benefit from
# the freshest possible data.
if cfg["auto_update_weights"]:
run_update_weights(cfg)
else:
log.info("Skipping sim weight update (--no-update-weights flag set)")
# ── Auto-train ──────────────────────────────────────────── # ── Auto-train ────────────────────────────────────────────
if cfg["auto_train"]: if cfg["auto_train"]:
if decisions_after < 100: if decisions_after < 100:
@@ -482,17 +544,17 @@ def main():
formatter_class=argparse.RawDescriptionHelpFormatter, formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=""" epilog="""
Examples: Examples:
# Run DeepSeek sims for 11.5 hours, then train: # Run DeepSeek sims for 11.5 hours, then update weights + train:
python overnight-run.py python overnight-run.py
# Run 20 batches of 50 games each, then train: # Run 20 batches of 50 games each, then update weights + train:
python overnight-run.py --batches 20 --games 50 python overnight-run.py --batches 20 --games 50
# Run for 6 hours with Java engine: # Run for 6 hours with Java engine:
python overnight-run.py --engine java --hours 6 --decks "Deck A" "Deck B" "Deck C" python overnight-run.py --engine java --hours 6 --decks "Deck A" "Deck B" "Deck C"
# Just run sims, no training: # Just run sims, skip both weight update and training:
python overnight-run.py --no-train python overnight-run.py --no-train --no-update-weights
# Custom training settings: # Custom training settings:
python overnight-run.py --epochs 200 --lr 0.0005 --patience 20 python overnight-run.py --epochs 200 --lr 0.0005 --patience 20
@@ -514,6 +576,12 @@ Examples:
help="Deck names (auto-detected if not specified)") help="Deck names (auto-detected if not specified)")
parser.add_argument("--no-train", action="store_true", parser.add_argument("--no-train", action="store_true",
help="Skip auto-training after sims") help="Skip auto-training after sims")
parser.add_argument("--no-update-weights", action="store_true",
help="Skip updating learned_weights.json after sims")
parser.add_argument("--weights-min-games", type=int, default=10,
help="Minimum games required for weight update (default: 10)")
parser.add_argument("--weights-lr", type=float, default=0.05,
help="Learning rate for sim weight update (default: 0.05)")
parser.add_argument("--epochs", type=int, default=100, parser.add_argument("--epochs", type=int, default=100,
help="Training epochs (default: 100)") help="Training epochs (default: 100)")
parser.add_argument("--lr", type=float, default=0.001, parser.add_argument("--lr", type=float, default=0.001,
@@ -536,6 +604,9 @@ Examples:
cfg["time_limit_hours"] = args.hours cfg["time_limit_hours"] = args.hours
cfg["pause_between_batches"] = args.pause cfg["pause_between_batches"] = args.pause
cfg["auto_train"] = not args.no_train cfg["auto_train"] = not args.no_train
cfg["auto_update_weights"] = not args.no_update_weights
cfg["weights_min_games"] = args.weights_min_games
cfg["weights_lr"] = args.weights_lr
cfg["train_epochs"] = args.epochs cfg["train_epochs"] = args.epochs
cfg["train_lr"] = args.lr cfg["train_lr"] = args.lr
cfg["train_batch_size"] = args.batch_size cfg["train_batch_size"] = args.batch_size