Correct the exploiter budget: it was a quarter of the target, not half

The exploiters ran 32.5M learner actions, not the 65.5M I recorded -- they train
one seat, so a 250x1024 run yields half of what the same shape gives the
both-seat self-play trainer. Against targets trained on 131M and 122.6M, that
makes the attacker roughly 4x underfunded.

Which means the absolute number does not support "ours is only 22.8%
exploitable". It supports exactly one claim: at a matched budget, league gives up
more. Whether the ordering survives a properly funded attacker is now the open
question, so the script takes --updates and --batch-games to run it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XBQKgvBbxbheiTF1AVy1Sh
This commit is contained in:
2026-07-15 05:50:58 +09:00
co-authored by Claude Opus 4.8
parent e51d14d0fb
commit 0c83824243
2 changed files with 17 additions and 7 deletions
+12 -5
View File
@@ -8,6 +8,7 @@ frozen policy could not defend.
from __future__ import annotations
import argparse
import json
from pathlib import Path
@@ -77,6 +78,12 @@ def _final_score(cfg, exploiter_params, frozen, matches: int) -> dict:
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("--updates", type=int, default=250, help="exploiter training updates")
parser.add_argument("--batch-games", type=int, default=1024)
parser.add_argument("--tag", default="", help="suffix for the run dir, to keep runs apart")
args = parser.parse_args()
match_cfg = load_config("configs/jax_ppo/match-selfplay.yaml")
old_cfg = load_config("configs/jax_ppo/balanced.yaml")
@@ -94,10 +101,10 @@ def main() -> None:
for name, frozen in targets.items():
print(f"\n===== training an exploiter against: {name} =====", flush=True)
cfg = load_config("configs/jax_ppo/match-selfplay.yaml")
cfg.ppo.batch_games = 1024
cfg.run.total_updates = 250
cfg.run.log_every = 25
slug = name.split()[0]
cfg.ppo.batch_games = args.batch_games
cfg.run.total_updates = args.updates
cfg.run.log_every = max(1, args.updates // 10)
slug = name.split()[0] + args.tag
state = train_exploiter(cfg, frozen, Path(f"runs/jax-ppo-match/exploit-{slug}"))
result = _final_score(cfg, state.params, frozen, MATCHES)
rows.append({"target": name, **result})
@@ -110,7 +117,7 @@ def main() -> None:
ci = f"[{row['wilson_low']:.3f}, {row['wilson_high']:.3f}]"
print(f"{row['target']:<28}{row['exploiter_win_rate']:>20.4f}{ci:>22}")
print("\nhigher = the frozen policy had more to farm. 0.5 = nothing found.")
Path("runs/jax-ppo-match/exploitability.json").write_text(json.dumps(rows, indent=2))
Path(f"runs/jax-ppo-match/exploitability{args.tag}.json").write_text(json.dumps(rows, indent=2))
if __name__ == "__main__":