summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ORAL_A.md148
-rwxr-xr-xexperiments/analyze_oral_a_apical.py100
-rwxr-xr-xexperiments/analyze_oral_a_bp.py72
-rwxr-xr-xexperiments/analyze_oral_a_short.py84
-rw-r--r--experiments/conv_run.py16
-rwxr-xr-xexperiments/oral_a_apical_screen.sh25
-rwxr-xr-xexperiments/oral_a_bp_reference.sh31
-rwxr-xr-xexperiments/oral_a_short_screen.py65
8 files changed, 539 insertions, 2 deletions
diff --git a/ORAL_A.md b/ORAL_A.md
new file mode 100644
index 0000000..e3427c4
--- /dev/null
+++ b/ORAL_A.md
@@ -0,0 +1,148 @@
+# Oral-A preregistration: standard CIFAR residual networks
+
+## Claim boundary and status
+
+This protocol asks whether the low-query learned-feedback backbone can train a
+standard deep convolutional family without reverse-mode credit assignment. It
+does **not** rescue the failed Oral-B biological-signature gate. With
+`nuisance_scale=0`, the predictor is inactive and SDIL reduces to learned
+direct-feedback calibration in the sense of Lansdell et al.; standard-network
+scaling is therefore an engineering/credit-assignment contribution, while the
+Harnett-specific innovation claim remains supported only in the separately
+audited mixed-traffic experiments.
+
+No CIFAR image/ResNet accuracy was observed before this protocol was frozen.
+Mechanics-only CPU checks are complete: standard ResNet parameter counts,
+option-A shortcuts, augmentation determinism, local Conv+BatchNorm eligibility,
+BatchNorm-coupled causal perturbations, running-stat isolation, and both apical
+vectorizers pass `experiments/cifar_image_smoke.py` and
+`experiments/conv_local_smoke.py`.
+
+## Fixed data, architecture, and accounting
+
+- CIFAR-10 uses the original Python batches under `/home/yurenh2/sdrn/data`.
+- Development uses a 45,000/5,000 training-only stratified split with seed 2027,
+ validation-index SHA-256
+ `8328b206a97c420e49e54e3eca4abe3274c4756b084355784ea3fb8059e4515b` and
+ exactly 500 validation examples per class.
+- Training augmentation is random 32x32 crop after four-pixel zero padding and
+ horizontal flip with probability 0.5. Validation/test are not augmented.
+- The architecture is the canonical CIFAR `6n+2` ResNet topology with 3x3
+ convolutions, BatchNorm, ReLU, global average pooling, and parameter-free
+ option-A shortcuts. ResNet-20/32/56 contain respectively 269,722, 464,154,
+ and 853,018 forward parameters.
+- BatchNorm's complete minibatch Jacobian is evaluated within its own layer.
+ No local update reads a downstream forward weight. Counterfactual `+/-`
+ trials are separate batches and do not update running statistics.
+- Because BatchNorm couples examples, causal calibration uses the batch-mean
+ loss derivative multiplied by batch size. This is an unbiased estimate of
+ the summed-loss hidden gradient. Logical batch queries, observed causal
+ scalars, per-example CE terms, forward-equivalent examples, MAC components,
+ peak memory, and wall time are all recorded separately.
+- SDIL uses one antithetic direction every four task updates (`K=1/every=4`),
+ selected before this project reached convolutional models by the passed C3
+ gate. `sigma=0.01`; no direction/cadence tuning is allowed here.
+
+## A1: establish a valid exact-BP reference
+
+The primary seed-0 ResNet-20 validation run uses 200 epochs, batch 128, SGD
+momentum 0.9, weight decay `1e-4`, learning rate 0.1 with 10x drops at epochs
+100 and 150, and no warmup. It must reach at least 90% final validation
+accuracy with finite loss.
+
+If and only if the primary fails, one predeclared architecture-recovery run is
+allowed: cosine learning rate from 0.1, five warmup epochs, and weight decay
+`5e-4`, with everything else fixed. The higher final validation endpoint is
+selected; if neither reaches 90%, the current implementation is not a valid
+standard-network testbed and all later Oral-A stages stop. This recovery tests
+the forward architecture/optimizer, not SDIL.
+
+## A2a: apical calibration screen with frozen forward weights
+
+On seed 0, ResNet-20, the first 10,000 post-split training examples, and 100
+feedback-only minibatches, cross:
+
+- vectorizer: `spatial_template`, `channel_gated`;
+- initial feedback scale: `0`, `1`;
+- apical regression rate: `0.001`, `0.01`, `0.1`.
+
+Forward weights, BN affine parameters, and the readout remain bitwise fixed.
+Each run uses K1 antithetic BatchNorm-coupled calibration and a fixed 32-example
+training-prefix alignment probe. A candidate is eligible only if all values are
+finite and early-third teaching/negative-gradient cosine is positive. Within
+each vectorizer family, select maximum early-third cosine, then maximum
+all-layer mean cosine, then lower `eta_A`, then lower `a_scale`. Both families
+must yield an eligible candidate or A2 stops.
+
+The spatial template has one class vector per spatial unit. The channel-gated
+candidate shares `A c + tanh(h) * G c` over positions; at ResNet-56 it has
+40,640 rather than 5,324,800 vectorizer parameters. This capacity/cost choice
+is frozen before accuracy is observed.
+
+## A2b: bounded short accuracy screen
+
+Use seed 0, ResNet-20, 10,000 training examples, 20 epochs, final validation
+only, cosine decay, no warmup, momentum 0.9, and weight decay `1e-4`:
+
+- one exact-BP run at hidden/output LR 0.1;
+- DFA at hidden LR `{0.01, 0.03, 0.1}`, output LR 0.1;
+- each A2a-selected SDIL vectorizer at the same three hidden rates, output LR
+ 0.1, its selected A hyperparameters, and 100 A-only warmup minibatches.
+
+Select DFA by maximum final validation accuracy, then lower MAC cost/rate.
+Select SDIL by maximum accuracy, except that a channel-gated candidate within
+0.5 points of the global maximum wins; remaining ties use lower total MACs,
+then lower hidden rate. Advancement requires BP at least 50%, SDIL at least
+35%, SDIL no more than 10 points below selected DFA, and all trajectories
+finite. Failure stops Oral-A rather than expanding the grid.
+
+## A3: full seed-0 ResNet-20 development gate
+
+Train the A1-selected BP schedule, A2b-selected DFA, and A2b-selected SDIL for
+200 epochs on all 45,000 training examples. DFA/SDIL retain their selected
+hidden base rate, use output base rate 0.1 and the same epoch-100/150 10x drops;
+SDIL retains its 100-step A-only warmup and K1/every4 joint calibration. No
+nuisance traffic or predictor learning is present.
+
+All of the following must hold to open the independent test panel:
+
+1. BP final validation accuracy is at least 90%.
+2. SDIL is within 5 points of BP and at least 2 points above tuned DFA.
+3. SDIL early-third teaching alignment is at least 0.05.
+4. Every loss/metric is finite and SDIL's estimated total training MACs do not
+ exceed exact BP's estimate.
+
+No recovery branch follows an A3 failure.
+
+## A4: independent ResNet-20/32/56 confirmation
+
+Only after A3 passes, run exact BP, selected DFA, and selected SDIL at depths
+20, 32, and 56 for model/data-loader seeds 10--14. Hyperparameters and epoch
+budgets are copied without depth-specific tuning. The CIFAR-10 test set is
+evaluated exactly once at the end of every run; no intermediate test metric is
+logged or used for selection.
+
+The Oral-A gate requires:
+
+1. all 45 trajectories are finite;
+2. at every depth, mean SDIL test accuracy is within 2 points of exact BP;
+3. at depth 56, SDIL exceeds DFA by at least 2 points;
+4. paired SDIL depth-20 to depth-56 accuracy changes by no less than -2 points;
+5. depth-56 early-third alignment is at least 0.05 and at least 30% of the
+ depth-20 value;
+6. SDIL remains nondominated among matched local methods in accuracy versus
+ estimated MACs, logical causal queries, and peak memory. Wall time is shown
+ but cannot substitute for these hardware-independent coordinates.
+
+Native BurstCCN and Dual Prop runs remain separate executable-fidelity
+comparisons because their architectures, epochs, and objectives differ. Their
+published multi-seed values are not pooled with this matched panel.
+
+## Stop rules
+
+- No test seed 10--14 may be touched before A3 passes.
+- No failed threshold may be weakened after observing an endpoint.
+- No extra depth, direction count, perturbation frequency, vectorizer, or
+ optimizer branch is added after its corresponding screen closes.
+- A strong standard-network result cannot be described as evidence that the
+ natural Harnett causal-role/error-derivative signature emerged.
diff --git a/experiments/analyze_oral_a_apical.py b/experiments/analyze_oral_a_apical.py
new file mode 100755
index 0000000..1d10edb
--- /dev/null
+++ b/experiments/analyze_oral_a_apical.py
@@ -0,0 +1,100 @@
+#!/usr/bin/env python3
+"""Apply the frozen A2a selector to convolutional apical-screen records."""
+import argparse
+import glob
+import json
+import math
+import os
+
+
+MODES = ("spatial_template", "channel_gated")
+EXPECTED_SCALES = (0.0, 1.0)
+EXPECTED_RATES = (0.001, 0.01, 0.1)
+
+
+def load(path):
+ with open(path) as handle:
+ record = json.load(handle)
+ args = record["args"]
+ if record["provenance"]["git_tracked_dirty"]:
+ raise ValueError(f"tracked-dirty result: {path}")
+ expected = {
+ "mode": "sdil", "depth": 20, "width": 16, "seed": 0,
+ "epochs": 0, "train_limit": 10000, "val_examples": 5000,
+ "a_warmup_steps": 100, "pert_directions": 1, "pert_every": 4,
+ "normalization": "batchnorm",
+ }
+ for key, value in expected.items():
+ if args.get(key) != value:
+ raise ValueError(f"{path}: {key}={args.get(key)!r}, expected {value!r}")
+ if record["split"]["validation_index_sha256"] != (
+ "8328b206a97c420e49e54e3eca4abe3274c4756b084355784ea3fb8059e4515b"):
+ raise ValueError(f"split drift: {path}")
+ diagnostics = record.get("diagnostics")
+ warmup = record.get("apical_warmup", {}).get("last")
+ if diagnostics is None or warmup is None:
+ raise ValueError(f"missing diagnostics/warmup: {path}")
+ values = diagnostics["teaching_negative_gradient_cosine"]
+ early_count = max(1, len(values) // 3)
+ metrics = {
+ "early_third_alignment": sum(values[:early_count]) / early_count,
+ "all_layer_alignment": sum(values) / len(values),
+ "calibration_mse": warmup["calibration_mse"],
+ "prediction_target_cosine": warmup["prediction_target_cosine"],
+ }
+ finite = (record["final"]["finite"]
+ and all(math.isfinite(value) for value in metrics.values()))
+ return {
+ "path": path,
+ "source_commit": record["provenance"]["git_commit"],
+ "vectorizer_mode": args["vectorizer_mode"],
+ "a_scale": float(args["a_scale"]),
+ "eta_A": float(args["eta_A"]),
+ "metrics": metrics,
+ "eligible": finite and metrics["early_third_alignment"] > 0.0,
+ "vectorizer_parameters": record["architecture"]["vectorizer_parameters"],
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--input", default="results/oral_a_apical_screen")
+ parser.add_argument("--out", default="results/oral_a_apical_selection.json")
+ args = parser.parse_args()
+ paths = sorted(glob.glob(os.path.join(args.input, "*.json")))
+ rows = [load(path) for path in paths]
+ observed = {(row["vectorizer_mode"], row["a_scale"], row["eta_A"])
+ for row in rows}
+ expected = {(mode, scale, rate) for mode in MODES
+ for scale in EXPECTED_SCALES for rate in EXPECTED_RATES}
+ if observed != expected or len(rows) != len(expected):
+ raise ValueError(f"incomplete A2a grid: missing={expected-observed}, extra={observed-expected}")
+ if len({row["source_commit"] for row in rows}) != 1:
+ raise ValueError("A2a source commits differ")
+ selected = {}
+ for mode in MODES:
+ eligible = [row for row in rows
+ if row["vectorizer_mode"] == mode and row["eligible"]]
+ if eligible:
+ eligible.sort(key=lambda row: (
+ -row["metrics"]["early_third_alignment"],
+ -row["metrics"]["all_layer_alignment"],
+ row["eta_A"], row["a_scale"]))
+ selected[mode] = eligible[0]
+ output = {
+ "protocol": "oral_a_A2a_v1",
+ "status": ("selected" if len(selected) == len(MODES)
+ else "failed_no_eligible_family"),
+ "rows": rows,
+ "selected": selected,
+ "confirmation_test_seeds_touched": False,
+ }
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
+ with open(args.out, "w") as handle:
+ json.dump(output, handle, indent=2, sort_keys=True)
+ handle.write("\n")
+ print(json.dumps({"status": output["status"], "selected": selected}, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/experiments/analyze_oral_a_bp.py b/experiments/analyze_oral_a_bp.py
new file mode 100755
index 0000000..d309273
--- /dev/null
+++ b/experiments/analyze_oral_a_bp.py
@@ -0,0 +1,72 @@
+#!/usr/bin/env python3
+"""Apply the frozen A1 exact-BP viability gate."""
+import argparse
+import json
+import math
+import os
+
+
+def read(path, variant):
+ with open(path) as handle:
+ record = json.load(handle)
+ args = record["args"]
+ expected = {
+ "mode": "bp", "depth": 20, "width": 16, "seed": 0,
+ "epochs": 200, "val_examples": 5000, "eval_split": "validation",
+ "normalization": "batchnorm",
+ }
+ for key, value in expected.items():
+ if args.get(key) != value:
+ raise ValueError(f"{path}: {key}={args.get(key)!r}, expected {value!r}")
+ if record["provenance"]["git_tracked_dirty"]:
+ raise ValueError(f"tracked-dirty result: {path}")
+ if record["split"]["validation_index_sha256"] != (
+ "8328b206a97c420e49e54e3eca4abe3274c4756b084355784ea3fb8059e4515b"):
+ raise ValueError(f"split drift: {path}")
+ accuracy = float(record["final"]["accuracy"])
+ loss = float(record["final"]["loss"])
+ return {
+ "variant": variant, "path": path,
+ "source_commit": record["provenance"]["git_commit"],
+ "accuracy": accuracy, "loss": loss,
+ "finite": record["final"]["finite"] and math.isfinite(accuracy + loss),
+ "test_evaluations": record["evaluation_protocol"]["test_evaluations"],
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--primary", default="results/oral_a_dev/bp_reference_primary.json")
+ parser.add_argument("--recovery", default="results/oral_a_dev/bp_reference_recovery.json")
+ parser.add_argument("--out", default="results/oral_a_bp_selection.json")
+ args = parser.parse_args()
+ primary = read(args.primary, "primary")
+ rows = [primary]
+ if primary["finite"] and primary["accuracy"] >= 0.90:
+ status = "passed_primary"
+ selected = primary
+ elif not os.path.exists(args.recovery):
+ status = "recovery_required"
+ selected = None
+ else:
+ recovery = read(args.recovery, "recovery")
+ rows.append(recovery)
+ selected = max(rows, key=lambda row: row["accuracy"] if row["finite"] else -1.0)
+ status = ("passed_recovery" if selected["finite"] and selected["accuracy"] >= 0.90
+ else "failed_no_viable_reference")
+ if any(row["test_evaluations"] for row in rows):
+ raise ValueError("A1 must not evaluate test")
+ output = {
+ "protocol": "oral_a_A1_v1", "status": status,
+ "rows": rows, "selected": selected,
+ "confirmation_test_seeds_touched": False,
+ }
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
+ with open(args.out, "w") as handle:
+ json.dump(output, handle, indent=2, sort_keys=True)
+ handle.write("\n")
+ print(json.dumps(output, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/experiments/analyze_oral_a_short.py b/experiments/analyze_oral_a_short.py
new file mode 100755
index 0000000..375718a
--- /dev/null
+++ b/experiments/analyze_oral_a_short.py
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+"""Apply the frozen A2b selector and advancement gate."""
+import argparse
+import glob
+import json
+import math
+import os
+
+
+def read(path):
+ with open(path) as handle:
+ record = json.load(handle)
+ args = record["args"]
+ expected = {
+ "depth": 20, "width": 16, "seed": 0, "epochs": 20,
+ "train_limit": 10000, "val_examples": 5000,
+ "eval_split": "validation", "normalization": "batchnorm",
+ }
+ for key, value in expected.items():
+ if args.get(key) != value:
+ raise ValueError(f"{path}: {key} drift")
+ if record["provenance"]["git_tracked_dirty"]:
+ raise ValueError(f"tracked-dirty result: {path}")
+ if record["evaluation_protocol"]["test_evaluations"]:
+ raise ValueError(f"short screen touched test: {path}")
+ accuracy = float(record["final"]["accuracy"])
+ loss = float(record["final"]["loss"])
+ return {
+ "path": path, "mode": args["mode"], "lr": float(args["lr"]),
+ "vectorizer_mode": args.get("vectorizer_mode"),
+ "a_scale": float(args.get("a_scale", 0.0)),
+ "eta_A": float(args.get("eta_A", 0.0)),
+ "accuracy": accuracy, "loss": loss,
+ "finite": record["final"]["finite"] and math.isfinite(accuracy + loss),
+ "total_macs": record["work"]["total_macs_estimate"],
+ "source_commit": record["provenance"]["git_commit"],
+ }
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--input", default="results/oral_a_short")
+ parser.add_argument("--out", default="results/oral_a_short_selection.json")
+ args = parser.parse_args()
+ rows = [read(path) for path in sorted(glob.glob(os.path.join(args.input, "*.json")))]
+ if len(rows) != 10:
+ raise ValueError(f"expected 10 A2b runs, found {len(rows)}")
+ if len({row["source_commit"] for row in rows}) != 1:
+ raise ValueError("A2b source commits differ")
+ bp = [row for row in rows if row["mode"] == "bp"]
+ dfa = [row for row in rows if row["mode"] == "dfa"]
+ sdil = [row for row in rows if row["mode"] == "sdil"]
+ if len(bp) != 1 or len(dfa) != 3 or len(sdil) != 6:
+ raise ValueError("A2b method grid is incomplete")
+ selected_dfa = sorted(
+ dfa, key=lambda row: (-row["accuracy"], row["total_macs"], row["lr"]))[0]
+ best_accuracy = max(row["accuracy"] for row in sdil if row["finite"])
+ gated_near_best = [row for row in sdil if row["finite"]
+ and row["vectorizer_mode"] == "channel_gated"
+ and row["accuracy"] >= best_accuracy - 0.005]
+ pool = gated_near_best or [row for row in sdil if row["finite"]]
+ selected_sdil = sorted(
+ pool, key=lambda row: (-row["accuracy"], row["total_macs"], row["lr"]))[0]
+ passed = (bp[0]["finite"] and selected_dfa["finite"] and selected_sdil["finite"]
+ and bp[0]["accuracy"] >= 0.50
+ and selected_sdil["accuracy"] >= 0.35
+ and selected_sdil["accuracy"] >= selected_dfa["accuracy"] - 0.10)
+ output = {
+ "protocol": "oral_a_A2b_v1",
+ "status": "selected" if passed else "failed_advancement_gate",
+ "rows": rows, "selected_bp": bp[0],
+ "selected_dfa": selected_dfa, "selected_sdil": selected_sdil,
+ "confirmation_test_seeds_touched": False,
+ }
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
+ with open(args.out, "w") as handle:
+ json.dump(output, handle, indent=2, sort_keys=True)
+ handle.write("\n")
+ print(json.dumps({key: output[key] for key in (
+ "status", "selected_bp", "selected_dfa", "selected_sdil")}, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/experiments/conv_run.py b/experiments/conv_run.py
index 9b41dd2..98c1f19 100644
--- a/experiments/conv_run.py
+++ b/experiments/conv_run.py
@@ -235,8 +235,14 @@ def run(args):
"epochs": [],
}
+ sync(args.device)
+ total_start = time.time()
+ predictor_warmup_wall = 0.0
+ apical_warmup_wall = 0.0
loader_state = train.g.get_state().clone()
if config is not None and config.learn_P and args.predictor_warmup_steps:
+ sync(args.device)
+ warmup_start = time.time()
iterator = iter(train)
for _ in range(args.predictor_warmup_steps):
try:
@@ -249,8 +255,12 @@ def run(args):
forward["hiddens"], config.eta_P, config.nuisance_scale)
counters["predictor_warmup_examples"] += x.shape[0]
train.g.set_state(loader_state)
+ sync(args.device)
+ predictor_warmup_wall = time.time() - warmup_start
if config is not None and args.a_warmup_steps:
+ sync(args.device)
+ warmup_start = time.time()
iterator = iter(train)
warmup_metrics = []
for _ in range(args.a_warmup_steps):
@@ -277,14 +287,14 @@ def run(args):
"steps": args.a_warmup_steps,
"last": warmup_metrics[-1],
}
+ sync(args.device)
+ apical_warmup_wall = time.time() - warmup_start
step = 0
train_wall = 0.0
eval_wall = 0.0
validation_evaluations = 0
test_evaluations = 0
- sync(args.device)
- total_start = time.time()
for epoch in range(args.epochs):
lr = scheduled_lr(args.lr, epoch, args)
output_lr = (scheduled_lr(args.output_lr, epoch, args)
@@ -393,6 +403,8 @@ def run(args):
"work": work_report(net, args.mode, counters),
"hardware": training_hardware,
"timing": {
+ "predictor_warmup_wall_s": predictor_warmup_wall,
+ "apical_warmup_wall_s": apical_warmup_wall,
"train_wall_s": train_wall,
"evaluation_wall_s": eval_wall,
"total_timed_wall_s": total_wall,
diff --git a/experiments/oral_a_apical_screen.sh b/experiments/oral_a_apical_screen.sh
new file mode 100755
index 0000000..7281def
--- /dev/null
+++ b/experiments/oral_a_apical_screen.sh
@@ -0,0 +1,25 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+device="${1:-cuda}"
+mkdir -p results/oral_a_apical_screen
+
+for vectorizer in spatial_template channel_gated; do
+ for a_scale in 0 1; do
+ for eta_a in 0.001 0.01 0.1; do
+ tag="${vectorizer}_a${a_scale}_etaA${eta_a}"
+ python3 experiments/conv_run.py \
+ --mode sdil --device "${device}" --depth 20 --width 16 \
+ --seed 0 --loader_seed 0 --batch_size 128 --epochs 0 \
+ --train_limit 10000 --val_examples 5000 --split_seed 2027 \
+ --eval_split validation --eval_every 0 --augment_train 1 \
+ --lr 0.03 --output_lr 0.1 --lr_schedule constant --warmup_epochs 0 \
+ --momentum 0.9 --weight_decay 1e-4 --normalization batchnorm \
+ --vectorizer_mode "${vectorizer}" --a_scale "${a_scale}" \
+ --eta_A "${eta_a}" --a_warmup_steps 100 \
+ --pert_sigma 0.01 --pert_directions 1 --pert_every 4 \
+ --alignment_probe 32 \
+ --out "results/oral_a_apical_screen/${tag}.json"
+ done
+ done
+done
diff --git a/experiments/oral_a_bp_reference.sh b/experiments/oral_a_bp_reference.sh
new file mode 100755
index 0000000..cf5669e
--- /dev/null
+++ b/experiments/oral_a_bp_reference.sh
@@ -0,0 +1,31 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+variant="${1:-primary}"
+device="${2:-cuda}"
+mkdir -p results/oral_a_dev
+
+common=(
+ --mode bp --device "${device}" --depth 20 --width 16 --seed 0 --loader_seed 0
+ --batch_size 128 --epochs 200 --val_examples 5000 --split_seed 2027
+ --eval_split validation --eval_every 20 --augment_train 1
+ --lr 0.1 --momentum 0.9 --normalization batchnorm
+)
+
+case "${variant}" in
+ primary)
+ python3 experiments/conv_run.py "${common[@]}" \
+ --lr_schedule step --lr_milestones 100,150 --lr_gamma 0.1 \
+ --warmup_epochs 0 --weight_decay 1e-4 \
+ --out results/oral_a_dev/bp_reference_primary.json
+ ;;
+ recovery)
+ python3 experiments/conv_run.py "${common[@]}" \
+ --lr_schedule cosine --warmup_epochs 5 --weight_decay 5e-4 \
+ --out results/oral_a_dev/bp_reference_recovery.json
+ ;;
+ *)
+ echo "variant must be primary or recovery" >&2
+ exit 2
+ ;;
+esac
diff --git a/experiments/oral_a_short_screen.py b/experiments/oral_a_short_screen.py
new file mode 100755
index 0000000..51df778
--- /dev/null
+++ b/experiments/oral_a_short_screen.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python3
+"""Run a deterministic shard of the frozen A2b short accuracy grid."""
+import argparse
+import json
+import os
+import subprocess
+import sys
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--selection", default="results/oral_a_apical_selection.json")
+ parser.add_argument("--device", default="cuda")
+ parser.add_argument("--shard_index", type=int, default=0)
+ parser.add_argument("--num_shards", type=int, default=1)
+ 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")
+ with open(args.selection) as handle:
+ selection = json.load(handle)
+ if selection["status"] != "selected":
+ raise ValueError("A2a did not select both vectorizer families")
+
+ common = [
+ sys.executable, "experiments/conv_run.py", "--device", args.device,
+ "--depth", "20", "--width", "16", "--seed", "0", "--loader_seed", "0",
+ "--batch_size", "128", "--epochs", "20", "--train_limit", "10000",
+ "--val_examples", "5000", "--split_seed", "2027",
+ "--eval_split", "validation", "--eval_every", "0", "--augment_train", "1",
+ "--lr_schedule", "cosine", "--warmup_epochs", "0", "--momentum", "0.9",
+ "--weight_decay", "1e-4", "--normalization", "batchnorm",
+ ]
+ jobs = []
+ jobs.append(("bp_lr0.1", common + [
+ "--mode", "bp", "--lr", "0.1",
+ "--out", "results/oral_a_short/bp_lr0.1.json"]))
+ for rate in (0.01, 0.03, 0.1):
+ jobs.append((f"dfa_lr{rate}", common + [
+ "--mode", "dfa", "--lr", str(rate), "--output_lr", "0.1",
+ "--a_scale", "1", "--vectorizer_mode", "spatial_template",
+ "--out", f"results/oral_a_short/dfa_lr{rate}.json"]))
+ for mode in ("spatial_template", "channel_gated"):
+ chosen = selection["selected"][mode]
+ for rate in (0.01, 0.03, 0.1):
+ tag = f"sdil_{mode}_lr{rate}"
+ jobs.append((tag, common + [
+ "--mode", "sdil", "--lr", str(rate), "--output_lr", "0.1",
+ "--vectorizer_mode", mode,
+ "--a_scale", str(chosen["a_scale"]),
+ "--eta_A", str(chosen["eta_A"]), "--a_warmup_steps", "100",
+ "--pert_sigma", "0.01", "--pert_directions", "1",
+ "--pert_every", "4", "--alignment_probe", "32",
+ "--out", f"results/oral_a_short/{tag}.json"]))
+ os.makedirs("results/oral_a_short", exist_ok=True)
+ selected_jobs = [job for index, job in enumerate(jobs)
+ if index % args.num_shards == args.shard_index]
+ for tag, command in selected_jobs:
+ print(tag, " ".join(command), flush=True)
+ if not args.dry_run:
+ subprocess.run(command, check=True)
+
+
+if __name__ == "__main__":
+ main()