diff options
| -rw-r--r-- | ORAL_A_V4.md | 94 | ||||
| -rw-r--r-- | README.md | 4 | ||||
| -rw-r--r-- | experiments/analyze_oral_a_v4_calibration.py | 153 | ||||
| -rw-r--r-- | experiments/oral_a_v4_calibration_screen.py | 54 | ||||
| -rw-r--r-- | sdil/conv.py | 10 |
5 files changed, 313 insertions, 2 deletions
diff --git a/ORAL_A_V4.md b/ORAL_A_V4.md new file mode 100644 index 0000000..db1af1a --- /dev/null +++ b/ORAL_A_V4.md @@ -0,0 +1,94 @@ +# Oral-A-v4: causal calibration of hierarchical feedback maps + +## Status and claim boundary + +The output-error-only V2/V3 feedback families failed their frozen early-layer +causal-capture gates. A held-out oracle then showed that ordinary downstream +activations are not enough, whereas gated 3x3 maps of the actual residual-DAG +child error fields can reconstruct early credit almost exactly. Fixed random +hierarchical FA subsequently improved the matched short ResNet-20 endpoint to +43.52% from DFA's 37.16%, but failed its frozen 50% full-run gate. + +V4 is the first trainable use of that diagnosed spatial hierarchy. Every +feedback convolution consumes its local child teaching field. One independent +Rademacher tensor is drawn in each feedback-parameter space; all induced parent +fields are injected in a single antithetic pair, and the scalar task-loss +derivative estimates each edge's causal target moment. The locally computed +prediction moment is subtracted before updating Q/R. Forward weights are never +copied or read by the learning rule, and no reverse-mode differentiation is +used. + +Recursive learned feedback is not claimed as the Harnett-specific novelty. +V4 is an engineering prerequisite for a later hierarchical innovation model +and must be compared with fixed HFA, learned FA/weight mirroring, and BurstCCN. +The frozen Oral-A-v1 failure remains failed; V4 cannot retroactively open A4. +No V4 GPU endpoint was observed before this protocol and its selector were +committed. CIFAR-10 test and confirmation seeds 10--14 remain untouched. + +## V4-0: mechanics gate + +Before any endpoint, `experiments/conv_local_smoke.py` must verify: + +- the simultaneous BatchNorm-coupled antithetic JVP to relative error below + `2e-6`; +- under an audit-only symmetric Q=W and R=-Wout-transpose copy, every predicted + local feedback moment equals its exact causal delta-rule target to relative + error below `2e-12`; +- the symmetric hierarchical field and local parameter update continue to + reproduce exact BP under the pre-existing thresholds; +- all legacy convolutional checks remain green. + +This gate passed with JVP error `6.16e-10`, delta-rule relative error +`2.30e-16`, hidden-field relative error `4.97e-16`, and parameter-update +absolute error `1.49e-8`. Actual training never makes the symmetric copy. + +## V4-1: frozen-forward causal-capture gate + +Use seed-0 ResNet-20, the first 10,000 development-training examples, batch +128, the frozen 45k/5k split, random hierarchical feedback scale 1, one +direction, `sigma=0.01`, perturbation seed 1000, and a fixed 64-example exact +gradient audit. Forward weights, readout, BatchNorm state, and affine +parameters remain bitwise fixed. + +Record one uncalibrated HFA reference and calibrate Q/R for 400 feedback-only +minibatches at `eta_A in {0.1, 1.0, 10.0}`. Select maximum early-third +teaching/negative-gradient cosine, then maximum all-layer cosine, then lower +rate. V4-1 passes only if all four records are finite and the selected method: + +1. reaches early-third alignment at least `0.05`; +2. reaches all-layer alignment at least `0.10`; +3. exceeds the matched uncalibrated HFA early alignment by at least `0.04`; +4. keeps every feedback/forward audit norm ratio in `[0.1, 3.0]`. + +The norm gate is audit-only and prevents a scale explosion from being hidden +by cosine. No extra rate, query count, initialization, warmup, normalization, +or per-layer schedule is added after endpoints are observed. + +## V4-2: bounded short accuracy gate + +Only a V4-1 pass opens a 10k-example, 20-epoch ResNet-20 validation screen. +Copy A2b exactly: batch 128, cosine decay, no warmup, momentum 0.9, weight decay +`1e-4`, output LR 0.1, and hidden LR `{0.03, 0.1}`. Use the V4-1-selected +feedback rate, 400 feedback-only warmup minibatches, then K1/every-4 joint +calibration. Select validation accuracy, then lower MACs/rate. + +Advance only if both runs are finite, selected V4 reaches at least 60%, exceeds +fixed HFA's 43.52% by at least 10 points, retains early alignment at least +0.05, and does not exceed `1.5x` exact-BP estimated MACs. Failure closes V4 +without a full run or an expanded grid. + +## V4-3: full validation and confirmation rule + +Only a V4-2 pass opens one 200-epoch seed-0 validation run on all 45,000 +development-training examples. Copy the selected hidden rate, output rate 0.1, +400-step feedback warmup, K1/every-4 joint calibration, and the A1 step drops +at epochs 100/150. It must be finite, finish within 5 points of BP 91.62%, beat +DFA 33.06% and fixed HFA 43.52% by at least 2 points, retain early alignment +0.05, and remain within `1.5x` BP MACs. There is no recovery branch. + +A complete V4-3 validation pass may move the strict reviewer forecast from 5 +to 6 and permits a new, separately frozen hierarchical-SDIL residualization +test. It does not itself establish Harnett-specific novelty. Only after that +innovation test passes may an independent ResNet-20/32/56 confirmation be +specified; no test endpoint is authorized by V4-1 or V4-2. + @@ -127,8 +127,8 @@ The learned-hierarchy development path perturbs the feedback convolution parameter spaces themselves. One antithetic pair estimates every edge's causal target moment simultaneously, while the current prediction moment is a local child/parent correlation. Its BatchNorm-coupled JVP and symmetric-limit delta -rule are executable smoke tests; no accuracy claim is attached until its own -causal-capture protocol is frozen and passed. +rule are executable smoke tests. `ORAL_A_V4.md` freezes the causal-capture and +conditional accuracy gates; no accuracy claim is attached until they pass. The subsequent V3 mechanism estimates the required A/G matrix statistics directly by perturbing the vectorizer parameter subspace. It remains diff --git a/experiments/analyze_oral_a_v4_calibration.py b/experiments/analyze_oral_a_v4_calibration.py new file mode 100644 index 0000000..b24ebda --- /dev/null +++ b/experiments/analyze_oral_a_v4_calibration.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +"""Audit and gate hierarchical feedback-parameter causal capture.""" +import argparse +import glob +import json +import math +import os + + +SPLIT_HASH = "8328b206a97c420e49e54e3eca4abe3274c4756b084355784ea3fb8059e4515b" +RATES = (0.1, 1.0, 10.0) + + +def load(path): + with open(path) as handle: + record = json.load(handle) + args = record["args"] + mode = args.get("mode") + if mode not in ("hfa", "lhfa"): + raise ValueError(f"{path}: unexpected mode") + expected = { + "depth": 20, "width": 16, "seed": 0, "loader_seed": 0, + "epochs": 0, "train_limit": 10000, "val_examples": 5000, + "split_seed": 2027, "eval_split": "validation", "eval_every": 0, + "normalization": "batchnorm", "a_scale": 1.0, + "pert_directions": 1, "pert_every": 4, "pert_sigma": 0.01, + "perturb_seed": 1000, "alignment_probe": 64, + } + for key, value in expected.items(): + if args.get(key) != value: + raise ValueError(f"{path}: {key} drift") + expected_warmup = 0 if mode == "hfa" else 400 + if args.get("a_warmup_steps") != expected_warmup: + raise ValueError(f"{path}: feedback warmup drift") + if mode == "lhfa" and float(args["eta_A"]) not in RATES: + raise ValueError(f"{path}: unregistered feedback rate") + if record["provenance"]["git_tracked_dirty"]: + raise ValueError(f"tracked-dirty result: {path}") + if record["split"]["validation_index_sha256"] != SPLIT_HASH: + raise ValueError(f"split drift: {path}") + if record["evaluation_protocol"]["test_evaluations"]: + raise ValueError(f"test touched: {path}") + expected_space = (None if mode == "hfa" + else "hierarchical_feedback_parameters") + if record.get("calibration_metric_space") != expected_space: + raise ValueError(f"metric-space drift: {path}") + diagnostics = record.get("diagnostics") + if diagnostics is None: + raise ValueError(f"missing diagnostics: {path}") + values = diagnostics["teaching_negative_gradient_cosine"] + early_count = max(1, len(values) // 3) + norm_ratios = diagnostics["feedback_forward_norm_ratio"] + feedback_cosines = diagnostics["feedback_forward_cosine"] + metrics = { + "early_third_alignment": sum(values[:early_count]) / early_count, + "all_layer_alignment": sum(values) / len(values), + "mean_feedback_forward_cosine": ( + sum(feedback_cosines) / len(feedback_cosines)), + "min_feedback_forward_norm_ratio": min(norm_ratios), + "max_feedback_forward_norm_ratio": max(norm_ratios), + } + warmup = record.get("apical_warmup", {}).get("mean") + if mode == "lhfa": + if warmup is None: + raise ValueError(f"missing calibration aggregate: {path}") + metrics.update({ + "mean_calibration_mse": warmup["calibration_mse"], + "mean_target_power": warmup["target_power"], + "mean_prediction_target_cosine": ( + warmup["prediction_target_cosine"]), + "mean_parameter_update_rms": warmup["parameter_update_rms"], + }) + finite = (bool(record["final"]["finite"]) + and all(math.isfinite(value) for value in metrics.values())) + return { + "path": path, "mode": mode, + "eta_A": (None if mode == "hfa" else float(args["eta_A"])), + "metrics": metrics, "finite": finite, + "logical_batch_loss_queries": int( + record["work"]["logical_batch_loss_queries"]), + "total_macs": int(record["work"]["total_macs_estimate"]), + "source_commit": record["provenance"]["git_commit"], + } + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("--input", default="results/oral_a_v4_calibration") + parser.add_argument( + "--out", default="results/oral_a_v4_calibration_gate.json") + args = parser.parse_args() + rows = [load(path) for path in sorted( + glob.glob(os.path.join(args.input, "*.json")))] + references = [row for row in rows if row["mode"] == "hfa"] + candidates = [row for row in rows if row["mode"] == "lhfa"] + if len(references) != 1 or len(candidates) != len(RATES): + raise ValueError("incomplete V4-1 method grid") + if {row["eta_A"] for row in candidates} != set(RATES): + raise ValueError("incomplete V4-1 rate grid") + if len({row["source_commit"] for row in rows}) != 1: + raise ValueError("V4-1 source commits differ") + eligible = [row for row in candidates if row["finite"]] + eligible.sort(key=lambda row: ( + -row["metrics"]["early_third_alignment"], + -row["metrics"]["all_layer_alignment"], row["eta_A"])) + selected = eligible[0] if eligible else None + reference = references[0] + checks = { + "all_four_records_finite": all(row["finite"] for row in rows), + "candidate_selected": selected is not None, + } + if selected is None: + checks.update({ + "early_third_at_least_0.05": False, + "all_layer_at_least_0.10": False, + "early_gain_over_fixed_hfa_at_least_0.04": False, + "feedback_norm_ratios_in_0.1_to_3": False, + }) + else: + metrics = selected["metrics"] + checks.update({ + "early_third_at_least_0.05": ( + metrics["early_third_alignment"] >= 0.05), + "all_layer_at_least_0.10": ( + metrics["all_layer_alignment"] >= 0.10), + "early_gain_over_fixed_hfa_at_least_0.04": ( + metrics["early_third_alignment"] + - reference["metrics"]["early_third_alignment"] >= 0.04), + "feedback_norm_ratios_in_0.1_to_3": ( + metrics["min_feedback_forward_norm_ratio"] >= 0.1 + and metrics["max_feedback_forward_norm_ratio"] <= 3.0), + }) + output = { + "protocol": "oral_a_v4_hierarchical_causal_capture_v1", + "status": "passed" if all(checks.values()) else "failed", + "checks": checks, "rows": rows, "matched_fixed_hfa": reference, + "selected_v4": selected, "confirmation_test_seeds_touched": False, + "review_score_before": 5, "review_score_after": 5, + "score_change_rule": "causal capture alone cannot raise score", + } + 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"], "checks": checks, + "reference": reference, "selected_v4": selected, + }, indent=2)) + + +if __name__ == "__main__": + main() + diff --git a/experiments/oral_a_v4_calibration_screen.py b/experiments/oral_a_v4_calibration_screen.py new file mode 100644 index 0000000..2e29bb6 --- /dev/null +++ b/experiments/oral_a_v4_calibration_screen.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python3 +"""Run a shard of the frozen hierarchical-feedback causal-capture screen.""" +import argparse +import os +import subprocess +import sys + + +def main(): + parser = argparse.ArgumentParser() + 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") + common = [ + sys.executable, "experiments/conv_run.py", "--device", args.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.1", + "--output_lr", "0.1", "--lr_schedule", "constant", + "--warmup_epochs", "0", "--momentum", "0.9", + "--weight_decay", "1e-4", "--normalization", "batchnorm", + "--a_scale", "1", "--pert_sigma", "0.01", + "--pert_directions", "1", "--pert_every", "4", + "--perturb_seed", "1000", "--alignment_probe", "64", + ] + jobs = [("fixed_hfa", common + [ + "--mode", "hfa", "--out", + "results/oral_a_v4_calibration/fixed_hfa.json", + ])] + for rate in (0.1, 1.0, 10.0): + tag = f"learned_hfa_etaA{rate}" + jobs.append((tag, common + [ + "--mode", "lhfa", "--eta_A", str(rate), + "--a_warmup_steps", "400", "--out", + f"results/oral_a_v4_calibration/{tag}.json", + ])) + os.makedirs("results/oral_a_v4_calibration", exist_ok=True) + for index, (tag, command) in enumerate(jobs): + if index % args.num_shards != args.shard_index: + continue + print(tag, " ".join(command), flush=True) + if not args.dry_run: + subprocess.run(command, check=True) + + +if __name__ == "__main__": + main() + diff --git a/sdil/conv.py b/sdil/conv.py index 55cdc62..ba4e980 100644 --- a/sdil/conv.py +++ b/sdil/conv.py @@ -748,11 +748,21 @@ def conv_hierarchical_alignment_report(net, x, y): for left, right in zip(teaching, negative_gradients)] for parameter in parameters: parameter.requires_grad_(False) + feedback_pairs = list(zip(net.Q[1:], net.W[1:])) + [ + (net.R_out, -net.W_out.t())] + feedback_forward_cosine = [float(F.cosine_similarity( + feedback.flatten(), target.flatten(), dim=0)) + for feedback, target in feedback_pairs] + feedback_forward_norm_ratio = [ + float(feedback.norm() / target.norm().clamp_min(1e-30)) + for feedback, target in feedback_pairs] return { "normalization_state": "training_batch_stats_without_running_update", "teaching_negative_gradient_cosine": values, "raw_negative_gradient_cosine": values, "innovation_negative_gradient_cosine": values, + "feedback_forward_cosine": feedback_forward_cosine, + "feedback_forward_norm_ratio": feedback_forward_norm_ratio, } |
