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.
- {braintrace-0.2.8 → braintrace-0.3.0}/PKG-INFO +2 -2
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/base.py +27 -16
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/param_dim_vjp.py +315 -65
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/random_projection_vjp.py +9 -11
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/snap_n.py +4 -3
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/snap_n_test.py +300 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/conv_vmap_correctness_test.py +81 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/exact_correctness_test.py +94 -0
- braintrace-0.3.0/braintrace/_algorithm/tests/input_check_test.py +597 -0
- braintrace-0.3.0/braintrace/_algorithm/tests/scan_descent_hint_test.py +205 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/scan_descent_support_test.py +20 -13
- braintrace-0.3.0/braintrace/_algorithm/tests/trace_broadcast_test.py +674 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/three_factor.py +3 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/three_factor_test.py +13 -1
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_base.py +201 -11
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_graph_executor.py +17 -3
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compile_test.py +46 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/base.py +28 -8
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/base_test.py +37 -3
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/canonicalize.py +227 -14
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/canonicalize_test.py +658 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/diagnostics.py +65 -3
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/graph.py +240 -21
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hid_param_op.py +257 -89
- braintrace-0.3.0/braintrace/_compiler/hid_param_op_test.py +415 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_group.py +426 -123
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_group_test.py +801 -4
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/hidden_pertubation.py +567 -10
- braintrace-0.3.0/braintrace/_compiler/hidden_pertubation_fuzz_test.py +349 -0
- braintrace-0.3.0/braintrace/_compiler/hidden_pertubation_test.py +1650 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/jaxpr_graph.py +65 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/module_info.py +30 -0
- braintrace-0.3.0/braintrace/_compiler/online_credit.py +570 -0
- braintrace-0.3.0/braintrace/_compiler/online_credit_test.py +800 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/output_bypass.py +2 -5
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/output_bypass_test.py +73 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/position_graph.py +387 -47
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/position_graph_test.py +301 -1
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/scan_descent.py +461 -44
- braintrace-0.3.0/braintrace/_compiler/scan_descent_leaves_test.py +315 -0
- braintrace-0.3.0/braintrace/_compiler/scan_descent_test.py +1550 -0
- braintrace-0.3.0/braintrace/_input_data.py +536 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/conv_test.py +11 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/dense_test.py +9 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/__init__.py +10 -0
- braintrace-0.3.0/braintrace/_testing/census.py +550 -0
- braintrace-0.3.0/braintrace/_testing/census_test.py +397 -0
- braintrace-0.3.0/braintrace/_testing/graph_hash.py +608 -0
- braintrace-0.3.0/braintrace/_testing/graph_hash_test.py +398 -0
- braintrace-0.3.0/braintrace/_testing/precondition_models.py +560 -0
- braintrace-0.3.0/braintrace/_testing/precondition_models_test.py +154 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/scenario_catalog_test.py +6 -3
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_version.py +1 -1
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_linear_test.py +36 -34
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/PKG-INFO +2 -2
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/SOURCES.txt +13 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/pyproject.toml +6 -1
- braintrace-0.2.8/braintrace/_compiler/hid_param_op_test.py +0 -226
- braintrace-0.2.8/braintrace/_compiler/hidden_pertubation_test.py +0 -551
- braintrace-0.2.8/braintrace/_compiler/scan_descent_test.py +0 -495
- braintrace-0.2.8/braintrace/_input_data.py +0 -259
- {braintrace-0.2.8 → braintrace-0.3.0}/LICENSE +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/README.md +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/__init___test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/_common.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/_common_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/axes.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/axes_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/base_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/d_rtrl.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/d_rtrl_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/dni.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/dni_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/e_prop.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/e_prop_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/graph_executor.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/graph_executor_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/io_dim_vjp.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/io_dim_vjp_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/ostl.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/ostl_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/param_dim_vjp_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/pp_prop.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/pp_prop_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/random_projection_vjp_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/sequence.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/sequence_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/approx_correctness_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_acceptance_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_discrimination_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/axis_golden_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/cross_check_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/diagnostic_exploration_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/output_bypass_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/public_api_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/scan_fusion_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/snn_model_correctness_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/transform_correctness_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/tests/while_support_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/uoro.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/uoro_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_base_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_algorithm/vjp_graph_executor_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compatible_imports.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compatible_imports_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compile.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/diagnostics_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/graph_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/jaxpr_graph_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/module_info_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/report.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/report_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/cell_relation_guardrail_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/compiler_oracle_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_compiler/tests/compiler_property_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_grad_exponential.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_grad_exponential_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_input_data_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_ops.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_ops_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_params.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_legacy/_params_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_misc.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_misc_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_primitive.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_primitive_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_registries.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/_registries_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/conv.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/dense.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/einsum.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/einsum_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/elemwise.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/elemwise_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/embedding.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/embedding_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/grouped.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/grouped_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/lora.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/lora_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/op_rule_oracle.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/op_rule_oracle_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/sparse.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_op/sparse_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_state_management.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_state_management_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/compiler_models.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/models.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_models.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_models_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/oracle_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_testing/scenario_catalog.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_typing.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_typing_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/_version_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/__init__.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/__init___test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_conv.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_conv_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_embedding.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_embedding_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_linear.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_readout.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_readout_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_rnn.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/nn/_rnn_test.py +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace/py.typed +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/dependency_links.txt +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/requires.txt +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/braintrace.egg-info/top_level.txt +0 -0
- {braintrace-0.2.8 → braintrace-0.3.0}/setup.cfg +0 -0
- {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.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Enabling Scalable Online Learning for Brain Dynamics.
|
|
5
|
-
Author-email: BrainTrace Developers <
|
|
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
|
-
|
|
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
|
|
338
|
-
f'supports scan descent
|
|
339
|
-
f"
|
|
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
|
-
|
|
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
|
|
731
|
-
# (
|
|
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
|
-
|
|
780
|
-
|
|
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
|
-
|
|
783
|
-
|
|
1006
|
+
batched: bool,
|
|
1007
|
+
folded: bool,
|
|
784
1008
|
) -> None:
|
|
785
|
-
"""
|
|
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
|
-
|
|
788
|
-
|
|
789
|
-
|
|
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
|
-
|
|
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
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
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
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
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
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
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
|
-
#
|
|
863
|
-
|
|
864
|
-
#
|
|
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``
|
|
872
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
558
|
-
|
|
559
|
-
|
|
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(
|
|
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,
|
|
131
|
-
|
|
132
|
-
|
|
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.
|