summaryrefslogtreecommitdiff
path: root/experiments/oral_a_dynamic_scaling.py
diff options
context:
space:
mode:
Diffstat (limited to 'experiments/oral_a_dynamic_scaling.py')
-rwxr-xr-xexperiments/oral_a_dynamic_scaling.py187
1 files changed, 187 insertions, 0 deletions
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()