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,133 @@
1
+ """Small policy networks and parameter helpers for RL benchmarks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections import OrderedDict
6
+ from typing import Dict
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+
11
+
12
+ class DiscreteMLPPolicy(nn.Module):
13
+ """MLP policy for discrete-action direct policy search."""
14
+
15
+ def __init__(self, obs_dim: int, hidden: int, action_dim: int):
16
+ super().__init__()
17
+ self.net = nn.Sequential(
18
+ nn.Linear(obs_dim, hidden),
19
+ nn.Tanh(),
20
+ nn.Linear(hidden, action_dim),
21
+ )
22
+
23
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
24
+ return self.net(obs)
25
+
26
+
27
+ def _quantize_int8_per_tensor(x: torch.Tensor) -> torch.Tensor:
28
+ """Per-tensor symmetric INT8 quantize/dequantize (no STE).
29
+
30
+ Forward: ``q = round(x / scale) * scale`` where ``scale = max|x| / 127``.
31
+ The ``round`` op has zero gradient (PyTorch returns 0), so backprop through
32
+ this layer produces a degenerate signal - exactly what we want to expose as
33
+ a failure mode for PPO/DQN.
34
+ """
35
+
36
+ if x.numel() == 0:
37
+ return x
38
+ max_abs = x.detach().abs().amax()
39
+ if not torch.isfinite(max_abs) or max_abs.item() == 0.0:
40
+ return x
41
+ scale = (max_abs / 127.0).clamp(min=1e-8)
42
+ return torch.round(x / scale) * scale
43
+
44
+
45
+ class NonDiffActivation(nn.Module):
46
+ """Non-differentiable activation layer.
47
+
48
+ Variants
49
+ --------
50
+ ``"int8"`` : Per-tensor symmetric INT8 quantize/dequantize via ``round``.
51
+ ``"binary"``: ``sign(x)`` activation (collapses to {-1, +1}).
52
+ ``"float32"``: Identity (sanity-check baseline).
53
+
54
+ None of the variants implement straight-through estimation (STE). Backprop
55
+ through ``round`` yields zero gradient and through ``sign`` yields zero
56
+ almost everywhere; PPO/DQN trained on a policy containing this layer
57
+ therefore receive no useful gradient past the non-diff op and stagnate at
58
+ random performance.
59
+ """
60
+
61
+ def __init__(self, mode: str = "binary"):
62
+ super().__init__()
63
+ mode = str(mode).lower()
64
+ if mode not in {"float32", "int8", "binary"}:
65
+ raise ValueError(f"NonDiffActivation mode must be float32/int8/binary; got {mode!r}")
66
+ self.mode = mode
67
+
68
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
69
+ if self.mode == "float32":
70
+ return x
71
+ if self.mode == "int8":
72
+ return _quantize_int8_per_tensor(x)
73
+ # binary
74
+ return torch.sign(x)
75
+
76
+
77
+ class NonDiffMLPPolicy(nn.Module):
78
+ """Discrete-action MLP policy with a non-differentiable activation layer.
79
+
80
+ Topology: ``Linear(obs_dim, hidden) -> NonDiffActivation(mode) -> Linear(hidden, action_dim)``.
81
+
82
+ Compared to :class:`DiscreteMLPPolicy` the only change is the inner Tanh
83
+ being replaced by a non-diff op. PolyStep treats the policy as a black box
84
+ and is unaffected; gradient methods (PPO, DQN) collapse because no useful
85
+ gradient flows through the non-diff op (no STE).
86
+ """
87
+
88
+ def __init__(self, obs_dim: int, hidden: int, action_dim: int, *, mode: str = "binary"):
89
+ super().__init__()
90
+ self.mode = str(mode).lower()
91
+ self.net = nn.Sequential(
92
+ nn.Linear(obs_dim, hidden),
93
+ NonDiffActivation(self.mode),
94
+ nn.Linear(hidden, action_dim),
95
+ )
96
+
97
+ def forward(self, obs: torch.Tensor) -> torch.Tensor:
98
+ return self.net(obs)
99
+
100
+
101
+ def stack_module_params(
102
+ module: nn.Module,
103
+ num_candidates: int,
104
+ *,
105
+ noise_scale: float = 0.0,
106
+ seed: int | None = None,
107
+ ) -> Dict[str, torch.Tensor]:
108
+ """Repeat a module's parameters along a candidate dimension.
109
+
110
+ The returned dictionary matches the closure contract expected by
111
+ ``PolyStepOptimizer.step``: each parameter has shape ``(N, *param.shape)``.
112
+ """
113
+
114
+ generator = None
115
+ if seed is not None:
116
+ generator = torch.Generator(device="cpu")
117
+ generator.manual_seed(seed)
118
+
119
+ stacked: OrderedDict[str, torch.Tensor] = OrderedDict()
120
+ for name, param in module.named_parameters():
121
+ values = param.detach().unsqueeze(0).repeat(num_candidates, *([1] * param.ndim))
122
+ if noise_scale > 0:
123
+ noise = torch.randn(values.shape, generator=generator, dtype=values.dtype)
124
+ values = values + noise.to(values.device) * noise_scale
125
+ stacked[name] = values
126
+ return stacked
127
+
128
+
129
+ def count_stacked_candidates(stacked_params: Dict[str, torch.Tensor]) -> int:
130
+ """Return the leading candidate dimension for a stacked parameter dict."""
131
+
132
+ first = next(iter(stacked_params.values()))
133
+ return int(first.shape[0])