polystep 0.7.0__py3-none-any.whl

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 (52) hide show
  1. polystep/__init__.py +176 -0
  2. polystep/_compiled.py +334 -0
  3. polystep/_step_blockwise.py +994 -0
  4. polystep/_step_momentum.py +128 -0
  5. polystep/_step_monolithic.py +1070 -0
  6. polystep/adaptive_subspace.py +762 -0
  7. polystep/api.py +307 -0
  8. polystep/ask_tell.py +164 -0
  9. polystep/benchmarks/__init__.py +62 -0
  10. polystep/benchmarks/baselines.py +604 -0
  11. polystep/benchmarks/rl/__init__.py +13 -0
  12. polystep/benchmarks/rl/cartpole.py +260 -0
  13. polystep/benchmarks/rl/gym_evaluator.py +304 -0
  14. polystep/benchmarks/rl/metrics.py +53 -0
  15. polystep/benchmarks/rl/policies.py +133 -0
  16. polystep/benchmarks/utils.py +1262 -0
  17. polystep/blockwise.py +549 -0
  18. polystep/cma.py +400 -0
  19. polystep/cma_subspace.py +404 -0
  20. polystep/cost_nn.py +633 -0
  21. polystep/costs.py +86 -0
  22. polystep/dynamics.py +138 -0
  23. polystep/epsilon.py +217 -0
  24. polystep/geometry.py +410 -0
  25. polystep/hybrid_subspace.py +1335 -0
  26. polystep/layers/__init__.py +16 -0
  27. polystep/layers/attention.py +243 -0
  28. polystep/layers/rnn.py +276 -0
  29. polystep/objectives/__init__.py +25 -0
  30. polystep/objectives/base.py +77 -0
  31. polystep/objectives/synthetic.py +232 -0
  32. polystep/optimizer.py +1405 -0
  33. polystep/projection/__init__.py +5 -0
  34. polystep/projection/sparse.py +291 -0
  35. polystep/py.typed +0 -0
  36. polystep/quadratic_model.py +285 -0
  37. polystep/solver.py +654 -0
  38. polystep/solvers/__init__.py +34 -0
  39. polystep/solvers/_prelude.py +129 -0
  40. polystep/solvers/base.py +77 -0
  41. polystep/solvers/greedy.py +159 -0
  42. polystep/solvers/kl_softmax.py +200 -0
  43. polystep/solvers/sinkhorn.py +522 -0
  44. polystep/solvers/softmax.py +151 -0
  45. polystep/solvers/tempered_softmax.py +97 -0
  46. polystep/subspace.py +694 -0
  47. polystep/transform.py +365 -0
  48. polystep-0.7.0.dist-info/METADATA +285 -0
  49. polystep-0.7.0.dist-info/RECORD +52 -0
  50. polystep-0.7.0.dist-info/WHEEL +5 -0
  51. polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
  52. polystep-0.7.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,404 @@
