polystep 0.7.0__tar.gz → 0.9.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.9.0/CHANGELOG.md +502 -0
- polystep-0.9.0/CITATION.cff +43 -0
- polystep-0.9.0/CONTRIBUTING.md +75 -0
- polystep-0.9.0/LIMITATIONS.md +233 -0
- polystep-0.9.0/MANIFEST.in +15 -0
- {polystep-0.7.0 → polystep-0.9.0}/PKG-INFO +64 -30
- {polystep-0.7.0 → polystep-0.9.0}/README.md +48 -24
- polystep-0.9.0/docs/api_overview.md +359 -0
- polystep-0.9.0/docs/performance.md +174 -0
- polystep-0.9.0/docs/reproducibility.md +122 -0
- {polystep-0.7.0 → polystep-0.9.0}/pyproject.toml +18 -5
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/__init__.py +16 -2
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_compiled.py +80 -22
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_step_blockwise.py +144 -145
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_step_momentum.py +9 -55
- polystep-0.9.0/src/polystep/_step_monolithic.py +1342 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/adaptive_subspace.py +50 -203
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/api.py +90 -35
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/ask_tell.py +1 -1
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/baselines.py +0 -18
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/gym_evaluator.py +0 -6
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/utils.py +55 -94
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/blockwise.py +0 -5
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/cma.py +37 -71
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/cma_subspace.py +91 -101
- polystep-0.9.0/src/polystep/cost_nn.py +1098 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/costs.py +39 -11
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/dynamics.py +86 -27
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/epsilon.py +1 -1
- polystep-0.9.0/src/polystep/factored_subspace.py +244 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/geometry.py +55 -109
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/hybrid_subspace.py +153 -173
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/rnn.py +1 -1
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/optimizer.py +689 -157
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/projection/sparse.py +12 -32
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/quadratic_model.py +22 -19
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solver.py +75 -32
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/_prelude.py +48 -6
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/base.py +3 -1
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/greedy.py +28 -8
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/kl_softmax.py +47 -34
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/sinkhorn.py +97 -53
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/softmax.py +17 -9
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/tempered_softmax.py +9 -7
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/subspace.py +34 -26
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/transform.py +9 -24
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/PKG-INFO +64 -30
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/SOURCES.txt +20 -4
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/requires.txt +10 -5
- polystep-0.9.0/tests/__init__.py +0 -0
- polystep-0.9.0/tests/conftest.py +272 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_ablation_solvers.py +0 -30
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_adaptive_subspace.py +36 -116
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_api.py +2 -42
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_baseline_fairness.py +7 -12
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_benchmarks.py +8 -49
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_blockwise.py +1 -36
- polystep-0.9.0/tests/test_checkpoint_resume.py +348 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_cma.py +0 -131
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_cma_subspace.py +24 -259
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_compiled.py +1 -181
- polystep-0.9.0/tests/test_correctness_regressions.py +1014 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_cost_nn.py +63 -20
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_dfo_features.py +12 -37
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_dynamics.py +0 -15
- polystep-0.9.0/tests/test_end_to_end_learning.py +125 -0
- polystep-0.9.0/tests/test_evaluator_compile_forward.py +88 -0
- polystep-0.9.0/tests/test_factored_subspace.py +107 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_geometry.py +0 -15
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_hardening.py +4 -18
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_hybrid_subspace.py +18 -202
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_integration.py +13 -20
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_layers.py +41 -43
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_mixed_precision.py +27 -11
- polystep-0.9.0/tests/test_mnist.py +392 -0
- polystep-0.9.0/tests/test_no_test_set_leakage.py +36 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_nondiff_data.py +13 -13
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_nondiff_models.py +24 -145
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_numerical_stress.py +31 -72
- polystep-0.9.0/tests/test_objectives.py +115 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_optimizer.py +78 -202
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_quadratic_model.py +0 -5
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_rl_benchmarks.py +2 -2
- polystep-0.9.0/tests/test_silent_wrongness.py +435 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_sinkhorn.py +1 -147
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_sinkhorn_correctness.py +33 -54
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_softmax.py +7 -47
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_softmax_integration.py +33 -95
- polystep-0.9.0/tests/test_sparse_delta_evaluator.py +202 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_sparse_integration.py +26 -102
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_sparse_projection.py +15 -54
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_subspace.py +12 -174
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_subspace_blockwise.py +30 -155
- polystep-0.9.0/tests/test_subspace_contract.py +131 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_transform.py +1 -14
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_turbo.py +26 -224
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_vmap_layer_compatibility.py +6 -50
- polystep-0.7.0/src/polystep/_step_monolithic.py +0 -1070
- polystep-0.7.0/src/polystep/cost_nn.py +0 -633
- polystep-0.7.0/tests/test_adaptive_mnist.py +0 -466
- polystep-0.7.0/tests/test_bug_regressions.py +0 -456
- polystep-0.7.0/tests/test_hybrid_mnist.py +0 -473
- polystep-0.7.0/tests/test_mnist.py +0 -546
- polystep-0.7.0/tests/test_no_test_set_leakage.py +0 -18
- polystep-0.7.0/tests/test_solver_numerics.py +0 -160
- {polystep-0.7.0 → polystep-0.9.0}/LICENSE +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/setup.cfg +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/cartpole.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/metrics.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/policies.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/attention.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/base.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/synthetic.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/projection/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/py.typed +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/__init__.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/dependency_links.txt +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/top_level.txt +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_ask_tell.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_biased_rotation.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_costs.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_fullspace_chunk_bound.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_fused_projection_rebuild.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_kl_softmax.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_mixed_precision_step.py +0 -0
- {polystep-0.7.0 → polystep-0.9.0}/tests/test_trust_region.py +0 -0
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.9.0 - 2026-07-27
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- `SparseDeltaEvaluator`: a full-space candidate replaces `particle_dim` scalars, so its
|
|
8
|
+
output differs from the base in at most that many columns. Propagates the delta through
|
|
9
|
+
elementwise layers and goes dense one `Linear` past the perturbed one, instead of a
|
|
10
|
+
dense `bmm` over `N` weight sets. Automatic for `nn.Sequential` of `Linear` and
|
|
11
|
+
elementwise layers, with a per-chunk fallback.
|
|
12
|
+
- `FactoredSubspace`: perturbs each 2D parameter by `dW = A @ B` with `B` fixed, so
|
|
13
|
+
`x (W + A B)^T = x W^T + (x B^T) A^T` and no candidate weight is built. Not the
|
|
14
|
+
default: the fixed `B` confines perturbations to `rank` input directions.
|
|
15
|
+
- `step(..., objective_token=...)` marks which objective a step measures, so cost rows
|
|
16
|
+
cannot be reused across minibatches. `release_evaluator()`, `resync_from_model()`,
|
|
17
|
+
`MinCostGreedySolver` and `TopKMeanSolver` are public.
|
|
18
|
+
- `MANIFEST.in`, so the sdist ships a runnable test suite. CI runs it from the tarball.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- `api.train()` never called `register_evaluator`, the only assignment of
|
|
23
|
+
`_cost_evaluator`, `_fused_inputs`, `_factored_evaluator` and `_sparse_delta_evaluator`,
|
|
24
|
+
so every fast evaluation path was unreachable through the training API.
|
|
25
|
+
- Cost-row reuse under `adaptive_probes` was per particle, but a candidate is the whole
|
|
26
|
+
configuration with one row replaced, so a stagnant particle's row was still measured
|
|
27
|
+
against every other particle's position. Reuse is now all or nothing.
|
|
28
|
+
- `multifidelity_screen` averaged direction contrast across particles, which carry
|
|
29
|
+
independent rotations, so the kept set was chosen at random. Ranked per particle now.
|
|
30
|
+
- Tied weights raised on both evaluation paths. `batch_unflatten` emits the canonical key
|
|
31
|
+
only; `unflatten` still carries aliases for `load_state_dict`.
|
|
32
|
+
- `restore_best` keyed on the OT cost when no callback was registered, restoring the step
|
|
33
|
+
with the cheapest probe cloud rather than the best weights. It also snapshots to CPU.
|
|
34
|
+
- Chunk size ignored the weight set a non-fused subspace builds per candidate and the vmap
|
|
35
|
+
activations. A 120K-param step peaked at 1661 MB in full space and 10565 MB in subspace
|
|
36
|
+
mode; now 946 MB and 1461 MB.
|
|
37
|
+
- `AdaptiveSubspace` and per-layer rotation replaced the projection while the coordinates
|
|
38
|
+
were non-zero, moving the weights with nothing evaluated behind them.
|
|
39
|
+
- `CMAAdaptiveSubspace` never rotated or absorbed, left its hyperparameters at `0.0` so
|
|
40
|
+
the covariance update was inert, normalized the evolution paths by `sigma` instead of
|
|
41
|
+
`step_radius` so `C_diag` decayed to its floor, carried state through basis rotations
|
|
42
|
+
untransformed, and accumulated at bf16 where `1 + c_1*x` rounds back to `1`.
|
|
43
|
+
- Anderson acceleration subtracted `dX` instead of `dX + dR`, so `anderson_depth > 0`
|
|
44
|
+
converged slower than no acceleration.
|
|
45
|
+
- `num_particles = 1` froze the optimizer: balanced OT with one row forces a uniform plan.
|
|
46
|
+
The default now selects `SoftmaxSolver` and warns.
|
|
47
|
+
- `scale_cost` divided before recentering, so adding a constant to every cost changed the
|
|
48
|
+
plan. `SinkhornSolver` also reported `ent_reg_cost` in the wrong frame; the plan and
|
|
49
|
+
duals were unaffected. The greedy and top-k solvers had the same reporting bug.
|
|
50
|
+
- Softmax paths recentred by the global minimum, so a row above it underflowed to `-inf`
|
|
51
|
+
and returned NaN; the fused kernel did not recenter at all.
|
|
52
|
+
- `sanitize_cost` mapped `-inf` to the worst finite value instead of the best, and used an
|
|
53
|
+
absolute penalty that entered the `scale_cost='mean'` reduction.
|
|
54
|
+
- `state_dict` captured only `SolverState`, so resume was not exact for `amortize_steps`,
|
|
55
|
+
`adaptive_probes`, `trust_region` or `HybridSubspace`. Format is 3; format 2 loads with
|
|
56
|
+
a warning.
|
|
57
|
+
- A stagnation absorb never cleared `stagnation_count`, redrawing the basis every step on
|
|
58
|
+
a plateau. `resync_from_model()` left momentum and rotation state on the old anchor.
|
|
59
|
+
- Probe losses accumulated in FP32 unconditionally, so a float64 objective whose costs
|
|
60
|
+
differ below FP32 resolution produced a constant cost matrix.
|
|
61
|
+
- Block-wise biased rotation raised on bf16 under `mixed_precision=True`. A zero descent
|
|
62
|
+
direction produced a singular rotation collapsing `+e0` and `-e0`.
|
|
63
|
+
- Smaller: the in-place evaluator dropped unknown parameter keys silently; the
|
|
64
|
+
trust-region ratio scored a Newton step that was never applied; displacement history
|
|
65
|
+
mixed bases; randomized PCA drew from the global RNG; rank transitions dropped
|
|
66
|
+
`HybridSubspace` settings; `data_dependent_init` had the wrong sign; a float64 model
|
|
67
|
+
with a subspace raised; a frozen model gave an opaque error; `pyproject.toml` declared
|
|
68
|
+
`torch>=2.4` where 0.2.0 announced `torch>=2.8`.
|
|
69
|
+
|
|
70
|
+
### Changed
|
|
71
|
+
|
|
72
|
+
- `adaptive_probes` and `adaptive_num_probe` default on under `block_strategy='monolithic'`.
|
|
73
|
+
- `use_csa` is refused with a warning: the OT step is a deterministic descent direction, so
|
|
74
|
+
CSA reads agreement as the normal state and `sigma` grows without bound.
|
|
75
|
+
- `multifidelity_screen` cuts the forward budget instead of only reweighting, and runs only
|
|
76
|
+
when `screen_fidelity/num_probe + screen_keep_ratio < 1`. Needs a cheap closure from
|
|
77
|
+
`screen_closure_from()`; `api.train` supplies one.
|
|
78
|
+
- Block-wise warns for options it ignores; `grouped` warns when combined with a subspace.
|
|
79
|
+
`LinearSubspace` amplifies rather than dilutes the perturbation, as its docstring said.
|
|
80
|
+
- `_step_blockwise` claimed blockwise cuts OT cost from `O(N^2)` to `O(N^2/L)`. The forward
|
|
81
|
+
count is identical; `L` solves over `P/L` rows replace one over `P`.
|
|
82
|
+
- Removed `get_sobol_rotation_matrices`, `HybridSubspace.reconstruct_base` and
|
|
83
|
+
`reconstruct_batch_delta`: no callers, no coverage.
|
|
84
|
+
|
|
85
|
+
### Performance
|
|
86
|
+
|
|
87
|
+
- `thin_qr` pins basis construction to one thread: `HybridSubspace` setup for the MNIST
|
|
88
|
+
example drops from 27.0 s to 97.8 ms. `apply_biased_rotation` uses modified Gram-Schmidt
|
|
89
|
+
instead of `torch.linalg.qr`, 7-33x faster at these sizes. Compiled kernels take radius
|
|
90
|
+
and epsilon as 0-d tensors, so a scheduled radius no longer recompiles every step.
|
|
91
|
+
- Example 06 on an RTX 5090: 1024 s to 845 s, shift-recovery +9.3 pp to +10.7 pp.
|
|
92
|
+
|
|
93
|
+
### Tests, examples and docs
|
|
94
|
+
|
|
95
|
+
- 967 tests in 262 s with an intermittent timeout failure, now 941 in 8 s with none.
|
|
96
|
+
`tests/test_subspace_contract.py` replaces per-class copies of the properties every
|
|
97
|
+
subspace shares. Removed tests that could not fail. Suite verified by mutation: eight
|
|
98
|
+
semantic changes to the source are all caught.
|
|
99
|
+
- `tests/test_nondiff_data.py` guarded nine tests on the `pysat` import but not on the
|
|
100
|
+
`experiments/` tree they import from, so they failed rather than skipped in the sdist
|
|
101
|
+
when pysat happened to be installed.
|
|
102
|
+
- Examples pin CPU threads, check optional imports before use instead of failing after the
|
|
103
|
+
run, and register the evaluator in hand-rolled loops. `python-sat` moved into the
|
|
104
|
+
`examples` extra, which example 04 needs.
|
|
105
|
+
- `release.yml` lint and test commands had drifted from `ci.yml` and it lacked the CPU
|
|
106
|
+
torch index; both now match. README and the docs corrected where they had drifted.
|
|
107
|
+
|
|
108
|
+
## 0.8.0 - 2026-07-24
|
|
109
|
+
|
|
110
|
+
### Added
|
|
111
|
+
|
|
112
|
+
- `compile_forward` on `NNCostEvaluator` / `PolyStepOptimizer`: CUDA-graphs the
|
|
113
|
+
forward+loss on the in-place path and replays it per candidate. Rescues the
|
|
114
|
+
memory-forced path; does not beat compiled vmap. See `docs/performance.md`.
|
|
115
|
+
- `TrainConfig(restore_best=True)` (default): restore the lowest-loss weights
|
|
116
|
+
before `train()` returns.
|
|
117
|
+
- `PolyStepOptimizer.state_dict()` / `load_state_dict()` for checkpoint-resume
|
|
118
|
+
(solver state, `ProgressiveEpsilon` internals, RNG). Model weights save separately.
|
|
119
|
+
|
|
120
|
+
### Changed
|
|
121
|
+
|
|
122
|
+
- `compile_evaluator` / `compile_forward` propagate to a registered evaluator via
|
|
123
|
+
`register_evaluator`, so they work outside `api.train()`.
|
|
124
|
+
- `train()` skips the extra per-step full-batch forward and its host syncs when no
|
|
125
|
+
callback consumes per-step metrics.
|
|
126
|
+
|
|
127
|
+
### Fixed
|
|
128
|
+
|
|
129
|
+
- Standalone `PolyStep` accepts any epsilon scheduler (`CosineEpsilon`,
|
|
130
|
+
`ProgressiveEpsilon`), not only `LinearEpsilon` (was `TypeError`).
|
|
131
|
+
- `mixed_precision=True` builds the `ParamLayout` after the BF16 cast, so full-space
|
|
132
|
+
candidate params are BF16 (were FP32).
|
|
133
|
+
- `evaluate_subspace_inplace` casts float inputs to the param dtype, fixing a mismatch
|
|
134
|
+
under `mixed_precision` + `HybridSubspace` + FP32 inputs.
|
|
135
|
+
- `cost_batch_size` subsampling keeps the seeded RNG and compares the full device on a
|
|
136
|
+
mismatch (was non-deterministic; fixes a `cuda:0`/`cuda:1` `randperm` crash).
|
|
137
|
+
- `PolyStep.run()` accepts a 1-D initial point (normalized to one particle).
|
|
138
|
+
- `ProgressiveEpsilon` no longer inflates epsilon every step under a fixed-iteration
|
|
139
|
+
Sinkhorn solver (`threshold<=0`).
|
|
140
|
+
- Greedy solvers: `MinCostGreedySolver` sanitizes non-finite costs; `TopKMeanSolver`
|
|
141
|
+
rejects `k=0`.
|
|
142
|
+
- `PolyStep.step()` uses the shared `sanitize_cost` (was an inline copy that synced and
|
|
143
|
+
could overflow the penalty to `inf`); `state.costs` records the raw mean cost.
|
|
144
|
+
- `HybridSubspace` docstring corrected (QR-orthonormal columns; `step_radius` retune).
|
|
145
|
+
- `NNCostEvaluator` compile docstrings match the shipped modes; the vmap-fallback
|
|
146
|
+
filter matches functorch markers instead of any "batched" error.
|
|
147
|
+
|
|
148
|
+
## 0.7.0 - 2026-07-20
|
|
149
|
+
|
|
150
|
+
### Added
|
|
151
|
+
|
|
152
|
+
- On PyPI now: `pip install polystep` (or `uv add polystep`).
|
|
153
|
+
|
|
154
|
+
### Changed
|
|
155
|
+
|
|
156
|
+
- Wider install range: `torch >= 2.4` (was 2.10) and `numpy >= 1.24` (was 2.0).
|
|
157
|
+
- Blockwise and subspace-blockwise steps reuse one buffer for the per-chunk
|
|
158
|
+
configs instead of reallocating it every chunk, so a step allocates less.
|
|
159
|
+
|
|
160
|
+
### Fixed
|
|
161
|
+
|
|
162
|
+
- Solvers stay accurate when the cost matrix has a large constant offset
|
|
163
|
+
(`|C|` far above `epsilon`). The cost is recentered before the log-domain
|
|
164
|
+
math, which leaves the transport plan unchanged, so `SinkhornSolver.matrix`,
|
|
165
|
+
`KLSoftmaxSolver(lam=0)`, and the softmax solvers no longer lose precision or
|
|
166
|
+
return NaN in that regime.
|
|
167
|
+
- `SinkhornSolver` computes `ent_reg_cost` from the actual plan mass, so it is
|
|
168
|
+
correct before convergence too, and it warns when the two marginals do not
|
|
169
|
+
sum to the same total.
|
|
170
|
+
- A 1D biased rotation returns the identity instead of flipping the axis it just
|
|
171
|
+
aligned (`SO(1)` cannot represent a reflection).
|
|
172
|
+
|
|
173
|
+
## 0.6.1 - 2026-07-15
|
|
174
|
+
|
|
175
|
+
### Added
|
|
176
|
+
|
|
177
|
+
- `examples/09_hard_decision_tree.py`: a hard oblique decision tree with strict
|
|
178
|
+
argmax routing and no relaxation, trained on an XOR checkerboard. PolyStep
|
|
179
|
+
optimizes the hard tree directly while OpenAI-ES and SPSA stall on its
|
|
180
|
+
piecewise-constant loss; a soft-tree Adam baseline is scored after hardening.
|
|
181
|
+
|
|
182
|
+
### Changed
|
|
183
|
+
|
|
184
|
+
- The HybridSubspace fused block-diagonal projection is rebuilt only when the
|
|
185
|
+
projection rotates. With the default `rotation_interval=0` the projection is
|
|
186
|
+
static, so the previous per-step `block_diag` rebuild was pure overhead.
|
|
187
|
+
|
|
188
|
+
### Fixed
|
|
189
|
+
|
|
190
|
+
- `mixed_precision=True` runs end to end. The barycentric and fused-softmax
|
|
191
|
+
projections cast the FP32 OT weights to the BF16 geometry dtype, `HybridSubspace`
|
|
192
|
+
runs its projection QR in FP32 (no BF16 CPU `geqrf`), and the cost evaluator
|
|
193
|
+
matches float inputs to the parameter dtype. Previously the first step raised a
|
|
194
|
+
dtype mismatch.
|
|
195
|
+
- The barycentric projection normalizes by the realized transport row sum instead
|
|
196
|
+
of the target marginal, so an unconverged Sinkhorn plan gives a
|
|
197
|
+
translation-invariant step. The softmax path is unchanged.
|
|
198
|
+
- Full-space monolithic steps bound the default evaluation chunk to a fixed memory
|
|
199
|
+
budget, avoiding the `O(n_params^2)` config buffer that ran out of memory at the
|
|
200
|
+
default `chunk_size=None`.
|
|
201
|
+
- `apply_biased_rotation` realigns the first search axis with the requested bias
|
|
202
|
+
(QR fixes the column only up to sign), so the biased search points toward descent
|
|
203
|
+
rather than ascent, and runs its QR and determinant in FP32 for BF16 inputs.
|
|
204
|
+
|
|
205
|
+
## 0.6.0 - 2026-07-12
|
|
206
|
+
|
|
207
|
+
### Added
|
|
208
|
+
|
|
209
|
+
- `examples/08_direct_loss_minimization.py`: direct F1 minimization on an imbalanced
|
|
210
|
+
checkerboard where PolyStepES beats both a biased gradient (Adam+STE) and OpenAI-ES.
|
|
211
|
+
- `experiments/runners/variant_sweep.py`: a reproducible sweep of the optimizer variants (solver, representation, block strategy,
|
|
212
|
+
adaptation flags, schedules, geometry) across nine tasks, ranked by forward-eval budget
|
|
213
|
+
with a state-mutation self-check that flags any variant whose state never changed.
|
|
214
|
+
|
|
215
|
+
### Changed
|
|
216
|
+
|
|
217
|
+
- CMA covariance updates use the covariance-metric offspring step `y = sqrt(C_diag) * z`,
|
|
218
|
+
so the evolution paths and rank-mu match the covariance-scaled sampling.
|
|
219
|
+
- AdaptiveSubspace runs the per-step basis QR on the GPU in fp32 for CUDA targets, several
|
|
220
|
+
times faster than the previous CPU round-trip for large models.
|
|
221
|
+
|
|
222
|
+
### Fixed
|
|
223
|
+
|
|
224
|
+
- `SinkhornSolver` rejects `max_iterations < 1` and validates user-supplied marginals
|
|
225
|
+
(finite, nonnegative, positive mass) instead of clamping to an infeasible plan.
|
|
226
|
+
- `PolyStepES` validates `dim`, `num_particles`, `epsilon`, and `step_radius` at construction.
|
|
227
|
+
- CMA covariance and step-size adaptation are disabled with a warning under non-monolithic
|
|
228
|
+
block strategies, where they previously never updated.
|
|
229
|
+
- `trust_region` now records its predicted improvement independent of `biased_rotation`, so
|
|
230
|
+
the step-radius multiplier adapts whenever `use_quadratic_model` and `num_probe >= 2` hold
|
|
231
|
+
instead of staying frozen at 1.0. `trust_region` also auto-enables the quadratic model.
|
|
232
|
+
- Heaviside stall detection uses the correct generation index (`generation + 1`).
|
|
233
|
+
|
|
234
|
+
### Removed
|
|
235
|
+
|
|
236
|
+
- Dead `compute_ot_weights` export; the rank-mu path uses the transport masses directly.
|
|
237
|
+
|
|
238
|
+
## 0.5.0 - 2026-07-07
|
|
239
|
+
|
|
240
|
+
### Added
|
|
241
|
+
|
|
242
|
+
- `PolyStepES` and `minimize`: an ask/tell interface so PolyStep drops into
|
|
243
|
+
evolution-strategy harnesses (evosax, NeuroEvoBench) as a gradient-free optimizer.
|
|
244
|
+
- `examples/07_binary_net_no_ste.py`: STE-free binary-net training, PolyStepES vs OpenAI-ES.
|
|
245
|
+
- `experiments/bench_ask_tell.py`: ask/tell comparison against a Gaussian ES on the synthetic suite.
|
|
246
|
+
|
|
247
|
+
### Changed
|
|
248
|
+
|
|
249
|
+
- CMA covariance rank-mu now estimates per-coordinate variance from the
|
|
250
|
+
transport-weighted polytope vertices, and drops the per-step scatter allocation.
|
|
251
|
+
- CSA calibrates the step-size against the running norm of the evolution path
|
|
252
|
+
instead of sqrt(n), so sigma no longer collapses on OT-scale steps.
|
|
253
|
+
- Anderson acceleration uses a Tikhonov ridge solve and only accepts steps that
|
|
254
|
+
do not regress the dual objective.
|
|
255
|
+
- Newton refinement keeps its step only when the quadratic model says it beats
|
|
256
|
+
the plain OT step.
|
|
257
|
+
- Amortized momentum coasts along the OT step, not the OT-plus-momentum step.
|
|
258
|
+
|
|
259
|
+
### Fixed
|
|
260
|
+
|
|
261
|
+
- Multi-fidelity screening no longer feeds its own dampened cost back in, which
|
|
262
|
+
had permanently starved low-contrast directions.
|
|
263
|
+
- Sinkhorn fixed-iteration mode reports convergence on a finite result, so
|
|
264
|
+
ProgressiveEpsilon stops inflating epsilon.
|
|
265
|
+
- `adaptive_omega` no longer erases the divergence back-off within the same check.
|
|
266
|
+
- Guarded the barycentric projection against a zero source marginal.
|
|
267
|
+
|
|
268
|
+
## 0.4.0 - 2026-06-21
|
|
269
|
+
|
|
270
|
+
### Added
|
|
271
|
+
|
|
272
|
+
- `solvers/_prelude.py`: one shared solver preamble (FP32 promotion, device-side
|
|
273
|
+
non-finite cost sanitization, device/dtype-aligned marginals and warm-start
|
|
274
|
+
duals) used by the Sinkhorn, softmax, and tempered-softmax solvers.
|
|
275
|
+
- `experiments/scripts/bench_eps_rescale.py` and `tests/test_hardening.py`.
|
|
276
|
+
|
|
277
|
+
### Changed
|
|
278
|
+
|
|
279
|
+
- Fused-softmax now honors every `scale_cost` mode (`'mean'/'max_cost'/float/None`),
|
|
280
|
+
matching `SoftmaxSolver`; the compiled kernel no longer scales internally.
|
|
281
|
+
- `TemperedSoftmaxSolver` gains FP32 promotion, non-finite handling, and an
|
|
282
|
+
autocast-disabled softmax (previously absent).
|
|
283
|
+
- Warm-start dual re-centering uses the valid gauge `f -> f+c, g -> g-c` instead
|
|
284
|
+
of independent mean subtraction, which perturbed the iterate under `omega != 1`.
|
|
285
|
+
- `BatchedLinearEvaluator` applies the real activation modules (exact
|
|
286
|
+
`LeakyReLU.negative_slope`, `GELU.approximate`) rather than hardcoded
|
|
287
|
+
functional defaults; non-default `Flatten` falls back to vmap.
|
|
288
|
+
- Low-level `PolyStep.step()` forwards `init_eps` like the optimizer path.
|
|
289
|
+
|
|
290
|
+
### Removed
|
|
291
|
+
|
|
292
|
+
- Low-rank Sinkhorn (`SinkhornSolver.{rank,gamma,auto_rank_threshold}`,
|
|
293
|
+
`_solve_low_rank`, `SinkhornResult.{_Q,_R,_g_lr}`, the `solve(seed=...)` arg,
|
|
294
|
+
and the `rank` params on `PolyStepOptimizer` / `PolyStep`).
|
|
295
|
+
|
|
296
|
+
### Fixed
|
|
297
|
+
|
|
298
|
+
- Re-validate `epsilon > 0` inside `SinkhornSolver.solve()` (schedules mutate
|
|
299
|
+
it); reject `check_every < 1`, `num_probe < 1`, and `scale_cost` of 0 / non-finite.
|
|
300
|
+
- Mezzadri rotation sign correction treats `sign(0)` as `+1` so an underflowed
|
|
301
|
+
QR diagonal can't null a column; dropped a redundant `Q.clone()`.
|
|
302
|
+
|
|
303
|
+
## 0.3.0 - 2026-05-27
|
|
304
|
+
|
|
305
|
+
Cleanup. No public API changes.
|
|
306
|
+
|
|
307
|
+
### Changed
|
|
308
|
+
|
|
309
|
+
- `solvers/sinkhorn.py`: dropped two GPU->CPU syncs per Sinkhorn solve.
|
|
310
|
+
`cost_scale` (the warm-start `clamp_` bound) stays on-device as a 0-d
|
|
311
|
+
tensor, and `ent_reg_cost = <f,a> + <g,b>` resolves both inner products
|
|
312
|
+
in one host transfer via `torch.stack([...]).sum().item()`. Same pattern
|
|
313
|
+
applied to the low-rank convergence loop and its `err_a/err_b` marginal
|
|
314
|
+
transfers.
|
|
315
|
+
- `_step_blockwise.py`: per-block fused-softmax `ent_cost_tensor.item()`
|
|
316
|
+
calls now defer to a single `torch.stack([...]).sum().item()` after the
|
|
317
|
+
per-block loop, mirroring `block_disp_terms` and `block_model_loss_terms`.
|
|
318
|
+
Saves `O(num_blocks)` syncs per step in the fused-softmax path.
|
|
319
|
+
- `cma.py`: `update_step_size_csa` now accepts an optional pre-computed
|
|
320
|
+
`p_sigma_norm`; both call sites (`_step_common.update_cma_state`,
|
|
321
|
+
`_step_monolithic.step`) forward the norm they already paid for in
|
|
322
|
+
`compute_heaviside_sigma`, removing one redundant `torch.norm(...).item()`
|
|
323
|
+
per CMA generation.
|
|
324
|
+
|
|
325
|
+
### Fixed
|
|
326
|
+
|
|
327
|
+
- `CMAAdaptiveSubspace.rotate()` now forwards `transport_matrix`,
|
|
328
|
+
`X_vertices`, and `X_current` to the wrapped `AdaptiveSubspace`, so the
|
|
329
|
+
`'ot_bias'` rotation mode fires when CMA is enabled (it previously fell
|
|
330
|
+
through to random rotation).
|
|
331
|
+
- `docs/api_overview.md`: the `PolyStepOptimizer.step` snippet used a
|
|
332
|
+
zero-argument lambda; replaced with the real
|
|
333
|
+
`closure(batched_params) -> losses` signature via `NNCostEvaluator`. The
|
|
334
|
+
`SparseRandomProjection` example used `input_dim` / `output_dim` keyword
|
|
335
|
+
args; the actual constructor takes `full_dim` / `subspace_dim`.
|
|
336
|
+
- Tightened two test tolerances that were 100-1000x looser than the
|
|
337
|
+
solver's convergence threshold (`tests/test_numerical_stress.py` row-sum
|
|
338
|
+
checks; `tests/test_ablation_solvers.py` tempered-softmax-vs-greedy
|
|
339
|
+
match).
|
|
340
|
+
|
|
341
|
+
## 0.2.2 - 2026-05-21
|
|
342
|
+
|
|
343
|
+
Codebase cleanup, test modernization, and documentation correctness pass.
|
|
344
|
+
|
|
345
|
+
### Breaking
|
|
346
|
+
|
|
347
|
+
- Vertex generator API (`get_orthoplex_vertices`, `get_simplex_vertices`,
|
|
348
|
+
`get_cube_vertices`) signature changed from `(dim, origin, radius)` to
|
|
349
|
+
`(dim, *, radius=1.0)`. The `origin` parameter has been removed - all
|
|
350
|
+
generators now produce unit-radius templates centered at the origin;
|
|
351
|
+
translation is handled by the solver. This affects direct callers only;
|
|
352
|
+
`PolyStep` and `PolyStepOptimizer` are unaffected.
|
|
353
|
+
|
|
354
|
+
### Fixed
|
|
355
|
+
|
|
356
|
+
- `compute_ot_weights` in `cma.py`: replaced linear softmax normalization
|
|
357
|
+
with entropy-based OT weight computation matching the theoretical
|
|
358
|
+
derivation.
|
|
359
|
+
- `@torch.inference_mode()` migration across `transform.py` (removed
|
|
360
|
+
unused `import warnings`).
|
|
361
|
+
- README and API overview: `max_iteration` -> `max_iterations` (matching
|
|
362
|
+
the actual `PolyStep` dataclass field).
|
|
363
|
+
- API overview: `AdaptiveSubspace` and `LinearSubspace` code snippets
|
|
364
|
+
updated from stale direct constructors to the `from_layout()` factory.
|
|
365
|
+
- API overview: `SolverState(X=X_init)` -> `solver.init_state(X_init)`.
|
|
366
|
+
|
|
367
|
+
### Test suite
|
|
368
|
+
|
|
369
|
+
- Modernized all geometry tests to the new `(dim, radius)` API; fixed 17
|
|
370
|
+
failing tests.
|
|
371
|
+
- Deleted duplicate `test_softmax_correctness.py` (was a copy of tests
|
|
372
|
+
already in `test_softmax.py`).
|
|
373
|
+
- Cleaned up numbered tags and stale pre/post-fix prose across ten
|
|
374
|
+
test files.
|
|
375
|
+
- Net: 905 collected tests across 45 files; fast-tier CI suite passes
|
|
376
|
+
880 (+ 2 skipped, 23 deselected) in ~20 s.
|
|
377
|
+
|
|
378
|
+
### CI
|
|
379
|
+
|
|
380
|
+
- Removed 11 `--ignore` entries from CI (7 pointed to non-existent files,
|
|
381
|
+
1 to a previously deleted file). CI now runs all non-slow, non-GPU tests
|
|
382
|
+
without explicit ignores.
|
|
383
|
+
- Fast experiment-infrastructure tests (`test_baseline_fairness`,
|
|
384
|
+
`test_nondiff_data`, `test_nondiff_models`, `test_rl_benchmarks`) are
|
|
385
|
+
now included in CI (+98 tests, <2 s overhead).
|
|
386
|
+
|
|
387
|
+
## 0.2.1 - 2026-05-14
|
|
388
|
+
|
|
389
|
+
### Added
|
|
390
|
+
|
|
391
|
+
- `examples/06_loihi_snn_polystep.py`: end-to-end skeleton for a
|
|
392
|
+
Loihi 2-style two-stage workflow. Stage 1 pretrains a hard-LIF
|
|
393
|
+
MNIST SNN with PolyStep using `PSTORCH_CONFIGS["snn"]`. Stage 2
|
|
394
|
+
adapts only the writable subset a real Loihi 2 chip exposes at
|
|
395
|
+
runtime (`fc2`, per-population `vth`, and `beta`; about 1.3% of
|
|
396
|
+
model parameters) under an `N(0, 1)` Gaussian input shift. Stage 2
|
|
397
|
+
uses TENT-style safeguards: mixed-batch (half clean / half shifted),
|
|
398
|
+
rank-8 probing on the writable subspace, and two probes per step.
|
|
399
|
+
Both stages use best-test early stopping (patience 4) since
|
|
400
|
+
zeroth-order test curves are noisier. Shifted-test evaluations
|
|
401
|
+
share a fixed seeded noise mask across pre / post / baseline so the
|
|
402
|
+
reported recovery is a paired comparison. On default settings the
|
|
403
|
+
example reaches ~83% best clean accuracy and a +13 pp paired
|
|
404
|
+
shift-recovery over the frozen-readout baseline with near-zero
|
|
405
|
+
clean-accuracy degradation in about 17 minutes on a single GPU.
|
|
406
|
+
Reported numbers shift by a few percentage points run-to-run on
|
|
407
|
+
CUDA because of non-deterministic cuBLAS reductions, but the
|
|
408
|
+
qualitative recovery is robust. The host loop is backend-agnostic:
|
|
409
|
+
`LoihiSpikeEvaluator` is the single swap point against a Lava
|
|
410
|
+
`netx` deployment path.
|
|
411
|
+
|
|
412
|
+
## 0.2.0 - 2026-05-14
|
|
413
|
+
|
|
414
|
+
Dependency floor lift, native fast-path adoption, and performance pass for
|
|
415
|
+
PyTorch 2.12.
|
|
416
|
+
|
|
417
|
+
### Changed
|
|
418
|
+
|
|
419
|
+
- Minimum Python is now 3.11 (was 3.10); minimum PyTorch is now 2.8 (was 2.4);
|
|
420
|
+
NumPy floor is now `>=2.0`.
|
|
421
|
+
- Ruff `target-version` bumped to `py311`. Classifiers updated to advertise
|
|
422
|
+
3.11 / 3.12 / 3.13 / 3.14.
|
|
423
|
+
|
|
424
|
+
### Performance
|
|
425
|
+
|
|
426
|
+
- `SinkhornSolver` convergence loop: batched the per-check scalar
|
|
427
|
+
measurements (err_a, err_b, dual norm, Lyapunov) into a single
|
|
428
|
+
device-to-host transfer, and switched the Anderson-acceleration
|
|
429
|
+
Lyapunov accept gate to a device-side `torch.where`. Eliminates 4-7
|
|
430
|
+
GPU-CPU syncs per `check_every` interval. Microbench (200 iter,
|
|
431
|
+
n=m=512, fp32 on RTX 5090, full Anderson + adaptive omega): **490 ms
|
|
432
|
+
-> 48 ms (~10x)**.
|
|
433
|
+
- Block-wise step (`_step_blockwise.py`): per-block displacement and
|
|
434
|
+
model-loss scalars are accumulated as device tensors and reduced to
|
|
435
|
+
the host once per step instead of twice per block.
|
|
436
|
+
- `NNCostEvaluator.evaluate` and `BatchedLinearEvaluator.evaluate` now
|
|
437
|
+
use `@torch.inference_mode()` instead of `@torch.no_grad()` (matches
|
|
438
|
+
the rest of `cost_nn.py`).
|
|
439
|
+
|
|
440
|
+
### Verified
|
|
441
|
+
|
|
442
|
+
- Full test suite (CPU + CUDA) green on PyTorch 2.12 / Python 3.14 / CUDA 13.0
|
|
443
|
+
on RTX 5090 (sm_120).
|
|
444
|
+
- All five `examples/` reproduce within tolerance (≤1% accuracy / ≤5% returns).
|
|
445
|
+
- `torch.compile(fullgraph=True)` (default in `_compiled.py:try_compile`)
|
|
446
|
+
warns on bypass in PyTorch 2.12 and will hard-error in 2.13; behavior
|
|
447
|
+
documented for the next dependency lift.
|
|
448
|
+
|
|
449
|
+
### Test suite
|
|
450
|
+
|
|
451
|
+
- Consolidated overlapping test files.
|
|
452
|
+
- Net: 935 -> 906 collected tests across 45 files (was 49); fast-tier suite
|
|
453
|
+
passes 885 (+ 2 skipped, 19 deselected) in ~21 s on RTX 5090.
|
|
454
|
+
|
|
455
|
+
## 0.1.0 - 2026-05-03 (Initial Release)
|
|
456
|
+
|
|
457
|
+
First public release alongside the arXiv preprint *Training Non-Differentiable
|
|
458
|
+
Networks via Optimal Transport*.
|
|
459
|
+
|
|
460
|
+
### Core
|
|
461
|
+
|
|
462
|
+
- `PolyStepOptimizer` for gradient-free training of any `nn.Module`
|
|
463
|
+
- Softmax solver (default; fast path) and log-domain Sinkhorn solver with
|
|
464
|
+
warm-started dual potentials
|
|
465
|
+
- Polytope sampling (orthoplex, simplex, cube) with random rotations
|
|
466
|
+
- Vectorised NN evaluation via `torch.func` + optional `torch.vmap`
|
|
467
|
+
- High-level `train()` API with callbacks and early stopping
|
|
468
|
+
|
|
469
|
+
### Subspace compression
|
|
470
|
+
|
|
471
|
+
- `LinearSubspace` - random projection
|
|
472
|
+
- `AdaptiveSubspace` - rotating orthogonal projection
|
|
473
|
+
- `HybridSubspace` - per-layer projections (recommended)
|
|
474
|
+
- `CMAAdaptiveSubspace` - CMA-ES covariance adaptation
|
|
475
|
+
- `SparseRandomProjection` - for models with 100M+ parameters
|
|
476
|
+
|
|
477
|
+
### Scalability
|
|
478
|
+
|
|
479
|
+
- Block-wise per-layer OT decomposition
|
|
480
|
+
- Chunked cost evaluation for memory control
|
|
481
|
+
- `torch.compile` support on GPU hot paths
|
|
482
|
+
- Mixed precision (BF16 model + FP32 solver)
|
|
483
|
+
|
|
484
|
+
### Layers
|
|
485
|
+
|
|
486
|
+
- `VmapSafeMultiHeadAttention` - attention compatible with `torch.vmap`
|
|
487
|
+
- `VmapSafeLSTM` - LSTM compatible with `torch.vmap`
|
|
488
|
+
|
|
489
|
+
### Examples and experiments
|
|
490
|
+
|
|
491
|
+
- 5 runnable demos under `examples/` (quickstart, SNN, RL, MAX-SAT, MNIST)
|
|
492
|
+
- Paper-reproduction harness under `experiments/runners/` covering SNN,
|
|
493
|
+
INT8, argmax, staircase, hard MoE, MAX-SAT (100K-1M vars), MNIST,
|
|
494
|
+
ETTh1 timeseries, RL, and GPT-2 head-only fine-tuning
|
|
495
|
+
- Ablations: OT vs softmax solver, epsilon / radius / particles / subspace
|
|
496
|
+
grid, MAX-SAT scaling
|
|
497
|
+
|
|
498
|
+
### Verification
|
|
499
|
+
|
|
500
|
+
- 879 unit tests pass (`pytest -q -m "not slow"`); ruff lint clean
|
|
501
|
+
- Reported numbers verified against multi-seed result JSONs (seeds
|
|
502
|
+
`{42, 123, 456, 789, 1337}`)
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
cff-version: 1.2.0
|
|
2
|
+
message: "If you use this software, please cite it as below."
|
|
3
|
+
type: software
|
|
4
|
+
title: "polystep: Gradient-Free Neural Network Training via Optimal Transport"
|
|
5
|
+
authors:
|
|
6
|
+
- family-names: Le
|
|
7
|
+
given-names: An T.
|
|
8
|
+
license: Apache-2.0
|
|
9
|
+
version: 0.9.0
|
|
10
|
+
date-released: "2026-07-27"
|
|
11
|
+
repository-code: "https://github.com/anindex/polystep"
|
|
12
|
+
keywords:
|
|
13
|
+
- gradient-free optimization
|
|
14
|
+
- optimal transport
|
|
15
|
+
- sinkhorn
|
|
16
|
+
- pytorch
|
|
17
|
+
- neural network training
|
|
18
|
+
- non-differentiable
|
|
19
|
+
preferred-citation:
|
|
20
|
+
type: article
|
|
21
|
+
title: "Training Non-Differentiable Networks via Optimal Transport"
|
|
22
|
+
authors:
|
|
23
|
+
- family-names: Le
|
|
24
|
+
given-names: An T.
|
|
25
|
+
journal: "arXiv preprint arXiv:2605.01928"
|
|
26
|
+
year: 2026
|
|
27
|
+
url: "https://arxiv.org/abs/2605.01928"
|
|
28
|
+
references:
|
|
29
|
+
- type: conference-paper
|
|
30
|
+
title: "Accelerating Motion Planning via Optimal Transport"
|
|
31
|
+
authors:
|
|
32
|
+
- family-names: Le
|
|
33
|
+
given-names: An T.
|
|
34
|
+
- family-names: Chalvatzaki
|
|
35
|
+
given-names: Georgia
|
|
36
|
+
- family-names: Biess
|
|
37
|
+
given-names: Armin
|
|
38
|
+
- family-names: Peters
|
|
39
|
+
given-names: Jan R.
|
|
40
|
+
conference:
|
|
41
|
+
name: "Advances in Neural Information Processing Systems (NeurIPS)"
|
|
42
|
+
year: 2023
|
|
43
|
+
url: "https://arxiv.org/abs/2309.15970"
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Contributing to polystep
|
|
2
|
+
|
|
3
|
+
## Getting Started
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
git clone https://github.com/anindex/polystep.git
|
|
7
|
+
cd polystep
|
|
8
|
+
pip install -e ".[dev]"
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Development Workflow
|
|
12
|
+
|
|
13
|
+
1. Create a branch from `main`
|
|
14
|
+
2. Make your changes
|
|
15
|
+
3. Run the checks below; they mirror what CI runs, so a green local run means a green CI run
|
|
16
|
+
4. Submit a pull request
|
|
17
|
+
|
|
18
|
+
## Running the checks
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# Lint and formatting, exactly as CI runs them
|
|
22
|
+
ruff check src/polystep/ tests/ examples/ experiments/scripts/
|
|
23
|
+
ruff format --check src/polystep/ tests/ examples/ experiments/scripts/
|
|
24
|
+
|
|
25
|
+
# Fast tests (recommended during development). The pyproject addopts deselect the
|
|
26
|
+
# slow tests; the gpu-marked ones are still collected and self-skip without CUDA.
|
|
27
|
+
pytest tests/ -v
|
|
28
|
+
|
|
29
|
+
# Exactly what CI selects, in parallel. About 9s.
|
|
30
|
+
pytest tests/ -m "not slow and not gpu" -n auto
|
|
31
|
+
|
|
32
|
+
# Every test, including the ones marked slow. These train on MNIST and take 15-30 min.
|
|
33
|
+
pytest tests/ -v -m ""
|
|
34
|
+
|
|
35
|
+
# With coverage
|
|
36
|
+
pytest tests/ --cov=polystep --cov-report=term-missing
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
`conftest.py` pins `torch.set_num_threads(1)` for everything except the `slow` tests.
|
|
40
|
+
The fast tests are small-tensor bound, where torch's intra-op pool costs more than it
|
|
41
|
+
saves. Unpinned the suite is both slower and erratic, which was tripping the 60s timeout
|
|
42
|
+
on the Sinkhorn-heavy tests; pinned it runs in about 9s. The `slow` tests train at batch
|
|
43
|
+
512 and get the machine default back. Override with `POLYSTEP_TEST_THREADS`.
|
|
44
|
+
|
|
45
|
+
CI also builds the sdist and runs the shipped test suite from the unpacked tarball. To
|
|
46
|
+
reproduce that:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
uv build --sdist
|
|
50
|
+
mkdir -p /tmp/sdist && tar xzf dist/*.tar.gz -C /tmp/sdist --strip-components=1
|
|
51
|
+
cd /tmp/sdist && pip install ".[dev]" && pytest tests/ -q
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Tests that read or import `experiments/` request the `require_experiments` fixture, which
|
|
55
|
+
skips them outside a repo checkout.
|
|
56
|
+
|
|
57
|
+
## Code Style
|
|
58
|
+
|
|
59
|
+
- Follow existing code conventions
|
|
60
|
+
- Use type hints for public functions
|
|
61
|
+
- Add docstrings with Args/Returns sections for public APIs
|
|
62
|
+
- Keep comments short and specific; explain why, not what
|
|
63
|
+
|
|
64
|
+
The package ships `py.typed`, so annotations on the public surface are part of the
|
|
65
|
+
contract. `mypy src/polystep/` currently reports errors, mostly narrowing complaints on
|
|
66
|
+
the deliberately duck-typed `subspace` argument, so it is not yet a CI gate. New code
|
|
67
|
+
should not add to the count.
|
|
68
|
+
|
|
69
|
+
## Reporting Issues
|
|
70
|
+
|
|
71
|
+
Please include:
|
|
72
|
+
- Python and PyTorch versions
|
|
73
|
+
- GPU model (if relevant)
|
|
74
|
+
- Minimal reproduction script
|
|
75
|
+
- Full error traceback
|