From 18536203bb1cc7f313f8f3ed2767f128e5407c3b Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Wed, 22 Jul 2026 20:41:53 -0500 Subject: protocol: freeze dynamic standard-depth recovery --- experiments/oral_a_dynamic_scaling.py | 187 ++++++++++++++++++++++++++++++++++ 1 file changed, 187 insertions(+) create mode 100755 experiments/oral_a_dynamic_scaling.py (limited to 'experiments/oral_a_dynamic_scaling.py') diff --git a/experiments/oral_a_dynamic_scaling.py b/experiments/oral_a_dynamic_scaling.py new file mode 100755 index 0000000..3de4561 --- /dev/null +++ b/experiments/oral_a_dynamic_scaling.py @@ -0,0 +1,187 @@ +#!/usr/bin/env python3 +"""Run shards of the R2-gated standard-depth dynamic-innovation panel.""" +import argparse +import hashlib +import json +import os +import subprocess +import sys + + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +PROTOCOL_PATH = os.path.join(ROOT, "ORAL_A_RECOVERY.md") +DEPTHS = (20, 32, 56) +SEEDS = tuple(range(10, 15)) +METHODS = ("bp", "dfa", "clean_kp", "dynamic") + + +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 git_output(*args): + return subprocess.run( + ["git", *args], cwd=ROOT, check=True, capture_output=True, + text=True).stdout.strip() + + +def require_prerequisites(d4_path, r2_path): + with open(d4_path) as handle: + d4 = json.load(handle) + with open(r2_path) as handle: + r2 = json.load(handle) + d4_digest = sha256(d4_path) + if not (d4.get("protocol") == + "kp_dynamic_neutral_projection_confirmation_v1" + and d4.get("status") == "passed" + and d4.get("review_score_after") == 7): + raise ValueError("oral-A recovery requires the complete D4 pass") + if not (r2.get("protocol") == "oral_b_td_confirmation_v1" + and r2.get("status") == "passed" + and r2.get("complete_grid") is True + and r2.get("oral_b_plasticity_innovation_established") is True + and r2.get("review_score_after") == 8 + and r2.get("d4_gate_sha256") == d4_digest): + raise ValueError("oral-A recovery requires the complete oral-B R2 pass") + + paths = [ + os.path.abspath(__file__), PROTOCOL_PATH, + os.path.abspath(d4_path), os.path.abspath(r2_path), + os.path.join(ROOT, "experiments", "conv_run.py"), + ] + relative = [os.path.relpath(path, ROOT) for path in paths] + tracked = all(subprocess.run( + ["git", "ls-files", "--error-unmatch", path], cwd=ROOT, + capture_output=True).returncode == 0 for path in relative) + dirty = bool(git_output("status", "--porcelain", "--untracked-files=no")) + if dirty or not tracked: + raise RuntimeError( + "oral-A recovery requires clean tracked runner, protocol, and gates") + return { + "git_commit": git_output("rev-parse", "HEAD"), + "protocol_sha256": sha256(PROTOCOL_PATH), + "d4_gate_sha256": d4_digest, + "r2_gate_sha256": sha256(r2_path), + } + + +def job_command(method, depth, seed, device, outdir): + if method in ("clean_kp", "dynamic") and depth == 20: + raise ValueError("depth-20 clean-KP/dynamic cells are reused from D4") + common = [ + sys.executable, "experiments/conv_run.py", + "--device", device, "--depth", str(depth), "--width", "16", + "--seed", str(seed), "--loader_seed", str(seed), + "--batch_size", "128", "--epochs", "200", "--train_limit", "0", + "--val_examples", "0", "--split_seed", "2027", + "--eval_split", "test", "--eval_every", "0", "--augment_train", "1", + "--lr_schedule", "step", "--lr_milestones", "100,150", + "--lr_gamma", "0.1", "--warmup_epochs", "0", + "--momentum", "0.9", "--weight_decay", "1e-4", + "--normalization", "batchnorm", "--a_scale", "1", + ] + if method == "bp": + specific = ["--mode", "bp", "--lr", "0.1"] + elif method == "dfa": + specific = [ + "--mode", "dfa", "--lr", "0.03", "--output_lr", "0.1", + "--vectorizer_mode", "spatial_template", "--alignment_probe", "32", + ] + elif method == "clean_kp": + specific = [ + "--mode", "kp", "--lr", "0.1", "--output_lr", "0.1", + "--alignment_probe", "32", + ] + elif method == "dynamic": + specific = [ + "--mode", "kp_traffic", "--lr", "0.1", "--output_lr", "0.1", + "--traffic_rule", "innovation", "--predictor_mode", "closed_form", + "--neutral_projection", "1", "--traffic_seed", str(5000 + seed), + "--traffic_ratio", "4", "--traffic_calibration_examples", "64", + "--learn_P", "1", "--eta_P", "0.1", + "--predictor_warmup_steps", "1", "--predictor_every", "0", + "--alignment_probe", "32", + ] + else: + raise ValueError(f"unknown oral-A method: {method}") + path = os.path.join(outdir, f"{method}_d{depth}_s{seed}.json") + return path, common + specific + ["--out", path] + + +def jobs(device, outdir): + result = [] + for depth in DEPTHS: + for seed in SEEDS: + for method in METHODS: + if depth == 20 and method in ("clean_kp", "dynamic"): + continue + path, command = job_command(method, depth, seed, device, outdir) + result.append((method, depth, seed, path, command)) + return result + + +def existing_matches(path, method, depth, seed, source_commit): + with open(path) as handle: + row = json.load(handle) + expected_mode = { + "bp": "bp", "dfa": "dfa", "clean_kp": "kp", "dynamic": "kp_traffic", + }[method] + args = row.get("args", {}) + return (args.get("mode") == expected_mode + and args.get("depth") == depth + and args.get("seed") == seed + and row.get("provenance", {}).get("git_commit") == source_commit + and row.get("provenance", {}).get("git_tracked_dirty") is False) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "--d4_gate", + default="results/kp_dynamic_projection_confirmation_gate.json") + parser.add_argument( + "--r2_gate", default="results/bci_td_confirmation_gate.json") + parser.add_argument("--device", default="cuda") + parser.add_argument("--method", choices=("all",) + METHODS, default="all") + parser.add_argument("--depth", type=int, choices=DEPTHS) + parser.add_argument("--seed", type=int, choices=SEEDS) + parser.add_argument("--shard-index", type=int, default=0) + parser.add_argument("--num-shards", type=int, default=1) + parser.add_argument("--outdir", default="results/oral_a_dynamic_scaling") + 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 index") + source = require_prerequisites(args.d4_gate, args.r2_gate) + selected = jobs(args.device, args.outdir) + if args.method != "all": + selected = [job for job in selected if job[0] == args.method] + if args.depth is not None: + selected = [job for job in selected if job[1] == args.depth] + if args.seed is not None: + selected = [job for job in selected if job[2] == args.seed] + selected = [job for index, job in enumerate(selected) + if index % args.num_shards == args.shard_index] + if not selected: + raise ValueError("no oral-A recovery jobs match the requested shard") + + os.makedirs(args.outdir, exist_ok=True) + for method, depth, seed, path, command in selected: + tag = f"{method}_d{depth}_s{seed}" + if os.path.exists(path): + if not existing_matches( + path, method, depth, seed, source["git_commit"]): + raise RuntimeError(f"refusing mismatched existing record: {path}") + print(f"preserving {tag}", flush=True) + continue + print(tag, " ".join(command), flush=True) + if not args.dry_run: + subprocess.run(command, cwd=ROOT, check=True) + + +if __name__ == "__main__": + main() -- cgit v1.2.3