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.
Files changed (149) hide show
  1. {braintrace-0.2.3 → braintrace-0.2.4}/PKG-INFO +1 -1
  2. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/__init__.py +8 -0
  3. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/__init___test.py +13 -4
  4. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/_common.py +9 -4
  5. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/base.py +37 -2
  6. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/e_prop.py +120 -58
  7. braintrace-0.2.4/braintrace/_algorithm/e_prop_test.py +445 -0
  8. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/graph_executor.py +14 -1
  9. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/graph_executor_test.py +21 -0
  10. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/io_dim_vjp.py +53 -7
  11. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/oracle.py +54 -0
  12. braintrace-0.2.4/braintrace/_algorithm/oracle_models.py +467 -0
  13. braintrace-0.2.4/braintrace/_algorithm/oracle_test.py +515 -0
  14. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ostl.py +23 -4
  15. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ostl_test.py +115 -0
  16. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/osttp.py +27 -3
  17. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/osttp_test.py +43 -0
  18. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/otpe.py +169 -58
  19. braintrace-0.2.4/braintrace/_algorithm/otpe_test.py +423 -0
  20. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ottt.py +109 -14
  21. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/ottt_test.py +118 -0
  22. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/param_dim_vjp.py +392 -61
  23. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/param_dim_vjp_test.py +409 -1
  24. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/conv_vmap_correctness_test.py +11 -0
  25. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/exact_correctness_test.py +63 -7
  26. braintrace-0.2.4/braintrace/_algorithm/tests/scan_descent_support_test.py +353 -0
  27. braintrace-0.2.4/braintrace/_algorithm/tests/while_support_test.py +256 -0
  28. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_base.py +77 -9
  29. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_graph_executor.py +108 -3
  30. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compatible_imports.py +23 -0
  31. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compile.py +4 -1
  32. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compile_test.py +11 -0
  33. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/__init__.py +7 -0
  34. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/base.py +99 -12
  35. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/base_test.py +178 -3
  36. braintrace-0.2.4/braintrace/_compiler/canonicalize.py +1068 -0
  37. braintrace-0.2.4/braintrace/_compiler/canonicalize_test.py +1266 -0
  38. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/diagnostics.py +40 -0
  39. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/diagnostics_test.py +10 -0
  40. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/graph.py +111 -6
  41. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/graph_test.py +81 -0
  42. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hid_param_op.py +211 -88
  43. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_group.py +470 -45
  44. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_group_test.py +486 -0
  45. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hidden_pertubation.py +186 -39
  46. braintrace-0.2.4/braintrace/_compiler/hidden_pertubation_test.py +446 -0
  47. braintrace-0.2.4/braintrace/_compiler/jaxpr_graph.py +312 -0
  48. braintrace-0.2.4/braintrace/_compiler/jaxpr_graph_test.py +320 -0
  49. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/module_info.py +60 -0
  50. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/module_info_test.py +29 -0
  51. braintrace-0.2.4/braintrace/_compiler/scan_descent.py +592 -0
  52. braintrace-0.2.4/braintrace/_compiler/scan_descent_test.py +484 -0
  53. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/scenario_catalog.py +326 -3
  54. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/scenario_catalog_test.py +461 -10
  55. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_ops.py +1 -1
  56. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_misc.py +49 -0
  57. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_misc_test.py +53 -0
  58. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/__init__.py +20 -2
  59. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_primitive.py +68 -12
  60. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_primitive_test.py +160 -17
  61. braintrace-0.2.4/braintrace/_op/_registries.py +403 -0
  62. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/_registries_test.py +80 -6
  63. braintrace-0.2.4/braintrace/_op/conv.py +899 -0
  64. braintrace-0.2.4/braintrace/_op/conv_test.py +1042 -0
  65. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/dense.py +128 -15
  66. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/dense_test.py +225 -30
  67. braintrace-0.2.4/braintrace/_op/einsum.py +340 -0
  68. braintrace-0.2.4/braintrace/_op/einsum_test.py +496 -0
  69. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/elemwise.py +64 -6
  70. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/elemwise_test.py +97 -6
  71. braintrace-0.2.4/braintrace/_op/embedding.py +292 -0
  72. braintrace-0.2.4/braintrace/_op/embedding_test.py +379 -0
  73. braintrace-0.2.4/braintrace/_op/grouped.py +441 -0
  74. braintrace-0.2.4/braintrace/_op/grouped_test.py +496 -0
  75. braintrace-0.2.4/braintrace/_op/lora.py +673 -0
  76. braintrace-0.2.4/braintrace/_op/lora_test.py +749 -0
  77. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/op_rule_oracle_test.py +15 -9
  78. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/sparse.py +111 -19
  79. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/sparse_test.py +279 -11
  80. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_version.py +1 -1
  81. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/__init__.py +7 -3
  82. braintrace-0.2.4/braintrace/nn/_embedding.py +81 -0
  83. braintrace-0.2.4/braintrace/nn/_embedding_test.py +75 -0
  84. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_linear.py +148 -7
  85. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_linear_test.py +44 -0
  86. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_readout.py +3 -1
  87. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/PKG-INFO +1 -1
  88. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/SOURCES.txt +16 -0
  89. braintrace-0.2.3/braintrace/_algorithm/e_prop_test.py +0 -218
  90. braintrace-0.2.3/braintrace/_algorithm/oracle_models.py +0 -195
  91. braintrace-0.2.3/braintrace/_algorithm/oracle_test.py +0 -175
  92. braintrace-0.2.3/braintrace/_algorithm/otpe_test.py +0 -215
  93. braintrace-0.2.3/braintrace/_compiler/hidden_pertubation_test.py +0 -126
  94. braintrace-0.2.3/braintrace/_op/_registries.py +0 -206
  95. braintrace-0.2.3/braintrace/_op/conv.py +0 -607
  96. braintrace-0.2.3/braintrace/_op/conv_test.py +0 -571
  97. braintrace-0.2.3/braintrace/_op/lora.py +0 -475
  98. braintrace-0.2.3/braintrace/_op/lora_test.py +0 -442
  99. {braintrace-0.2.3 → braintrace-0.2.4}/LICENSE +0 -0
  100. {braintrace-0.2.3 → braintrace-0.2.4}/README.md +0 -0
  101. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/__init__.py +0 -0
  102. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/_common_test.py +0 -0
  103. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/base_test.py +0 -0
  104. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/d_rtrl.py +0 -0
  105. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/d_rtrl_test.py +0 -0
  106. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/io_dim_vjp_test.py +0 -0
  107. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/pp_prop.py +0 -0
  108. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/pp_prop_test.py +0 -0
  109. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/approx_correctness_test.py +0 -0
  110. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/cross_check_test.py +0 -0
  111. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/diagnostic_exploration_test.py +0 -0
  112. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/public_api_test.py +0 -0
  113. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/scan_fusion_test.py +0 -0
  114. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/tests/transform_correctness_test.py +0 -0
  115. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_base_test.py +0 -0
  116. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_algorithm/vjp_graph_executor_test.py +0 -0
  117. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compatible_imports_test.py +0 -0
  118. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/hid_param_op_test.py +0 -0
  119. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/model4test.py +0 -0
  120. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/report.py +0 -0
  121. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/report_test.py +0 -0
  122. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/cell_relation_guardrail_test.py +0 -0
  123. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/compiler_oracle_test.py +0 -0
  124. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_compiler/tests/compiler_property_test.py +0 -0
  125. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_docs_examples_test.py +0 -0
  126. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_etrace_model_test.py +0 -0
  127. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_grad_exponential.py +0 -0
  128. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_grad_exponential_test.py +0 -0
  129. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_input_data.py +0 -0
  130. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_input_data_test.py +0 -0
  131. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/__init__.py +0 -0
  132. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_ops_test.py +0 -0
  133. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_params.py +0 -0
  134. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_legacy/_params_test.py +0 -0
  135. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_op/op_rule_oracle.py +0 -0
  136. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_state_managment.py +0 -0
  137. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_state_managment_test.py +0 -0
  138. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/_typing.py +0 -0
  139. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_conv.py +0 -0
  140. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_conv_test.py +0 -0
  141. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_readout_test.py +0 -0
  142. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_rnn.py +0 -0
  143. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/nn/_rnn_test.py +0 -0
  144. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace/py.typed +0 -0
  145. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/dependency_links.txt +0 -0
  146. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/requires.txt +0 -0
  147. {braintrace-0.2.3 → braintrace-0.2.4}/braintrace.egg-info/top_level.txt +0 -0
  148. {braintrace-0.2.3 → braintrace-0.2.4}/pyproject.toml +0 -0
  149. {braintrace-0.2.3 → braintrace-0.2.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrace
3
- Version: 0.2.3
3
+ Version: 0.2.4
4
4
  Summary: Enabling Scalable Online Learning for Brain Dynamics.
5
5
  Author-email: BrainTrace Developers <chao.brain@qq.com>
6
6
  License: Apache-2.0 license
@@ -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=2)
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(model)
176
+ algo = braintrace.D_RTRL(
177
+ model, control_flow=braintrace.ControlFlowPolicy(scan_descent='off'))
170
178
  with pytest.raises(NotImplementedError):
