summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-07-26 12:27:52 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-07-26 12:27:52 -0500
commit00baa870b8b61a0974438233d9240eebb40c384f (patch)
treebae281f38fe7512a0e0ad46da43f6262eb3c94dc
parent8f9364e8ee2e2a19eb83b58452fa0e8cb155353c (diff)
experiment: freeze KP teaching-signal ablation
-rw-r--r--KP_TEACHING_SIGNAL_ABLATION.md83
-rw-r--r--experiments/analyze_kp_teaching_signal.py275
-rw-r--r--experiments/conv_local_smoke.py52
-rw-r--r--experiments/conv_run.py6
-rw-r--r--experiments/kp_teaching_signal_ablation.py130
-rw-r--r--sdil/conv.py14
6 files changed, 553 insertions, 7 deletions
diff --git a/KP_TEACHING_SIGNAL_ABLATION.md b/KP_TEACHING_SIGNAL_ABLATION.md
new file mode 100644
index 0000000..7b1207e
--- /dev/null
+++ b/KP_TEACHING_SIGNAL_ABLATION.md
@@ -0,0 +1,83 @@
+# KP teaching-signal ablation
+
+## Question
+
+The dynamic SDIL image model and clean KP use the same reciprocal KP credit
+transport. Their relevant local signals are
+
+```
+clean KP: u = s
+raw KP: u = a = s + t
+matched raw KP: u = ||r|| / ||a|| * a
+dynamic SDIL: u = r = s + t - prediction(t).
+```
+
+Here `s` is the ordinary clean KP instruction and `t` is four-RMS
+soma-predictable apical traffic. This experiment therefore changes neither
+the forward architecture nor the feedback-learning substrate. It asks
+whether the subtractive innovation operation is necessary to recover useful
+credit when the same KP substrate receives mixed apical traffic.
+
+Raw and norm-matched KP execute the same frozen 64-example slow fit and the
+same instruction-off fast neutral projection on every ordinary minibatch.
+They report the projection but do not apply its subtractive direction. Raw
+uses the mixed apical vector unchanged. Matched raw preserves that vector's
+direction and changes only its per-example norm to the projected innovation
+norm. Thus a matched-raw deficit cannot be attributed to a larger update
+norm, and neither deficit can be attributed to a cheaper observation budget
+for SDIL.
+
+This is a conditional robustness intervention, not a claim that dynamic SDIL
+outperforms clean KP when no nuisance traffic is present.
+
+## KTS-0: mechanics
+
+Before any endpoint launch, the CPU mechanics test must establish:
+
+- raw with the sham projection is bitwise equal to `s + t`;
+- matched raw has projected innovation's per-example norm to relative error
+ below `1e-12` and raw's direction to error below `1e-12`;
+- raw, matched, and innovation receive the identical instruction-off
+ projection report;
+- the projection observes zero task-instruction examples and changes no slow
+ predictor parameter;
+- all three rules form their local KP forward and reciprocal correlations from
+ the selected used vector without reverse-mode differentiation.
+
+## KTS-1: frozen validation endpoint
+
+KTS-1 inherits the passed
+`results/kp_dynamic_projection_full_gate.json` endpoint without rerunning or
+selecting it: clean KP is 91.26% and dynamic SDIL is 91.18% on the frozen
+45,000/5,000 CIFAR-10 split. The only new endpoints are `raw` and `matched`.
+Use ResNet-20, width 16, model/data-loader/traffic seed 0/0/4000, batch 128,
+200 epochs, augmentation, batch normalization, SGD momentum 0.9, weight decay
+`1e-4`, learning rate 0.1 with 0.1 drops at epochs 100 and 150, traffic ratio
+4, one closed-form 64-example predictor fit, a frozen predictor thereafter,
+and the fast instruction-off projection on every task batch. Evaluate the
+validation split exactly once at the endpoint and never evaluate test.
+
+Both jobs must share one clean tracked source revision containing this
+protocol, its runner, analyzer, mechanics test, and the sham-control
+implementation. No endpoint may be deleted, replaced, or relaunched under
+the same name. A nonfinite trajectory is retained as a terminal control
+collapse, not repaired by lowering the learning rate or traffic strength.
+
+The mechanism advantage passes only if every provenance, split, argument,
+traffic, observation, query, projection, norm-control, and cost invariant
+passes and dynamic SDIL exceeds raw by at least 5 percentage points and
+matched raw by at least 3 percentage points. A nonfinite control counts as a
+performance/stability failure of that control, while the raw record and its
+failure location remain part of the evidence. A finite control must have a
+valid endpoint accuracy. Every job must use zero task-loss queries, one
+64-example slow fit, one instruction-off projection observation per ordinary
+example, no predictor updates during task learning, at most 1.34 times the
+frozen BP affine-MAC estimate, and separately reported positive elementwise
+work.
+
+Passing KTS-1 establishes that innovation subtraction, rather than KP
+transport alone or signal-norm rescaling, is necessary under the specified
+soma-predictable traffic. It does not establish superiority to Dual Prop,
+BP, or clean KP in the clean setting. Multi-seed and cross-architecture
+confirmation remain subsequent gates.
+
diff --git a/experiments/analyze_kp_teaching_signal.py b/experiments/analyze_kp_teaching_signal.py
new file mode 100644
index 0000000..db8649a
--- /dev/null
+++ b/experiments/analyze_kp_teaching_signal.py
@@ -0,0 +1,275 @@
+#!/usr/bin/env python3
+"""Audit the frozen KTS-1 raw-KP teaching-signal controls."""
+import argparse
+import hashlib
+import json
+import math
+import os
+import statistics
+import subprocess
+
+from kp_teaching_signal_ablation import ROOT, RULES, command
+
+
+def require(condition, message):
+ if not condition:
+ raise ValueError(message)
+
+
+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 close(left, right, tolerance=1e-12):
+ return abs(float(left) - float(right)) <= tolerance
+
+
+def finite(value):
+ return isinstance(value, (int, float)) and math.isfinite(value)
+
+
+def same_scalar(left, right, tolerance=1e-7):
+ left = float(left)
+ right = float(right)
+ if math.isnan(left) or math.isnan(right):
+ return math.isnan(left) and math.isnan(right)
+ if math.isinf(left) or math.isinf(right):
+ return left == right
+ return abs(left - right) <= tolerance
+
+
+def same_list(left, right, tolerance=1e-7):
+ return len(left) == len(right) and all(
+ same_scalar(a, b, tolerance) for a, b in zip(left, right))
+
+
+def validate_record(path, rule, source_commit, bp_reference_macs):
+ with open(path) as handle:
+ row = json.load(handle)
+ require(row["provenance"]["git_commit"] == source_commit,
+ f"{path}: source commit")
+ require(row["provenance"]["git_tracked_dirty"] is False,
+ f"{path}: dirty source")
+ expected = command("cuda", rule, path)[2:-2]
+ expected_args = {}
+ for index in range(0, len(expected), 2):
+ key = expected[index].removeprefix("--")
+ expected_args[key] = expected[index + 1]
+ actual = row["args"]
+ numeric = {
+ "depth": int, "width": int, "seed": int, "loader_seed": int,
+ "batch_size": int, "epochs": int, "train_limit": int,
+ "val_examples": int, "split_seed": int, "eval_every": int,
+ "augment_train": int, "warmup_epochs": int, "learn_P": int,
+ "predictor_warmup_steps": int, "predictor_every": int,
+ "neutral_projection": int, "traffic_seed": int,
+ "traffic_calibration_examples": int, "alignment_probe": int,
+ "lr_gamma": float, "momentum": float, "weight_decay": float,
+ "lr": float, "output_lr": float, "eta_P": float,
+ "traffic_ratio": float,
+ }
+ for key, expected_value in expected_args.items():
+ if key in ("device", "out"):
+ continue
+ caster = numeric.get(key, str)
+ require(actual[key] == caster(expected_value),
+ f"{path}: argument {key}")
+
+ require(row["split"]["train_examples"] == 45000, f"{path}: train split")
+ require(row["split"]["validation_examples"] == 5000,
+ f"{path}: validation split")
+ require(row["split"]["test_examples"] == 10000, f"{path}: test split")
+ require(row["evaluation_protocol"] == {
+ "validation_evaluations": 1,
+ "test_evaluations": 0,
+ "test_used_for_selection": False,
+ }, f"{path}: evaluation protocol")
+ counters = row["counters"]
+ require(counters["ordinary_examples"] == 9_000_000,
+ f"{path}: ordinary observations")
+ require(counters["neutral_projection_examples"]
+ == counters["ordinary_examples"],
+ f"{path}: projection observation budget")
+ require(counters["predictor_update_examples"] == 64,
+ f"{path}: slow fit observations")
+ require(counters["logical_batch_loss_queries"] == 0,
+ f"{path}: task loss queries")
+ require(counters["causal_scalar_observations"] == 0,
+ f"{path}: causal observations")
+ require(row["work"]["neutral_projection_observations"]
+ == counters["ordinary_examples"],
+ f"{path}: reported projection observations")
+ require(row["work"]["elementwise_operations_estimate"] > 0,
+ f"{path}: elementwise work")
+ require(row["work"]["total_macs_estimate"]
+ <= 1.34 * bp_reference_macs,
+ f"{path}: MAC ceiling")
+
+ calibration = row["traffic_calibration"]
+ require(max(abs(float(value) - 4.0) for value in
+ calibration["realized_traffic_instruction_rms_ratio"])
+ <= 1e-5, f"{path}: traffic ratio")
+ warmup = row["predictor_warmup"]
+ require(warmup["mode"] == "closed_form" and warmup["examples"] == 64,
+ f"{path}: closed-form fit")
+ require(warmup["closed_form_fit"]["stability_margin"] == 0.0,
+ f"{path}: predictor margin")
+
+ projections = [epoch["neutral_projection"] for epoch in row["epochs"]]
+ require(all(value["instruction_observations"] == 0
+ for value in projections),
+ f"{path}: projection instruction leakage")
+ require(min(value["minimum_observations"] for value in projections) > 0
+ and max(value["maximum_observations"]
+ for value in projections) <= 128,
+ f"{path}: projection batch observations")
+ require(max(value["maximum_post_projection_traffic_rms_ratio"]
+ for value in projections) <= 1e-5,
+ f"{path}: projected traffic remainder")
+ require(max(value["maximum_absolute_post_projection_soma_slope"]
+ for value in projections) <= 1e-5,
+ f"{path}: projected soma slope")
+
+ diagnostics = row["diagnostics"]
+ if rule == "raw":
+ require(same_list(
+ diagnostics["used_negative_gradient_cosine"],
+ diagnostics["raw_negative_gradient_cosine"]),
+ f"{path}: raw direction")
+ else:
+ require(same_list(
+ diagnostics["used_negative_gradient_cosine"],
+ diagnostics["matched_negative_gradient_cosine"]),
+ f"{path}: matched direction")
+ if row["final"]["finite"]:
+ require(diagnostics["max_norm_match_relative_error"] <= 1e-6,
+ f"{path}: matched norm")
+ require(diagnostics["max_norm_match_direction_error"] <= 1e-6,
+ f"{path}: matched direction preservation")
+
+ for epoch in row["epochs"]:
+ mixed = epoch["mixed_apical"]
+ if all(finite(value) for value in mixed.values()):
+ if rule == "raw":
+ require(close(
+ mixed["teaching_rms"], mixed["raw_apical_rms"], 1e-10),
+ f"{path}: epoch raw signal")
+ else:
+ require(close(
+ mixed["teaching_rms"], mixed["innovation_rms"], 1e-9),
+ f"{path}: epoch matched norm")
+ require(epoch["neutral_projection"]["instruction_observations"] == 0,
+ f"{path}: epoch projection leakage")
+ accuracy = row["final"]["accuracy"]
+ require(finite(accuracy) and 0.0 <= accuracy <= 1.0,
+ f"{path}: endpoint accuracy")
+ return row
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--outdir", default="results/kp_teaching_signal_ablation")
+ parser.add_argument(
+ "--dynamic",
+ default="results/kp_dynamic_projection_full/dynamic.json")
+ parser.add_argument(
+ "--prerequisite",
+ default="results/kp_dynamic_projection_full_gate.json")
+ parser.add_argument(
+ "--out", default="results/kp_teaching_signal_ablation_gate.json")
+ args = parser.parse_args()
+
+ tracked_dirty = subprocess.run(
+ ["git", "status", "--porcelain", "--untracked-files=no"],
+ cwd=ROOT, check=True, capture_output=True, text=True).stdout.strip()
+ require(not tracked_dirty, "analysis requires a clean tracked source")
+ source_commit = subprocess.run(
+ ["git", "rev-parse", "HEAD"], cwd=ROOT, check=True,
+ capture_output=True, text=True).stdout.strip()
+ with open(args.prerequisite) as handle:
+ prerequisite = json.load(handle)
+ require(prerequisite["protocol"]
+ == "kp_dynamic_neutral_projection_full_v1",
+ "wrong prerequisite protocol")
+ require(prerequisite["status"] == "passed", "prerequisite did not pass")
+ with open(args.dynamic) as handle:
+ dynamic = json.load(handle)
+ require(close(dynamic["final"]["accuracy"],
+ prerequisite["metrics"]["accuracy"]),
+ "dynamic record/gate mismatch")
+
+ rows = {
+ rule: validate_record(
+ os.path.join(args.outdir, f"{rule}.json"), rule, source_commit,
+ prerequisite["metrics"]["bp_total_macs"])
+ for rule in RULES
+ }
+ dynamic_accuracy = float(dynamic["final"]["accuracy"])
+ raw_accuracy = float(rows["raw"]["final"]["accuracy"])
+ matched_accuracy = float(rows["matched"]["final"]["accuracy"])
+ raw_gap = dynamic_accuracy - raw_accuracy
+ matched_gap = dynamic_accuracy - matched_accuracy
+ raw_finite = bool(rows["raw"]["final"]["finite"])
+ matched_finite = bool(rows["matched"]["final"]["finite"])
+ checks = {
+ "two_frozen_controls_present": len(rows) == 2,
+ "dynamic_reference_is_finite": bool(dynamic["final"]["finite"]),
+ "raw_degrades_or_collapses_by_at_least_5_points": (
+ (not raw_finite) or raw_gap >= 0.05),
+ "matched_degrades_or_collapses_by_at_least_3_points": (
+ (not matched_finite) or matched_gap >= 0.03),
+ "raw_and_matched_use_identical_source": (
+ len({row["provenance"]["git_commit"]
+ for row in rows.values()}) == 1),
+ "all_control_invariants_audited": True,
+ }
+ status = "passed" if all(checks.values()) else "failed"
+ report = {
+ "protocol": "kp_teaching_signal_ablation_validation_v1",
+ "status": status,
+ "checks": checks,
+ "metrics": {
+ "dynamic_accuracy": dynamic_accuracy,
+ "clean_kp_accuracy": prerequisite["metrics"][
+ "clean_kp_accuracy"],
+ "raw_accuracy": raw_accuracy,
+ "matched_accuracy": matched_accuracy,
+ "dynamic_minus_raw_points": 100.0 * raw_gap,
+ "dynamic_minus_matched_points": 100.0 * matched_gap,
+ "raw_finite": raw_finite,
+ "matched_finite": matched_finite,
+ "control_mean_wall_s": statistics.mean(
+ row["timing"]["total_timed_wall_s"]
+ for row in rows.values()),
+ "source_commit": source_commit,
+ "prerequisite_sha256": sha256(args.prerequisite),
+ },
+ "claim": (
+ "Under four-RMS soma-predictable apical traffic, innovation "
+ "subtraction is necessary relative to both raw and norm-matched "
+ "KP controls."
+ if status == "passed" else
+ "The frozen validation endpoint does not establish a KP "
+ "teaching-signal advantage."
+ ),
+ "scope_limit": (
+ "This is a conditional robustness result on one validation seed; "
+ "it is not clean-setting or strong-baseline superiority."
+ ),
+ }
+ os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
+ with open(args.out, "w") as handle:
+ json.dump(report, handle, indent=2, sort_keys=True)
+ handle.write("\n")
+ print(json.dumps(report, indent=2, sort_keys=True))
+ if status != "passed":
+ raise SystemExit(1)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/experiments/conv_local_smoke.py b/experiments/conv_local_smoke.py
index cdf1ad0..1b18048 100644
--- a/experiments/conv_local_smoke.py
+++ b/experiments/conv_local_smoke.py
@@ -1023,6 +1023,51 @@ def kp_mixed_traffic_checks():
assert all(torch.equal(before, after) for before, after in zip(
frozen_before_projection, net.P_traffic + net.P_traffic_bias))
+ # Raw and matched controls pay for the identical neutral projection but do
+ # not apply its subtractive direction. Raw must remain exactly the mixed
+ # apical vector. Matched may borrow only projected innovation's norm.
+ projected_raw = net.mixed_apical_components(
+ instruction, forward["hiddens"], "raw",
+ neutral_projection=True)
+ projected_matched = net.mixed_apical_components(
+ instruction, forward["hiddens"], "matched",
+ neutral_projection=True)
+ sham_raw_error = max(float((left - right).abs().max())
+ for left, right in zip(
+ projected_raw["used"], projected_raw["raw"]))
+ sham_projection_report_error = max(
+ abs(projected_raw["neutral_projection"][key]
+ - projected_matched["neutral_projection"][key])
+ for key in (
+ "pre_projection_traffic_rms_ratio",
+ "post_projection_traffic_rms_ratio",
+ "max_absolute_pre_projection_soma_slope",
+ "max_absolute_post_projection_soma_slope",
+ "max_positive_post_projection_soma_slope",
+ "min_post_projection_soma_slope",
+ "max_absolute_correction_slope",
+ ))
+ sham_matched_norm_errors = []
+ sham_matched_direction_errors = []
+ for raw, innovation, matched in zip(
+ projected_matched["raw"], projected_matched["innovation"],
+ projected_matched["matched"]):
+ raw_flat = raw.flatten(1)
+ innovation_flat = innovation.flatten(1)
+ matched_flat = matched.flatten(1)
+ sham_matched_norm_errors.append(float((
+ (matched_flat.norm(dim=1) - innovation_flat.norm(dim=1)).abs()
+ / innovation_flat.norm(dim=1).clamp_min(1e-30)).max()))
+ sham_matched_direction_errors.append(float((F.cosine_similarity(
+ raw_flat, matched_flat, dim=1) - 1.0).abs().max()))
+ assert sham_raw_error == 0.0
+ assert sham_projection_report_error == 0.0
+ assert max(sham_matched_norm_errors) < 1e-12
+ assert max(sham_matched_direction_errors) < 1e-12
+ assert projected_raw["neutral_projection"]["instruction_observations"] == 0
+ assert all(torch.equal(before, after) for before, after in zip(
+ frozen_before_projection, net.P_traffic + net.P_traffic_bias))
+
for slope, bias in zip(net.P_traffic, net.P_traffic_bias):
slope.zero_()
bias.zero_()
@@ -1142,6 +1187,13 @@ def kp_mixed_traffic_checks():
"post_projection_traffic_rms_ratio"],
"kp_traffic_projected_residual_slope": projection[
"max_absolute_post_projection_soma_slope"],
+ "kp_traffic_sham_raw_error": sham_raw_error,
+ "kp_traffic_sham_projection_report_error": (
+ sham_projection_report_error),
+ "kp_traffic_sham_matched_norm_error": max(
+ sham_matched_norm_errors),
+ "kp_traffic_sham_matched_direction_error": max(
+ sham_matched_direction_errors),
"kp_traffic_matched_norm_error": max(norm_errors),
"kp_traffic_matched_direction_error": max(direction_errors),
"kp_traffic_reciprocal_correlation_error": max(correlation_errors),
diff --git a/experiments/conv_run.py b/experiments/conv_run.py
index b1372f5..15510c8 100644
--- a/experiments/conv_run.py
+++ b/experiments/conv_run.py
@@ -261,13 +261,13 @@ def run(args):
if args.traffic_calibration_examples < 1 or args.traffic_ratio <= 0:
raise ValueError("invalid mixed-traffic calibration")
if args.neutral_projection:
- if (args.traffic_rule != "innovation"
- or args.predictor_mode != "closed_form"
+ if (args.predictor_mode != "closed_form"
or args.predictor_warmup_steps != 1
or args.predictor_every != 0):
raise ValueError(
"neutral projection requires frozen one-step closed-form "
- "innovation prediction")
+ "prediction; raw and matched rules compute it as a sham "
+ "control without applying the subtractive direction")
elif args.predictor_every < 1:
raise ValueError("mixed-traffic KP requires a predictor cadence")
if (args.predictor_mode == "closed_form"
diff --git a/experiments/kp_teaching_signal_ablation.py b/experiments/kp_teaching_signal_ablation.py
new file mode 100644
index 0000000..537b262
--- /dev/null
+++ b/experiments/kp_teaching_signal_ablation.py
@@ -0,0 +1,130 @@
+#!/usr/bin/env python3
+"""Run the frozen raw-KP controls for the KTS-1 validation endpoint."""
+import argparse
+import hashlib
+import json
+import os
+import subprocess
+
+
+ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
+PROTOCOL = os.path.join(ROOT, "KP_TEACHING_SIGNAL_ABLATION.md")
+PREREQUISITE = os.path.join(
+ ROOT, "results", "kp_dynamic_projection_full_gate.json")
+RULES = ("raw", "matched")
+
+
+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_source():
+ with open(PREREQUISITE) as handle:
+ gate = json.load(handle)
+ if not (
+ gate.get("protocol") == "kp_dynamic_neutral_projection_full_v1"
+ and gate.get("status") == "passed"
+ and gate.get("independent_confirmation_opened") is True
+ ):
+ raise RuntimeError("KTS-1 requires the passed dynamic validation gate")
+ required = [
+ PROTOCOL,
+ os.path.abspath(__file__),
+ os.path.join(ROOT, "experiments", "analyze_kp_teaching_signal.py"),
+ os.path.join(ROOT, "experiments", "conv_local_smoke.py"),
+ os.path.join(ROOT, "experiments", "conv_run.py"),
+ os.path.join(ROOT, "sdil", "conv.py"),
+ ]
+ for path in required:
+ relative = os.path.relpath(path, ROOT)
+ subprocess.run(
+ ["git", "ls-files", "--error-unmatch", relative],
+ cwd=ROOT, check=True, capture_output=True)
+ if git_output("status", "--porcelain", "--untracked-files=no"):
+ raise RuntimeError("KTS-1 requires a clean tracked source")
+ return {
+ "git_commit": git_output("rev-parse", "HEAD"),
+ "protocol_sha256": sha256(PROTOCOL),
+ "prerequisite_sha256": sha256(PREREQUISITE),
+ }
+
+
+def command(device, rule, path):
+ return [
+ "python", "experiments/conv_run.py",
+ "--device", device,
+ "--depth", "20",
+ "--width", "16",
+ "--seed", "0",
+ "--loader_seed", "0",
+ "--batch_size", "128",
+ "--epochs", "200",
+ "--train_limit", "0",
+ "--val_examples", "5000",
+ "--split_seed", "2027",
+ "--eval_split", "validation",
+ "--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",
+ "--mode", "kp_traffic",
+ "--lr", "0.1",
+ "--output_lr", "0.1",
+ "--learn_P", "1",
+ "--eta_P", "0.1",
+ "--predictor_mode", "closed_form",
+ "--predictor_warmup_steps", "1",
+ "--predictor_every", "0",
+ "--neutral_projection", "1",
+ "--traffic_rule", rule,
+ "--traffic_ratio", "4.0",
+ "--traffic_seed", "4000",
+ "--traffic_calibration_examples", "64",
+ "--alignment_probe", "32",
+ "--out", path,
+ ]
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument("--device", default="cuda")
+ parser.add_argument("--rule", choices=("all",) + RULES, default="all")
+ parser.add_argument(
+ "--outdir", default="results/kp_teaching_signal_ablation")
+ parser.add_argument("--dry-run", action="store_true")
+ args = parser.parse_args()
+ source = require_source()
+ rules = RULES if args.rule == "all" else (args.rule,)
+ os.makedirs(args.outdir, exist_ok=True)
+ for rule in rules:
+ path = os.path.join(args.outdir, f"{rule}.json")
+ if os.path.exists(path):
+ raise RuntimeError(f"refusing to overwrite frozen endpoint: {path}")
+ cmd = command(args.device, rule, path)
+ print(json.dumps({
+ "rule": rule,
+ "command": cmd,
+ "source": source,
+ }, sort_keys=True), flush=True)
+ if not args.dry_run:
+ subprocess.run(cmd, cwd=ROOT, check=True)
+
+
+if __name__ == "__main__":
+ main()
+
diff --git a/sdil/conv.py b/sdil/conv.py
index ebb29ab..1034202 100644
--- a/sdil/conv.py
+++ b/sdil/conv.py
@@ -820,7 +820,16 @@ class CIFARKPMixedTrafficResNet(CIFARKPResNet):
def mixed_apical_components(self, instruction, hiddens, rule,
compute_matched=False,
neutral_projection=False):
- """Return used, raw, innovation, matched, and traffic fields."""
+ """Return used, raw, innovation, matched, and traffic fields.
+
+ When ``neutral_projection`` is enabled, every rule pays for and
+ reports the same instruction-off projection. Only the innovation
+ rule applies the subtractive direction. Raw keeps the unmodified
+ apical vector, while matched keeps its direction and borrows only the
+ projected innovation norm. This makes raw and matched strict sham
+ controls for the subtractive operation without changing their local
+ information or observation budget.
+ """
if rule not in ("raw", "matched", "innovation"):
raise ValueError(f"unknown mixed-traffic rule: {rule}")
if not (len(instruction) == len(hiddens) == self.n_hidden):
@@ -829,9 +838,6 @@ class CIFARKPMixedTrafficResNet(CIFARKPResNet):
projected_neutral = None
projection_report = None
if neutral_projection:
- if rule != "innovation":
- raise ValueError(
- "neutral projection is defined only for innovation")
projected_neutral, projection_report = (
self.neutral_residual_projection(hiddens))
raw = []