polystep 0.9.0__tar.gz → 0.10.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 (163) hide show
  1. polystep-0.10.0/CHANGELOG.md +575 -0
  2. {polystep-0.9.0 → polystep-0.10.0}/CITATION.cff +2 -2
  3. polystep-0.10.0/CONTRIBUTING.md +93 -0
  4. polystep-0.10.0/LIMITATIONS.md +234 -0
  5. {polystep-0.9.0/src/polystep.egg-info → polystep-0.10.0}/PKG-INFO +113 -80
  6. polystep-0.10.0/README.md +294 -0
  7. {polystep-0.9.0 → polystep-0.10.0}/docs/api_overview.md +82 -44
  8. polystep-0.10.0/docs/performance.md +490 -0
  9. {polystep-0.9.0 → polystep-0.10.0}/docs/reproducibility.md +25 -33
  10. {polystep-0.9.0 → polystep-0.10.0}/pyproject.toml +14 -14
  11. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/__init__.py +5 -33
  12. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/_compiled.py +14 -31
  13. polystep-0.10.0/src/polystep/_serialization.py +206 -0
  14. polystep-0.10.0/src/polystep/_step_blockwise.py +671 -0
  15. polystep-0.10.0/src/polystep/_step_core.py +34 -0
  16. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/_step_momentum.py +10 -12
  17. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/_step_monolithic.py +586 -514
  18. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/adaptive_subspace.py +47 -64
  19. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/api.py +129 -132
  20. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/ask_tell.py +13 -8
  21. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/__init__.py +0 -26
  22. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/baselines.py +8 -263
  23. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/rl/cartpole.py +2 -4
  24. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/rl/gym_evaluator.py +2 -5
  25. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/rl/policies.py +46 -21
  26. polystep-0.10.0/src/polystep/benchmarks/threads.py +88 -0
  27. polystep-0.10.0/src/polystep/benchmarks/utils.py +529 -0
  28. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/blockwise.py +61 -235
  29. polystep-0.10.0/src/polystep/cma.py +195 -0
  30. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/cma_subspace.py +44 -166
  31. polystep-0.10.0/src/polystep/cost_nn.py +1857 -0
  32. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/costs.py +1 -1
  33. polystep-0.10.0/src/polystep/dynamics.py +106 -0
  34. polystep-0.10.0/src/polystep/epsilon.py +162 -0
  35. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/factored_subspace.py +7 -16
  36. polystep-0.10.0/src/polystep/geometry.py +277 -0
  37. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/hybrid_subspace.py +196 -366
  38. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/layers/attention.py +6 -11
  39. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/layers/rnn.py +4 -13
  40. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/objectives/base.py +1 -8
  41. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/optimizer.py +425 -366
  42. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/projection/sparse.py +27 -8
  43. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/quadratic_model.py +108 -96
  44. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solver.py +84 -290
  45. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solvers/__init__.py +3 -6
  46. polystep-0.9.0/src/polystep/solvers/_prelude.py → polystep-0.10.0/src/polystep/solvers/_shared.py +140 -5
  47. polystep-0.10.0/src/polystep/solvers/base.py +42 -0
  48. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solvers/greedy.py +31 -34
  49. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solvers/kl_softmax.py +35 -33
  50. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solvers/sinkhorn.py +69 -86
  51. polystep-0.10.0/src/polystep/solvers/softmax.py +81 -0
  52. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/solvers/tempered_softmax.py +3 -38
  53. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/subspace.py +61 -59
  54. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/transform.py +43 -35
  55. {polystep-0.9.0 → polystep-0.10.0/src/polystep.egg-info}/PKG-INFO +113 -80
  56. {polystep-0.9.0 → polystep-0.10.0}/src/polystep.egg-info/SOURCES.txt +21 -10
  57. {polystep-0.9.0 → polystep-0.10.0}/src/polystep.egg-info/requires.txt +4 -4
  58. {polystep-0.9.0 → polystep-0.10.0}/tests/conftest.py +19 -17
  59. {polystep-0.9.0 → polystep-0.10.0}/tests/test_ablation_solvers.py +61 -9
  60. {polystep-0.9.0 → polystep-0.10.0}/tests/test_adaptive_subspace.py +236 -173
  61. polystep-0.10.0/tests/test_amortization.py +253 -0
  62. {polystep-0.9.0 → polystep-0.10.0}/tests/test_api.py +27 -25
  63. {polystep-0.9.0 → polystep-0.10.0}/tests/test_ask_tell.py +26 -5
  64. {polystep-0.9.0 → polystep-0.10.0}/tests/test_baseline_fairness.py +3 -5
  65. {polystep-0.9.0 → polystep-0.10.0}/tests/test_benchmarks.py +38 -2
  66. polystep-0.10.0/tests/test_biased_rotation.py +100 -0
  67. {polystep-0.9.0 → polystep-0.10.0}/tests/test_blockwise.py +34 -171
  68. polystep-0.10.0/tests/test_candidate_autocast.py +98 -0
  69. {polystep-0.9.0 → polystep-0.10.0}/tests/test_checkpoint_resume.py +75 -3
  70. polystep-0.10.0/tests/test_cma.py +325 -0
  71. {polystep-0.9.0 → polystep-0.10.0}/tests/test_cma_subspace.py +116 -66
  72. polystep-0.10.0/tests/test_compiled.py +261 -0
  73. polystep-0.10.0/tests/test_cost_nn.py +547 -0
  74. {polystep-0.9.0 → polystep-0.10.0}/tests/test_costs.py +4 -22
  75. polystep-0.10.0/tests/test_dfo.py +507 -0
  76. polystep-0.10.0/tests/test_diagnostics.py +95 -0
  77. polystep-0.10.0/tests/test_dynamics.py +137 -0
  78. {polystep-0.9.0 → polystep-0.10.0}/tests/test_end_to_end_learning.py +39 -0
  79. polystep-0.10.0/tests/test_epsilon.py +269 -0
  80. {polystep-0.9.0 → polystep-0.10.0}/tests/test_evaluator_compile_forward.py +25 -0
  81. polystep-0.10.0/tests/test_fastpath_parity.py +168 -0
  82. polystep-0.10.0/tests/test_fused_projection.py +208 -0
  83. polystep-0.10.0/tests/test_geometry.py +220 -0
  84. {polystep-0.9.0 → polystep-0.10.0}/tests/test_hybrid_subspace.py +293 -216
  85. polystep-0.9.0/tests/test_hardening.py → polystep-0.10.0/tests/test_input_validation.py +37 -9
  86. {polystep-0.9.0 → polystep-0.10.0}/tests/test_integration.py +85 -116
  87. {polystep-0.9.0 → polystep-0.10.0}/tests/test_kl_softmax.py +106 -2
  88. {polystep-0.9.0 → polystep-0.10.0}/tests/test_layers.py +111 -119
  89. polystep-0.10.0/tests/test_math_guards.py +118 -0
  90. polystep-0.10.0/tests/test_mixed_precision.py +501 -0
  91. {polystep-0.9.0 → polystep-0.10.0}/tests/test_mnist.py +95 -185
  92. {polystep-0.9.0 → polystep-0.10.0}/tests/test_no_test_set_leakage.py +14 -2
  93. {polystep-0.9.0 → polystep-0.10.0}/tests/test_nondiff_data.py +0 -6
  94. {polystep-0.9.0 → polystep-0.10.0}/tests/test_nondiff_models.py +170 -211
  95. {polystep-0.9.0 → polystep-0.10.0}/tests/test_numerical_stress.py +13 -45
  96. {polystep-0.9.0 → polystep-0.10.0}/tests/test_optimizer.py +128 -171
  97. polystep-0.10.0/tests/test_per_sample.py +70 -0
  98. {polystep-0.9.0 → polystep-0.10.0}/tests/test_quadratic_model.py +77 -21
  99. polystep-0.9.0/tests/test_correctness_regressions.py → polystep-0.10.0/tests/test_regressions.py +175 -136
  100. {polystep-0.9.0 → polystep-0.10.0}/tests/test_rl_benchmarks.py +0 -2
  101. polystep-0.10.0/tests/test_sequential_fast_path.py +362 -0
  102. polystep-0.9.0/tests/test_silent_wrongness.py → polystep-0.10.0/tests/test_silent_failures.py +130 -7
  103. {polystep-0.9.0 → polystep-0.10.0}/tests/test_sinkhorn.py +42 -184
  104. polystep-0.9.0/tests/test_sinkhorn_correctness.py → polystep-0.10.0/tests/test_sinkhorn_numerics.py +76 -21
  105. polystep-0.10.0/tests/test_site_vmap.py +174 -0
  106. {polystep-0.9.0 → polystep-0.10.0}/tests/test_softmax.py +15 -7
  107. {polystep-0.9.0 → polystep-0.10.0}/tests/test_softmax_integration.py +81 -109
  108. polystep-0.10.0/tests/test_sparse_delta_evaluator.py +472 -0
  109. polystep-0.10.0/tests/test_sparse_integration.py +181 -0
  110. {polystep-0.9.0 → polystep-0.10.0}/tests/test_sparse_projection.py +53 -91
  111. {polystep-0.9.0 → polystep-0.10.0}/tests/test_subspace.py +100 -153
  112. {polystep-0.9.0 → polystep-0.10.0}/tests/test_subspace_blockwise.py +113 -64
  113. polystep-0.10.0/tests/test_subspace_delta.py +249 -0
  114. polystep-0.10.0/tests/test_subspace_gain.py +64 -0
  115. polystep-0.10.0/tests/test_transform.py +290 -0
  116. polystep-0.10.0/tests/test_trust_region.py +184 -0
  117. {polystep-0.9.0 → polystep-0.10.0}/tests/test_vmap_layer_compatibility.py +16 -46
  118. polystep-0.9.0/CHANGELOG.md +0 -502
  119. polystep-0.9.0/CONTRIBUTING.md +0 -75
  120. polystep-0.9.0/LIMITATIONS.md +0 -233
  121. polystep-0.9.0/README.md +0 -261
  122. polystep-0.9.0/docs/performance.md +0 -174
  123. polystep-0.9.0/src/polystep/_step_blockwise.py +0 -993
  124. polystep-0.9.0/src/polystep/benchmarks/utils.py +0 -1223
  125. polystep-0.9.0/src/polystep/cma.py +0 -366
  126. polystep-0.9.0/src/polystep/cost_nn.py +0 -1098
  127. polystep-0.9.0/src/polystep/dynamics.py +0 -197
  128. polystep-0.9.0/src/polystep/epsilon.py +0 -217
  129. polystep-0.9.0/src/polystep/geometry.py +0 -356
  130. polystep-0.9.0/src/polystep/solvers/base.py +0 -79
  131. polystep-0.9.0/src/polystep/solvers/softmax.py +0 -159
  132. polystep-0.9.0/tests/test_biased_rotation.py +0 -53
  133. polystep-0.9.0/tests/test_cma.py +0 -296
  134. polystep-0.9.0/tests/test_compiled.py +0 -270
  135. polystep-0.9.0/tests/test_cost_nn.py +0 -545
  136. polystep-0.9.0/tests/test_dfo_features.py +0 -317
  137. polystep-0.9.0/tests/test_dynamics.py +0 -201
  138. polystep-0.9.0/tests/test_fullspace_chunk_bound.py +0 -49
  139. polystep-0.9.0/tests/test_fused_projection_rebuild.py +0 -53
  140. polystep-0.9.0/tests/test_geometry.py +0 -233
  141. polystep-0.9.0/tests/test_mixed_precision.py +0 -235
  142. polystep-0.9.0/tests/test_mixed_precision_step.py +0 -104
  143. polystep-0.9.0/tests/test_sparse_delta_evaluator.py +0 -202
  144. polystep-0.9.0/tests/test_sparse_integration.py +0 -291
  145. polystep-0.9.0/tests/test_transform.py +0 -279
  146. polystep-0.9.0/tests/test_trust_region.py +0 -97
  147. polystep-0.9.0/tests/test_turbo.py +0 -478
  148. {polystep-0.9.0 → polystep-0.10.0}/LICENSE +0 -0
  149. {polystep-0.9.0 → polystep-0.10.0}/MANIFEST.in +0 -0
  150. {polystep-0.9.0 → polystep-0.10.0}/setup.cfg +0 -0
  151. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/rl/__init__.py +0 -0
  152. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/benchmarks/rl/metrics.py +0 -0
  153. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/layers/__init__.py +0 -0
  154. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/objectives/__init__.py +0 -0
  155. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/objectives/synthetic.py +0 -0
  156. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/projection/__init__.py +0 -0
  157. {polystep-0.9.0 → polystep-0.10.0}/src/polystep/py.typed +0 -0
  158. {polystep-0.9.0 → polystep-0.10.0}/src/polystep.egg-info/dependency_links.txt +0 -0
  159. {polystep-0.9.0 → polystep-0.10.0}/src/polystep.egg-info/top_level.txt +0 -0
  160. {polystep-0.9.0 → polystep-0.10.0}/tests/__init__.py +0 -0
  161. {polystep-0.9.0 → polystep-0.10.0}/tests/test_factored_subspace.py +0 -0
  162. {polystep-0.9.0 → polystep-0.10.0}/tests/test_objectives.py +0 -0
  163. {polystep-0.9.0 → polystep-0.10.0}/tests/test_subspace_contract.py +0 -0
