braintrace 0.2.8__tar.gz → 0.3.0__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 (178) hide show
  1. {braintrace-0.2.8 → braintrace-0.3.0}/PKG-INFO +2 -2
  2. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/base.py +27 -16
  3. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/param_dim_vjp.py +315 -65
  4. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/random_projection_vjp.py +9 -11
  5. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/snap_n.py +4 -3
  6. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/snap_n_test.py +300 -0
  7. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/conv_vmap_correctness_test.py +81 -0
  8. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/exact_correctness_test.py +94 -0
  9. braintrace-0.3.0/braintrace/_algorithm/tests/input_check_test.py +597 -0
  10. braintrace-0.3.0/braintrace/_algorithm/tests/scan_descent_hint_test.py +205 -0
  11. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/scan_descent_support_test.py +20 -13
  12. braintrace-0.3.0/braintrace/_algorithm/tests/trace_broadcast_test.py +674 -0
  13. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/three_factor.py +3 -0
  14. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/three_factor_test.py +13 -1
  15. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_base.py +201 -11
  16. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_graph_executor.py +17 -3
  17. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compile_test.py +46 -0
  18. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/base.py +28 -8
  19. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/base_test.py +37 -3
  20. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/canonicalize.py +227 -14
  21. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/canonicalize_test.py +658 -0
  22. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/diagnostics.py +65 -3
  23. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/graph.py +240 -21
  24. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hid_param_op.py +257 -89
  25. braintrace-0.3.0/braintrace/_compiler/hid_param_op_test.py +415 -0
  26. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_group.py +426 -123
  27. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_group_test.py +801 -4
  28. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_pertubation.py +567 -10
  29. braintrace-0.3.0/braintrace/_compiler/hidden_pertubation_fuzz_test.py +349 -0
  30. braintrace-0.3.0/braintrace/_compiler/hidden_pertubation_test.py +1650 -0
  31. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/jaxpr_graph.py +65 -0
  32. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/module_info.py +30 -0
  33. braintrace-0.3.0/braintrace/_compiler/online_credit.py +570 -0
  34. braintrace-0.3.0/braintrace/_compiler/online_credit_test.py +800 -0
  35. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/output_bypass.py +2 -5
  36. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/output_bypass_test.py +73 -0
  37. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/position_graph.py +387 -47
  38. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/position_graph_test.py +301 -1
  39. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/scan_descent.py +461 -44
  40. braintrace-0.3.0/braintrace/_compiler/scan_descent_leaves_test.py +315 -0
  41. braintrace-0.3.0/braintrace/_compiler/scan_descent_test.py +1550 -0
  42. braintrace-0.3.0/braintrace/_input_data.py +536 -0
  43. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/conv_test.py +11 -0
  44. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/dense_test.py +9 -0
  45. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/__init__.py +10 -0
  46. braintrace-0.3.0/braintrace/_testing/census.py +550 -0
  47. braintrace-0.3.0/braintrace/_testing/census_test.py +397 -0
  48. braintrace-0.3.0/braintrace/_testing/graph_hash.py +608 -0
  49. braintrace-0.3.0/braintrace/_testing/graph_hash_test.py +398 -0
  50. braintrace-0.3.0/braintrace/_testing/precondition_models.py +560 -0
  51. braintrace-0.3.0/braintrace/_testing/precondition_models_test.py +154 -0
  52. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/scenario_catalog_test.py +6 -3
  53. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_version.py +1 -1
  54. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_linear_test.py +36 -34
  55. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/PKG-INFO +2 -2
  56. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/SOURCES.txt +13 -0
  57. {braintrace-0.2.8 → braintrace-0.3.0}/pyproject.toml +6 -1
  58. braintrace-0.2.8/braintrace/_compiler/hid_param_op_test.py +0 -226
  59. braintrace-0.2.8/braintrace/_compiler/hidden_pertubation_test.py +0 -551
  60. braintrace-0.2.8/braintrace/_compiler/scan_descent_test.py +0 -495
  61. braintrace-0.2.8/braintrace/_input_data.py +0 -259
  62. {braintrace-0.2.8 → braintrace-0.3.0}/LICENSE +0 -0
  63. {braintrace-0.2.8 → braintrace-0.3.0}/README.md +0 -0
  64. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/__init__.py +0 -0
  65. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/__init___test.py +0 -0
  66. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/__init__.py +0 -0
  67. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/_common.py +0 -0
  68. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/_common_test.py +0 -0
  69. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/axes.py +0 -0
  70. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/axes_test.py +0 -0
  71. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/base_test.py +0 -0
  72. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/d_rtrl.py +0 -0
  73. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/d_rtrl_test.py +0 -0
  74. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/dni.py +0 -0
  75. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/dni_test.py +0 -0
  76. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/e_prop.py +0 -0
  77. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/e_prop_test.py +0 -0
  78. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/graph_executor.py +0 -0
  79. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/graph_executor_test.py +0 -0
  80. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/io_dim_vjp.py +0 -0
  81. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/io_dim_vjp_test.py +0 -0
  82. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/ostl.py +0 -0
  83. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/ostl_test.py +0 -0
  84. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/param_dim_vjp_test.py +0 -0
  85. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/pp_prop.py +0 -0
  86. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/pp_prop_test.py +0 -0
  87. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/random_projection_vjp_test.py +0 -0
  88. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/sequence.py +0 -0
  89. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/sequence_test.py +0 -0
  90. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/approx_correctness_test.py +0 -0
  91. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_acceptance_test.py +0 -0
  92. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_discrimination_test.py +0 -0
  93. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_golden_test.py +0 -0
  94. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/cross_check_test.py +0 -0
  95. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/diagnostic_exploration_test.py +0 -0
  96. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/output_bypass_test.py +0 -0
  97. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/public_api_test.py +0 -0
  98. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/scan_fusion_test.py +0 -0
  99. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/snn_model_correctness_test.py +0 -0
  100. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/transform_correctness_test.py +0 -0
  101. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/while_support_test.py +0 -0
  102. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/uoro.py +0 -0
  103. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/uoro_test.py +0 -0
  104. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_base_test.py +0 -0
  105. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_graph_executor_test.py +0 -0
  106. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compatible_imports.py +0 -0
  107. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compatible_imports_test.py +0 -0
  108. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compile.py +0 -0
  109. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/__init__.py +0 -0
  110. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/diagnostics_test.py +0 -0
  111. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/graph_test.py +0 -0
  112. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/jaxpr_graph_test.py +0 -0
  113. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/module_info_test.py +0 -0
  114. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/report.py +0 -0
  115. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/report_test.py +0 -0
  116. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/cell_relation_guardrail_test.py +0 -0
  117. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/compiler_oracle_test.py +0 -0
  118. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/compiler_property_test.py +0 -0
  119. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_grad_exponential.py +0 -0
  120. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_grad_exponential_test.py +0 -0
  121. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_input_data_test.py +0 -0
  122. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/__init__.py +0 -0
  123. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_ops.py +0 -0
  124. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_ops_test.py +0 -0
  125. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_params.py +0 -0
  126. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_params_test.py +0 -0
  127. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_misc.py +0 -0
  128. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_misc_test.py +0 -0
  129. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/__init__.py +0 -0
  130. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_primitive.py +0 -0
  131. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_primitive_test.py +0 -0
  132. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_registries.py +0 -0
  133. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_registries_test.py +0 -0
  134. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/conv.py +0 -0
  135. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/dense.py +0 -0
  136. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/einsum.py +0 -0
  137. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/einsum_test.py +0 -0
  138. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/elemwise.py +0 -0
  139. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/elemwise_test.py +0 -0
  140. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/embedding.py +0 -0
  141. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/embedding_test.py +0 -0
  142. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/grouped.py +0 -0
  143. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/grouped_test.py +0 -0
  144. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/lora.py +0 -0
  145. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/lora_test.py +0 -0
  146. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/op_rule_oracle.py +0 -0
  147. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/op_rule_oracle_test.py +0 -0
  148. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/sparse.py +0 -0
  149. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/sparse_test.py +0 -0
  150. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_state_management.py +0 -0
  151. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_state_management_test.py +0 -0
  152. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/compiler_models.py +0 -0
  153. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/models.py +0 -0
  154. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle.py +0 -0
  155. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_models.py +0 -0
  156. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_models_test.py +0 -0
  157. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_test.py +0 -0
  158. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/scenario_catalog.py +0 -0
  159. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_typing.py +0 -0
  160. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_typing_test.py +0 -0
  161. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_version_test.py +0 -0
  162. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/__init__.py +0 -0
  163. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/__init___test.py +0 -0
  164. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_conv.py +0 -0
  165. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_conv_test.py +0 -0
  166. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_embedding.py +0 -0
  167. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_embedding_test.py +0 -0
  168. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_linear.py +0 -0
  169. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_readout.py +0 -0
  170. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_readout_test.py +0 -0
  171. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_rnn.py +0 -0
  172. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_rnn_test.py +0 -0
  173. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/py.typed +0 -0
  174. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/dependency_links.txt +0 -0
  175. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/requires.txt +0 -0
  176. {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/top_level.txt +0 -0
  177. {braintrace-0.2.8 → braintrace-0.3.0}/setup.cfg +0 -0
  178. {braintrace-0.2.8 → braintrace-0.3.0}/setup.py +0 -0
@@ -1,8 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: braintrace
3
- Version: 0.2.8
3
+ Version: 0.3.0
4
4
  Summary: Enabling Scalable Online Learning for Brain Dynamics.
5
- Author-email: BrainTrace Developers <chao.brain@qq.com>
5
+ Author-email: BrainTrace Developers <chaobrain@outlook.com>
6
6
  License: Apache-2.0 license
7
7
  Project-URL: Homepage, https://github.com/chaobrain/braintrace
8
8
  Project-URL: Bug Tracker, https://github.com/chaobrain/braintrace/issues
@@ -66,6 +66,26 @@ class EligibilityTrace(brainstate.ShortTermState):
66
66
  __module__ = 'braintrace'
67
67
 
68
68
 
69
+ def descended_scan_length(graph: ETraceGraph) -> Optional[int]:
70
+ """The length of the longest scan that *graph* descends, or ``None``.
71
+
72
+ A descended scan holds an ETP relation or a hidden group in its body; a
73
+ group can be descended without any relation (all body weights used
74
+ through plain ops), so both are read. Unrolling the scan, with
75
+ ``ControlFlowPolicy(scan_unroll_limit=length)``, removes the descent.
76
+ """
77
+ lengths = [r.control_flow_context.scan.length
78
+ for r in graph.hidden_param_op_relations
79
+ if r.control_flow_context is not None]
80
+ lengths += [g.descent.scan.length for g in graph.hidden_groups if g.descent is not None]
81
+ return max(lengths) if lengths else None
82
+
83
+
84
+ #: The algorithms the scan-descent refusal names: they run a descended scan
85
+ #: with their default configuration (``scan_descent_hint_test`` checks it).
86
+ DESCENT_ALGORITHMS: Tuple[str, ...] = ('D_RTRL', 'EProp')
87
+
88
+
69
89
  class ETraceAlgorithm(SequenceDriverMixin, brainstate.nn.Module):
70
90
  r"""Provide the base interface for eligibility-trace algorithms.
71
91
 
@@ -314,19 +334,8 @@ class ETraceAlgorithm(SequenceDriverMixin, brainstate.nn.Module):
314
334
  # (all body weights routed through plain ops), so check both.
315
335
  # Gate here — after the graph is built, before any trace state
316
336
  # is initialized against it.
317
- if (
318
- (
319
- any(
320
- r.control_flow_context is not None
321
- for r in self.graph.hidden_param_op_relations
322
- )
323
- or any(
324
- g.descent is not None
325
- for g in self.graph.hidden_groups
326
- )
327
- )
328
- and not getattr(self, '_supports_scan_descent', False)
329
- ):
337
+ length = descended_scan_length(self.graph)
338
+ if length is not None and not getattr(self, '_supports_scan_descent', False):
330
339
  # Engines whose refusal has an axis-level reason (rather than
331
340
  # merely missing support) state it via this hook, so the message
332
341
  # names the coordinate instead of only the mechanism.
@@ -334,9 +343,11 @@ class ETraceAlgorithm(SequenceDriverMixin, brainstate.nn.Module):
334
343
  raise NotImplementedError(
335
344
  f'{type(self).__name__} does not support structured scan '
336
345
  f'descent yet (an ETP relation or hidden group was '
337
- f'discovered inside a scan body). Use an algorithm that '
338
- f'supports scan descent, or set '
339
- f"ControlFlowPolicy(scan_descent='off')."
346
+ f'discovered inside the body of a length-{length} scan). Use '
347
+ f'an algorithm that supports scan descent '
348
+ f'({" or ".join(DESCENT_ALGORITHMS)}), or unroll the '
349
+ f'scan with ControlFlowPolicy(scan_unroll_limit={length}) '
350
+ f'(compile time grows with the length).'
340
351
  + (f' {reason}' if reason else '')
341
352
  )
342
353
 
@@ -16,7 +16,7 @@
16
16
  from __future__ import annotations
17
17
 
18
18
  from functools import partial
19
- from typing import Callable, Dict, Tuple, Optional, Sequence, Any
19
+ from typing import Callable, Dict, List, Tuple, Optional, Sequence, Any
20
20
 
21
21
  import brainstate
22
22
  import jax
@@ -31,6 +31,7 @@ from braintrace._compiler import (
31
31
  gather_learning_signal,
32
32
  widen_instant_term,
33
33
  )
34
+ from braintrace._compatible_imports import shaped_aval
34
35
  from braintrace._op import (
35
36
  etp_conv_p,
36
37
  etp_elemwise_p,
@@ -82,6 +83,211 @@ def _cast_to_dtype(tree: Any, dtype: Any) -> Any:
82
83
  return jax.tree.map(lambda a: a.astype(dtype), tree, is_leaf=u.math.is_quantity)
83
84
 
84
85
 
86
+ def _abstract(v: Any) -> jax.ShapeDtypeStruct:
87
+ aval = shaped_aval(v)
88
+ return jax.ShapeDtypeStruct(tuple(aval.shape), aval.dtype)
89
+
90
+
91
+ def _hidden_out_avals(relation: HiddenParamOpRelation, group: HiddenGroup) -> List[jax.ShapeDtypeStruct]:
92
+ """The avals of the hidden values that *relation*'s ``y`` reaches in *group*.
93
+
94
+ The executor reads ``df`` off the all-ones jvp of the relation's map from
95
+ ``y`` to these values, concatenated by the group, so ``df`` has their
96
+ shapes: ``(*varshape, num_state)`` once concatenated. For a relation in a
97
+ descended scan the map is one substep's (body-scoped), as the fold
98
+ consumes it.
99
+ """
100
+ i = [g.index for g in relation.hidden_groups].index(group.index)
101
+ return [_abstract(v) for v in relation.y_to_hidden_group_jaxprs[i].outvars]
102
+
103
+
104
+ def _reaches_without_broadcast(relation: HiddenParamOpRelation, group: HiddenGroup) -> bool:
105
+ """Whether every hidden value that *relation*'s ``y`` reaches in *group* has ``y``'s shape.
106
+
107
+ Then no broadcast adds an axis between ``y`` and its states, so the trace
108
+ that the ``init_drtrl`` rule sizes from ``x`` and ``y`` is kept without
109
+ computing the instantaneous term. This does not prove that the two shapes
110
+ agree: a ``y`` that drives a whole ``HiddenGroupState`` (``y`` of shape
111
+ ``(4, 2)`` into a state of shape ``(4, 2)`` whose ``varshape`` is ``(4,)``)
112
+ takes this path, and its first update fails as it did before the trace was
113
+ sized from the term (#244).
114
+ """
115
+ y_shape = tuple(shaped_aval(relation.y_var).shape)
116
+ return all(tuple(a.shape) == y_shape for a in _hidden_out_avals(relation, group))
117
+
118
+
119
+ def _reached_states(relation: HiddenParamOpRelation, group: HiddenGroup) -> str:
120
+ """The hidden states of *group* that *relation*'s ``y`` reaches, with their shapes, for a message."""
121
+ reached = [
122
+ (path, tuple(shaped_aval(var).shape))
123
+ for path, var in zip(group.hidden_paths, group.hidden_outvars)
124
+ if path in relation.connected_hidden_paths
125
+ ]
126
+ shapes = list(dict.fromkeys(shape for _, shape in reached))
127
+ if len(shapes) == 1:
128
+ return f"the hidden state(s) {[path for path, _ in reached]} of shape {shapes[0]}"
129
+ return f"the hidden states {[path for path, _ in reached]} of shapes {[shape for _, shape in reached]}"
130
+
131
+
132
+ def _abstract_instant_term(
133
+ relation: HiddenParamOpRelation,
134
+ group: HiddenGroup,
135
+ fast_solve: bool,
136
+ ) -> Tuple[Dict[str, jax.ShapeDtypeStruct], jax.ShapeDtypeStruct]:
137
+ """The abstract :func:`relation_instant_term` of *relation* for *group*, and its ``df``.
138
+
139
+ Raises
140
+ ------
141
+ ValueError
142
+ If the term cannot be computed, naming the operation, the weight and
143
+ the shapes.
144
+ """
145
+ x = None if relation.x_var is None or relation.primitive is etp_elemwise_p else _abstract(relation.x_var)
146
+ weights = {k: _abstract(v) for k, v in relation.trainable_vars.items()}
147
+
148
+ def _term(x_: Any, outs: List[jax.Array], w: Dict[str, Any]) -> Any:
149
+ df = group.concat_hidden(outs)
150
+ return relation_instant_term(relation, group, x_, df, w, fast_solve=fast_solve), df
151
+
152
+ try:
153
+ return jax.eval_shape(_term, x, _hidden_out_avals(relation, group), weights)
154
+ except (ValueError, TypeError) as err:
155
+ y_shape = tuple(shaped_aval(relation.y_var).shape)
156
+ raise ValueError(
157
+ f"The eligibility trace of the ETP operation '{relation.primitive.name}' on the "
158
+ f"weight {relation.path} cannot be sized: its output has shape {y_shape} and "
159
+ f"reaches {_reached_states(relation, group)}, and the term its update adds for "
160
+ f"these states cannot be computed ({type(err).__name__}: {err})."
161
+ ) from err
162
+
163
+
164
+ def _size_trace_from_instant_term(
165
+ relation: HiddenParamOpRelation,
166
+ group: HiddenGroup,
167
+ init_val: Dict[str, Any],
168
+ fast_solve: bool,
169
+ ) -> Dict[str, Any]:
170
+ """Allocate each trace leaf at the shape of its instantaneous term (#224).
171
+
172
+ The trace update adds :func:`relation_instant_term`, ``diag(D_f) ⊗ x``,
173
+ whose ``df`` has the hidden group's shape. A primitive's ``init_drtrl``
174
+ rule sizes the trace from ``x`` and ``y`` only, so when ``y`` reaches its
175
+ hidden state only by broadcasting (an unbatched output into a batched
176
+ state, a batch of one), the term has the axes the broadcast adds and the
177
+ trace would grow at its first update: a loop over the steps, or the
178
+ substep fold of a descended scan, could not carry it. Such a leaf is
179
+ allocated, zero, at the term's shape, and a leaf that already has it is
180
+ kept. Any other leaf raises, an init larger than the term included: an
181
+ unproven shape is not taken on trust.
182
+
183
+ When ``y`` reaches every hidden value of the group with its own shape
184
+ (:func:`_reaches_without_broadcast`), the init is kept without the
185
+ abstract evaluation, which costs about 0.5 ms per (relation, group).
186
+ Otherwise the gradient solve is checked, abstractly, to reduce the trace
187
+ to the weight's shape (:func:`_refuse_broadcast_along_a_weight_axis`).
188
+
189
+ Returns
190
+ -------
191
+ dict
192
+ The (possibly re-allocated) trace.
193
+
194
+ Raises
195
+ ------
196
+ ValueError
197
+ If the term cannot be computed, or a leaf neither has its shape nor
198
+ grows to it by broadcasting.
199
+ NotImplementedError
200
+ If the gradient solve cannot reduce the trace to the weight's shape
201
+ (#231).
202
+ """
203
+ if _reaches_without_broadcast(relation, group):
204
+ return init_val
205
+ term, df = _abstract_instant_term(relation, group, fast_solve)
206
+ out: Dict[str, Any] = {}
207
+ for key, leaf in init_val.items():
208
+ shape, term_shape = tuple(leaf.shape), tuple(term[key].shape)
209
+ try:
210
+ joint = tuple(jnp.broadcast_shapes(shape, term_shape))
211
+ except ValueError:
212
+ joint = None
213
+ if shape == term_shape:
214
+ out[key] = leaf
215
+ elif joint == term_shape: # the term adds the axes of a broadcast
216
+ out[key] = jnp.zeros(term_shape, dtype=leaf.dtype)
217
+ else:
218
+ raise ValueError(
219
+ f"The eligibility trace that the init_drtrl rule of the ETP operation "
220
+ f"'{relation.primitive.name}' allocates for the leaf '{key}' of the weight "
221
+ f"{relation.path} has shape {shape}, which is not the shape {term_shape} "
222
+ f"of its instantaneous term (the term the trace update adds for the hidden "
223
+ f"state(s) {list(group.hidden_paths)}) and does not broadcast to it."
224
+ )
225
+ _refuse_broadcast_along_a_weight_axis(relation, group, out, df, fast_solve)
226
+ return out
227
+
228
+
229
+ def _refuse_broadcast_along_a_weight_axis(
230
+ relation: HiddenParamOpRelation,
231
+ group: HiddenGroup,
232
+ trace: Dict[str, Any],
233
+ df: Any,
234
+ fast_solve: bool,
235
+ ) -> None:
236
+ """Refuse a trace that the gradient solve cannot reduce to the weight (#231).
237
+
238
+ A broadcast that adds or expands batch axes leaves the trace's weight part
239
+ intact, and the solve sums the batch. A broadcast along an axis of the
240
+ weight (``W`` of shape ``(3, 1)`` whose ``(2, 1)`` output drives ``(2, 4)``
241
+ states, or an ``etp_elemwise`` weight of shape ``(1,)`` or ``(1, 4)``
242
+ scaling ``(2, 4)`` states) puts the trace in the hidden state's positions,
243
+ and the solve returns a gradient of the wrong shape (a custom VJP error at
244
+ the first gradient). This checks, abstractly, that the solve and the batch
245
+ reduction of :func:`_solve_param_dim_weight_gradients` give each weight its
246
+ own shape.
247
+
248
+ Raises
249
+ ------
250
+ NotImplementedError
251
+ If they do not.
252
+ """
253
+ weight_vals = {
254
+ relation.trainable_paths[k]: relation.trainable_param_states[k].value
255
+ for k in relation.trainable_paths
256
+ }
257
+ batched = is_batched_primitive(relation.primitive)
258
+
259
+ def _solve(dg: Any, trace_: Any, values: Any) -> Dict[Path, PyTree]:
260
+ grads, folded = relation_solve_to_param(relation, group, dg, trace_, values, fast_solve=fast_solve)
261
+ routed: Dict[Path, PyTree] = {}
262
+ _route_grads_by_path(relation, grads, values, routed)
263
+ sums: Dict[Path, Dict[Any, PyTree]] = {}
264
+ add_param_grad(sums, routed, values, batched=batched, folded=folded)
265
+ return reduce_param_batch_axes(sums, values)
266
+
267
+ cause: Optional[Exception] = None
268
+ try:
269
+ solved = jax.eval_shape(_solve, df, trace, weight_vals)
270
+ bad = [p for p, g in solved.items()
271
+ if [tuple(a.shape) for a in jax.tree.leaves(g)]
272
+ != [tuple(u.math.shape(a)) for a in jax.tree.leaves(weight_vals[p])]]
273
+ except (ValueError, TypeError) as err: # the solve cannot even take the trace
274
+ bad, cause = list(weight_vals), err
275
+ if not bad:
276
+ return
277
+ y_shape = tuple(shaped_aval(relation.y_var).shape)
278
+ trace_shapes = {k: tuple(v.shape) for k, v in trace.items()}
279
+ raise NotImplementedError(
280
+ f"The output of the ETP operation '{relation.primitive.name}' on the weight "
281
+ f"{relation.path} has shape {y_shape}, and it reaches "
282
+ f"{_reached_states(relation, group)} by broadcasting along an axis of "
283
+ f"the weight. Its eligibility trace then lives in the hidden state's positions "
284
+ f"({trace_shapes}), and the gradient solve does not reduce it to the weight's "
285
+ f"shape yet (https://github.com/chaobrain/braintrace/issues/231). Compute the "
286
+ f"output with the hidden state's shape, for example with a weight that has the "
287
+ f"state's number of units."
288
+ ) from cause
289
+
290
+
85
291
  def _init_param_dim_state(
86
292
  etrace_bwg: Dict[ETraceWG_Key, brainstate.State],
87
293
  relation: HiddenParamOpRelation,
@@ -136,6 +342,7 @@ def _init_param_dim_state(
136
342
  f'Primitive {relation.primitive.name} init_drtrl must return a dict; '
137
343
  f'got {type(init_val).__name__}.'
138
344
  )
345
+ init_val = _size_trace_from_instant_term(relation, group, init_val, fast_solve)
139
346
  fp = get_fast_path_rules(relation.primitive)
140
347
  use_fast = fast_solve and fp is not None and fp.applicable(relation.eqn_params)
141
348
  if use_fast:
@@ -240,7 +447,12 @@ def _update_param_dim_etrace_scan_fn(
240
447
  for rels in by_scan.values():
241
448
  trace_keys = [(id(r.y_var), g.index)
242
449
  for r in rels for g in r.hidden_groups]
243
- x_keys = [etrace_x_key(r.x_var) for r in rels if r.x_var is not None]
450
+ # a relation lifted from outside the scan (#211) has one x per outer
451
+ # step, constant over the substeps: it is closed over, not scanned
452
+ x_keys = [etrace_x_key(r.x_var) for r in rels
453
+ if r.x_var is not None and not r.control_flow_context.lifted]
454
+ x_const = {etrace_x_key(r.x_var): etrace_xs_at_t[etrace_x_key(r.x_var)]
455
+ for r in rels if r.x_var is not None and r.control_flow_context.lifted}
244
456
  df_keys = [etrace_df_key(r.y_var, g.index)
245
457
  for r in rels for g in r.hidden_groups]
246
458
  g_idx = sorted({g.index for r in rels for g in r.hidden_groups})
@@ -256,10 +468,11 @@ def _update_param_dim_etrace_scan_fn(
256
468
  carry: Dict[ETraceWG_Key, jax.Array],
257
469
  sliced: Any,
258
470
  _rels: Any = rels,
471
+ _x_const: Dict = x_const,
259
472
  ) -> Tuple[Dict[ETraceWG_Key, jax.Array], None]:
260
473
  xs_t, dfs_t, diags_t = sliced
261
474
  carry = {**carry, **_apply_relation_step(
262
- carry, xs_t, dfs_t, diags_t, _rels,
475
+ carry, {**_x_const, **xs_t}, dfs_t, diags_t, _rels,
263
476
  weight_path_to_vals, fast_solve, trace_dtype)}
264
477
  return carry, None
265
478
 
@@ -727,8 +940,8 @@ def relation_solve_to_param(
727
940
  # that lost its leading batch axis, while the weight trace keeps the
728
941
  # singleton batch from ``init_drtrl``. Both the batched ``dt_to_t`` and
729
942
  # the closed-form solve map a shared leading batch axis, so re-insert
730
- # the matching singleton on the cotangent; the trailing solve-time sum
731
- # (``has_batched`` branch in :func:`reduce_param_batch_axes`) collapses it
943
+ # the matching singleton on the cotangent; the solve-time batch sum
944
+ # (:func:`add_param_grad`, :func:`reduce_param_batch_axes`) collapses it
732
945
  # again.
733
946
  if batched:
734
947
  _trace_lead = jax.tree.leaves(etrace_data_unitless)[0].shape[0]
@@ -776,52 +989,102 @@ def relation_solve_to_param(
776
989
  return fn_unit_restore(dg_weight_dict), bool(use_fast and batched)
777
990
 
778
991
 
779
- def reduce_param_batch_axes(
780
- temp_data: Dict[Path, PyTree],
992
+ # How a gradient contribution's batch axes reduce (:func:`add_param_grad`).
993
+ _REDUCED = 'reduced' # folded into the fast-path einsum, or no axis to reduce
994
+ _BATCH = 'batch' # a batched primitive's leading batch axis
995
+ _LEADING = 'leading' # the leading axes the weight does not have
996
+
997
+
998
+ def _leaves(tree: PyTree) -> List[Any]:
999
+ return jax.tree.leaves(tree, is_leaf=lambda a: isinstance(a, u.Quantity))
1000
+
1001
+
1002
+ def add_param_grad(
1003
+ sums: Dict[Path, Dict[Any, PyTree]],
1004
+ routed: Dict[Path, PyTree],
781
1005
  weight_vals: Dict[Path, PyTree],
782
- folded_paths: set,
783
- batched_paths: set,
1006
+ batched: bool,
1007
+ folded: bool,
784
1008
  ) -> None:
785
- """Collapse leading batch axes on routed parameter gradients, in place.
1009
+ """Add one (relation, hidden group) gradient, routed per weight path.
1010
+
1011
+ How a contribution's batch axes reduce depends on the relation that
1012
+ produced it:
1013
+
1014
+ * folded into the fast-path einsum (*folded*): already reduced;
1015
+ * a batched primitive (*batched*): its leading batch axis is summed;
1016
+ * otherwise, the leading axes the weight does not have are summed. An
1017
+ unbatched primitive usually has none, but an output that reaches
1018
+ batched states by broadcasting (#224) gets their batch axes, and so
1019
+ does ``etp_elemwise`` under ``brainstate.mixin.Batching()``: it has no
1020
+ ``x`` carrier, so neither its input nor its output reveals the batch.
786
1021
 
787
- Paths routed through the fast-path einsum (*folded_paths*) were already
788
- reduced via ``fold_batch``; unbatched-primitive paths (not in
789
- *batched_paths*) never grew a batch axis and must be left intact.
1022
+ One weight can be read by relations of different kinds, batched and
1023
+ unbatched, so each contribution is filed under its reduction and its leaf
1024
+ shapes, and :func:`reduce_param_batch_axes` reduces every such bucket
1025
+ before it adds them. Contributions of one kind and shape are summed before
1026
+ the reduction, so a weight read in one way reduces as it always did.
790
1027
 
791
1028
  Parameters
792
1029
  ----------
793
- temp_data : dict
794
- ``{path: gradient}``, mutated in place.
1030
+ sums : dict
1031
+ ``{path: {bucket: gradient}}``, mutated in place.
1032
+ routed : dict
1033
+ ``{path: gradient}`` of one (relation, hidden group).
795
1034
  weight_vals : dict
796
1035
  ``{path: ParamState value}``, used as the rank reference.
797
- folded_paths : set
798
- Paths whose batch axis was folded into the einsum.
799
- batched_paths : set
800
- Paths owned by a batched primitive.
1036
+ batched : bool
1037
+ Whether the relation's primitive is batched.
1038
+ folded : bool
1039
+ Whether its batch axis was folded into the fast-path einsum.
801
1040
  """
802
- for key, val in temp_data.items():
803
- if key in folded_paths:
804
- continue
805
- if key in batched_paths:
806
- temp_data[key] = jax.tree.map(lambda x: u.math.sum(x, axis=0), val)
1041
+ for path, grad in routed.items():
1042
+ leaves = _leaves(grad)
1043
+ if folded:
1044
+ how = _REDUCED
1045
+ elif batched:
1046
+ how = _BATCH
1047
+ elif any(u.math.ndim(g) > u.math.ndim(w) for g, w in zip(leaves, _leaves(weight_vals[path]))):
1048
+ how = _LEADING
807
1049
  else:
808
- # Unbatched-primitive paths usually carry no batch axis. But under
809
- # ``brainstate.mixin.Batching()`` a diagonal op with no ``x`` carrier
810
- # (``etp_elemwise``: its output is the weight itself, so neither its
811
- # input nor output rank reveals the batch) still acquires a leading
812
- # batch axis from the batched hidden state it feeds. ``is_batched_
813
- # primitive`` does not flag it, so reduce any leading axes the
814
- # parameter itself does not have. This is a no-op for genuinely
815
- # unbatched paths (e.g. ``etp_mv``) whose gradient already matches
816
- # the parameter rank, and for the per-lane vmap path.
817
- ref = weight_vals[key]
818
- temp_data[key] = jax.tree.map(
819
- lambda g, p: (
820
- u.math.sum(g, axis=tuple(range(u.math.ndim(g) - u.math.ndim(p))))
821
- if u.math.ndim(g) > u.math.ndim(p) else g
822
- ),
823
- val, ref,
824
- )
1050
+ how = _REDUCED
1051
+ bucket = (how, tuple(tuple(u.math.shape(g)) for g in leaves))
1052
+ _update_dict(sums.setdefault(path, {}), bucket, grad)
1053
+
1054
+
1055
+ def reduce_param_batch_axes(
1056
+ sums: Dict[Path, Dict[Any, PyTree]],
1057
+ weight_vals: Dict[Path, PyTree],
1058
+ ) -> Dict[Path, PyTree]:
1059
+ """Reduce the batch axes of each bucket of :func:`add_param_grad`, then add them.
1060
+
1061
+ Parameters
1062
+ ----------
1063
+ sums : dict
1064
+ ``{path: {bucket: gradient}}`` from :func:`add_param_grad`.
1065
+ weight_vals : dict
1066
+ ``{path: ParamState value}``, used as the rank reference.
1067
+
1068
+ Returns
1069
+ -------
1070
+ dict
1071
+ ``{path: gradient}``, each with its weight's shape.
1072
+ """
1073
+ out: Dict[Path, PyTree] = {}
1074
+ for path, buckets in sums.items():
1075
+ for (how, _), grad in buckets.items():
1076
+ if how == _BATCH:
1077
+ grad = jax.tree.map(lambda x: u.math.sum(x, axis=0), grad)
1078
+ elif how == _LEADING:
1079
+ grad = jax.tree.map(
1080
+ lambda g, p: (
1081
+ u.math.sum(g, axis=tuple(range(u.math.ndim(g) - u.math.ndim(p))))
1082
+ if u.math.ndim(g) > u.math.ndim(p) else g
1083
+ ),
1084
+ grad, weight_vals[path],
1085
+ )
1086
+ _update_dict(out, path, grad)
1087
+ return out
825
1088
 
826
1089
 
827
1090
  def _solve_param_dim_weight_gradients(
@@ -859,21 +1122,14 @@ def _solve_param_dim_weight_gradients(
859
1122
  Whether to use the per-primitive fast contraction instead of the legacy
860
1123
  vmap path.
861
1124
  """
862
- # update the etrace weight gradients
863
- temp_data: Dict[Path, PyTree] = dict()
864
- # Paths whose gradient was already batch-reduced inside the fast-path einsum
865
- # (fold_batch). The trailing batch-sum must skip these.
866
- folded_paths: set = set()
867
- # Paths owned by a *batched* primitive: only these carry a leading batch axis
868
- # in ``temp_data`` and so only these may be batch-summed. A model can mix
869
- # batched and unbatched primitives in one solve — under
1125
+ # the gradient contributions per weight path, bucketed by how their batch
1126
+ # axes reduce (``add_param_grad``): one weight can be read by a batched and
1127
+ # an unbatched relation, e.g. ``matmul(x, W) + matmul(x[0], W)``, and under
870
1128
  # ``brainstate.nn.Vmap(vmap_states='new')`` a conv stays batched while a
871
- # ``Linear`` dispatches to the unbatched ``etp_mv`` (1-D per-lane input) — and
872
- # summing the unbatched gradient would collapse its leading (in-feature) axis.
873
- batched_paths: set = set()
1129
+ # ``Linear`` dispatches to the unbatched ``etp_mv``
1130
+ sums: Dict[Path, Dict[Any, PyTree]] = dict()
874
1131
  for relation in weight_hidden_relations:
875
- if is_batched_primitive(relation.primitive):
876
- batched_paths.update(relation.trainable_paths.values())
1132
+ batched = is_batched_primitive(relation.primitive)
877
1133
 
878
1134
  group: HiddenGroup
879
1135
  for group in relation.hidden_groups:
@@ -887,20 +1143,14 @@ def _solve_param_dim_weight_gradients(
887
1143
  weight_vals,
888
1144
  fast_solve=fast_solve,
889
1145
  )
890
- if batch_folded:
891
- folded_paths.update(relation.trainable_paths.values())
892
1146
 
893
1147
  # Route per-key to owning ParamState path.
894
- _route_grads_by_path(relation, dg_weight_dict, weight_vals, temp_data)
895
-
896
- #
897
- # Step 3:
898
- #
899
- # sum up the batched weight gradients
900
- reduce_param_batch_axes(temp_data, weight_vals, folded_paths, batched_paths)
1148
+ routed: Dict[Path, PyTree] = dict()
1149
+ _route_grads_by_path(relation, dg_weight_dict, weight_vals, routed)
1150
+ add_param_grad(sums, routed, weight_vals, batched=batched, folded=batch_folded)
901
1151
 
902
- # update the weight gradients
903
- for key, val in temp_data.items():
1152
+ # reduce the batch axes and update the weight gradients
1153
+ for key, val in reduce_param_batch_axes(sums, weight_vals).items():
904
1154
  _update_dict(dG_weights, key, val)
905
1155
 
906
1156
 
@@ -45,6 +45,7 @@ from braintrace._typing import (
45
45
  from ._common import _route_grads_by_path, _update_dict
46
46
  from .axes import ETraceConfig
47
47
  from .param_dim_vjp import (
48
+ add_param_grad,
48
49
  reduce_param_batch_axes,
49
50
  relation_instant_term,
50
51
  relation_solve_to_param,
@@ -529,17 +530,12 @@ class RandomProjectionVjpAlgorithm(ETraceVjpAlgorithm):
529
530
  ``einsum``'s shared-axis restriction, conv's bias reduction and LoRA's
530
531
  chaining through the current weights.
531
532
  """
532
- temp: Dict[Path, PyTree] = {}
533
- folded_paths: set = set()
534
- batched_paths: set = set()
533
+ sums: Dict[Path, Dict[Any, PyTree]] = {}
535
534
 
536
535
  relation: HiddenParamOpRelation
537
536
  for relation in self.graph.hidden_param_op_relations:
538
537
  if group not in relation.hidden_groups:
539
538
  continue
540
- if is_batched_primitive(relation.primitive):
541
- batched_paths.update(relation.trainable_paths.values())
542
-
543
539
  weights_dict = relation_weights_dict(relation, weight_vals)
544
540
  x = (
545
541
  None if relation.primitive is etp_elemwise_p
@@ -554,12 +550,14 @@ class RandomProjectionVjpAlgorithm(ETraceVjpAlgorithm):
554
550
  relation, group, nu, instant, weight_vals,
555
551
  fast_solve=self.fast_solve,
556
552
  )
557
- if batch_folded:
558
- folded_paths.update(relation.trainable_paths.values())
559
- _route_grads_by_path(relation, proj_dict, weight_vals, temp)
553
+ routed: Dict[Path, PyTree] = {}
554
+ _route_grads_by_path(relation, proj_dict, weight_vals, routed)
555
+ add_param_grad(
556
+ sums, routed, weight_vals,
557
+ batched=is_batched_primitive(relation.primitive), folded=batch_folded,
558
+ )
560
559
 
561
- reduce_param_batch_axes(temp, weight_vals, folded_paths, batched_paths)
562
- return temp
560
+ return reduce_param_batch_axes(sums, weight_vals)
563
561
 
564
562
  # ------------------------------------------------------------------ #
565
563
  # the step
@@ -127,9 +127,10 @@ class SnAp(ParamDimVjpAlgorithm):
127
127
  mixing primitive it finds. Two primitive families yield a *precise* pattern:
128
128
  dense (``etp_mm`` / ``etp_mv``, all-to-all) and sparse (``etp_sp_mm`` /
129
129
  ``etp_sp_mv``, the structural pattern of ``sparse_mat``, transposed). For
130
- **every other** primitive, more than one mixing equation, any control flow
131
- around it, or a non-position-preserving tail, the analysis falls back to the
132
- all-to-all pattern and emits a
130
+ **every other** primitive, more than one mixing equation, a mixing equation
131
+ inside a loop that runs more than once, a ``cond`` or call-like equation on
132
+ the hidden path, or a non-position-preserving tail, the analysis falls back
133
+ to the all-to-all pattern and emits a
133
134
  ``DiagnosticKind.SNAP_PATTERN_CONSERVATIVE`` diagnostic. A conservative
134
135
  pattern is always *correct* — it retains a superset of the true influence —
135
136
  but it costs saturated memory, so the diagnostic is worth reading.