From bc3b656233c8731e7b8182087cf74a0079baadbd Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Mon, 27 Jul 2026 13:28:00 -0500 Subject: [PATCH 11/19] experiment: register plain CNN P1 selector grid --- crossover_grid.py | 228 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 228 insertions(+) create mode 100644 crossover_grid.py diff --git a/crossover_grid.py b/crossover_grid.py new file mode 100644 index 0000000..dae3316 --- /dev/null +++ b/crossover_grid.py @@ -0,0 +1,228 @@ +#!/usr/bin/env python3 +"""Immutable command registry for the plain-CNN crossover stages.""" +import argparse +import glob +import hashlib +import json +import os +import subprocess +import sys +import time + + +ROOT = os.path.dirname(os.path.abspath(__file__)) +MAIN_ROOT = "/home/yurenh2/sdil" +PROTOCOL = os.path.join(MAIN_ROOT, "PLAIN_CNN_CROSSOVER.md") + + +def output(*args): + return subprocess.run( + args, cwd=ROOT, check=True, capture_output=True, + text=True).stdout.strip() + + +def sha256(path): + digest = hashlib.sha256() + with open(path, "rb") as handle: + for chunk in iter(lambda: handle.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def source_report(): + commit = output("git", "rev-parse", "HEAD") + dirty = output( + "git", "status", "--porcelain", "--untracked-files=no") + if dirty: + raise RuntimeError("crossover runs require clean tracked author source") + main_commit = subprocess.run( + ["git", "rev-parse", "HEAD"], cwd=MAIN_ROOT, check=True, + capture_output=True, text=True).stdout.strip() + main_dirty = subprocess.run( + ["git", "status", "--porcelain", "--untracked-files=no"], + cwd=MAIN_ROOT, check=True, capture_output=True, + text=True).stdout.strip() + if main_dirty: + raise RuntimeError("crossover runs require clean tracked main source") + return { + "author_crossover_commit": commit, + "main_commit": main_commit, + "protocol_path": PROTOCOL, + "protocol_sha256": sha256(PROTOCOL), + "python": sys.executable, + "cuda_visible_devices": os.environ.get("CUDA_VISIBLE_DEVICES"), + } + + +def rate_tag(rate): + return f"{rate:g}".replace(".", "p") + + +def p1_jobs(): + # Expensive jobs are deliberately interleaved across modulo shards. + specifications = [ + ("ep", 0.003), + ("dualprop", 0.025), + ("ep", 0.01), + ("ff", 0.03), + ("ep", 0.03), + ("pepita", 0.01), + ("fa", 0.003), + ("bp", 0.025), + ("fa", 0.01), + ("dfa", 0.003), + ("fa", 0.03), + ("dfa", 0.01), + ("clean_kp", 0.003), + ("dfa", 0.03), + ("clean_kp", 0.01), + ("sdil", 0.003), + ("clean_kp", 0.03), + ("sdil", 0.01), + ("sdil", 0.03), + ] + return [p1_command(method, rate) for method, rate in specifications] + + +def p1_command(method, rate): + cli_method = { + "dualprop": "dualprop-lagr-ff", + "clean_kp": "clean-kp", + }.get(method, method) + runner = "train_ff.py" if method == "ff" else "train.py" + name = f"plain-p1-{method}-minicnn-lr{rate_tag(rate)}" + command = [ + sys.executable, runner, + "--model", "miniCNN", + "--dataset", "cifar10", + "--num-epochs", "10", + "--batch-size", "100", + "--learning-rate", str(rate), + "--learning-rate-final", str(rate), + "--warmup-learning-rate", str(rate), + "--warmup-epochs", "0", + "--decay-epochs", "10", + "--momentum", "0.9", + "--weight-decay", "5e-4", + "--dtype", "float32", + "--param-dtype", "float32", + "--percent-train", "90", + "--percent-val", "10", + "--seeds", "0", + "--feedback-seed", "1729", + "--gradient-diagnostics", "none", + "--spectral-diagnostics", "none", + "--test-policy", "none", + "--early-stop-policy", "none", + "--learning-algorithm", cli_method, + "--experiment-name", name, + ] + if method == "pepita": + command.extend([ + "--optimizer-schedule", "pepita", + "--pepita-projection-scale", "0.05", + ]) + else: + command.extend(["--optimizer-schedule", "author"]) + if method == "ff": + command.extend([ + "--ff-threshold", "2.0", + "--ff-score-from-layer", "1", + ]) + if method == "ep": + command.extend([ + "--ep-beta", "0.5", + "--ep-dt", "0.5", + "--ep-free-steps", "20", + "--ep-nudge-steps", "4", + ]) + if method == "dualprop": + command.extend([ + "--loss", "sce", + "--alpha", "0.0", + "--beta", "0.1", + "--inference-sequence", "fwK", + "--inference-passes-nudged", "16", + ]) + if method == "sdil": + command.extend([ + "--sdil-traffic-ratio", "4", + "--sdil-traffic-seed", "4000", + "--sdil-calibration-examples", "64", + ]) + return { + "stage": "p1", + "method": method, + "architecture": "minicnn", + "rate": rate, + "experiment_name": name, + "command": command, + } + + +def completed_histogram(experiment_name): + paths = glob.glob(os.path.join( + ROOT, "runs", experiment_name, "*", "hist.npy")) + if not paths: + return None + if len(paths) != 1: + raise RuntimeError( + f"expected one result for {experiment_name}, found {len(paths)}") + return paths[0] + + +def run_job(job, source, dry_run): + result = completed_histogram(job["experiment_name"]) + if result is not None: + print(f"preserving {job['experiment_name']} -> {result}", flush=True) + return + print("RUN", " ".join(job["command"]), flush=True) + if dry_run: + return + started = time.time() + subprocess.run(job["command"], cwd=ROOT, check=True) + result = completed_histogram(job["experiment_name"]) + if result is None: + raise RuntimeError(f"job produced no histogram: {job['experiment_name']}") + manifest = { + **job, + "source": source, + "histogram": result, + "histogram_sha256": sha256(result), + "driver_wall_seconds": time.time() - started, + "completed_unix_time": time.time(), + } + manifest_path = os.path.join( + os.path.dirname(result), "crossover_manifest.json") + with open(manifest_path, "w") as handle: + json.dump(manifest, handle, indent=2, sort_keys=True) + handle.write("\n") + print(f"DONE {job['experiment_name']} -> {manifest_path}", flush=True) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--stage", choices=("p1",), default="p1") + parser.add_argument("--shard-index", type=int, default=0) + parser.add_argument("--num-shards", type=int, default=1) + parser.add_argument("--method") + parser.add_argument("--dry-run", action="store_true") + args = parser.parse_args() + if not 0 <= args.shard_index < args.num_shards: + raise ValueError("invalid shard") + source = source_report() + jobs = p1_jobs() + if args.method: + jobs = [job for job in jobs if job["method"] == args.method] + jobs = [ + job for index, job in enumerate(jobs) + if index % args.num_shards == args.shard_index + ] + if not jobs: + raise ValueError("no jobs selected") + for job in jobs: + run_job(job, source, args.dry_run) + + +if __name__ == "__main__": + main() -- 2.54.0