@@ -0,0 +1,575 @@
1
+ # Changelog
2
+
3
+ ## 0.10.0 - 2026-07-30
4
+
5
+ New default polytope, four site-aware evaluation paths, and a pass over fast-path
6
+ correctness. Retune `step_radius` wherever `polytope_type` was left at its default.
7
+ Measured numbers are in `docs/performance.md`.
8
+
9
+ ### Breaking
10
+
11
+ - `polytope_type` defaults to `'simplex'` (was `'orthoplex'`): `k+1` evaluations per step
12
+ instead of `2k`. Features reading the antithetic ordering (`use_quadratic_model`,
13
+ `newton_refinement`, `trust_region`, `multifidelity_screen`) warn on another polytope.
14
+ - Rotations use a sign-corrected QR at or below `geometry._QR_MAX_BATCH` and Householder
15
+ reflections above. Both Haar on `SO(d)`, but the draw per seed differs from 0.9.0 and
16
+ across the threshold, so seeded runs do not reproduce step for step.
17
+ - `compile_forward` defaults to the in-place path; pass `False` to opt out.
18
+ - `adaptive_num_probe` defaults on only when `num_probe > 1`.
19
+ - Momentum is zeroed on a basis change instead of carried into the new basis.
20
+ - `projection_mode` removed: `'structured'` truncated its block-diagonal from the right,
21
+ leaving the trailing output rows with a zero projection.
22
+ - `use_csa` raises `TypeError`; use `use_adaptive_radius`. Gone with it:
23
+ `polystep.cma.update_step_size_csa`, `SolverState.sigma`/`.use_csa`,
24
+ `CMAAdaptiveSubspace.d_sigma`/`expected_norm`. A 0.9.0 checkpoint still loads.
25
+ - Top-level exports removed. Still importable from their modules: `PolyStep`
26
+ (`polystep.solver`), `VmapSafeLSTMCell` (`polystep.layers`). Deleted outright:
27
+ `Solver`, `SoftmaxResult`, `SinkhornResult`, `get_device`, `POLYTOPE_NUM_VERTICES_MAP`,
28
+ `get_sampled_polytope_vertices`, `get_probe_points`, `compute_nn_cost_matrix`,
29
+ `update_adaptive_radius`, `StyblinskiTang`, `Levy`, `Griewank`, `Beale`, `Branin`.
30
+ - `PolyStep` drops `layout`, `block_strategy` and `block_group_size`, with
31
+ `polystep.blockwise.compute_block_cost_matrix` and `blocks_to_layout_flat_batch`.
32
+ `PolyStep.compile` defaults to `False`.
33
+ - `update_evolution_path_sigma` takes `C_diag=None` for an already-whitened displacement;
34
+ `update_covariance_diagonal` takes `trace` and normalizes before clamping.
35
+ - Rejected at construction instead of failing later: `epsilon`/`ent_epsilon <= 0`,
36
+ `probe_radius <= 0`, `step_radius < 0`, `cost_batch_size=0`,
37
+ `AdaptiveSubspace(subspace_dim > full_dim)`, unknown `block_strategy`/`polytope_type`.
38
+ - `numpy` is no longer a core dependency; it moved to the `examples`, `dev` and
39
+ `experiments` extras.
40
+ - `step_radius` is not comparable across subspace classes: `LinearSubspace` amplifies by
41
+ `sqrt(num_params / num_coords)` where `HybridSubspace` and `FactoredSubspace` are unit
42
+ gain.
43
+
44
+ ### Added
45
+
46
+ - `SubspaceDeltaEvaluator` and `SiteVmapEvaluator` score candidates without building a
47
+ candidate weight. The second assumes nothing about the module set, so it covers conv,
48
+ normalization, attention and custom `forward`.
49
+ - `polystep_elementwise` and `polystep_weight_transform` put a custom layer on the
50
+ batched paths, probed at build time so a false declaration falls back rather than
51
+ returning a wrong loss.
52
+ - `candidate_autocast`: BF16 candidate arithmetic with parameters at their own dtype.
53
+ - `use_quadratic_model` and `trust_region` run at `num_probe=1`, reading curvature from
54
+ `L(+s) + L(-s) - 2 L(0)` with one shared `f(X)` per particle.
55
+ - `solver='kl_softmax'`, interpolating softmax (`kl_softmax_lam=0`) and Sinkhorn (`inf`).
56
+ - `AdaptiveSubspace.rotation_interval`, `NNCostEvaluator(per_sample=True)` and
57
+ `get_diagnostics()` (`ess`, `rho`, `evals` per iteration).
58
+ - `examples/10_cnn_mnist.py` (LeNet-5) and `examples/11_transformer_selective_copy.py`,
59
+ both forward-pass only, with `--compare` against the materializing path.
60
+
61
+ ### Fixed
62
+
63
+ - A model computing in two dtypes raised in subspace mode: every per-layer projection was
64
+ built at the layout's `dominant_dtype`, and `SiteVmapEvaluator` cast inputs to the
65
+ perturbed layer's dtype rather than the model's. `HybridSubspace`, `LinearSubspace` and
66
+ `LowRankSubspace` now train such a model at each parameter's own dtype; full space and
67
+ `AdaptiveSubspace` raise at construction naming the fix.
68
+ - Fast-path selection accepted models it scored wrong: the bmm plan deduplicated shared
69
+ modules, loss subclasses and forward hooks were skipped, tied-weight detection merged a
70
+ square parameter with its transpose, aliasing was keyed on the first element's address,
71
+ frozen parameters raised `KeyError` or dropped a bias, `ParamLayout` merged differently
72
+ shaped views of one buffer, and `register_evaluator` and `reset_vmap()` left stale
73
+ plans. All decline or rebuild now.
74
+ - Numerics: the Sinkhorn and KL-softmax plans overflowed to `inf` after a dual
75
+ divergence, giving NaN parameters; `sanitize_cost` overflowed its own penalty near
76
+ dtype max; the in-place path's FP32 loss buffer collapsed distinct FP64 candidates;
77
+ site-aware chunks ignored `candidate_autocast`; the CMA clamp ran before renormalizing.
78
+ - Basis changes leaked state: rotations left velocity, the Newton direction and the
79
+ transport EMA in the replaced basis; `HybridSubspace` kept displacement history across
80
+ one and rotated at step 0; a rank transition never rebuilt the fused projection, so
81
+ `_fused_P` kept the old rank's width; `RankSchedule` applied stage 0 late and omitted
82
+ `_applied_rank` from `state_dict`; `_write_params` wrote a stale buffer snapshot.
83
+ - Step accounting: block-wise averaged loss, ESS and rho over blocks rather than
84
+ candidates; the trust-region ratio compared across a rotation and a minibatch and
85
+ double-shrank; `adaptive_probes` could latch permanently and reused rows across a
86
+ covariance change; `adaptive_num_probe` left a stale Newton direction;
87
+ `multifidelity_screen` billed stage one at the wrong fidelity.
88
+ - Solvers: `MinCostGreedySolver` stepped on an all-infeasible row; `TopKMeanSolver` gave
89
+ mass to masked vertices; `KLSoftmaxSolver` built its plan one dual update behind and
90
+ never converged below the working dtype's smallest normal; `SinkhornSolver` reset
91
+ non-finite duals silently; the low-level `PolyStep` froze at one particle and left
92
+ diagnostics empty.
93
+ - Config that silently did nothing: `LinearEpsilon` accepted a negative `decay`,
94
+ `max_subspace_dim` was not a bound, `RankSchedule` rebuilt the subspace every step,
95
+ `ProgressiveEpsilon` never advanced outside the monolithic step, explicit CMA rates
96
+ were discarded, `svd_ratio=0` still forced an SVD, and `TernaryLinear`'s default
97
+ threshold returned a constant.
98
+ - `examples/05` annealed epsilon to 0.1, which concentrates the plan toward argmax so the
99
+ effective step grows to the full `step_radius` as `step_radius` anneals; the last epoch
100
+ diverged on every seed. `examples/10` built its schedules positionally, so its radii
101
+ were constant.
102
+ - Crashes and unclear failures now raise, warn or decline: empty cost matrices, tiny
103
+ solver temperatures, an evaluator left alive after an exception in `train()`, NaN
104
+ fitness in `PolyStepES.tell`, `torch.set_default_device`, vector-only models, fp16 CPU
105
+ and CUDA `particle_dim > 2` rotation paths, a singular biased-rotation frame, and an
106
+ unprojected weight site.
107
+
108
+ ### Performance
109
+
110
+ - `SubspaceDeltaEvaluator` reads the projection as a strided view instead of gathering
111
+ and transposing a column block per chunk, and batches over columns rather than `d_out`.
112
+ Losses bit-identical.
113
+ - `HybridSubspace.init_projections` memoizes the step-0 basis, so an absorb re-anchors
114
+ the origin without redrawing it. The QR was 30% of the step under the default
115
+ `absorb_mode='stagnation'`.
116
+ - Haar rotations pick QR or Householder reflections by batch size, each on the side of
117
+ the crossover where it wins.
118
+ - Chunks break at parameter and coordinate boundaries, so the site-aware paths stop
119
+ falling back mid-sweep, and block-wise mode reaches the delta and factored evaluators.
120
+ - The non-differentiable model zoo takes the batched paths; rate-coded SNNs compute the
121
+ static input current once instead of per timestep, bit-identical.
122
+ - `SparseDeltaEvaluator` takes candidates grouped by particle, so each gather runs once
123
+ per group. `mixed_precision=True` no longer disables the subspace-delta path.
124
+ - The block-diagonal fuse is capped at 32 MB and skips a single dense block, past which
125
+ the padding read costs more than the launches saved.
126
+ - Every example pins a thread count below `nproc`, where the OpenMP spin-wait takes over.
127
+ Ten pin 1; `07` pins 8, its own objective being wide enough to pay for the pool.
128
+ - Smaller: cached per-chunk casts and projections, one `sanitize_cost` per step,
129
+ candidates written straight into layout order, `torch._foreach_copy_` weight restore,
130
+ bias folded into `baddbmm`, batched sparse chunk projection, and a Haar guard that no
131
+ longer forces `dim-1` host syncs on CUDA.
132
+
133
+ ## 0.9.0 - 2026-07-27
134
+
135
+ ### Added
136
+
137
+ - `SparseDeltaEvaluator`: a full-space candidate replaces `particle_dim` scalars, so its
138
+ output differs from the base in at most that many columns. Propagates the delta through
139
+ elementwise layers and goes dense one `Linear` past the perturbed one, instead of a
140
+ dense `bmm` over `N` weight sets. Automatic for `nn.Sequential` of `Linear` and
141
+ elementwise layers, with a per-chunk fallback.
142
+ - `FactoredSubspace`: perturbs each 2D parameter by `dW = A @ B` with `B` fixed, so
143
+ `x (W + A B)^T = x W^T + (x B^T) A^T` and no candidate weight is built. Not the
144
+ default: the fixed `B` confines perturbations to `rank` input directions.
145
+ - `step(..., objective_token=...)` marks which objective a step measures, so cost rows
146
+ cannot be reused across minibatches. `release_evaluator()`, `resync_from_model()`,
147
+ `MinCostGreedySolver` and `TopKMeanSolver` are public.
148
+ - `MANIFEST.in`, so the sdist ships a runnable test suite. CI runs it from the tarball.
149
+
150
+ ### Fixed
151
+
152
+ - `api.train()` never called `register_evaluator`, the only assignment of
153
+ `_cost_evaluator`, `_fused_inputs`, `_factored_evaluator` and
154
+ `_sparse_delta_evaluator`, so every fast evaluation path was unreachable through the
155
+ training API.
156
+ - Cost-row reuse under `adaptive_probes` was per particle, but a candidate is the whole
157
+ configuration with one row replaced, so a stagnant particle's row was still measured
158
+ against every other particle's position. Reuse is now all or nothing.
159
+ - `multifidelity_screen` averaged direction contrast across particles, which carry
160
+ independent rotations, so the kept set was chosen at random. Ranked per particle now.
161
+ - Tied weights raised on both evaluation paths. `batch_unflatten` emits the canonical key
162
+ only; `unflatten` still carries aliases for `load_state_dict`.
163
+ - `restore_best` keyed on the OT cost when no callback was registered, restoring the step
164
+ with the cheapest probe cloud rather than the best weights. It also snapshots to CPU.
165
+ - Chunk size ignored the weight set a non-fused subspace builds per candidate and the
166
+ vmap activations. A 120K-param step peaked at 1661 MB in full space and 10565 MB in
167
+ subspace mode; now 946 MB and 1461 MB.
168
+ - `AdaptiveSubspace` and per-layer rotation replaced the projection while the coordinates
169
+ were non-zero, moving the weights with nothing evaluated behind them.
170
+ - `CMAAdaptiveSubspace` never rotated or absorbed, left its hyperparameters at `0.0` so
171
+ the covariance update was inert, normalized the evolution paths by `sigma` instead of
172
+ `step_radius` so `C_diag` decayed to its floor, carried state through basis rotations
173
+ untransformed, and accumulated at bf16 where `1 + c_1*x` rounds back to `1`.
174
+ - Anderson acceleration subtracted `dX` instead of `dX + dR`, so `anderson_depth > 0`
175
+ converged slower than no acceleration.
176
+ - `num_particles = 1` froze the optimizer: balanced OT with one row forces a uniform
177
+ plan. The default now selects `SoftmaxSolver` and warns.
178
+ - `scale_cost` divided before recentering, so adding a constant to every cost changed the
179
+ plan. `SinkhornSolver` also reported `ent_reg_cost` in the wrong frame; the plan and
180
+ duals were unaffected. The greedy and top-k solvers had the same reporting bug.
181
+ - Softmax paths recentred by the global minimum, so a row above it underflowed to `-inf`
182
+ and returned NaN; the fused kernel did not recenter at all.
183
+ - `sanitize_cost` mapped `-inf` to the worst finite value instead of the best, and used
184
+ an absolute penalty that entered the `scale_cost='mean'` reduction.
185
+ - `state_dict` captured only `SolverState`, so resume was not exact for `amortize_steps`,
186
+ `adaptive_probes`, `trust_region` or `HybridSubspace`. Format is 3; format 2 loads with
187
+ a warning.
188
+ - A stagnation absorb never cleared `stagnation_count`, redrawing the basis every step on
189
+ a plateau. `resync_from_model()` left momentum and rotation state on the old anchor.
190
+ - Probe losses accumulated in FP32 unconditionally, so a float64 objective whose costs
191
+ differ below FP32 resolution produced a constant cost matrix.
192
+ - Block-wise biased rotation raised on bf16 under `mixed_precision=True`. A zero descent
193
+ direction produced a singular rotation collapsing `+e0` and `-e0`.
194
+ - Smaller: the in-place evaluator dropped unknown parameter keys silently; the
195
+ trust-region ratio scored a Newton step that was never applied; displacement history
196
+ mixed bases; randomized PCA drew from the global RNG; rank transitions dropped
197
+ `HybridSubspace` settings; `data_dependent_init` had the wrong sign; a float64 model
198
+ with a subspace raised; a frozen model gave an opaque error; `pyproject.toml` declared
199
+ `torch>=2.4` where 0.2.0 announced `torch>=2.8`.
200
+
201
+ ### Changed
202
+
203
+ - `adaptive_probes` and `adaptive_num_probe` default on under
204
+ `block_strategy='monolithic'`.
205
+ - `use_csa` is refused with a warning: the OT step is a deterministic descent direction,
206
+ so CSA reads agreement as the normal state and `sigma` grows without bound.
207
+ - `multifidelity_screen` cuts the forward budget instead of only reweighting, and runs
208
+ only
209
+ when `screen_fidelity/num_probe + screen_keep_ratio < 1`. Needs a cheap closure from
210
+ `screen_closure_from()`; `api.train` supplies one.
211
+ - Block-wise warns for options it ignores; `grouped` warns when combined with a subspace.
212
+ `LinearSubspace` amplifies rather than dilutes the perturbation, as its docstring said.
213
+ - `_step_blockwise` claimed blockwise cuts OT cost from `O(N^2)` to `O(N^2/L)`. The
214
+ forward count is identical; `L` solves over `P/L` rows replace one over `P`.
215
+ - Removed `get_sobol_rotation_matrices`, `HybridSubspace.reconstruct_base` and
216
+ `reconstruct_batch_delta`: no callers, no coverage.
217
+
218
+ ### Performance
219
+
220
+ - `thin_qr` pins basis construction to one thread: `HybridSubspace` setup for the MNIST
221
+ example drops from 27.0 s to 97.8 ms. `apply_biased_rotation` uses modified
222
+ Gram-Schmidt instead of `torch.linalg.qr`, 7-33x faster at these sizes. Compiled
223
+ kernels take radius and epsilon as 0-d tensors, so a scheduled radius no longer
224
+ recompiles every step.
225
+ - Example 06 on an RTX 5090: 1024 s to 845 s, shift-recovery +9.3 pp to +10.7 pp.
226
+
227
+ ### Examples
228
+
229
+ - Examples pin CPU threads, check optional imports before use instead of failing after
230
+ the run, and register the evaluator in hand-rolled loops. `python-sat` moved into the
231
+ `examples` extra, which example 04 needs.
232
+
233
+ ## 0.8.0 - 2026-07-24
234
+
235
+ ### Added
236
+
237
+ - `compile_forward` on `NNCostEvaluator` / `PolyStepOptimizer`: CUDA-graphs the
238
+ forward+loss on the in-place path and replays it per candidate. Rescues the
239
+ memory-forced path; does not beat compiled vmap. See `docs/performance.md`.
240
+ - `TrainConfig(restore_best=True)` (default): restore the lowest-loss weights before
241
+ `train()` returns.
242
+ - `PolyStepOptimizer.state_dict()` / `load_state_dict()` for checkpoint-resume (solver
243
+ state, `ProgressiveEpsilon` internals, RNG). Model weights save separately.
244
+
245
+ ### Changed
246
+
247
+ - `compile_evaluator` / `compile_forward` propagate to a registered evaluator via
248
+ `register_evaluator`, so they work outside `api.train()`.
249
+ - `train()` skips the extra per-step full-batch forward and its host syncs when no
250
+ callback consumes per-step metrics.
251
+
252
+ ### Fixed
253
+
254
+ - Standalone `PolyStep` accepts any epsilon scheduler (`CosineEpsilon`,
255
+ `ProgressiveEpsilon`), not only `LinearEpsilon` (was `TypeError`).
256
+ - `mixed_precision=True` builds the `ParamLayout` after the BF16 cast, so full-space
257
+ candidate params are BF16 (were FP32).
258
+ - `evaluate_subspace_inplace` casts float inputs to the param dtype, fixing a mismatch
259
+ under `mixed_precision` + `HybridSubspace` + FP32 inputs.
260
+ - `cost_batch_size` subsampling keeps the seeded RNG and compares the full device on a
261
+ mismatch (was non-deterministic; fixes a `cuda:0`/`cuda:1` `randperm` crash).
262
+ - `PolyStep.run()` accepts a 1-D initial point (normalized to one particle).
263
+ - `ProgressiveEpsilon` no longer inflates epsilon every step under a fixed-iteration
264
+ Sinkhorn solver (`threshold<=0`).
265
+ - Greedy solvers: `MinCostGreedySolver` sanitizes non-finite costs; `TopKMeanSolver`
266
+ rejects `k=0`.
267
+ - `PolyStep.step()` uses the shared `sanitize_cost` (was an inline copy that synced and
268
+ could overflow the penalty to `inf`); `state.costs` records the raw mean cost.
269
+ - `HybridSubspace` docstring corrected (QR-orthonormal columns; `step_radius` retune).
270
+ - `NNCostEvaluator` compile docstrings match the shipped modes; the vmap-fallback filter
271
+ matches functorch markers instead of any "batched" error.
272
+
273
+ ## 0.7.0 - 2026-07-20
274
+
275
+ ### Added
276
+
277
+ - On PyPI now: `pip install polystep` (or `uv add polystep`).
278
+
279
+ ### Changed
280
+
281
+ - Wider install range: `torch >= 2.4` (was 2.10) and `numpy >= 1.24` (was 2.0).
282
+ - Blockwise and subspace-blockwise steps reuse one buffer for the per-chunk configs
283
+ instead of reallocating it every chunk, so a step allocates less.
284
+
285
+ ### Fixed
286
+
287
+ - Solvers stay accurate when the cost matrix has a large constant offset (`|C|` far above
288
+ `epsilon`). The cost is recentered before the log-domain math, which leaves the
289
+ transport plan unchanged, so `SinkhornSolver.matrix`, `KLSoftmaxSolver(lam=0)`, and the
290
+ softmax solvers no longer lose precision or return NaN in that regime.
291
+ - `SinkhornSolver` computes `ent_reg_cost` from the actual plan mass, so it is correct
292
+ before convergence too, and it warns when the two marginals do not sum to the same
293
+ total.
294
+ - A 1D biased rotation returns the identity instead of flipping the axis it just aligned
295
+ (`SO(1)` cannot represent a reflection).
296
+
297
+ ## 0.6.1 - 2026-07-15
298
+
299
+ ### Added
300
+
301
+ - `examples/09_hard_decision_tree.py`: a hard oblique decision tree with strict argmax
302
+ routing and no relaxation, trained on an XOR checkerboard. PolyStep optimizes the hard
303
+ tree directly while OpenAI-ES and SPSA stall on its piecewise-constant loss; a
304
+ soft-tree Adam baseline is scored after hardening.
305
+
306
+ ### Changed
307
+
308
+ - The HybridSubspace fused block-diagonal projection is rebuilt only when the projection
309
+ rotates. With the default `rotation_interval=0` the projection is static, so the
310
+ previous per-step `block_diag` rebuild was pure overhead.
311
+
312
+ ### Fixed
313
+
314
+ - `mixed_precision=True` runs end to end. The barycentric and fused-softmax projections
315
+ cast the FP32 OT weights to the BF16 geometry dtype, `HybridSubspace` runs its
316
+ projection QR in FP32 (no BF16 CPU `geqrf`), and the cost evaluator matches float
317
+ inputs to the parameter dtype. Previously the first step raised a dtype mismatch.
318
+ - The barycentric projection normalizes by the realized transport row sum instead of the
319
+ target marginal, so an unconverged Sinkhorn plan gives a translation-invariant step.
320
+ The softmax path is unchanged.
321
+ - Full-space monolithic steps bound the default evaluation chunk to a fixed memory
322
+ budget, avoiding the `O(n_params^2)` config buffer that ran out of memory at the
323
+ default `chunk_size=None`.
324
+ - `apply_biased_rotation` realigns the first search axis with the requested bias (QR
325
+ fixes the column only up to sign), so the biased search points toward descent rather
326
+ than ascent, and runs its QR and determinant in FP32 for BF16 inputs.
327
+
328
+ ## 0.6.0 - 2026-07-12
329
+
330
+ ### Added
331
+
332
+ - `examples/08_direct_loss_minimization.py`: direct F1 minimization on an imbalanced
333
+ checkerboard where PolyStepES beats both a biased gradient (Adam+STE) and OpenAI-ES.
334
+ - `experiments/runners/variant_sweep.py`: a reproducible sweep of the optimizer variants
335
+ (solver, representation, block strategy, adaptation flags, schedules, geometry) across
336
+ nine tasks, ranked by forward-eval budget with a state-mutation self-check that flags
337
+ any variant whose state never changed.
338
+
339
+ ### Changed
340
+
341
+ - CMA covariance updates use the covariance-metric offspring step `y = sqrt(C_diag) * z`,
342
+ so the evolution paths and rank-mu match the covariance-scaled sampling.
343
+ - AdaptiveSubspace runs the per-step basis QR on the GPU in fp32 for CUDA targets,
344
+ several times faster than the previous CPU round-trip for large models.
345
+
346
+ ### Fixed
347
+
348
+ - `SinkhornSolver` rejects `max_iterations < 1` and validates user-supplied marginals
349
+ (finite, nonnegative, positive mass) instead of clamping to an infeasible plan.
350
+ - `PolyStepES` validates `dim`, `num_particles`, `epsilon`, and `step_radius` at
351
+ construction.
352
+ - CMA covariance and step-size adaptation are disabled with a warning under
353
+ non-monolithic block strategies, where they previously never updated.
354
+ - `trust_region` now records its predicted improvement independent of `biased_rotation`,
355
+ so the step-radius multiplier adapts whenever `use_quadratic_model` and
356
+ `num_probe >= 2` hold instead of staying frozen at 1.0. `trust_region` also
357
+ auto-enables the quadratic model.
358
+ - Heaviside stall detection uses the correct generation index (`generation + 1`).
359
+
360
+ ### Removed
361
+
362
+ - Dead `compute_ot_weights` export; the rank-mu path uses the transport masses directly.
363
+
364
+ ## 0.5.0 - 2026-07-07
365
+
366
+ ### Added
367
+
368
+ - `PolyStepES` and `minimize`: an ask/tell interface so PolyStep drops into
369
+ evolution-strategy harnesses (evosax, NeuroEvoBench) as a gradient-free optimizer.
370
+ - `examples/07_binary_net_no_ste.py`: STE-free binary-net training, PolyStepES vs
371
+ OpenAI-ES.
372
+ - `experiments/bench_ask_tell.py`: ask/tell comparison against a Gaussian ES on the
373
+ synthetic suite.
374
+
375
+ ### Changed
376
+
377
+ - CMA covariance rank-mu now estimates per-coordinate variance from the
378
+ transport-weighted polytope vertices, and drops the per-step scatter allocation.
379
+ - CSA calibrates the step-size against the running norm of the evolution path instead of
380
+ sqrt(n), so sigma no longer collapses on OT-scale steps.
381
+ - Anderson acceleration uses a Tikhonov ridge solve and only accepts steps that do not
382
+ regress the dual objective.
383
+ - Newton refinement keeps its step only when the quadratic model says it beats the plain
384
+ OT step.
385
+ - Amortized momentum coasts along the OT step, not the OT-plus-momentum step.
386
+
387
+ ### Fixed
388
+
389
+ - Multi-fidelity screening no longer feeds its own dampened cost back in, which had
390
+ permanently starved low-contrast directions.
391
+ - Sinkhorn fixed-iteration mode reports convergence on a finite result, so
392
+ ProgressiveEpsilon stops inflating epsilon.
393
+ - `adaptive_omega` no longer erases the divergence back-off within the same check.
394
+ - Guarded the barycentric projection against a zero source marginal.
395
+
396
+ ## 0.4.0 - 2026-06-21
397
+
398
+ ### Added
399
+
400
+ - `solvers/_shared.py`: one set of shared solver helpers (FP32 promotion, device-side
401
+ non-finite cost sanitization, device/dtype-aligned marginals and warm-start duals) used
402
+ by the Sinkhorn, softmax, and tempered-softmax solvers.
403
+ - `experiments/scripts/bench_eps_rescale.py` and `tests/test_input_validation.py`.
404
+
405
+ ### Changed
406
+
407
+ - Fused-softmax now honors every `scale_cost` mode (`'mean'/'max_cost'/float/None`),
408
+ matching `SoftmaxSolver`; the compiled kernel no longer scales internally.
409
+ - `TemperedSoftmaxSolver` gains FP32 promotion, non-finite handling, and an
410
+ autocast-disabled softmax (previously absent).
411
+ - Warm-start dual re-centering uses the valid gauge `f -> f+c, g -> g-c` instead of
412
+ independent mean subtraction, which perturbed the iterate under `omega != 1`.
413
+ - `BatchedLinearEvaluator` applies the real activation modules (exact
414
+ `LeakyReLU.negative_slope`, `GELU.approximate`) rather than hardcoded functional
415
+ defaults; non-default `Flatten` falls back to vmap.
416
+ - Low-level `PolyStep.step()` forwards `init_eps` like the optimizer path.
417
+
418
+ ### Removed
419
+
420
+ - Low-rank Sinkhorn (`SinkhornSolver.{rank,gamma,auto_rank_threshold}`,
421
+ `_solve_low_rank`, `SinkhornResult.{_Q,_R,_g_lr}`, the `solve(seed=...)` arg, and the
422
+ `rank` params on `PolyStepOptimizer` / `PolyStep`).
423
+
424
+ ### Fixed
425
+
426
+ - Re-validate `epsilon > 0` inside `SinkhornSolver.solve()` (schedules mutate
427
+ it); reject `check_every < 1`, `num_probe < 1`, and `scale_cost` of 0 / non-finite.
428
+ - Mezzadri rotation sign correction treats `sign(0)` as `+1` so an underflowed QR
429
+ diagonal can't null a column; dropped a redundant `Q.clone()`.
430
+
431
+ ## 0.3.0 - 2026-05-27
432
+
433
+ Cleanup. No public API changes.
434
+
435
+ ### Changed
436
+
437
+ - `solvers/sinkhorn.py`: dropped two GPU->CPU syncs per Sinkhorn solve. `cost_scale` (the
438
+ warm-start `clamp_` bound) stays on-device as a 0-d
439
+ tensor, and `ent_reg_cost = <f,a> + <g,b>` resolves both inner products
440
+ in one host transfer via `torch.stack([...]).sum().item()`. Same pattern applied to the
441
+ low-rank convergence loop and its `err_a/err_b` marginal transfers.
442
+ - `_step_blockwise.py`: per-block fused-softmax `ent_cost_tensor.item()` calls now defer
443
+ to a single `torch.stack([...]).sum().item()` after the per-block loop, mirroring
444
+ `block_disp_terms` and `block_model_loss_terms`. Saves `O(num_blocks)` syncs per step
445
+ in the fused-softmax path.
446
+ - `cma.py`: `update_step_size_csa` now accepts an optional pre-computed `p_sigma_norm`;
447
+ both call sites (`_step_common.update_cma_state`, `_step_monolithic.step`) forward the
448
+ norm they already paid for in `compute_heaviside_sigma`, removing one redundant
449
+ `torch.norm(...).item()` per CMA generation.
450
+
451
+ ### Fixed
452
+
453
+ - `CMAAdaptiveSubspace.rotate()` now forwards `transport_matrix`, `X_vertices`, and
454
+ `X_current` to the wrapped `AdaptiveSubspace`, so the `'ot_bias'` rotation mode fires
455
+ when CMA is enabled (it previously fell through to random rotation).
456
+ - `docs/api_overview.md`: the `PolyStepOptimizer.step` snippet used a zero-argument
457
+ lambda; replaced with the real `closure(batched_params) -> losses` signature via
458
+ `NNCostEvaluator`. The `SparseRandomProjection` example used `input_dim` / `output_dim`
459
+ keyword args; the actual constructor takes `full_dim` / `subspace_dim`.
460
+ - Tightened two test tolerances that were 100-1000x looser than the solver's convergence
461
+ threshold (`tests/test_numerical_stress.py` row-sum checks;
462
+ `tests/test_ablation_solvers.py` tempered-softmax-vs-greedy match).
463
+
464
+ ## 0.2.2 - 2026-05-21
465
+
466
+ Codebase cleanup, test modernization, and documentation correctness pass.
467
+
468
+ ### Breaking
469
+
470
+ - Vertex generator API (`get_orthoplex_vertices`, `get_simplex_vertices`,
471
+ `get_cube_vertices`) signature changed from `(dim, origin, radius)` to
472
+ `(dim, *, radius=1.0)`. The `origin` parameter has been removed - all generators now
473
+ produce unit-radius templates centered at the origin; translation is handled by the
474
+ solver. This affects direct callers only; `PolyStep` and `PolyStepOptimizer` are
475
+ unaffected.
476
+
477
+ ### Fixed
478
+
479
+ - `compute_ot_weights` in `cma.py`: replaced linear softmax normalization with
480
+ entropy-based OT weight computation matching the theoretical derivation.
481
+ - `@torch.inference_mode()` migration across `transform.py` (removed unused
482
+ `import warnings`).
483
+ - README and API overview: `max_iteration` -> `max_iterations` (matching the actual
484
+ `PolyStep` dataclass field).
485
+ - API overview: `AdaptiveSubspace` and `LinearSubspace` code snippets updated from stale
486
+ direct constructors to the `from_layout()` factory.
487
+ - API overview: `SolverState(X=X_init)` -> `solver.init_state(X_init)`.
488
+
489
+ ## 0.2.1 - 2026-05-14
490
+
491
+ ### Added
492
+
493
+ - `examples/06_loihi_snn_polystep.py`: end-to-end skeleton for a Loihi 2-style two-stage
494
+ workflow. Stage 1 pretrains a hard-LIF MNIST SNN with PolyStep using
495
+ `PSTORCH_CONFIGS["snn"]`. Stage 2 adapts only the writable subset a real Loihi 2 chip
496
+ exposes at runtime (`fc2`, per-population `vth`, and `beta`; about 1.3% of model
497
+ parameters) under an `N(0, 1)` Gaussian input shift. Stage 2 uses TENT-style
498
+ safeguards: mixed-batch (half clean / half shifted), rank-8 probing on the writable
499
+ subspace, and two probes per step. Both stages use best-test early stopping (patience
500
+ 4) since zeroth-order test curves are noisier. Shifted-test evaluations share a fixed
501
+ seeded noise mask across pre / post / baseline so the reported recovery is a paired
502
+ comparison. On default settings the example reaches ~83% best clean accuracy and a +13
503
+ pp paired shift-recovery over the frozen-readout baseline with near-zero clean-accuracy
504
+ degradation in about 17 minutes on a single GPU. Reported numbers shift by a few
505
+ percentage points run-to-run on CUDA because of non-deterministic cuBLAS reductions,
506
+ but the qualitative recovery is robust. The host loop is backend-agnostic:
507
+ `LoihiSpikeEvaluator` is the single swap point against a Lava `netx` deployment path.
508
+
509
+ ## 0.2.0 - 2026-05-14
510
+
511
+ Dependency floor lift, native fast-path adoption, and performance pass for PyTorch 2.12.
512
+
513
+ ### Changed
514
+
515
+ - Minimum Python is now 3.11 (was 3.10); minimum PyTorch is now 2.8 (was 2.4); NumPy
516
+ floor is now `>=2.0`.
517
+ - Ruff `target-version` bumped to `py311`. Classifiers updated to advertise 3.11 / 3.12 /
518
+ 3.13 / 3.14.
519
+
520
+ ### Performance
521
+
522
+ - `SinkhornSolver` convergence loop: batched the per-check scalar measurements (err_a,
523
+ err_b, dual norm, Lyapunov) into a single device-to-host transfer, and switched the
524
+ Anderson-acceleration Lyapunov accept gate to a device-side `torch.where`. Eliminates
525
+ 4-7 GPU-CPU syncs per `check_every` interval. Microbench (200 iter, n=m=512, fp32 on
526
+ RTX 5090, full Anderson + adaptive omega): **490 ms -> 48 ms (~10x)**.
527
+ - Block-wise step (`_step_blockwise.py`): per-block displacement and model-loss scalars
528
+ are accumulated as device tensors and reduced to the host once per step instead of
529
+ twice per block.
530
+ - `NNCostEvaluator.evaluate` and `BatchedLinearEvaluator.evaluate` now use
531
+ `@torch.inference_mode()` instead of `@torch.no_grad()` (matches the rest of
532
+ `cost_nn.py`).
533
+
534
+ ## 0.1.0 - 2026-05-03 (Initial Release)
535
+
536
+ First public release alongside the arXiv preprint *Training Non-Differentiable Networks
537
+ via Optimal Transport*.
538
+
539
+ ### Core
540
+
541
+ - `PolyStepOptimizer` for gradient-free training of any `nn.Module`
542
+ - Softmax solver (default; fast path) and log-domain Sinkhorn solver with warm-started
543
+ dual potentials
544
+ - Polytope sampling (orthoplex, simplex, cube) with random rotations
545
+ - Vectorised NN evaluation via `torch.func` + optional `torch.vmap`
546
+ - High-level `train()` API with callbacks and early stopping
547
+
548
+ ### Subspace compression
549
+
550
+ - `LinearSubspace` - random projection
551
+ - `AdaptiveSubspace` - rotating orthogonal projection
552
+ - `HybridSubspace` - per-layer projections (recommended)
553
+ - `CMAAdaptiveSubspace` - CMA-ES covariance adaptation
554
+ - `SparseRandomProjection` - for models with 100M+ parameters
555
+
556
+ ### Scalability
557
+
558
+ - Block-wise per-layer OT decomposition
559
+ - Chunked cost evaluation for memory control
560
+ - `torch.compile` support on GPU hot paths
561
+ - Mixed precision (BF16 model + FP32 solver)
562
+
563
+ ### Layers
564
+
565
+ - `VmapSafeMultiHeadAttention` - attention compatible with `torch.vmap`
566
+ - `VmapSafeLSTM` - LSTM compatible with `torch.vmap`
567
+
568
+ ### Examples and experiments
569
+
570
+ - 5 runnable demos under `examples/` (quickstart, SNN, RL, MAX-SAT, MNIST)
571
+ - Paper-reproduction harness under `experiments/runners/` covering SNN, INT8, argmax,
572
+ staircase, hard MoE, MAX-SAT (100K-1M vars), MNIST, ETTh1 timeseries, RL, and GPT-2
573
+ head-only fine-tuning
574
+ - Ablations: OT vs softmax solver, epsilon / radius / particles / subspace grid, MAX-SAT
575
+ scaling
@@ -6,8 +6,8 @@ authors:
6
6
  - family-names: Le
