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.
Files changed (130) hide show
  1. polystep-0.9.0/CHANGELOG.md +502 -0
  2. polystep-0.9.0/CITATION.cff +43 -0
  3. polystep-0.9.0/CONTRIBUTING.md +75 -0
  4. polystep-0.9.0/LIMITATIONS.md +233 -0
  5. polystep-0.9.0/MANIFEST.in +15 -0
  6. {polystep-0.7.0 → polystep-0.9.0}/PKG-INFO +64 -30
  7. {polystep-0.7.0 → polystep-0.9.0}/README.md +48 -24
  8. polystep-0.9.0/docs/api_overview.md +359 -0
  9. polystep-0.9.0/docs/performance.md +174 -0
  10. polystep-0.9.0/docs/reproducibility.md +122 -0
  11. {polystep-0.7.0 → polystep-0.9.0}/pyproject.toml +18 -5
  12. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/__init__.py +16 -2
  13. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_compiled.py +80 -22
  14. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_step_blockwise.py +144 -145
  15. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/_step_momentum.py +9 -55
  16. polystep-0.9.0/src/polystep/_step_monolithic.py +1342 -0
  17. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/adaptive_subspace.py +50 -203
  18. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/api.py +90 -35
  19. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/ask_tell.py +1 -1
  20. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/baselines.py +0 -18
  21. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/gym_evaluator.py +0 -6
  22. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/utils.py +55 -94
  23. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/blockwise.py +0 -5
  24. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/cma.py +37 -71
  25. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/cma_subspace.py +91 -101
  26. polystep-0.9.0/src/polystep/cost_nn.py +1098 -0
  27. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/costs.py +39 -11
  28. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/dynamics.py +86 -27
  29. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/epsilon.py +1 -1
  30. polystep-0.9.0/src/polystep/factored_subspace.py +244 -0
  31. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/geometry.py +55 -109
  32. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/hybrid_subspace.py +153 -173
  33. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/rnn.py +1 -1
  34. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/optimizer.py +689 -157
  35. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/projection/sparse.py +12 -32
  36. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/quadratic_model.py +22 -19
  37. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solver.py +75 -32
  38. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/_prelude.py +48 -6
  39. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/base.py +3 -1
  40. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/greedy.py +28 -8
  41. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/kl_softmax.py +47 -34
  42. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/sinkhorn.py +97 -53
  43. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/softmax.py +17 -9
  44. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/tempered_softmax.py +9 -7
  45. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/subspace.py +34 -26
  46. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/transform.py +9 -24
  47. {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/PKG-INFO +64 -30
  48. {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/SOURCES.txt +20 -4
  49. {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/requires.txt +10 -5
  50. polystep-0.9.0/tests/__init__.py +0 -0
  51. polystep-0.9.0/tests/conftest.py +272 -0
  52. {polystep-0.7.0 → polystep-0.9.0}/tests/test_ablation_solvers.py +0 -30
  53. {polystep-0.7.0 → polystep-0.9.0}/tests/test_adaptive_subspace.py +36 -116
  54. {polystep-0.7.0 → polystep-0.9.0}/tests/test_api.py +2 -42
  55. {polystep-0.7.0 → polystep-0.9.0}/tests/test_baseline_fairness.py +7 -12
  56. {polystep-0.7.0 → polystep-0.9.0}/tests/test_benchmarks.py +8 -49
  57. {polystep-0.7.0 → polystep-0.9.0}/tests/test_blockwise.py +1 -36
  58. polystep-0.9.0/tests/test_checkpoint_resume.py +348 -0
  59. {polystep-0.7.0 → polystep-0.9.0}/tests/test_cma.py +0 -131
  60. {polystep-0.7.0 → polystep-0.9.0}/tests/test_cma_subspace.py +24 -259
  61. {polystep-0.7.0 → polystep-0.9.0}/tests/test_compiled.py +1 -181
  62. polystep-0.9.0/tests/test_correctness_regressions.py +1014 -0
  63. {polystep-0.7.0 → polystep-0.9.0}/tests/test_cost_nn.py +63 -20
  64. {polystep-0.7.0 → polystep-0.9.0}/tests/test_dfo_features.py +12 -37
  65. {polystep-0.7.0 → polystep-0.9.0}/tests/test_dynamics.py +0 -15
  66. polystep-0.9.0/tests/test_end_to_end_learning.py +125 -0
  67. polystep-0.9.0/tests/test_evaluator_compile_forward.py +88 -0
  68. polystep-0.9.0/tests/test_factored_subspace.py +107 -0
  69. {polystep-0.7.0 → polystep-0.9.0}/tests/test_geometry.py +0 -15
  70. {polystep-0.7.0 → polystep-0.9.0}/tests/test_hardening.py +4 -18
  71. {polystep-0.7.0 → polystep-0.9.0}/tests/test_hybrid_subspace.py +18 -202
  72. {polystep-0.7.0 → polystep-0.9.0}/tests/test_integration.py +13 -20
  73. {polystep-0.7.0 → polystep-0.9.0}/tests/test_layers.py +41 -43
  74. {polystep-0.7.0 → polystep-0.9.0}/tests/test_mixed_precision.py +27 -11
  75. polystep-0.9.0/tests/test_mnist.py +392 -0
  76. polystep-0.9.0/tests/test_no_test_set_leakage.py +36 -0
  77. {polystep-0.7.0 → polystep-0.9.0}/tests/test_nondiff_data.py +13 -13
  78. {polystep-0.7.0 → polystep-0.9.0}/tests/test_nondiff_models.py +24 -145
  79. {polystep-0.7.0 → polystep-0.9.0}/tests/test_numerical_stress.py +31 -72
  80. polystep-0.9.0/tests/test_objectives.py +115 -0
  81. {polystep-0.7.0 → polystep-0.9.0}/tests/test_optimizer.py +78 -202
  82. {polystep-0.7.0 → polystep-0.9.0}/tests/test_quadratic_model.py +0 -5
  83. {polystep-0.7.0 → polystep-0.9.0}/tests/test_rl_benchmarks.py +2 -2
  84. polystep-0.9.0/tests/test_silent_wrongness.py +435 -0
  85. {polystep-0.7.0 → polystep-0.9.0}/tests/test_sinkhorn.py +1 -147
  86. {polystep-0.7.0 → polystep-0.9.0}/tests/test_sinkhorn_correctness.py +33 -54
  87. {polystep-0.7.0 → polystep-0.9.0}/tests/test_softmax.py +7 -47
  88. {polystep-0.7.0 → polystep-0.9.0}/tests/test_softmax_integration.py +33 -95
  89. polystep-0.9.0/tests/test_sparse_delta_evaluator.py +202 -0
  90. {polystep-0.7.0 → polystep-0.9.0}/tests/test_sparse_integration.py +26 -102
  91. {polystep-0.7.0 → polystep-0.9.0}/tests/test_sparse_projection.py +15 -54
  92. {polystep-0.7.0 → polystep-0.9.0}/tests/test_subspace.py +12 -174
  93. {polystep-0.7.0 → polystep-0.9.0}/tests/test_subspace_blockwise.py +30 -155
  94. polystep-0.9.0/tests/test_subspace_contract.py +131 -0
  95. {polystep-0.7.0 → polystep-0.9.0}/tests/test_transform.py +1 -14
  96. {polystep-0.7.0 → polystep-0.9.0}/tests/test_turbo.py +26 -224
  97. {polystep-0.7.0 → polystep-0.9.0}/tests/test_vmap_layer_compatibility.py +6 -50
  98. polystep-0.7.0/src/polystep/_step_monolithic.py +0 -1070
  99. polystep-0.7.0/src/polystep/cost_nn.py +0 -633
  100. polystep-0.7.0/tests/test_adaptive_mnist.py +0 -466
  101. polystep-0.7.0/tests/test_bug_regressions.py +0 -456
  102. polystep-0.7.0/tests/test_hybrid_mnist.py +0 -473
  103. polystep-0.7.0/tests/test_mnist.py +0 -546
  104. polystep-0.7.0/tests/test_no_test_set_leakage.py +0 -18
  105. polystep-0.7.0/tests/test_solver_numerics.py +0 -160
  106. {polystep-0.7.0 → polystep-0.9.0}/LICENSE +0 -0
  107. {polystep-0.7.0 → polystep-0.9.0}/setup.cfg +0 -0
  108. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/__init__.py +0 -0
  109. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/__init__.py +0 -0
  110. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/cartpole.py +0 -0
  111. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/metrics.py +0 -0
  112. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/benchmarks/rl/policies.py +0 -0
  113. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/__init__.py +0 -0
  114. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/layers/attention.py +0 -0
  115. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/__init__.py +0 -0
  116. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/base.py +0 -0
  117. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/objectives/synthetic.py +0 -0
  118. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/projection/__init__.py +0 -0
  119. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/py.typed +0 -0
  120. {polystep-0.7.0 → polystep-0.9.0}/src/polystep/solvers/__init__.py +0 -0
  121. {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/dependency_links.txt +0 -0
  122. {polystep-0.7.0 → polystep-0.9.0}/src/polystep.egg-info/top_level.txt +0 -0
  123. {polystep-0.7.0 → polystep-0.9.0}/tests/test_ask_tell.py +0 -0
  124. {polystep-0.7.0 → polystep-0.9.0}/tests/test_biased_rotation.py +0 -0
  125. {polystep-0.7.0 → polystep-0.9.0}/tests/test_costs.py +0 -0
  126. {polystep-0.7.0 → polystep-0.9.0}/tests/test_fullspace_chunk_bound.py +0 -0
  127. {polystep-0.7.0 → polystep-0.9.0}/tests/test_fused_projection_rebuild.py +0 -0
  128. {polystep-0.7.0 → polystep-0.9.0}/tests/test_kl_softmax.py +0 -0
  129. {polystep-0.7.0 → polystep-0.9.0}/tests/test_mixed_precision_step.py +0 -0
  130. {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