diff options
| author | Scott Gasch <[email protected]> | 2026-10-13 09:07:23 -0700 |
|---|---|---|
| committer | Scott Gasch <[email protected]> | 2026-10-13 09:07:30 -0700 |
| commit | 78881b8e6422b7980e5f71780ae4bed862b93e72 (patch) | |
| tree | a433fdce69fd27359679ad91e0ebedd52e2506cd /src/eval_tune | |
| parent | f03bcac9ce3c6542310010bfbef2b126bb6ae861 (diff) | |
Revert "Minor changes; eval code was play tested as ~even."
This reverts commit f03bcac9ce3c6542310010bfbef2b126bb6ae861.
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Diffstat (limited to 'src/eval_tune')
| -rwxr-xr-x | src/eval_tune/match_play.py | 177 | ||||
| -rwxr-xr-x | src/eval_tune/test_vs_head.sh | 3 |
2 files changed, 52 insertions, 128 deletions
diff --git a/src/eval_tune/match_play.py b/src/eval_tune/match_play.py index af7f011..4eecfd6 100755 --- a/src/eval_tune/match_play.py +++ b/src/eval_tune/match_play.py @@ -64,19 +64,12 @@ _MOVE_RE = re.compile(r"^move (\S+)") # and against real engine output. A per-move stats block trails every # move, not just script-run suite summaries -- see EBF/first-move-beta # discussion in session history. -# Matches both the old report format ("Searched for 0.3 seconds, saw 123 -# nodes (45 qnodes) ( 969 nps).") and the new, more compact one ("Searched -# for 0.3s, saw 123 nodes (45 qnodes) (969knps)."): "seconds"/"s", and an -# optional "k" multiplier before "nps" (new format reports already-scaled -# thousands-of-nodes-per-second). _SEARCHED_RE = re.compile( - r"^Searched for\s+([\d.]+)\s*(?:seconds|s), saw (\d+) nodes " - r"\((\d+) qnodes\) \(\s*([\d.]+)\s*(k?)nps\)\.") + r"^Searched for\s+([\d.]+) seconds, saw (\d+) nodes " + r"\((\d+) qnodes\) \(\s*([\d.]+) nps\)\.") _DEPTH_RE = re.compile(r"^tellothers d(\d+),") -# Old: "First move beta cutoff rate was 93.681 percent." -# New: "First move beta cutoff rate was 93.681%." _FIRST_MOVE_BETA_RE = re.compile( - r"^First move beta cutoff rate was\s+([\d.]+)\s*(?:percent|%)\.") + r"^First move beta cutoff rate was\s+([\d.]+) percent\.") # 2026-09-18: per-iteration line UtilPrintPV (util.c) prints once per # completed depth during a move's search (both under-xboard "%2u %6d # %5u %-12llu %s" and human "%2u[+-]? %5s %-9s %-11llu %s" formats -- @@ -157,77 +150,23 @@ def _dbg(tag, msg): print(f"[{tag}] {msg}", file=sys.stderr, flush=True) -def _sample_one_opening(fb, file_size, rng, min_ply, max_ply): - """One attempt at pulling a random opening position out of an - already-open pgn pool file: seek to a random byte offset, find the - next game's header block, parse just that one game, and walk a - random number of its plies. Returns a FEN, or None if this attempt - didn't land on a usable game (truncated tail, too-short game, etc.) - -- callers retry elsewhere on None rather than treating it as fatal.""" - offset = rng.randint(0, max(file_size - 1, 0)) - fb.seek(offset) - fb.readline() # discard partial line at the seek point - # scan forward to the start of the next game's header block - start = fb.tell() - line = fb.readline() - while line and not line.startswith(b"[Event "): - start = fb.tell() - line = fb.readline() - if not line: - return None # landed past the last game in the file; retry elsewhere - # read forward to the START of the game AFTER this one (or EOF) - fb.seek(start) - fb.readline() # consume this game's own "[Event " line - end = fb.tell() - line = fb.readline() - while line and not line.startswith(b"[Event "): - end = fb.tell() - line = fb.readline() - fb.seek(start) - raw = fb.read(end - start).decode("utf-8", errors="replace") - game = chess.pgn.read_game(io.StringIO(raw)) - if game is None: - return None - moves = list(game.mainline_moves()) - if len(moves) < min_ply: - return None - board = game.board() - ply = rng.randint(min_ply, min(max_ply, len(moves))) - for mv in moves[:ply]: - board.push(mv) - return board.fen() - - -def sample_openings(pgn_paths, n, min_ply=8, max_ply=20, seed=0, +def sample_openings(pgn_path, n, min_ply=8, max_ply=20, seed=0, cache_dir="opening_cache"): - """Pick n random (roughly) opening positions out of one or more pgn - pools by seeking to random byte offsets and parsing just the one game - found there, rather than sequentially chess.pgn.read_game()-ing from - the start -- on a several-hundred-MB pool, replaying every game up to + """Pick n random (roughly) opening positions out of a pgn pool by + seeking to random byte offsets and parsing just the one game found + there, rather than sequentially chess.pgn.read_game()-ing from the + start -- on a several-hundred-MB pool, replaying every game up to the sample point (even while mostly skipping them) means real I/O and SAN-parsing cost that scales with how far into the file the first few accepted samples happen to land, which was observed taking minutes for as few as 2 requested openings. Cached like tune_eval_dna.py's position_cache, keyed on pgn identity + params, - since repeated cycle.sh runs against the same pool(s) shouldn't pay - this more than once. - - `pgn_paths` may name several pools at once (e.g. the TWIC game - archive alongside a curated opening-book PGN) to widen the set of - starting positions beyond whatever one file happens to contain -- - every match otherwise draws its "variety" from a single source, - which just means every match looks like that source's opening - repertoire. Each attempt picks a source file weighted by byte size - (a bigger pool has proportionally more distinct games to offer), so - pools aren't implicitly up- or down-weighted just by being listed - first or being much smaller than the others.""" - if isinstance(pgn_paths, (str, Path)): - pgn_paths = [pgn_paths] - stats = [Path(p).stat() for p in pgn_paths] - key_parts = [f"{p}|{st.st_mtime}|{st.st_size}" - for p, st in zip(pgn_paths, stats)] + since repeated cycle.sh runs against the same pool shouldn't pay + this more than once.""" + st = Path(pgn_path).stat() key = hashlib.sha256( - ("||".join(key_parts) + f"||{n}|{min_ply}|{max_ply}|{seed}").encode() + f"{pgn_path}|{st.st_mtime}|{st.st_size}|{n}|{min_ply}|{max_ply}|{seed}" + .encode() ).hexdigest()[:16] cache_path = Path(cache_dir) / f"openings_{key}.txt" if cache_path.exists(): @@ -237,36 +176,49 @@ def sample_openings(pgn_paths, n, min_ply=8, max_ply=20, seed=0, return openings rng = random.Random(seed) - file_sizes = [st.st_size for st in stats] - total_size = sum(file_sizes) or 1 - # Precompute cumulative weights once for weighted source selection - # per attempt, rather than re-summing on every pick. - cum_weights = [] - running = 0 - for sz in file_sizes: - running += sz - cum_weights.append(running) - + file_size = st.st_size openings = [] attempts = 0 max_attempts = n * 50 + 200 # generous; each attempt is one seek+one game parse - file_handles = [open(p, "rb") for p in pgn_paths] - try: + with open(pgn_path, "rb") as fb: while len(openings) < n and attempts < max_attempts: attempts += 1 - pick = rng.uniform(0, total_size) - idx = next(i for i, cw in enumerate(cum_weights) if pick <= cw) - fen = _sample_one_opening(file_handles[idx], file_sizes[idx], - rng, min_ply, max_ply) - if fen is not None: - openings.append(fen) - finally: - for fb in file_handles: - fb.close() + offset = rng.randint(0, max(file_size - 1, 0)) + fb.seek(offset) + fb.readline() # discard partial line at the seek point + # scan forward to the start of the next game's header block + start = fb.tell() + line = fb.readline() + while line and not line.startswith(b"[Event "): + start = fb.tell() + line = fb.readline() + if not line: + continue # landed past the last game in the file; retry elsewhere + # read forward to the START of the game AFTER this one (or EOF) + fb.seek(start) + fb.readline() # consume this game's own "[Event " line + end = fb.tell() + line = fb.readline() + while line and not line.startswith(b"[Event "): + end = fb.tell() + line = fb.readline() + fb.seek(start) + raw = fb.read(end - start).decode("utf-8", errors="replace") + game = chess.pgn.read_game(io.StringIO(raw)) + if game is None: + continue + moves = list(game.mainline_moves()) + if len(moves) < min_ply: + continue + board = game.board() + ply = rng.randint(min_ply, min(max_ply, len(moves))) + for mv in moves[:ply]: + board.push(mv) + openings.append(board.fen()) if len(openings) < n: print(f"warning: only found {len(openings)} openings " - f"(wanted {n}) across {pgn_paths}", file=sys.stderr) + f"(wanted {n}) in {pgn_path}", file=sys.stderr) cache_path.parent.mkdir(parents=True, exist_ok=True) cache_path.write_text("\n".join(openings) + "\n" if openings else "") @@ -811,16 +763,7 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument("head_engine", help="path to the baseline/head typhoon binary") ap.add_argument("candidate_engine", help="path to the candidate typhoon binary") - ap.add_argument("--pgn", required=True, nargs="+", - help="one or more pgn pools to sample starting " - "positions from (e.g. --pgn ../../pgn/" - "twic_filtered.pgn ../../pgn/self_play_games.pgn). " - "Each candidate opening's source file is picked " - "weighted by that file's byte size, so listing " - "several pools widens the set of starting " - "positions instead of implicitly favoring " - "whichever is listed first or is largest/" - "smallest.") + ap.add_argument("--pgn", required=True) ap.add_argument("--games", type=int, default=500, help="total games; rounded down to an even number " "since each opening is played as a pair " @@ -894,27 +837,7 @@ def main(): ap.add_argument("--log", default="match_history.log") ap.add_argument("--pgn-out", default="../self_play_games.pgn", help="every played game is appended here as PGN") - ap.add_argument("--opening-seed", type=int, default=None, metavar="N", - help="seed for opening-position sampling. Default: " - "a fresh random seed every run, so successive " - "invocations draw a DIFFERENT set of starting " - "positions from --pgn rather than silently " - "replaying whatever sample a previous run " - "already cached under opening_cache/ for these " - "exact (pgn, n, min_ply, max_ply) params -- that " - "caching exists to make a single run's sampling " - "cheap to redo (e.g. a crashed/resumed run), not " - "to pin one fixed opening set across unrelated " - "runs. Pass an explicit value to reproduce a " - "specific run's exact opening set.") args = ap.parse_args() - if args.opening_seed is None: - # SystemRandom, not random.Random/time-based: back-to-back runs - # (e.g. a shell loop launching several matches within the same - # second) must not collide on the same seed and silently resample - # the same "random" set. - args.opening_seed = random.SystemRandom().randrange(2**31) - print(f"OPENING_SEED={args.opening_seed}", file=sys.stderr) # --- resolve the per-side time controls once, loudly --------------- # Deliberately strict: a time-odds match that silently degrades into @@ -970,7 +893,7 @@ def main(): candidate_engine = str(Path(args.candidate_engine).resolve()) n_pairs = args.games // 2 - openings = sample_openings(args.pgn, n_pairs, seed=args.opening_seed) + openings = sample_openings(args.pgn, n_pairs) n_games = len(openings) * 2 jobs = [] diff --git a/src/eval_tune/test_vs_head.sh b/src/eval_tune/test_vs_head.sh index dbddf10..6f37039 100755 --- a/src/eval_tune/test_vs_head.sh +++ b/src/eval_tune/test_vs_head.sh @@ -105,7 +105,7 @@ fi python3 ./match_play.py \ ${HEAD} \ ${CANDIDATE} \ - --pgn ../../pgn/twic_filtered.pgn ../../pgn/lichess*.pgn \ + --pgn ../../pgn/twic_filtered.pgn \ --games ${GAMES} \ --workers 18 \ --post \ @@ -113,4 +113,5 @@ python3 ./match_play.py \ ${SPRT_ARGS} \ --scratch /usr/local/tmp/typhoon_match_${LABEL} \ --log ${LABEL}.log \ + --pgn-out ../${LABEL}_games.pgn \ "$@" |