7
7
  given-names: An T.
8
8
  license: Apache-2.0
9
- version: 0.9.0
10
- date-released: "2026-07-27"
9
+ version: 0.10.0
10
+ date-released: "2026-07-29"
11
11
  repository-code: "https://github.com/anindex/polystep"
12
12
  keywords:
13
13
  - gradient-free optimization
@@ -0,0 +1,93 @@
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 are the lint and test commands CI runs
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 .
23
+ ruff format --check .
24
+
25
+ # Fast tests (recommended during development). The pyproject addopts deselect both
26
+ # the slow and the gpu markers, so neither is collected.
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. Those train on MNIST; about 90 s total.
33
+ pytest tests/ -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
+ which train at batch 512 and get `cpu_count() - 8`. The fast tests are small-tensor
41
+ bound, where torch's intra-op pool costs more than it saves; unpinned the suite is both
42
+ slower and erratic, which was tripping the timeout on the Sinkhorn-heavy tests. Serial it
43
+ runs in about 18 s. `POLYSTEP_TEST_THREADS` overrides the fast count.
44
+
45
+ CI runs the fast suite on Python 3.11-3.14 per push, and the `slow` marker nightly (or on
46
+ `workflow_dispatch`), since those legs download MNIST.
47
+
48
+ It also builds the sdist, checks that it collects, and runs `tests/test_api.py` from the
49
+ unpacked tarball. To reproduce that:
50
+
51
+ ```bash
52
+ uv build --sdist
53
+ mkdir -p /tmp/sdist && tar xzf dist/*.tar.gz -C /tmp/sdist --strip-components=1
54
+ cd /tmp/sdist && uv pip install --system ".[dev]" && pytest tests/test_api.py -q
55
+ ```
56
+
57
+ Tests that read or import `experiments/` request the `require_experiments` fixture, which
58
+ skips them outside a repo checkout.
59
+
60
+ ## Cutting a release
61
+
62
+ `release.yml` publishes on a GitHub Release through PyPI trusted publishing. It refuses
63
+ to publish unless the tag, `src/polystep/__init__.py`, and `CITATION.cff` all carry the
64
+ same version and `CHANGELOG.md` has a section for it, so update all four together:
65
+
66
+ ```bash
67
+ # 1. bump __version__ in src/polystep/__init__.py
68
+ # 2. bump version and date-released in CITATION.cff
69
+ # 3. add a "## X.Y.Z - YYYY-MM-DD" section to CHANGELOG.md
70
+ # 4. tag as vX.Y.Z and publish the GitHub Release
71
+ ```
72
+
73
+ `pyproject.toml` reads the version from `polystep.__version__`, so it needs no edit.
74
+
75
+ ## Code Style
76
+
77
+ - Follow existing code conventions
78
+ - Use type hints for public functions
79
+ - Add docstrings with Args/Returns sections for public APIs
80
+ - Keep comments short and specific; explain why, not what
81
+
82
+ The package ships `py.typed`, so annotations on the public surface are part of the
83
+ contract. `mypy src/polystep/` currently reports errors, mostly narrowing complaints on
84
+ the duck-typed `subspace` argument, so it is not yet a CI gate. New code should not add
85
+ to the count.
86
+
87
+ ## Reporting Issues
88
+
89
+ Please include:
90
+ - Python and PyTorch versions
91
+ - GPU model (if relevant)
92
+ - Minimal reproduction script
93
+ - Full error traceback