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,128 @@
1
+ """Amortized momentum step and related helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import math
6
+ from typing import Callable
7
+
8
+ import torch
9
+
10
+
11
+ def step_momentum(opt, closure: Callable) -> float:
12
+ """Cheap momentum step: apply EMA transport direction with decay.
13
+
14
+ Applies the EMA-smoothed transport direction without a validation
15
+ forward pass. NaN safety is guaranteed by reverting to the
16
+ pre-step position if non-finite values appear.
17
+
18
+ Args:
19
+ closure: ``closure(batched_params) -> losses`` (unused - kept
20
+ for API compatibility with other ``_step_*`` methods).
21
+
22
+ Returns:
23
+ Reused cost from the last OT step.
24
+ """
25
+ state = opt._state
26
+ opt._update_sampling_projection()
27
+ if opt._transport_direction_ema is None:
28
+ return state.costs[-1] if state.costs else float("inf")
29
+
30
+ # Linear decay: first momentum step gets full strength, last gets minimal
31
+ phase = (opt._amortize_counter % opt.amortize_steps) / opt.amortize_steps
32
+ decay = 1.0 - phase
33
+
34
+ # Store pre-step X for NaN safety
35
+ X_old = state.X.clone()
36
+
37
+ # Choose momentum direction: Newton (if available) or EMA transport
38
+ if opt.use_quadratic_model and opt._newton_direction is not None:
39
+ direction = opt._newton_direction
40
+ else:
41
+ direction = opt._transport_direction_ema
42
+
43
+ # Apply direction with decay.
44
+ state.X = state.X + decay * direction
45
+
46
+ # NaN check - revert to pre-step state and use previous cost
47
+ if not torch.isfinite(state.X).all():
48
+ state.X = X_old
49
+ opt._transport_direction = None
50
+ opt._transport_direction_ema = None
51
+ opt._newton_direction = None
52
+ opt._sync_model()
53
+ prev_cost = state.costs[-1] if state.costs else float("inf")
54
+ state.costs.append(prev_cost)
55
+ state.linear_convergence.append(True)
56
+ state.displacement_sqnorms.append(0.0)
57
+ state.iteration_count += 1
58
+ return prev_cost
59
+
60
+ # Sync model parameters from updated particles
61
+ opt._sync_model()
62
+
63
+ # Reuse last cost (no validation forward pass)
64
+ cost = state.costs[-1] if state.costs else float("inf")
65
+
66
+ # Update diagnostics
67
+ disp_sqnorm = torch.mean(torch.sum((state.X - X_old) ** 2, dim=-1)).item()
68
+ state.costs.append(cost)
69
+ state.linear_convergence.append(True)
70
+ state.displacement_sqnorms.append(disp_sqnorm)
71
+ state.iteration_count += 1
72
+
73
+ return cost
74
+
75
+
76
+ def evaluate_current_loss(opt, closure: Callable) -> float:
77
+ """Evaluate model loss at current particle position via single forward pass.
78
+
79
+ Not called by the core optimization loop (momentum validation was
80
+ removed), but retained as a public diagnostic utility.
81
+
82
+ Builds a batched param config (batch=1) from current state.X and
83
+ calls the closure. Handles both subspace and full-space modes.
84
+
85
+ Returns:
86
+ Scalar loss value (float).
87
+ """
88
+ state = opt._state
89
+ with torch.no_grad():
90
+ if opt.subspace is not None:
91
+ flat_sub = state.X.reshape(-1)[: state.subspace.subspace_dim].unsqueeze(0)
92
+ if opt._mixed_precision and state.projection is not None:
93
+ flat_sub = flat_sub.to(dtype=state.projection.dtype)
94
+ if opt._adaptive or opt._cma_subspace:
95
+ val_params = state.subspace.reconstruct_batch(
96
+ opt._sampling_projection,
97
+ state.base_params,
98
+ flat_sub,
99
+ )
100
+ elif opt._hybrid:
101
+ val_params = state.subspace.reconstruct_batch(
102
+ state.hybrid_projections,
103
+ state.base_params,
104
+ flat_sub,
105
+ )
106
+ else:
107
+ val_params = state.subspace.reconstruct_batch(
108
+ state.base_params,
109
+ flat_sub,
110
+ )
111
+ else:
112
+ flat_config = state.X.reshape(1, -1)
113
+ layout_flat = opt.layout.padded_size
114
+ if flat_config.shape[1] >= layout_flat:
115
+ flat_for_layout = flat_config[:, :layout_flat]
116
+ else:
117
+ flat_for_layout = torch.nn.functional.pad(
118
+ flat_config,
119
+ (0, layout_flat - flat_config.shape[1]),
120
+ )
121
+ val_params = opt.layout.batch_unflatten(flat_for_layout)
122
+
123
+ val_loss_tensor = closure(val_params)
124
+ val_loss = val_loss_tensor.mean().item()
125
+ # Guard against NaN propagation - treat as infinite loss
126
+ if math.isnan(val_loss):
127
+ return float("inf")
128
+ return val_loss