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