summaryrefslogtreecommitdiff
path: root/external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch
diff options
context:
space:
mode:
Diffstat (limited to 'external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch')
-rw-r--r--external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch242
1 files changed, 242 insertions, 0 deletions
diff --git a/external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch b/external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch
new file mode 100644
index 0000000..896350b
--- /dev/null
+++ b/external/dualprop_patches/0004-crossover-add-architecture-compatible-PEPITA.patch
@@ -0,0 +1,242 @@
+From 3bfbe71fcdc138764408a5e1646b66c464308b8b Mon Sep 17 00:00:00 2001
+From: YurenHao0426 <Blackhao0426@gmail.com>
+Date: Mon, 27 Jul 2026 13:09:05 -0500
+Subject: [PATCH 04/19] crossover: add architecture-compatible PEPITA
+
+---
+ config/cli_config.py | 8 ++++--
+ src/models.py | 37 ++++++++++++++++++++++++++++
+ src/training_utils.py | 50 ++++++++++++++++++++++++++++++++++++--
+ tests/local_rules_smoke.py | 32 ++++++++++++++++++++++++
+ train.py | 3 ++-
+ 5 files changed, 125 insertions(+), 5 deletions(-)
+
+diff --git a/config/cli_config.py b/config/cli_config.py
+index e1756e1..3088436 100644
+--- a/config/cli_config.py
++++ b/config/cli_config.py
+@@ -42,12 +42,16 @@ parser.add_argument('--experiment-name', default='test', help='A string denoting
+
+ parser.add_argument('--model', default='VGG16', choices=['VGG16', 'VGGlike', 'CNN', 'miniCNN', 'MLP'], help='')
+
+-parser.add_argument('--learning-algorithm', default='dualprop-lagr-ff', choices=['backprop', 'fa', 'dfa', 'dualprop-lagr-ff', 'dualprop-raovr-ff', 'dualprop-raovr-dampened-ff'])
++parser.add_argument('--learning-algorithm', default='dualprop-lagr-ff', choices=['backprop', 'fa', 'dfa', 'pepita', 'dualprop-lagr-ff', 'dualprop-raovr-ff', 'dualprop-raovr-dampened-ff'])
+
+ parser.add_argument(
+ '--feedback-seed', default=1729, type=int,
+ help='Independent fixed-feedback initialization seed for FA/DFA.')
+
++parser.add_argument(
++ '--pepita-projection-scale', default=0.05, type=float,
++ help='Multiplier on PEPITA He-uniform output-error-to-input projection.')
++
+ parser.add_argument(
+ '--gradient-diagnostics', default='full', choices=['none', 'full'],
+ help=('Compute the exact BP reference gradient and layerwise cosine on '
+@@ -128,7 +132,7 @@ elif config.model == "MLP":
+ dense_features = [1024, 1024, config.num_classes]
+
+ # Load model
+-modeltype = {"backprop":cnn_abstract, "fa": cnn_abstract, "dfa": cnn_abstract, "dualprop-lagr-ff": cnn_dualprop_Lagr_ff, "dualprop-raovr-ff": cnn_dualprop_RAOVR_ff, "dualprop-raovr-dampened-ff": cnn_dualprop_RAOVR_dampened_ff}
++modeltype = {"backprop":cnn_abstract, "fa": cnn_abstract, "dfa": cnn_abstract, "pepita": cnn_abstract, "dualprop-lagr-ff": cnn_dualprop_Lagr_ff, "dualprop-raovr-ff": cnn_dualprop_RAOVR_ff, "dualprop-raovr-dampened-ff": cnn_dualprop_RAOVR_dampened_ff}
+ activation={"relu": relu, "hs": hs, "sigmoid": sigmoid, "tanh": tanh}
+ config.model = modeltype[config.learning_algorithm](loss_func, Conv, Dense, activation[config.activation], config.num_classes, config.beta, config.alpha, config.dtype, config.param_dtype,
+ kernels=kernels, strides=strides, features=features, mp = mp,
+diff --git a/src/models.py b/src/models.py
+index edc432c..127268a 100644
+--- a/src/models.py
++++ b/src/models.py
+@@ -194,6 +194,43 @@ class cnn_abstract(nn.Module, ABC):
+ _, linear_pullback = jax.vjp(linear_map, s[i])
+ fields[i] = linear_pullback(linear_field)[0]
+ return fields
++
++ def pepita_correlation_objective(self, modulated_states, original_linear,
++ modulated_linear, one_hot):
++ """Architecture-compatible PEPITA/ERIN two-presentation update.
++
++ Hidden fields are the first-minus-second post-ReLU activity
++ differences from the PEPITA equation. They multiply the modulated
++ presynaptic state directly, without differentiating a nonlinearity.
++ Convolutional correlations follow the reference code's additional
++ average over spatial positions. The readout uses the second-pass
++ softmax error.
++ """
++ objective = 0.0
++ batch_size = modulated_states[0].shape[0]
++ for i, layer in enumerate(self.layers):
++ x = jax.lax.stop_gradient(modulated_states[i])
++ if i == self.num_layers - 1:
++ field = jax.lax.stop_gradient(
++ jax.nn.softmax(modulated_states[-1], axis=-1) - one_hot)
++ prediction = layer(x)
++ objective += jnp.sum(prediction * field) / batch_size
++ elif i < self.num_convlayers:
++ field = jax.lax.stop_gradient(
++ self.act(original_linear[i])
++ - self.act(modulated_linear[i]))
++ prediction = layer.call_without_pooling(x)
++ spatial_positions = prediction.shape[1] * prediction.shape[2]
++ objective += (
++ jnp.sum(prediction * field)
++ / (batch_size * spatial_positions))
++ else:
++ field = jax.lax.stop_gradient(
++ self.act(original_linear[i])
++ - self.act(modulated_linear[i]))
++ prediction = layer(x)
++ objective += jnp.sum(prediction * field) / batch_size
++ return objective
+
+ def init_states_to_zero(self, x0):
+ s = [x0]
+diff --git a/src/training_utils.py b/src/training_utils.py
+index 2b6b18a..f5fae62 100644
+--- a/src/training_utils.py
++++ b/src/training_utils.py
+@@ -219,17 +219,23 @@ def create_train_state(rng, model, image_dims, lr, wlr, lrf, momentum, weight_de
+
+
+ def create_local_feedback(rng, model, params, image_dims, learning_algorithm,
+- num_classes):
++ num_classes, pepita_projection_scale=0.05):
+ """Create fixed feedback without reading a forward parameter value.
+
+ FA uses an independently initialized model-shaped linear parameter tree.
+ DFA uses one independent output-to-hidden tensor per hidden population.
+ The forward tree is used only to infer activation shapes for DFA.
+ """
+- if learning_algorithm not in ("fa", "dfa"):
++ if learning_algorithm not in ("fa", "dfa", "pepita"):
+ return None
+ w, h, channels = image_dims
+ dummy = jnp.ones([1, w, h, channels])
++ if learning_algorithm == "pepita":
++ input_units = w * h * channels
++ limit = jnp.sqrt(6.0 / input_units) * pepita_projection_scale
++ return jax.random.uniform(
++ rng, (num_classes, w, h, channels),
++ minval=-limit, maxval=limit, dtype=dummy.dtype)
+ if learning_algorithm == "fa":
+ return unfreeze(model.init(rng, dummy)["params"])
+
+@@ -301,6 +307,10 @@ def train_epoch(state, train_ds, batch_size, rng, augmentation_on,
+ state, local_feedback, learning_algorithm, image,
+ labels_onehot, labels, batch_rng, augmentation_on,
+ gradient_diagnostics)
++ elif learning_algorithm == "pepita":
++ state, metrics = train_step_pepita(
++ state, local_feedback, image, labels_onehot, labels,
++ batch_rng, augmentation_on, gradient_diagnostics)
+ elif learning_algorithm != "backprop":
+ state, metrics = train_step(
+ state, image, labels_onehot, labels, batch_rng, inf_rng,
+@@ -332,6 +342,42 @@ def direct_feedback_fields(states, output_field, direct_feedback):
+ return fields
+
+
++@jax.jit
++def train_step_pepita(state, input_feedback, image, labels_onehot, labels,
++ batch_rng, augmentation_on, gradient_diagnostics):
++ """One PEPITA step: ordinary forward, error-modulated forward, local rule."""
++ image = jax.lax.cond(
++ augmentation_on, vmap_augment_train, no_aug, image, batch_rng)
++ states, linear = state.apply_fn(
++ {"params": state.params}, image, method="ff_with_local_cache")
++ output_error = jax.lax.stop_gradient(
++ jax.nn.softmax(states[-1], axis=-1) - labels_onehot)
++ input_error = jnp.tensordot(
++ output_error, input_feedback, axes=((-1,), (0,)))
++ modulated_image = jax.lax.stop_gradient(image + input_error)
++ modulated_states, modulated_linear = state.apply_fn(
++ {"params": state.params}, modulated_image,
++ method="ff_with_local_cache")
++ states = tree_map(jax.lax.stop_gradient, states)
++ linear = tree_map(jax.lax.stop_gradient, linear)
++ modulated_states = tree_map(jax.lax.stop_gradient, modulated_states)
++ modulated_linear = tree_map(jax.lax.stop_gradient, modulated_linear)
++
++ def local_objective(params):
++ return state.apply_fn(
++ {"params": params}, modulated_states, linear, modulated_linear,
++ labels_onehot, method="pepita_correlation_objective")
++
++ grads = jax.grad(local_objective)(state.params)
++ metrics = compute_metrics(
++ image=image, labels_onehot=labels_onehot, labels=labels, state=state)
++ metrics = jax.lax.cond(
++ gradient_diagnostics, ref_grad_and_angle, no_ref_grad_and_angle,
++ state, grads, image, labels_onehot, metrics)
++ state = state.apply_gradients(grads=grads)
++ return state, metrics
++
++
+ @partial(jax.jit, static_argnames=("learning_algorithm",))
+ def train_step_local(state, local_feedback, learning_algorithm, image,
+ labels_onehot, labels, batch_rng, augmentation_on,
+diff --git a/tests/local_rules_smoke.py b/tests/local_rules_smoke.py
+index 2366e61..e7da52e 100644
+--- a/tests/local_rules_smoke.py
++++ b/tests/local_rules_smoke.py
+@@ -123,12 +123,44 @@ def main():
+ )
+ assert all(bool(jnp.all(jnp.isfinite(field))) for field in dfa_fields)
+
++ pepita_projection = create_local_feedback(
++ jax.random.PRNGKey(1731), model, params, (8, 8, 2), "pepita", 3,
++ pepita_projection_scale=0.05)
++ pepita_error = jax.nn.softmax(states[-1], axis=-1) - one_hot
++ modulated_x = x + jnp.tensordot(
++ pepita_error, pepita_projection, axes=((-1,), (0,)))
++ modulated_states, modulated_linear = model.apply(
++ {"params": params}, modulated_x, method="ff_with_local_cache")
++ pepita_grads = jax.grad(
++ lambda candidate: model.apply(
++ {"params": candidate}, modulated_states, linear,
++ modulated_linear, one_hot,
++ method="pepita_correlation_objective")
++ )(params)
++ output_error = jax.nn.softmax(modulated_states[-1], axis=-1) - one_hot
++ output_input = modulated_states[-2].reshape((x.shape[0], -1))
++ manual_kernel = output_input.T @ output_error / x.shape[0]
++ manual_bias = output_error.mean(axis=0)
++ readout_leaves = jax.tree_util.tree_leaves(pepita_grads["d00"])
++ readout_kernel = next(value for value in readout_leaves if value.ndim == 2)
++ readout_bias = next(value for value in readout_leaves if value.ndim == 1)
++ pepita_readout_error = max(
++ float(jnp.max(jnp.abs(readout_kernel - manual_kernel))),
++ float(jnp.max(jnp.abs(readout_bias - manual_bias))),
++ )
++ assert pepita_readout_error < 2e-12, pepita_readout_error
++ projection_limit = (6.0 / (8 * 8 * 2)) ** 0.5 * 0.05
++ assert float(jnp.max(jnp.abs(pepita_projection))) <= projection_limit
++ assert bool(jnp.all(jnp.isfinite(flat(pepita_grads))))
++
+ report = {
+ "symmetric_fa_bp_relative_error": symmetric_error,
+ "independent_feedback_forward_cosine": feedback_cosine,
+ "feedback_independence_error": feedback_independence_error,
+ "detached_local_boundary_error": local_boundary_error,
+ "dfa_hidden_maps": len(direct),
++ "pepita_readout_equation_max_error": pepita_readout_error,
++ "pepita_projection_shape": list(pepita_projection.shape),
+ "status": "passed",
+ }
+ print(json.dumps(report, indent=2, sort_keys=True))
+diff --git a/train.py b/train.py
+index c030158..ba5f6ac 100644
+--- a/train.py
++++ b/train.py
+@@ -48,7 +48,8 @@ for experiment_index, seed in enumerate(config.seeds):
+ state = create_train_state(init_rng, config.model, config.image_dims, config.learning_rate, config.warmup_learning_rate, config.learning_rate_final, config.momentum, config.weight_decay, config.num_epochs, config.warmup_epochs, config.decay_epochs, steps_per_epoch)
+ local_feedback = create_local_feedback(
+ jax.random.PRNGKey(config.feedback_seed), config.model, state.params,
+- config.image_dims, config.learning_algorithm, config.num_classes)
++ config.image_dims, config.learning_algorithm, config.num_classes,
++ pepita_projection_scale=config.pepita_projection_scale)
+
+
+ del init_rng # Must not be used anymore.
+--
+2.54.0
+