braintrace 0.2.3__tar.gz → 0.2.4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {braintrace-0.2.3 → braintrace-0.2.4}/PKG-INFO +1 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/__init__.py +8 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/__init___test.py +13 -4
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/_common.py +9 -4
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/base.py +37 -2
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/e_prop.py +120 -58
- braintrace-0.2.4/braintrace/_algorithm/e_prop_test.py +445 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/graph_executor.py +14 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/graph_executor_test.py +21 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/io_dim_vjp.py +53 -7
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/oracle.py +54 -0
- braintrace-0.2.4/braintrace/_algorithm/oracle_models.py +467 -0
- braintrace-0.2.4/braintrace/_algorithm/oracle_test.py +515 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ostl.py +23 -4
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ostl_test.py +115 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/osttp.py +27 -3
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/osttp_test.py +43 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/otpe.py +169 -58
- braintrace-0.2.4/braintrace/_algorithm/otpe_test.py +423 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ottt.py +109 -14
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ottt_test.py +118 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/param_dim_vjp.py +392 -61
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/param_dim_vjp_test.py +409 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/conv_vmap_correctness_test.py +11 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/exact_correctness_test.py +63 -7
- braintrace-0.2.4/braintrace/_algorithm/tests/scan_descent_support_test.py +353 -0
- braintrace-0.2.4/braintrace/_algorithm/tests/while_support_test.py +256 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_base.py +77 -9
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_graph_executor.py +108 -3
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compatible_imports.py +23 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compile.py +4 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compile_test.py +11 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/__init__.py +7 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/base.py +99 -12
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/base_test.py +178 -3
- braintrace-0.2.4/braintrace/_compiler/canonicalize.py +1068 -0
- braintrace-0.2.4/braintrace/_compiler/canonicalize_test.py +1266 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/diagnostics.py +40 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/diagnostics_test.py +10 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/graph.py +111 -6
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/graph_test.py +81 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hid_param_op.py +211 -88
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_group.py +470 -45
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_group_test.py +486 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_pertubation.py +186 -39
- braintrace-0.2.4/braintrace/_compiler/hidden_pertubation_test.py +446 -0
- braintrace-0.2.4/braintrace/_compiler/jaxpr_graph.py +312 -0
- braintrace-0.2.4/braintrace/_compiler/jaxpr_graph_test.py +320 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/module_info.py +60 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/module_info_test.py +29 -0
- braintrace-0.2.4/braintrace/_compiler/scan_descent.py +592 -0
- braintrace-0.2.4/braintrace/_compiler/scan_descent_test.py +484 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/scenario_catalog.py +326 -3
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/scenario_catalog_test.py +461 -10
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_ops.py +1 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_misc.py +49 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_misc_test.py +53 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/__init__.py +20 -2
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_primitive.py +68 -12
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_primitive_test.py +160 -17
- braintrace-0.2.4/braintrace/_op/_registries.py +403 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_registries_test.py +80 -6
- braintrace-0.2.4/braintrace/_op/conv.py +899 -0
- braintrace-0.2.4/braintrace/_op/conv_test.py +1042 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/dense.py +128 -15
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/dense_test.py +225 -30
- braintrace-0.2.4/braintrace/_op/einsum.py +340 -0
- braintrace-0.2.4/braintrace/_op/einsum_test.py +496 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/elemwise.py +64 -6
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/elemwise_test.py +97 -6
- braintrace-0.2.4/braintrace/_op/embedding.py +292 -0
- braintrace-0.2.4/braintrace/_op/embedding_test.py +379 -0
- braintrace-0.2.4/braintrace/_op/grouped.py +441 -0
- braintrace-0.2.4/braintrace/_op/grouped_test.py +496 -0
- braintrace-0.2.4/braintrace/_op/lora.py +673 -0
- braintrace-0.2.4/braintrace/_op/lora_test.py +749 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/op_rule_oracle_test.py +15 -9
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/sparse.py +111 -19
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/sparse_test.py +279 -11
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_version.py +1 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/__init__.py +7 -3
- braintrace-0.2.4/braintrace/nn/_embedding.py +81 -0
- braintrace-0.2.4/braintrace/nn/_embedding_test.py +75 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_linear.py +148 -7
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_linear_test.py +44 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_readout.py +3 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/PKG-INFO +1 -1
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/SOURCES.txt +16 -0
- braintrace-0.2.3/braintrace/_algorithm/e_prop_test.py +0 -218
- braintrace-0.2.3/braintrace/_algorithm/oracle_models.py +0 -195
- braintrace-0.2.3/braintrace/_algorithm/oracle_test.py +0 -175
- braintrace-0.2.3/braintrace/_algorithm/otpe_test.py +0 -215
- braintrace-0.2.3/braintrace/_compiler/hidden_pertubation_test.py +0 -126
- braintrace-0.2.3/braintrace/_op/_registries.py +0 -206
- braintrace-0.2.3/braintrace/_op/conv.py +0 -607
- braintrace-0.2.3/braintrace/_op/conv_test.py +0 -571
- braintrace-0.2.3/braintrace/_op/lora.py +0 -475
- braintrace-0.2.3/braintrace/_op/lora_test.py +0 -442
- {braintrace-0.2.3 → braintrace-0.2.4}/LICENSE +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/README.md +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/__init__.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/_common_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/base_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/d_rtrl.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/d_rtrl_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/io_dim_vjp_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/pp_prop.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/pp_prop_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/approx_correctness_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/cross_check_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/diagnostic_exploration_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/public_api_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/scan_fusion_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/transform_correctness_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_base_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_graph_executor_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compatible_imports_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hid_param_op_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/model4test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/report.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/report_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/cell_relation_guardrail_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/compiler_oracle_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/compiler_property_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_docs_examples_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_etrace_model_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_grad_exponential.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_grad_exponential_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_input_data.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_input_data_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/__init__.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_ops_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_params.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_params_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/op_rule_oracle.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_state_managment.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_state_managment_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_typing.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_conv.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_conv_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_readout_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_rnn.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_rnn_test.py +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/py.typed +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/dependency_links.txt +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/requires.txt +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/top_level.txt +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/pyproject.toml +0 -0
- {braintrace-0.2.3 → braintrace-0.2.4}/setup.cfg +0 -0
|
@@ -94,6 +94,7 @@ from ._algorithm import (
|
|
|
94
94
|
PresynapticTrace,
|
|
95
95
|
)
|
|
96
96
|
from ._compiler import (
|
|
97
|
+
ControlFlowPolicy,
|
|
97
98
|
ETraceGraph,
|
|
98
99
|
compile_etrace_graph,
|
|
99
100
|
HiddenParamOpRelation,
|
|
@@ -115,6 +116,9 @@ from ._compiler import (
|
|
|
115
116
|
from ._op import (
|
|
116
117
|
ETPPrimitive,
|
|
117
118
|
matmul,
|
|
119
|
+
grouped_matmul,
|
|
120
|
+
embedding,
|
|
121
|
+
einsum,
|
|
118
122
|
element_wise,
|
|
119
123
|
conv,
|
|
120
124
|
sparse_matmul,
|
|
@@ -168,6 +172,9 @@ __all__ = [
|
|
|
168
172
|
|
|
169
173
|
# ETP primitives (user API)
|
|
170
174
|
'matmul',
|
|
175
|
+
'grouped_matmul',
|
|
176
|
+
'embedding',
|
|
177
|
+
'einsum',
|
|
171
178
|
'element_wise',
|
|
172
179
|
'conv',
|
|
173
180
|
'sparse_matmul',
|
|
@@ -185,6 +192,7 @@ __all__ = [
|
|
|
185
192
|
'ETraceGraphExecutor',
|
|
186
193
|
|
|
187
194
|
# compiler
|
|
195
|
+
'ControlFlowPolicy',
|
|
188
196
|
'ETraceGraph',
|
|
189
197
|
'compile_etrace_graph',
|
|
190
198
|
'HiddenGroup',
|
|
@@ -148,7 +148,14 @@ def test_weight_used_inside_scan_raises_not_implemented():
|
|
|
148
148
|
within a control-flow op. Previously (F-SCAN-WEIGHT) the message construction
|
|
149
149
|
indexed the hidden-path map with the weight var and a KeyError escaped instead;
|
|
150
150
|
the message now renders the weight variable directly, so the intended
|
|
151
|
-
NotImplementedError surfaces.
|
|
151
|
+
NotImplementedError surfaces.
|
|
152
|
+
|
|
153
|
+
Since Phase 2 canonicalization, statically short scans are unrolled at
|
|
154
|
+
extraction time instead of erroring, so this uses a length above
|
|
155
|
+
``ControlFlowPolicy.scan_unroll_limit`` to reach the unsupported-op path
|
|
156
|
+
(after a SCAN_UNROLL_SKIPPED warning). Since Phase 4, structured scan
|
|
157
|
+
descent handles over-limit ETP scans by default, so this pins the
|
|
158
|
+
opt-out path via ``ControlFlowPolicy(scan_descent='off')``."""
|
|
152
159
|
|
|
153
160
|
class ScanModel(brainstate.nn.Module):
|
|
154
161
|
def __init__(self):
|
|
@@ -160,15 +167,17 @@ def test_weight_used_inside_scan_raises_not_implemented():
|
|
|
160
167
|
def update(self, x):
|
|
161
168
|
def body(carry, _):
|
|
162
169
|
return braintrace.matmul(carry, self.w.value), None # weight inside scan
|
|
163
|
-
out, _ = jax.lax.scan(body, self.h.value, xs=None, length=
|
|
170
|
+
out, _ = jax.lax.scan(body, self.h.value, xs=None, length=64)
|
|
164
171
|
self.h.value = jax.nn.tanh(out)
|
|
165
172
|
return self.h.value
|
|
166
173
|
|
|
167
174
|
model = ScanModel()
|
|
168
175
|
brainstate.nn.init_all_states(model, batch_size=1)
|
|
169
|
-
algo = braintrace.D_RTRL(
|
|
176
|
+
algo = braintrace.D_RTRL(
|
|
177
|
+
model, control_flow=braintrace.ControlFlowPolicy(scan_descent='off'))
|
|
170
178
|
with pytest.raises(NotImplementedError):
|
|
171
|
-
|
|
179
|
+
with pytest.warns(UserWarning, match='NOT unrolled'):
|
|
180
|
+
algo.compile_graph(jnp.ones((1, 4), dtype='float32'))
|
|
172
181
|
|
|
173
182
|
|
|
174
183
|
# --- Task 6: legacy + nn deprecations emit DeprecationWarning ----------------
|
|
@@ -131,9 +131,14 @@ class PresynapticTrace(_ZeroResetState):
|
|
|
131
131
|
|
|
132
132
|
|
|
133
133
|
class KappaFilter(_ZeroResetState):
|
|
134
|
-
r"""Low-pass
|
|
134
|
+
r"""Low-pass filter helper state.
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
:class:`~braintrace.EProp` no longer uses this class directly — it filters
|
|
137
|
+
the eligibility trace internally instead. ``KappaFilter`` remains public
|
|
138
|
+
and available for user-side filtering of an output-side (or any other)
|
|
139
|
+
signal outside the algorithm's own hooks.
|
|
140
|
+
|
|
141
|
+
The filter smooths the signal following
|
|
137
142
|
:math:`x_{\mathrm{filt}} \leftarrow (1-\kappa) \cdot x + \kappa \cdot x_{\mathrm{filt}}`.
|
|
138
143
|
|
|
139
144
|
Parameters
|
|
@@ -234,7 +239,7 @@ class FixedRandomFeedback:
|
|
|
234
239
|
|
|
235
240
|
def __init__(self, n_target: int, n_layer: int, key: Any, init_scale: float = 0.1) -> None:
|
|
236
241
|
self.B = jax.lax.stop_gradient(
|
|
237
|
-
init_scale *
|
|
242
|
+
init_scale * brainstate.random.normal(size=(n_target, n_layer), key=key)
|
|
238
243
|
)
|
|
239
244
|
self.n_target = int(n_target)
|
|
240
245
|
self.n_layer = int(n_layer)
|
|
@@ -451,7 +456,7 @@ def _route_grads_by_path(
|
|
|
451
456
|
"""Route per-key gradients from a dict-API rule into per-path pytrees.
|
|
452
457
|
|
|
453
458
|
Both D-RTRL and ES-D-RTRL share this bookkeeping: for each key in
|
|
454
|
-
``per_key_grads`` (returned by ``xy_to_dw`` or ``
|
|
459
|
+
``per_key_grads`` (returned by ``xy_to_dw`` or ``dt_to_t``), look up the
|
|
455
460
|
owning ``ParamState`` path and the leaf index from ``relation``, accumulate
|
|
456
461
|
into ``per_path``, then wrap with ``_wrap_leaves_as_pytree`` and merge into
|
|
457
462
|
``target_dict`` via ``_update_dict``.
|
|
@@ -81,8 +81,13 @@ class ETraceAlgorithm(brainstate.nn.Module):
|
|
|
81
81
|
The other states.
|
|
82
82
|
is_compiled : bool
|
|
83
83
|
Whether the etrace algorithm has been compiled.
|
|
84
|
-
running_index : brainstate.
|
|
85
|
-
|
|
84
|
+
running_index : brainstate.LongTermState[int]
|
|
85
|
+
Zero-based count of previously-completed ``update()`` calls: it reads
|
|
86
|
+
``0`` during the first call, ``1`` during the second, and so on --
|
|
87
|
+
i.e. the value used *inside* a given ``update()`` is the
|
|
88
|
+
pre-increment count, not the 1-based call number. It is incremented
|
|
89
|
+
by one immediately after each ``update()`` completes, and reset back
|
|
90
|
+
to ``0`` by ``reset_state()``.
|
|
86
91
|
"""
|
|
87
92
|
__module__ = 'braintrace'
|
|
88
93
|
|
|
@@ -267,6 +272,36 @@ class ETraceAlgorithm(brainstate.nn.Module):
|
|
|
267
272
|
# --- the model etrace graph -- #
|
|
268
273
|
self.graph_executor.compile_graph(*args)
|
|
269
274
|
|
|
275
|
+
# Structured scan descent (Phase 4): relations discovered inside
|
|
276
|
+
# a descended scan body need a per-substep trace fold, and
|
|
277
|
+
# hidden groups descended from a scan body carry a leading
|
|
278
|
+
# substep axis on their Jacobians — both only handled by
|
|
279
|
+
# algorithms declaring ``_supports_scan_descent = True``. A
|
|
280
|
+
# descended *group* can exist without any descended relation
|
|
281
|
+
# (all body weights routed through plain ops), so check both.
|
|
282
|
+
# Gate here — after the graph is built, before any trace state
|
|
283
|
+
# is initialized against it.
|
|
284
|
+
if (
|
|
285
|
+
(
|
|
286
|
+
any(
|
|
287
|
+
r.control_flow_context is not None
|
|
288
|
+
for r in self.graph.hidden_param_op_relations
|
|
289
|
+
)
|
|
290
|
+
or any(
|
|
291
|
+
g.descent is not None
|
|
292
|
+
for g in self.graph.hidden_groups
|
|
293
|
+
)
|
|
294
|
+
)
|
|
295
|
+
and not getattr(self, '_supports_scan_descent', False)
|
|
296
|
+
):
|
|
297
|
+
raise NotImplementedError(
|
|
298
|
+
f'{type(self).__name__} does not support structured scan '
|
|
299
|
+
f'descent yet (an ETP relation or hidden group was '
|
|
300
|
+
f'discovered inside a scan body). Use an algorithm that '
|
|
301
|
+
f'supports scan descent, or set '
|
|
302
|
+
f"ControlFlowPolicy(scan_descent='off')."
|
|
303
|
+
)
|
|
304
|
+
|
|
270
305
|
# --- the initialization of the states --- #
|
|
271
306
|
self.init_etrace_state(*args)
|
|
272
307
|
|
|
@@ -20,8 +20,10 @@ eligibility trace and a *learning signal* broadcast from the readout. This
|
|
|
20
20
|
module builds on ``D_RTRL``'s per-parameter trace and adds the two ingredients
|
|
21
21
|
that make the rule biologically plausible:
|
|
22
22
|
|
|
23
|
-
- An optional κ-filter on each
|
|
24
|
-
(:math:`\\bar
|
|
23
|
+
- An optional κ-filter on each weight's *eligibility trace*
|
|
24
|
+
(:math:`\\bar e = F_\\kappa(e)`), matching the paper's low-pass eligibility
|
|
25
|
+
filter. The trailing per-hidden-state axis is filtered elementwise, so
|
|
26
|
+
multi-state HiddenGroups (``num_state > 1``) never mix across states.
|
|
25
27
|
- An optional random-feedback variant (``feedback='random'``) that replaces the
|
|
26
28
|
readout's symmetric gradient with a fixed random projection, removing the
|
|
27
29
|
weight-transport requirement.
|
|
@@ -31,13 +33,14 @@ See :class:`EProp` for the mathematical formulation, references, and an example.
|
|
|
31
33
|
|
|
32
34
|
from __future__ import annotations
|
|
33
35
|
|
|
34
|
-
from typing import Any, Dict, Optional
|
|
36
|
+
from typing import Any, Dict, Optional, Sequence
|
|
35
37
|
|
|
36
38
|
import brainstate
|
|
37
39
|
import jax
|
|
38
40
|
import jax.numpy as jnp
|
|
39
41
|
|
|
40
|
-
from .
|
|
42
|
+
from braintrace._typing import ETraceWG_Key, Path, PyTree
|
|
43
|
+
from ._common import FixedRandomFeedback, _reset_state_in_a_dict
|
|
41
44
|
from .param_dim_vjp import ParamDimVjpAlgorithm
|
|
42
45
|
|
|
43
46
|
__all__ = ['EProp']
|
|
@@ -69,16 +72,17 @@ class EProp(ParamDimVjpAlgorithm):
|
|
|
69
72
|
Here :math:`h_j^t` is the hidden state of neuron :math:`j` at time
|
|
70
73
|
:math:`t`, :math:`x_i^t` the presynaptic input, :math:`D_j^t` the
|
|
71
74
|
hidden-to-hidden (recurrent) Jacobian diagonal, :math:`D_{f,j}^t` the
|
|
72
|
-
state-to-output Jacobian, and :math:`\kappa \in [0, 1)` the
|
|
73
|
-
low-pass factor. The learning signal is
|
|
75
|
+
state-to-output Jacobian, and :math:`\kappa \in [0, 1)` the
|
|
76
|
+
eligibility-trace low-pass factor. The learning signal is
|
|
74
77
|
|
|
75
78
|
.. math::
|
|
76
79
|
|
|
77
80
|
L_j^t =
|
|
78
81
|
\begin{cases}
|
|
79
|
-
\dfrac{\partial \mathcal{L}}{\partial h_j^t}
|
|
82
|
+
\ell_j^t = \dfrac{\partial \mathcal{L}}{\partial h_j^t}
|
|
80
83
|
& \text{(symmetric feedback, standard backprop through readout)} \\[2ex]
|
|
81
|
-
\big(B
|
|
84
|
+
\big(B\,\hat\ell^t\big)_j,
|
|
85
|
+
\quad \hat\ell^t = \dfrac{\ell^t}{\lVert \ell^t \rVert + \varepsilon}
|
|
82
86
|
& \text{(random feedback: a fixed random projection } B\text{)} .
|
|
83
87
|
\end{cases}
|
|
84
88
|
|
|
@@ -87,10 +91,20 @@ class EProp(ParamDimVjpAlgorithm):
|
|
|
87
91
|
only on quantities local to the synapse and is updated forward in time. The
|
|
88
92
|
learning signal :math:`L_j^t` is broadcast from the readout. E-prop is
|
|
89
93
|
therefore *online* (no backward pass through time) and uses memory linear in
|
|
90
|
-
the number of parameters. With ``kappa_filter_decay > 0``
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
+
the number of parameters. With ``kappa_filter_decay > 0`` each weight's
|
|
95
|
+
*eligibility trace* is additionally low-pass filtered (elementwise over the
|
|
96
|
+
trailing per-hidden-state axis, so multi-state HiddenGroups are filtered
|
|
97
|
+
per state with no cross-state mixing); with ``feedback='random'`` the
|
|
98
|
+
symmetric readout gradient :math:`\ell^t` is L2-normalized (removing its
|
|
99
|
+
dependence on the *magnitude* of the real readout weights, since
|
|
100
|
+
reverse-AD only ever exposes :math:`\ell^t = W_\mathrm{out}^\top \delta^t`,
|
|
101
|
+
never the pre-readout error :math:`\delta^t` itself) and then projected
|
|
102
|
+
through a frozen random matrix :math:`B`, removing the biologically
|
|
103
|
+
implausible weight-transport requirement. Normalization removes the
|
|
104
|
+
*scale* dependence on :math:`W_\mathrm{out}`; a residual *directional*
|
|
105
|
+
dependence on the readout weights remains, since full direction-
|
|
106
|
+
independence would require the pre-readout error :math:`\delta^t`
|
|
107
|
+
explicitly, which the current hook contract does not provide.
|
|
94
108
|
|
|
95
109
|
Parameters
|
|
96
110
|
----------
|
|
@@ -99,13 +113,19 @@ class EProp(ParamDimVjpAlgorithm):
|
|
|
99
113
|
feedback : {'symmetric', 'random'}, default 'symmetric'
|
|
100
114
|
``'symmetric'`` uses reverse-AD's :math:`\partial \mathcal{L}/\partial h`
|
|
101
115
|
(standard backprop through the readout). ``'random'`` replaces the
|
|
102
|
-
readout gradient with a frozen random projection
|
|
103
|
-
``random_feedback_key``).
|
|
116
|
+
readout gradient with a frozen random projection of its L2-normalized
|
|
117
|
+
direction (requires ``random_feedback_key``). The projection matrix is
|
|
118
|
+
square (hidden-dim × hidden-dim): E-prop's hooks only see
|
|
119
|
+
:math:`\partial\mathcal L/\partial h`, which has no visibility into a
|
|
120
|
+
separate readout layer's width, so ``feedback='random'`` assumes a
|
|
121
|
+
single, direct readout whose output dimensionality equals the
|
|
122
|
+
HiddenGroup's own width.
|
|
104
123
|
kappa_filter_decay : float in [0, 1), default 0.0
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
filtering
|
|
124
|
+
Eligibility-trace low-pass factor :math:`\kappa` (see
|
|
125
|
+
:math:`\bar{e}_{ji}^t` above). If ``> 0``, each trainable weight's raw
|
|
126
|
+
eligibility trace is filtered every step, per hidden-state channel.
|
|
127
|
+
``0`` disables filtering (the algorithm then reduces exactly to
|
|
128
|
+
:class:`~braintrace.D_RTRL`).
|
|
109
129
|
random_feedback_key : jax.random.PRNGKey, optional
|
|
110
130
|
Seed for the random-feedback matrices. Required when
|
|
111
131
|
``feedback='random'``; ignored otherwise.
|
|
@@ -180,69 +200,111 @@ class EProp(ParamDimVjpAlgorithm):
|
|
|
180
200
|
self.feedback = feedback
|
|
181
201
|
self.kappa_filter_decay = float(kappa_filter_decay)
|
|
182
202
|
self._random_feedback_key = random_feedback_key
|
|
183
|
-
self.
|
|
203
|
+
# Keyed exactly like ``self.etrace_bwg`` (ETraceWG_Key == (id(y_var),
|
|
204
|
+
# group.index)): one filter state per trainable weight / HiddenGroup
|
|
205
|
+
# relation, holding a PyTree that mirrors the raw trace's own
|
|
206
|
+
# structure and shape (including the trailing num_state axis).
|
|
207
|
+
self._trace_filters: Dict[ETraceWG_Key, brainstate.State] = {}
|
|
184
208
|
self._random_feedback: Dict[int, FixedRandomFeedback] = {}
|
|
185
209
|
|
|
186
210
|
def init_etrace_state(self, *args: Any, **kwargs: Any) -> None:
|
|
187
211
|
super().init_etrace_state(*args, **kwargs)
|
|
188
|
-
self.
|
|
212
|
+
self._trace_filters = {}
|
|
189
213
|
self._random_feedback = {}
|
|
190
214
|
if self.kappa_filter_decay > 0.0:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
)
|
|
215
|
+
# One filter per raw-trace key, initialised to zeros with the
|
|
216
|
+
# exact PyTree structure/shape of that trace (batch axis, weight
|
|
217
|
+
# shape, and the trailing num_state axis all included) -- no
|
|
218
|
+
# reduction, so per-state channels never mix.
|
|
219
|
+
for trace_key, trace_state in self.etrace_bwg.items():
|
|
220
|
+
self._trace_filters[trace_key] = brainstate.ShortTermState(
|
|
221
|
+
jax.tree.map(jnp.zeros_like, trace_state.value)
|
|
222
|
+
)
|
|
200
223
|
if self.feedback == 'random':
|
|
201
|
-
|
|
202
|
-
assert
|
|
224
|
+
rf_key = self._random_feedback_key
|
|
225
|
+
assert rf_key is not None # constructor enforces a key when feedback='random'
|
|
226
|
+
# Collect the (group id -> width) pairs needed first so the
|
|
227
|
+
# random draws below are made under a single seeded scope, using
|
|
228
|
+
# only `brainstate.random` (never `jax.random` directly).
|
|
229
|
+
groups_needed: Dict[int, int] = {}
|
|
203
230
|
for rel in self.graph.hidden_param_op_relations:
|
|
204
231
|
for group in rel.hidden_groups:
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
# n_target == n_layer — square projection over reverse-AD signal.
|
|
232
|
+
groups_needed.setdefault(group.index, int(group.varshape[-1]))
|
|
233
|
+
with brainstate.random.seed_context(rf_key):
|
|
234
|
+
for gid, n_layer in groups_needed.items():
|
|
235
|
+
# n_target == n_layer — square projection over reverse-AD
|
|
236
|
+
# signal (see the class docstring's single-readout note).
|
|
211
237
|
self._random_feedback[gid] = FixedRandomFeedback(
|
|
212
|
-
n_target=n_layer,
|
|
238
|
+
n_target=n_layer,
|
|
239
|
+
n_layer=n_layer,
|
|
240
|
+
key=brainstate.random.split_key(),
|
|
241
|
+
init_scale=0.1,
|
|
213
242
|
)
|
|
214
243
|
|
|
215
244
|
def reset_state(self, batch_size: Optional[int] = None, **kwargs: Any) -> None:
|
|
216
245
|
super().reset_state(batch_size=batch_size, **kwargs)
|
|
217
|
-
|
|
218
|
-
flt.reset_state(batch_size=batch_size)
|
|
246
|
+
_reset_state_in_a_dict(self._trace_filters, batch_size)
|
|
219
247
|
|
|
220
248
|
def _compute_learning_signal(self, dl_autodiff: Any, args: Any) -> Any:
|
|
221
249
|
signals = list(dl_autodiff)
|
|
222
250
|
if self.feedback == 'random' and self._random_feedback:
|
|
223
|
-
# dl_autodiff[g].shape == (*varshape, num_state)
|
|
224
|
-
#
|
|
251
|
+
# dl_autodiff[g].shape == (*varshape, num_state); varshape[-1] is
|
|
252
|
+
# the n_layer (hidden-width) axis, i.e. axis -2 of the full array.
|
|
253
|
+
# `s` is proportional to the *real* readout weights (reverse-AD
|
|
254
|
+
# only ever exposes W_out^T @ delta, never delta itself), so a
|
|
255
|
+
# linear projection through any fixed B cannot remove that
|
|
256
|
+
# dependency. L2-normalizing `s` per num_state channel over the
|
|
257
|
+
# n_layer axis strips the magnitude dependence on W_out (while
|
|
258
|
+
# keeping direction and the per-state axis untouched) before
|
|
259
|
+
# projecting through the frozen random matrix.
|
|
225
260
|
def _project(B: Any, s: Any) -> Any:
|
|
226
|
-
|
|
261
|
+
norm = jnp.sqrt(jnp.sum(jnp.square(s), axis=-2, keepdims=True))
|
|
262
|
+
s_normalized = s / (norm + 1e-8)
|
|
263
|
+
return jnp.einsum('...lj,lk->...kj', s_normalized, B)
|
|
227
264
|
|
|
228
265
|
signals = [
|
|
229
266
|
_project(self._random_feedback[gid].B, s)
|
|
230
267
|
if gid in self._random_feedback else s
|
|
231
268
|
for gid, s in enumerate(signals)
|
|
232
269
|
]
|
|
233
|
-
if self._kappa_filters:
|
|
234
|
-
# KappaFilter state carries varshape, but signal has an extra
|
|
235
|
-
# trailing num_state axis. Collapse num_state for filter purposes;
|
|
236
|
-
# broadcast the filtered value back.
|
|
237
|
-
def _filter(flt: Any, s: Any) -> Any:
|
|
238
|
-
# collapse num_state tail: sum over last axis produces shape (*varshape,)
|
|
239
|
-
collapsed = s.sum(axis=-1)
|
|
240
|
-
filtered = flt.update(collapsed)
|
|
241
|
-
return jnp.expand_dims(filtered, axis=-1).astype(s.dtype)
|
|
242
|
-
|
|
243
|
-
signals = [
|
|
244
|
-
_filter(self._kappa_filters[gid], s)
|
|
245
|
-
if gid in self._kappa_filters else s
|
|
246
|
-
for gid, s in enumerate(signals)
|
|
247
|
-
]
|
|
248
270
|
return signals
|
|
271
|
+
|
|
272
|
+
def _solve_weight_gradients(
|
|
273
|
+
self,
|
|
274
|
+
running_index: int,
|
|
275
|
+
etrace_h2w_at_t: Dict[ETraceWG_Key, PyTree],
|
|
276
|
+
dl_to_hidden_groups: Sequence[jax.Array],
|
|
277
|
+
weight_vals: Dict[Path, PyTree],
|
|
278
|
+
dl_to_nonetws_at_t: Dict[Path, PyTree],
|
|
279
|
+
dl_to_etws_at_t: Optional[Dict[Path, PyTree]],
|
|
280
|
+
) -> Any:
|
|
281
|
+
"""Low-pass filter the raw eligibility trace, then delegate to D-RTRL.
|
|
282
|
+
|
|
283
|
+
Implements :math:`\\bar e_{ji}^t = \\kappa\\,\\bar e_{ji}^{t-1} + e_{ji}^t`
|
|
284
|
+
per weight-key, applied elementwise (``jax.tree.map`` over the trace's
|
|
285
|
+
own PyTree, including its trailing num_state axis) so multi-state
|
|
286
|
+
HiddenGroups are filtered independently per state -- never summed
|
|
287
|
+
across states and broadcast back.
|
|
288
|
+
"""
|
|
289
|
+
if self._trace_filters:
|
|
290
|
+
kappa = self.kappa_filter_decay
|
|
291
|
+
filtered: Dict[ETraceWG_Key, PyTree] = {}
|
|
292
|
+
for key, trace in etrace_h2w_at_t.items():
|
|
293
|
+
flt = self._trace_filters.get(key)
|
|
294
|
+
if flt is None:
|
|
295
|
+
filtered[key] = trace
|
|
296
|
+
continue
|
|
297
|
+
new_val = jax.tree.map(
|
|
298
|
+
lambda prev, e: kappa * prev + e, flt.value, trace
|
|
299
|
+
)
|
|
300
|
+
flt.value = new_val
|
|
301
|
+
filtered[key] = new_val
|
|
302
|
+
etrace_h2w_at_t = filtered
|
|
303
|
+
return super()._solve_weight_gradients(
|
|
304
|
+
running_index,
|
|
305
|
+
etrace_h2w_at_t,
|
|
306
|
+
dl_to_hidden_groups,
|
|
307
|
+
weight_vals,
|
|
308
|
+
dl_to_nonetws_at_t,
|
|
309
|
+
dl_to_etws_at_t,
|
|
310
|
+
)
|