171
- algo.compile_graph(jnp.ones((1, 4), dtype='float32'))
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 output-side filter used by EProp.
134
+ r"""Low-pass filter helper state.
135
135
 
136
- The filter smooths the output-side signal following
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 * jax.random.normal(key, (n_target, n_layer))
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 ``yw_to_w``), look up the
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.ParamState[int]
85
- The running index.
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 HiddenGroup's learning signal
24
- (:math:`\\bar L = F_\\kappa(L)`), matching the paper's readout-side low-pass.
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 ._common import FixedRandomFeedback, KappaFilter
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 readout-side
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\,e^t\big)_j
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`` the learning
91
- signal is additionally low-pass filtered; with ``feedback='random'`` the
92
- symmetric readout gradient is replaced by a frozen random matrix, removing
93
- the biologically implausible weight-transport requirement.
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 (requires
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
- Readout-side low-pass factor :math:`\kappa`. If ``> 0``, each
106
- HiddenGroup's learning signal is filtered each step
107
- (:math:`\bar L^t = (1-\kappa)L^t + \kappa\bar L^{t-1}`). ``0`` disables
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._kappa_filters: Dict[int, KappaFilter] = {}
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._kappa_filters = {}
212
+ self._trace_filters = {}
189
213
  self._random_feedback = {}
190
214
  if self.kappa_filter_decay > 0.0:
191
- for rel in self.graph.hidden_param_op_relations:
192
- for group in rel.hidden_groups:
193
- gid = group.index
194
- if gid in self._kappa_filters:
195
- continue
196
- zeros = jnp.zeros(group.varshape, dtype=jnp.float32)
197
- self._kappa_filters[gid] = KappaFilter(
198
- zeros, self.kappa_filter_decay
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
- key = self._random_feedback_key
202
- assert key is not None # constructor enforces a key when feedback='random'
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
- gid = group.index
206
- if gid in self._random_feedback:
207
- continue
208
- key, sub = jax.random.split(key)
209
- n_layer = int(group.varshape[-1])
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, n_layer=n_layer, key=sub, init_scale=0.1
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
- for flt in self._kappa_filters.values():
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). Project over the
224
- # trailing n_layer axis (-2), preserving num_state on the tail.
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
- return jnp.einsum('...lj,lk->...kj', s, B)
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
+ )