1
+ """CMAAdaptiveSubspace: CMA-ES covariance adaptation on top of AdaptiveSubspace.
2
+
3
+ Wraps an ``AdaptiveSubspace`` with the separable CMA-ES (sep-CMA-ES) variant
4
+ of the Covariance Matrix Adaptation Evolution Strategy. sep-CMA-ES uses a
5
+ diagonal covariance instead of a full one, reducing memory from O(n^2) to O(n)
6
+ and compute from O(n^3) to O(n) per update -- which is what makes it usable
7
+ in the high-dimensional subspaces that show up in neural network training.
8
+
9
+ Key CMA-ES concepts integrated:
10
+
11
+ 1. **Evolution Paths (p_c, p_sigma)**: Cumulative sums of displacement directions
12
+ over generations. These track the "momentum" of the search and are used to
13
+ update covariance and step-size.
14
+
15
+ 2. **Diagonal Covariance (C_diag)**: Per-dimension scaling of the search distribution.
16
+ Dimensions that consistently show movement get higher variance, allowing the
17
+ optimizer to stretch the search ellipsoid along productive directions.
18
+
19
+ 3. **Cumulative Step-size Adaptation (CSA)**: Uses p_sigma to detect if steps are
20
+ too short (should increase sigma) or too long (should decrease sigma) relative
21
+ to what would be expected under random selection.
22
+
23
+ 4. **Covariance Scaling**: The projection matrix P can be scaled by sqrt(C_diag)
24
+ to modify the effective search distribution in parameter space.
25
+
26
+ Design Decision:
27
+ CMA state (p_c, p_sigma, C_diag, sigma, generation) lives in ``SolverState``,
28
+ not in this class. This maintains JIT compatibility and enables checkpointing
29
+ of the full optimizer state. CMAAdaptiveSubspace only stores static config
30
+ (hyperparameters) and provides methods that operate on the state.
31
+
32
+ Example::
33
+
34
+ from polystep import AdaptiveSubspace, CMAAdaptiveSubspace, SolverState
35
+ import torch.nn as nn
36
+
37
+ model = nn.Linear(100, 10)
38
+ base = AdaptiveSubspace.auto_from_params(model)
39
+ cma_sub = CMAAdaptiveSubspace.from_adaptive_subspace(base)
40
+
41
+ # Initialize CMA state (store in SolverState)
42
+ cma_state = cma_sub.init_cma_state(device='cuda')
43
+ # cma_state = {'p_c': ..., 'p_sigma': ..., 'C_diag': ...}
44
+
45
+ # Apply covariance scaling to projection
46
+ P_scaled = cma_sub.apply_covariance_scaling(P, cma_state['C_diag'])
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ import math
52
+ from dataclasses import dataclass
53
+ from typing import Dict, Optional, Tuple, TYPE_CHECKING
54
+
55
+ import torch
56
+
57
+ from .adaptive_subspace import AdaptiveSubspace
58
+
59
+ from .cma import compute_cma_hyperparameters
60
+
61
+ if TYPE_CHECKING:
62
+ pass
63
+
64
+
65
+ @dataclass
66
+ class CMAAdaptiveSubspace:
67
+ """CMA-ES enhanced adaptive subspace via composition.
68
+
69
+ Wraps an ``AdaptiveSubspace`` instance and adds CMA-ES hyperparameters
70
+ and covariance-related methods. The underlying AdaptiveSubspace handles
71
+ projection initialization, rotation, and core reconstruction operations.
72
+
73
+ CMA-ES hyperparameters are auto-computed from subspace_dim using the
74
+ standard Hansen formulas (see ``compute_cma_hyperparameters``).
75
+
76
+ Attributes:
77
+ base: The wrapped AdaptiveSubspace instance.
78
+ c_c: Learning rate for covariance evolution path update.
79
+ c_sigma: Learning rate for step-size evolution path update.
80
+ c_1: Learning rate for rank-one covariance update.
81
+ c_mu: Learning rate for rank-mu covariance update.
82
+ d_sigma: Damping factor for step-size adaptation.
83
+ expected_norm: Expected length of N(0,I) random vector (chi_n).
84
+ mu_eff: Effective population size for weighted recombination.
85
+ cov_min: Minimum allowed value for C_diag entries.
86
+ cov_max: Maximum allowed value for C_diag entries.
87
+ """
88
+
89
+ base: AdaptiveSubspace
90
+ # CMA-ES hyperparameters (auto-computed from subspace_dim)
91
+ c_c: float = 0.0
92
+ c_sigma: float = 0.0
93
+ c_1: float = 0.0
94
+ c_mu: float = 0.0
95
+ d_sigma: float = 0.0
96
+ expected_norm: float = 0.0
97
+ mu_eff: float = 1.0
98
+ # Numerical stability bounds
99
+ cov_min: float = 1e-6
100
+ cov_max: float = 1e6
101
+
102
+ # ------------------------------------------------------------------
103
+ # Delegated properties
104
+ # ------------------------------------------------------------------
105
+
106
+ @property
107
+ def full_dim(self) -> int:
108
+ """Total flattened parameter count (delegated to base)."""
109
+ return self.base.full_dim
110
+
111
+ @property
112
+ def subspace_dim(self) -> int:
113
+ """Subspace dimension / rank (delegated to base)."""
114
+ return self.base.subspace_dim
115
+
116
+ @property
117
+ def compression_ratio(self) -> float:
118
+ """Compression ratio: subspace_dim / full_dim (delegated to base)."""
119
+ return self.base.compression_ratio
120
+
121
+ @property
122
+ def rotation_mode(self) -> str:
123
+ """Rotation mode: 'random' or 'displacement' (delegated to base)."""
124
+ return self.base.rotation_mode
125
+
126
+ # ------------------------------------------------------------------
127
+ # Delegated methods
128
+ # ------------------------------------------------------------------
129
+
130
+ def init_projection(
131
+ self,
132
+ generator: Optional[torch.Generator] = None,
133
+ device: Optional[torch.device] = None,
134
+ dtype: Optional[torch.dtype] = None,
135
+ ) -> torch.Tensor:
136
+ """Initialize projection matrix (delegated to base).
137
+
138
+ Args:
139
+ generator: Optional torch.Generator for reproducibility.
140
+ device: Target device for the projection matrix. If None, uses CPU.
141
+ dtype: Optional dtype for projection matrix. If None, uses float32.
142
+ Use bfloat16 for mixed precision mode to reduce memory.
143
+
144
+ Returns:
145
+ Projection matrix P with shape (full_dim, subspace_dim).
146
+ """
147
+ return self.base.init_projection(generator=generator, device=device, dtype=dtype)
148
+
149
+ def rotate(
150
+ self,
151
+ projection: torch.Tensor,
152
+ step: int,
153
+ total_steps: int,
154
+ displacement_history: Optional[torch.Tensor] = None,
155
+ generator: Optional[torch.Generator] = None,
156
+ # OT-bias mode inputs (forwarded to base.rotate; ignored otherwise)
157
+ transport_matrix: Optional[torch.Tensor] = None,
158
+ X_vertices: Optional[torch.Tensor] = None,
159
+ X_current: Optional[torch.Tensor] = None,
160
+ ) -> torch.Tensor:
161
+ """Rotate projection basis (delegated to base).
162
+
163
+ Args:
164
+ projection: Current projection matrix P.
165
+ step: Current optimization step.
166
+ total_steps: Total number of optimization steps.
167
+ displacement_history: Optional displacement history tensor.
168
+ generator: Optional torch.Generator for reproducibility.
169
+ transport_matrix: OT transport plan (only used when the wrapped
170
+ AdaptiveSubspace is in ``'ot_bias'`` rotation mode).
171
+ X_vertices: Polytope vertex positions (ot_bias mode only).
172
+ X_current: Current particle positions (ot_bias mode only).
173
+
174
+ Returns:
175
+ New projection matrix P_new.
176
+ """
177
+ return self.base.rotate(
178
+ projection,
179
+ step,
180
+ total_steps,
181
+ displacement_history,
182
+ generator,
183
+ transport_matrix=transport_matrix,
184
+ X_vertices=X_vertices,
185
+ X_current=X_current,
186
+ )
187
+
188
+ def apply_perturbation(
189
+ self,
190
+ projection: torch.Tensor,
191
+ base_sd: Dict[str, torch.Tensor],
192
+ flat_subspace: torch.Tensor,
193
+ ) -> Dict[str, torch.Tensor]:
194
+ """Reconstruct full state_dict from base + subspace coords (delegated to base).
195
+
196
+ Args:
197
+ projection: Projection matrix P.
198
+ base_sd: Base state_dict.
199
+ flat_subspace: Subspace coordinate vector.
200
+
201
+ Returns:
202
+ New state_dict with perturbed parameters.
203
+ """
204
+ return self.base.apply_perturbation(projection, base_sd, flat_subspace)
205
+
206
+ def reconstruct_batch(
207
+ self,
208
+ projection: torch.Tensor,
209
+ base_sd: Dict[str, torch.Tensor],
210
+ flat_subspace_batch: torch.Tensor,
211
+ ) -> Dict[str, torch.Tensor]:
212
+ """Vectorized reconstruction for N probe points (delegated to base).
213
+
214
+ Args:
215
+ projection: Projection matrix P.
216
+ base_sd: Base state_dict.
217
+ flat_subspace_batch: Batch of subspace coordinates (N, subspace_dim).
218
+
219
+ Returns:
220
+ Dict with batched perturbed params {key: (N, *shape)}.
221
+ """
222
+ return self.base.reconstruct_batch(projection, base_sd, flat_subspace_batch)
223
+
224
+ def absorb(
225
+ self,
226
+ projection: torch.Tensor,
227
+ base_sd: Dict[str, torch.Tensor],
228
+ flat_subspace: torch.Tensor,
229
+ ) -> Tuple[Dict[str, torch.Tensor], torch.Tensor]:
230
+ """Fold subspace perturbation into base weights (delegated to base).
231
+
232
+ Args:
233
+ projection: Projection matrix P.
234
+ base_sd: Base state_dict.
235
+ flat_subspace: Current subspace vector.
236
+
237
+ Returns:
238
+ Tuple of (new_base_sd, zeroed_subspace_vector).
239
+ """
240
+ return self.base.absorb(projection, base_sd, flat_subspace)
241
+
242
+ def should_absorb(self, stagnation_count: int, iteration: int) -> bool:
243
+ """Check whether absorb should be triggered (delegated to base).
244
+
245
+ Args:
246
+ stagnation_count: Consecutive steps without improvement.
247
+ iteration: Current iteration number.
248
+
249
+ Returns:
250
+ True if absorb should be triggered.
251
+ """
252
+ return self.base.should_absorb(stagnation_count, iteration)
253
+
254
+ # ------------------------------------------------------------------
255
+ # CMA-specific methods
256
+ # ------------------------------------------------------------------
257
+
258
+ def apply_covariance_scaling(
259
+ self,
260
+ projection: torch.Tensor,
261
+ C_diag: torch.Tensor,
262
+ ) -> torch.Tensor:
263
+ """Scale projection columns by sqrt(C_diag) for covariance-adapted sampling.
264
+
265
+ In CMA-ES, the search distribution is N(m, sigma^2 * C). With diagonal
266
+ covariance C = diag(C_diag), sampling x ~ N(m, sigma^2 * C) is equivalent
267
+ to sampling z ~ N(0, I) and computing x = m + sigma * C^{1/2} * z.
268
+
269
+ This method applies the C^{1/2} scaling to the projection matrix, so
270
+ that sampling in the original subspace coordinates and then projecting
271
+ gives the covariance-scaled effect in full parameter space.
272
+
273
+ Args:
274
+ projection: Projection matrix P of shape (full_dim, subspace_dim).
275
+ C_diag: Diagonal covariance entries of shape (subspace_dim,).
276
+
277
+ Returns:
278
+ Scaled projection P_scaled = P @ diag(sqrt(C_diag)).
279
+ """
280
+ # Clamp C_diag for numerical stability
281
+ C_diag_clamped = torch.clamp(C_diag, min=self.cov_min, max=self.cov_max)
282
+ # Scale columns: P_scaled[:, i] = P[:, i] * sqrt(C_diag[i])
283
+ sqrt_C = torch.sqrt(C_diag_clamped)
284
+ return projection * sqrt_C.unsqueeze(0)
285
+
286
+ def init_cma_state(
287
+ self,
288
+ device: str | torch.device = "cpu",
289
+ dtype: torch.dtype = torch.float32,
290
+ ) -> Dict[str, torch.Tensor]:
291
+ """Initialize CMA-ES state tensors.
292
+
293
+ Creates the initial evolution paths and diagonal covariance for a fresh
294
+ CMA-ES optimization run. These should be stored in SolverState.
295
+
296
+ Initial values:
297
+ - p_c: zeros (no accumulated covariance direction yet)
298
+ - p_sigma: zeros (no accumulated step-size direction yet)
299
+ - C_diag: ones (isotropic initial covariance)
300
+
301
+ Args:
302
+ device: Target device for state tensors.
303
+ dtype: Target dtype for state tensors.
304
+
305
+ Returns:
306
+ Dict with keys 'p_c', 'p_sigma', 'C_diag', each of shape (subspace_dim,).
307
+ """
308
+ subspace_dim = self.base.subspace_dim
309
+ return {
310
+ "p_c": torch.zeros(subspace_dim, device=device, dtype=dtype),
311
+ "p_sigma": torch.zeros(subspace_dim, device=device, dtype=dtype),
312
+ "C_diag": torch.ones(subspace_dim, device=device, dtype=dtype),
313
+ }
314
+
315
+ # ------------------------------------------------------------------
316
+ # Factory methods
317
+ # ------------------------------------------------------------------
318
+
319
+ @classmethod
320
+ def from_adaptive_subspace(
321
+ cls,
322
+ base: AdaptiveSubspace,
323
+ mu_eff: Optional[float] = None,
324
+ cov_min: float = 1e-6,
325
+ cov_max: float = 1e6,
326
+ ) -> "CMAAdaptiveSubspace":
327
+ """Create CMAAdaptiveSubspace by wrapping an existing AdaptiveSubspace.
328
+
329
+ CMA-ES hyperparameters (c_c, c_sigma, c_1, c_mu, d_sigma, expected_norm)
330
+ are automatically computed from the subspace dimension using the standard
331
+ Hansen formulas.
332
+
333
+ Args:
334
+ base: The AdaptiveSubspace to wrap.
335
+ mu_eff: Effective population size. If None, defaults to
336
+ subspace_dim / 4 (heuristic for OT-based selection).
337
+ cov_min: Minimum allowed C_diag entry (numerical stability).
338
+ cov_max: Maximum allowed C_diag entry (numerical stability).
339
+
340
+ Returns:
341
+ CMAAdaptiveSubspace wrapping the base instance.
342
+ """
343
+ n = base.subspace_dim
344
+
345
+ # Default mu_eff: fraction of subspace dim, capped per Hansen CMA tutorial.
346
+ # Hansen recommends mu_eff ~ mu ~ lambda/2, which scales as O(sqrt(n)).
347
+ # Cap at 5*sqrt(n) to avoid over-aggressive step-size adaptation.
348
+ if mu_eff is None:
349
+ mu_eff = max(1.0, min(n / 4.0, 5.0 * math.sqrt(n)))
350
+
351
+ # Compute CMA hyperparameters from dimension and mu_eff
352
+ hyperparams = compute_cma_hyperparameters(n, mu_eff)
353
+
354
+ return cls(
355
+ base=base,
356
+ c_c=hyperparams["c_c"],
357
+ c_sigma=hyperparams["c_sigma"],
358
+ c_1=hyperparams["c_1"],
359
+ c_mu=hyperparams["c_mu"],
360
+ d_sigma=hyperparams["d_sigma"],
361
+ expected_norm=hyperparams["expected_norm"],
362
+ mu_eff=mu_eff,
363
+ cov_min=cov_min,
364
+ cov_max=cov_max,
365
+ )
366
+
367
+ @classmethod
368
+ def auto_from_params(
369
+ cls,
370
+ model: torch.nn.Module,
371
+ compression_target: float = 0.05,
372
+ min_rank: int = 64,
373
+ max_rank: int = 4096,
374
+ mu_eff: Optional[float] = None,
375
+ cov_min: float = 1e-6,
376
+ cov_max: float = 1e6,
377
+ **kwargs,
378
+ ) -> "CMAAdaptiveSubspace":
379
+ """Create CMAAdaptiveSubspace directly from an nn.Module.
380
+
381
+ Convenience factory that first creates an AdaptiveSubspace with
382
+ ``auto_from_params``, then wraps it with CMA-ES functionality.
383
+
384
+ Args:
385
+ model: Any PyTorch module.
386
+ compression_target: Target ratio of subspace_dim / full_dim.
387
+ min_rank: Minimum subspace dimension.
388
+ max_rank: Maximum subspace dimension.
389
+ mu_eff: Effective population size for CMA (see from_adaptive_subspace).
390
+ cov_min: Minimum allowed C_diag entry.
391
+ cov_max: Maximum allowed C_diag entry.
392
+ **kwargs: Additional arguments passed to AdaptiveSubspace.auto_from_params.
393
+
394
+ Returns:
395
+ CMAAdaptiveSubspace configured for the model.
396
+ """
397
+ base = AdaptiveSubspace.auto_from_params(
398
+ model,
399
+ compression_target=compression_target,
400
+ min_rank=min_rank,
401
+ max_rank=max_rank,
402
+ **kwargs,
403
+ )
404
+ return cls.from_adaptive_subspace(base, mu_eff=mu_eff, cov_min=cov_min, cov_max=cov_max)