polystep 0.7.0__tar.gz → 0.8.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.
- {polystep-0.7.0 → polystep-0.8.0}/PKG-INFO +15 -7
- {polystep-0.7.0 → polystep-0.8.0}/README.md +8 -5
- {polystep-0.7.0 → polystep-0.8.0}/pyproject.toml +7 -2
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/__init__.py +1 -1
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/_step_blockwise.py +3 -7
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/_step_monolithic.py +27 -7
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/adaptive_subspace.py +1 -1
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/api.py +63 -25
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/cost_nn.py +145 -40
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/geometry.py +3 -4
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/hybrid_subspace.py +24 -3
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/optimizer.py +132 -9
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/projection/sparse.py +10 -2
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/quadratic_model.py +4 -6
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solver.py +27 -15
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/greedy.py +14 -2
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/sinkhorn.py +6 -6
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep.egg-info/PKG-INFO +15 -7
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep.egg-info/SOURCES.txt +2 -1
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep.egg-info/requires.txt +6 -1
- polystep-0.8.0/tests/test_compile_forward_inplace.py +88 -0
- polystep-0.7.0/tests/test_bug_regressions.py → polystep-0.8.0/tests/test_correctness_regressions.py +4 -2
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_mixed_precision.py +18 -7
- {polystep-0.7.0 → polystep-0.8.0}/LICENSE +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/setup.cfg +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/_compiled.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/_step_momentum.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/ask_tell.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/baselines.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/rl/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/rl/cartpole.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/rl/gym_evaluator.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/rl/metrics.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/rl/policies.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/benchmarks/utils.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/blockwise.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/cma.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/cma_subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/costs.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/dynamics.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/epsilon.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/layers/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/layers/attention.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/layers/rnn.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/objectives/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/objectives/base.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/objectives/synthetic.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/projection/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/py.typed +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/_prelude.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/base.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/kl_softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/solvers/tempered_softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep/transform.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep.egg-info/dependency_links.txt +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/src/polystep.egg-info/top_level.txt +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_ablation_solvers.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_adaptive_mnist.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_adaptive_subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_api.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_ask_tell.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_baseline_fairness.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_benchmarks.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_biased_rotation.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_blockwise.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_cma.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_cma_subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_compiled.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_cost_nn.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_costs.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_dfo_features.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_dynamics.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_fullspace_chunk_bound.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_fused_projection_rebuild.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_geometry.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_hardening.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_hybrid_mnist.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_hybrid_subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_integration.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_kl_softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_layers.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_mixed_precision_step.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_mnist.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_no_test_set_leakage.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_nondiff_data.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_nondiff_models.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_numerical_stress.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_optimizer.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_quadratic_model.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_rl_benchmarks.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_sinkhorn.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_sinkhorn_correctness.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_softmax_integration.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_solver_numerics.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_sparse_integration.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_sparse_projection.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_subspace.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_subspace_blockwise.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_transform.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_trust_region.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_turbo.py +0 -0
- {polystep-0.7.0 → polystep-0.8.0}/tests/test_vmap_layer_compatibility.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: polystep
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: PyTorch PolyStep Optimizer - gradient-free neural network training via entropic optimal transport
|
|
5
5
|
Author: An T. Le
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -31,7 +31,7 @@ Requires-Dist: pytest>=8.0; extra == "dev"
|
|
|
31
31
|
Requires-Dist: pytest-timeout>=2.2; extra == "dev"
|
|
32
32
|
Requires-Dist: pytest-rerunfailures>=14.0; extra == "dev"
|
|
33
33
|
Requires-Dist: pytest-cov>=5.0; extra == "dev"
|
|
34
|
-
Requires-Dist: ruff
|
|
34
|
+
Requires-Dist: ruff==0.15.12; extra == "dev"
|
|
35
35
|
Provides-Extra: experiments
|
|
36
36
|
Requires-Dist: scipy>=1.11; extra == "experiments"
|
|
37
37
|
Requires-Dist: statsmodels>=0.14; extra == "experiments"
|
|
@@ -39,6 +39,11 @@ Requires-Dist: pandas>=2.0; extra == "experiments"
|
|
|
39
39
|
Requires-Dist: python-sat; extra == "experiments"
|
|
40
40
|
Requires-Dist: torchvision>=0.25; extra == "experiments"
|
|
41
41
|
Requires-Dist: cma>=3.3; extra == "experiments"
|
|
42
|
+
Requires-Dist: evotorch>=0.5; extra == "experiments"
|
|
43
|
+
Requires-Dist: nevergrad>=1.0; extra == "experiments"
|
|
44
|
+
Requires-Dist: snntorch>=0.7; extra == "experiments"
|
|
45
|
+
Requires-Dist: tabulate>=0.9; extra == "experiments"
|
|
46
|
+
Requires-Dist: datasets>=2.14; extra == "experiments"
|
|
42
47
|
Requires-Dist: gymnasium>=0.29; extra == "experiments"
|
|
43
48
|
Provides-Extra: rl
|
|
44
49
|
Requires-Dist: gymnasium[box2d]>=0.29; extra == "rl"
|
|
@@ -68,7 +73,7 @@ Based on the Sinkhorn Step algorithm ([Le et al., NeurIPS 2023](https://arxiv.or
|
|
|
68
73
|
4. **Update** the parameters by barycentric projection from the weighted vertices.
|
|
69
74
|
|
|
70
75
|
<p align="center">
|
|
71
|
-
<img src="docs/figures/method_diagram.png" width="840"
|
|
76
|
+
<img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/method_diagram.png" width="840"
|
|
72
77
|
alt="One PolyStep: subspace projection, polytope probes, cost matrix, soft entropic-OT assignment, and barycentric projection, plus the softmax-to-full-OT solver continuum.">
|
|
73
78
|
</p>
|
|
74
79
|
|
|
@@ -161,8 +166,8 @@ See [`examples/`](examples/) for runnable demos covering SNN, RL, MAX-SAT, MNIST
|
|
|
161
166
|
|
|
162
167
|
<table>
|
|
163
168
|
<tr>
|
|
164
|
-
<td align="center"><img src="docs/figures/polystep_snn_progress.gif" width="330"><br><sub>Spiking net (hard LIF thresholds) trained with forward passes only</sub></td>
|
|
165
|
-
<td align="center"><img src="docs/figures/rl_cartpole_policy.gif" width="330"><br><sub>CartPole policy search: no value function, no gradients</sub></td>
|
|
169
|
+
<td align="center"><img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/polystep_snn_progress.gif" width="330"><br><sub>Spiking net (hard LIF thresholds) trained with forward passes only</sub></td>
|
|
170
|
+
<td align="center"><img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/rl_cartpole_policy.gif" width="330"><br><sub>CartPole policy search: no value function, no gradients</sub></td>
|
|
166
171
|
</tr>
|
|
167
172
|
</table>
|
|
168
173
|
|
|
@@ -182,7 +187,7 @@ If your model is fully differentiable, Adam/SGD will be faster and more accurate
|
|
|
182
187
|
|
|
183
188
|
Zeroth-order optimization splits into two camps. Memory-efficient fine-tuning (MeZO and successors) runs zeroth-order updates on differentiable networks to skip the backprop tape, but still assumes a useful local gradient. Evolution strategies and SPSA (CMA-ES, OpenAI-ES) optimize black-box objectives by estimating a local gradient from small perturbations.
|
|
184
189
|
|
|
185
|
-
PolyStep is a randomized direct search: it probes a finite-radius polytope around the current parameters and moves toward the lowest-cost vertices through an optimal-transport barycenter. When the loss is piecewise-constant or
|
|
190
|
+
PolyStep is a randomized direct search: it probes a finite-radius polytope around the current parameters and moves toward the lowest-cost vertices through an optimal-transport barycenter. When the loss is piecewise-constant or truly non-differentiable, the local gradient is zero almost everywhere, so finite-difference estimates carry no signal and ES/SPSA stall, while a finite radius steps across the flat regions. This is the setting where direct search (generalized pattern search, MADS) has convergence guarantees on nonsmooth and discontinuous objectives that gradient-estimate methods lack. Example 09 shows the separation on a hard decision tree.
|
|
186
191
|
|
|
187
192
|
Recent gradient-free work targets the same regime, including low-rank evolution strategies for spiking networks (arXiv:2605.30361) and gradient-free trust regions for recurrent spiking networks (arXiv:2601.21572). PolyStep is complementary: a general-purpose direct-search optimizer with subspace compression.
|
|
188
193
|
|
|
@@ -227,7 +232,9 @@ Among gradient-free optimizers PolyStep is the strongest on every task here, at
|
|
|
227
232
|
- **Softmax OT solver** with an entropic Sinkhorn alternative.
|
|
228
233
|
- **Subspace compression**: `HybridSubspace` (recommended), `AdaptiveSubspace`, and sparse projection for very large models.
|
|
229
234
|
- **Block-wise OT** for per-layer decomposition.
|
|
230
|
-
- **`torch.compile`** opt-in on hot paths
|
|
235
|
+
- **`torch.compile`** opt-in on the OT hot paths (`compile=True`) and on the
|
|
236
|
+
candidate forward (`compile_evaluator` = fusion; `compile_forward` = CUDA
|
|
237
|
+
graphs on the in-place path). See [`docs/performance.md`](docs/performance.md).
|
|
231
238
|
- **Vmap-safe layers**: drop-in attention and LSTM that play nicely with `torch.vmap`.
|
|
232
239
|
- **Sub-linear memory**: forward-only evaluation, no BPTT activation tape (~30x savings at long SNN horizons).
|
|
233
240
|
- **CMA-ES inspired adaptation** of subspace covariance (experimental, monolithic step only; `use_adaptive_radius` is the stable default).
|
|
@@ -259,6 +266,7 @@ docs/ API overview, reproducibility guide
|
|
|
259
266
|
| [`examples/`](examples/) | 8 runnable demos with output figures |
|
|
260
267
|
| [`experiments/`](experiments/) | Full paper reproduction harness |
|
|
261
268
|
| [`docs/api_overview.md`](docs/api_overview.md) | API reference |
|
|
269
|
+
| [`docs/performance.md`](docs/performance.md) | Per-step cost & the compile flags (`compile_evaluator`, `compile_forward`) |
|
|
262
270
|
| [`LIMITATIONS.md`](LIMITATIONS.md) | Known limitations |
|
|
263
271
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Contribution guidelines |
|
|
264
272
|
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
|
@@ -20,7 +20,7 @@ Based on the Sinkhorn Step algorithm ([Le et al., NeurIPS 2023](https://arxiv.or
|
|
|
20
20
|
4. **Update** the parameters by barycentric projection from the weighted vertices.
|
|
21
21
|
|
|
22
22
|
<p align="center">
|
|
23
|
-
<img src="docs/figures/method_diagram.png" width="840"
|
|
23
|
+
<img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/method_diagram.png" width="840"
|
|
24
24
|
alt="One PolyStep: subspace projection, polytope probes, cost matrix, soft entropic-OT assignment, and barycentric projection, plus the softmax-to-full-OT solver continuum.">
|
|
25
25
|
</p>
|
|
26
26
|
|
|
@@ -113,8 +113,8 @@ See [`examples/`](examples/) for runnable demos covering SNN, RL, MAX-SAT, MNIST
|
|
|
113
113
|
|
|
114
114
|
<table>
|
|
115
115
|
<tr>
|
|
116
|
-
<td align="center"><img src="docs/figures/polystep_snn_progress.gif" width="330"><br><sub>Spiking net (hard LIF thresholds) trained with forward passes only</sub></td>
|
|
117
|
-
<td align="center"><img src="docs/figures/rl_cartpole_policy.gif" width="330"><br><sub>CartPole policy search: no value function, no gradients</sub></td>
|
|
116
|
+
<td align="center"><img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/polystep_snn_progress.gif" width="330"><br><sub>Spiking net (hard LIF thresholds) trained with forward passes only</sub></td>
|
|
117
|
+
<td align="center"><img src="https://raw.githubusercontent.com/anindex/polystep/main/docs/figures/rl_cartpole_policy.gif" width="330"><br><sub>CartPole policy search: no value function, no gradients</sub></td>
|
|
118
118
|
</tr>
|
|
119
119
|
</table>
|
|
120
120
|
|
|
@@ -134,7 +134,7 @@ If your model is fully differentiable, Adam/SGD will be faster and more accurate
|
|
|
134
134
|
|
|
135
135
|
Zeroth-order optimization splits into two camps. Memory-efficient fine-tuning (MeZO and successors) runs zeroth-order updates on differentiable networks to skip the backprop tape, but still assumes a useful local gradient. Evolution strategies and SPSA (CMA-ES, OpenAI-ES) optimize black-box objectives by estimating a local gradient from small perturbations.
|
|
136
136
|
|
|
137
|
-
PolyStep is a randomized direct search: it probes a finite-radius polytope around the current parameters and moves toward the lowest-cost vertices through an optimal-transport barycenter. When the loss is piecewise-constant or
|
|
137
|
+
PolyStep is a randomized direct search: it probes a finite-radius polytope around the current parameters and moves toward the lowest-cost vertices through an optimal-transport barycenter. When the loss is piecewise-constant or truly non-differentiable, the local gradient is zero almost everywhere, so finite-difference estimates carry no signal and ES/SPSA stall, while a finite radius steps across the flat regions. This is the setting where direct search (generalized pattern search, MADS) has convergence guarantees on nonsmooth and discontinuous objectives that gradient-estimate methods lack. Example 09 shows the separation on a hard decision tree.
|
|
138
138
|
|
|
139
139
|
Recent gradient-free work targets the same regime, including low-rank evolution strategies for spiking networks (arXiv:2605.30361) and gradient-free trust regions for recurrent spiking networks (arXiv:2601.21572). PolyStep is complementary: a general-purpose direct-search optimizer with subspace compression.
|
|
140
140
|
|
|
@@ -179,7 +179,9 @@ Among gradient-free optimizers PolyStep is the strongest on every task here, at
|
|
|
179
179
|
- **Softmax OT solver** with an entropic Sinkhorn alternative.
|
|
180
180
|
- **Subspace compression**: `HybridSubspace` (recommended), `AdaptiveSubspace`, and sparse projection for very large models.
|
|
181
181
|
- **Block-wise OT** for per-layer decomposition.
|
|
182
|
-
- **`torch.compile`** opt-in on hot paths
|
|
182
|
+
- **`torch.compile`** opt-in on the OT hot paths (`compile=True`) and on the
|
|
183
|
+
candidate forward (`compile_evaluator` = fusion; `compile_forward` = CUDA
|
|
184
|
+
graphs on the in-place path). See [`docs/performance.md`](docs/performance.md).
|
|
183
185
|
- **Vmap-safe layers**: drop-in attention and LSTM that play nicely with `torch.vmap`.
|
|
184
186
|
- **Sub-linear memory**: forward-only evaluation, no BPTT activation tape (~30x savings at long SNN horizons).
|
|
185
187
|
- **CMA-ES inspired adaptation** of subspace covariance (experimental, monolithic step only; `use_adaptive_radius` is the stable default).
|
|
@@ -211,6 +213,7 @@ docs/ API overview, reproducibility guide
|
|
|
211
213
|
| [`examples/`](examples/) | 8 runnable demos with output figures |
|
|
212
214
|
| [`experiments/`](experiments/) | Full paper reproduction harness |
|
|
213
215
|
| [`docs/api_overview.md`](docs/api_overview.md) | API reference |
|
|
216
|
+
| [`docs/performance.md`](docs/performance.md) | Per-step cost & the compile flags (`compile_evaluator`, `compile_forward`) |
|
|
214
217
|
| [`LIMITATIONS.md`](LIMITATIONS.md) | Known limitations |
|
|
215
218
|
| [`CONTRIBUTING.md`](CONTRIBUTING.md) | Contribution guidelines |
|
|
216
219
|
| [`CHANGELOG.md`](CHANGELOG.md) | Release history |
|
|
@@ -43,7 +43,7 @@ dev = [
|
|
|
43
43
|
"pytest-timeout>=2.2",
|
|
44
44
|
"pytest-rerunfailures>=14.0",
|
|
45
45
|
"pytest-cov>=5.0",
|
|
46
|
-
"ruff
|
|
46
|
+
"ruff==0.15.12", # pinned to match CI so `ruff format` never disagrees local vs CI
|
|
47
47
|
]
|
|
48
48
|
# Full experiment reproduction (paper results, ablations, baselines)
|
|
49
49
|
experiments = [
|
|
@@ -52,7 +52,12 @@ experiments = [
|
|
|
52
52
|
"pandas>=2.0",
|
|
53
53
|
"python-sat", # unusual pre-release versioning, left unpinned to stay installable
|
|
54
54
|
"torchvision>=0.25",
|
|
55
|
-
"cma>=3.3",
|
|
55
|
+
"cma>=3.3", # run_elevation / run_moe / run_maxsat baselines
|
|
56
|
+
"evotorch>=0.5", # CMAES baseline (benchmarks.baselines.train_cmaes)
|
|
57
|
+
"nevergrad>=1.0", # nevergrad baseline (benchmarks.baselines)
|
|
58
|
+
"snntorch>=0.7", # SNN spike datasets (benchmarks.utils)
|
|
59
|
+
"tabulate>=0.9", # results tables (benchmarks.utils)
|
|
60
|
+
"datasets>=2.14", # HuggingFace SST-2 / text loaders (benchmarks.utils)
|
|
56
61
|
"gymnasium>=0.29",
|
|
57
62
|
]
|
|
58
63
|
# RL experiments (Box2D envs)
|
|
@@ -55,9 +55,7 @@ def step_blockwise(opt, closure: Callable) -> float:
|
|
|
55
55
|
# Probe-radius jitter (Thm. 4.2 condition (iv); no-op when probe_radius_jitter == 0).
|
|
56
56
|
probe_r = opt._apply_probe_radius_jitter(probe_r)
|
|
57
57
|
|
|
58
|
-
#
|
|
59
|
-
if X.dim() == 1:
|
|
60
|
-
X = X.unsqueeze(0)
|
|
58
|
+
# state.X is always 2-D (num_particles, particle_dim); no reshape needed.
|
|
61
59
|
|
|
62
60
|
# Convert layout-indexed flat to block-indexed flat before splitting.
|
|
63
61
|
# Per-layer blocks pad each entry independently, creating different
|
|
@@ -484,9 +482,7 @@ def step_subspace_blockwise(opt, closure: Callable) -> float:
|
|
|
484
482
|
# Probe-radius jitter (Thm. 4.2 condition (iv); no-op when probe_radius_jitter == 0).
|
|
485
483
|
probe_r = opt._apply_probe_radius_jitter(probe_r)
|
|
486
484
|
|
|
487
|
-
#
|
|
488
|
-
if X.dim() == 1:
|
|
489
|
-
X = X.unsqueeze(0)
|
|
485
|
+
# state.X is always 2-D (num_sub_particles, subspace_particle_dim); no reshape needed.
|
|
490
486
|
|
|
491
487
|
# Get subspace dimension
|
|
492
488
|
sub_dim = opt.subspace.subspace_dim
|
|
@@ -645,7 +641,7 @@ def step_subspace_blockwise(opt, closure: Callable) -> float:
|
|
|
645
641
|
# projection: (full_dim, sub_dim)
|
|
646
642
|
# reconstruct_batch needs projection argument for AdaptiveSubspace
|
|
647
643
|
# Match dtype with projection for mixed precision compatibility
|
|
648
|
-
if opt._mixed_precision and state.projection is not None:
|
|
644
|
+
if opt._mixed_precision and state.projection is not None and state.projection.dtype is not None:
|
|
649
645
|
base_batch = base_batch.to(dtype=state.projection.dtype)
|
|
650
646
|
if opt._adaptive or opt._cma_subspace:
|
|
651
647
|
chunk_params = state.subspace.reconstruct_batch(
|
|
@@ -474,8 +474,12 @@ def step_monolithic(opt, closure: Callable) -> float:
|
|
|
474
474
|
ot_result = opt.solver.solve(**solve_kwargs)
|
|
475
475
|
state.last_solve_eps = ot_epsilon
|
|
476
476
|
|
|
477
|
-
#
|
|
478
|
-
|
|
477
|
+
# Update progressive epsilon from solver stats. Skip in fixed-iteration
|
|
478
|
+
# Sinkhorn mode (threshold <= 0): there the solver always returns
|
|
479
|
+
# converged=True with n_iters == max_iterations, so the ratio is 1.0 and the
|
|
480
|
+
# increase branch would push epsilon to max_epsilon every step. Progressive
|
|
481
|
+
# epsilon is only paired with SinkhornSolver, so threshold always exists.
|
|
482
|
+
if opt._progressive_epsilon is not None and getattr(opt.solver, "threshold", 1.0) > 0:
|
|
479
483
|
opt._progressive_epsilon.update(
|
|
480
484
|
n_iters=ot_result.n_iters,
|
|
481
485
|
max_iterations=getattr(opt.solver, "max_iterations", 1),
|
|
@@ -972,11 +976,27 @@ def step_monolithic(opt, closure: Callable) -> float:
|
|
|
972
976
|
state.base_params = new_base
|
|
973
977
|
# Reset subspace coordinates to zero
|
|
974
978
|
state.X = torch.zeros_like(state.X)
|
|
975
|
-
# Regenerate ALL per-layer projections
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
)
|
|
979
|
+
# Regenerate ALL per-layer projections. Default: fresh RANDOM redraw. Opt-in
|
|
980
|
+
# absorb_aligned_active: bias the new basis toward the just-completed window's productive
|
|
981
|
+
# directions (displacement-SVD) BEFORE the history is zeroed below. Safe here because the
|
|
982
|
+
# duals are reset in this same block, so no live Sinkhorn dual is disrupted (the reason
|
|
983
|
+
# per-step rotation is disabled). Lets a smaller rank track descent across absorbs.
|
|
984
|
+
if getattr(hybrid_sub, "absorb_aligned_active", False) and state.displacement_history_count > 0:
|
|
985
|
+
hist = state.displacement_history[: state.displacement_history_count]
|
|
986
|
+
svd_ratio = hybrid_sub.get_svd_ratio(state.iteration_count, opt.max_iterations or 1)
|
|
987
|
+
state.hybrid_projections = hybrid_sub._rotate_all_displacement(
|
|
988
|
+
state.hybrid_projections,
|
|
989
|
+
hist,
|
|
990
|
+
svd_ratio,
|
|
991
|
+
state.X.device,
|
|
992
|
+
state.X.dtype,
|
|
993
|
+
state.iteration_count,
|
|
994
|
+
)
|
|
995
|
+
else:
|
|
996
|
+
state.hybrid_projections = hybrid_sub.init_projections(
|
|
997
|
+
state.X.device,
|
|
998
|
+
state.X.dtype,
|
|
999
|
+
)
|
|
980
1000
|
# Reset displacement history
|
|
981
1001
|
state.displacement_history.zero_()
|
|
982
1002
|
state.displacement_history_idx = 0
|
|
@@ -542,7 +542,7 @@ class AdaptiveSubspace:
|
|
|
542
542
|
Returns:
|
|
543
543
|
SVD ratio in [svd_ratio_init, svd_ratio_final].
|
|
544
544
|
"""
|
|
545
|
-
progress = min(1.0, step / max(1, total_steps))
|
|
545
|
+
progress = min(1.0, step / max(1, total_steps or 1))
|
|
546
546
|
return self.svd_ratio_init + progress * (self.svd_ratio_final - self.svd_ratio_init)
|
|
547
547
|
|
|
548
548
|
# ------------------------------------------------------------------
|
|
@@ -8,6 +8,7 @@ epoch iteration, or batch handling.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
import copy
|
|
11
12
|
from dataclasses import dataclass
|
|
12
13
|
from typing import Callable, List, Optional
|
|
13
14
|
|
|
@@ -76,12 +77,20 @@ class TrainConfig:
|
|
|
76
77
|
log_every: Step interval for built-in logging. Must be > 0.
|
|
77
78
|
callbacks: List of ``TrainCallback`` instances. ``None`` is
|
|
78
79
|
normalized to an empty list.
|
|
80
|
+
restore_best: If ``True`` (default), snapshot the model weights
|
|
81
|
+
whenever the tracked loss reaches a new minimum and restore that
|
|
82
|
+
best snapshot before ``train()`` returns. Gradient-free search is
|
|
83
|
+
noisy and updates the model in place, so the *last* step is not
|
|
84
|
+
necessarily the best; this reverts a late divergence. The tracked
|
|
85
|
+
loss is the exact full-batch loss when a callback consumes per-step
|
|
86
|
+
metrics, otherwise the (cheap, already-computed) OT cost proxy.
|
|
79
87
|
"""
|
|
80
88
|
|
|
81
89
|
epochs: int = 10
|
|
82
90
|
batch_size: int = 32
|
|
83
91
|
log_every: int = 10
|
|
84
92
|
callbacks: Optional[List[TrainCallback]] = None
|
|
93
|
+
restore_best: bool = True
|
|
85
94
|
|
|
86
95
|
def __post_init__(self):
|
|
87
96
|
if self.epochs <= 0:
|
|
@@ -134,7 +143,8 @@ def train(
|
|
|
134
143
|
evaluator = NNCostEvaluator(
|
|
135
144
|
model,
|
|
136
145
|
loss_fn=loss_fn,
|
|
137
|
-
compile_vmap=getattr(optimizer, "
|
|
146
|
+
compile_vmap=getattr(optimizer, "compile_evaluator", False),
|
|
147
|
+
compile_forward=getattr(optimizer, "compile_forward", False),
|
|
138
148
|
)
|
|
139
149
|
callbacks = list(config.callbacks)
|
|
140
150
|
global_step = 0
|
|
@@ -146,6 +156,10 @@ def train(
|
|
|
146
156
|
except StopIteration:
|
|
147
157
|
raise ValueError("Model has no trainable parameters")
|
|
148
158
|
|
|
159
|
+
# Track the lowest-loss weights so a noisy run is not left at a late step.
|
|
160
|
+
best_loss = float("inf")
|
|
161
|
+
best_state = None
|
|
162
|
+
|
|
149
163
|
for epoch in range(config.epochs):
|
|
150
164
|
epoch_loss_sum = 0.0
|
|
151
165
|
epoch_loss_count = 0
|
|
@@ -158,8 +172,17 @@ def train(
|
|
|
158
172
|
cost_bs = getattr(optimizer, "cost_batch_size", None)
|
|
159
173
|
if cost_bs is not None and cost_bs < inputs.shape[0]:
|
|
160
174
|
gen = getattr(optimizer, "_generator", None)
|
|
161
|
-
if gen is not None
|
|
162
|
-
|
|
175
|
+
if gen is not None:
|
|
176
|
+
if gen.device == inputs.device:
|
|
177
|
+
idx = torch.randperm(inputs.shape[0], device=inputs.device, generator=gen)[:cost_bs]
|
|
178
|
+
else:
|
|
179
|
+
# Devices differ (seeded CPU generator with CUDA inputs,
|
|
180
|
+
# or cuda:0 vs cuda:1). randperm needs the generator and
|
|
181
|
+
# tensor on the same device: sample on the generator's
|
|
182
|
+
# device to keep the seed, then move the index.
|
|
183
|
+
idx = torch.randperm(inputs.shape[0], device=gen.device, generator=gen)[:cost_bs].to(
|
|
184
|
+
inputs.device
|
|
185
|
+
)
|
|
163
186
|
else:
|
|
164
187
|
idx = torch.randperm(inputs.shape[0], device=inputs.device)[:cost_bs]
|
|
165
188
|
cost_inputs = inputs[idx]
|
|
@@ -174,32 +197,43 @@ def train(
|
|
|
174
197
|
|
|
175
198
|
optimizer.step(closure)
|
|
176
199
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
200
|
+
state = optimizer.state
|
|
201
|
+
ot_cost = state.costs[-1] if state.costs else 0.0
|
|
202
|
+
|
|
203
|
+
if callbacks:
|
|
204
|
+
# A callback consumes per-step metrics: compute the exact
|
|
205
|
+
# full-batch training loss (separate from the OT cost) and build
|
|
206
|
+
# the full metrics dict (velocity_mag included).
|
|
207
|
+
with torch.no_grad():
|
|
208
|
+
output = model(inputs)
|
|
209
|
+
train_loss = loss_fn(output, targets).item()
|
|
210
|
+
metrics = {
|
|
211
|
+
"step": global_step,
|
|
212
|
+
"epoch": epoch,
|
|
213
|
+
"loss": train_loss,
|
|
214
|
+
"ot_cost": ot_cost,
|
|
215
|
+
"displacement": (state.displacement_sqnorms[-1] if state.displacement_sqnorms else 0.0),
|
|
216
|
+
"velocity_mag": (torch.norm(state.velocity).item() if state.velocity is not None else 0.0),
|
|
217
|
+
"converged": (state.linear_convergence[-1] if state.linear_convergence else False),
|
|
218
|
+
"absorb_count": getattr(state, "absorb_count", 0),
|
|
219
|
+
}
|
|
220
|
+
for cb in callbacks:
|
|
221
|
+
if cb.on_step_end(metrics):
|
|
222
|
+
stop = True
|
|
223
|
+
break
|
|
224
|
+
else:
|
|
225
|
+
# No callback consumes per-step metrics: skip the extra
|
|
226
|
+
# full-batch forward + its host syncs (.item()/velocity norm)
|
|
227
|
+
# and track the OT cost as the loss signal for logging / best.
|
|
228
|
+
train_loss = ot_cost
|
|
181
229
|
|
|
182
230
|
epoch_loss_sum += train_loss
|
|
183
231
|
epoch_loss_count += 1
|
|
184
232
|
|
|
185
|
-
#
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
"epoch": epoch,
|
|
190
|
-
"loss": train_loss,
|
|
191
|
-
"ot_cost": state.costs[-1] if state.costs else 0.0,
|
|
192
|
-
"displacement": (state.displacement_sqnorms[-1] if state.displacement_sqnorms else 0.0),
|
|
193
|
-
"velocity_mag": (torch.norm(state.velocity).item() if state.velocity is not None else 0.0),
|
|
194
|
-
"converged": (state.linear_convergence[-1] if state.linear_convergence else False),
|
|
195
|
-
"absorb_count": getattr(state, "absorb_count", 0),
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
# Invoke on_step_end callbacks
|
|
199
|
-
for cb in callbacks:
|
|
200
|
-
if cb.on_step_end(metrics):
|
|
201
|
-
stop = True
|
|
202
|
-
break
|
|
233
|
+
# Snapshot the weights at the lowest tracked loss.
|
|
234
|
+
if config.restore_best and train_loss < best_loss:
|
|
235
|
+
best_loss = train_loss
|
|
236
|
+
best_state = copy.deepcopy(model.state_dict())
|
|
203
237
|
|
|
204
238
|
if stop:
|
|
205
239
|
break
|
|
@@ -218,6 +252,10 @@ def train(
|
|
|
218
252
|
for cb in callbacks:
|
|
219
253
|
cb.on_epoch_end(epoch_metrics)
|
|
220
254
|
|
|
255
|
+
# Restore the best-seen weights (load_state_dict copies in place, keeping dtype/device).
|
|
256
|
+
if config.restore_best and best_state is not None:
|
|
257
|
+
model.load_state_dict(best_state)
|
|
258
|
+
|
|
221
259
|
return model
|
|
222
260
|
|
|
223
261
|
|