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.
- polystep/__init__.py +176 -0
- polystep/_compiled.py +334 -0
- polystep/_step_blockwise.py +994 -0
- polystep/_step_momentum.py +128 -0
- polystep/_step_monolithic.py +1070 -0
- polystep/adaptive_subspace.py +762 -0
- polystep/api.py +307 -0
- polystep/ask_tell.py +164 -0
- polystep/benchmarks/__init__.py +62 -0
- polystep/benchmarks/baselines.py +604 -0
- polystep/benchmarks/rl/__init__.py +13 -0
- polystep/benchmarks/rl/cartpole.py +260 -0
- polystep/benchmarks/rl/gym_evaluator.py +304 -0
- polystep/benchmarks/rl/metrics.py +53 -0
- polystep/benchmarks/rl/policies.py +133 -0
- polystep/benchmarks/utils.py +1262 -0
- polystep/blockwise.py +549 -0
- polystep/cma.py +400 -0
- polystep/cma_subspace.py +404 -0
- polystep/cost_nn.py +633 -0
- polystep/costs.py +86 -0
- polystep/dynamics.py +138 -0
- polystep/epsilon.py +217 -0
- polystep/geometry.py +410 -0
- polystep/hybrid_subspace.py +1335 -0
- polystep/layers/__init__.py +16 -0
- polystep/layers/attention.py +243 -0
- polystep/layers/rnn.py +276 -0
- polystep/objectives/__init__.py +25 -0
- polystep/objectives/base.py +77 -0
- polystep/objectives/synthetic.py +232 -0
- polystep/optimizer.py +1405 -0
- polystep/projection/__init__.py +5 -0
- polystep/projection/sparse.py +291 -0
- polystep/py.typed +0 -0
- polystep/quadratic_model.py +285 -0
- polystep/solver.py +654 -0
- polystep/solvers/__init__.py +34 -0
- polystep/solvers/_prelude.py +129 -0
- polystep/solvers/base.py +77 -0
- polystep/solvers/greedy.py +159 -0
- polystep/solvers/kl_softmax.py +200 -0
- polystep/solvers/sinkhorn.py +522 -0
- polystep/solvers/softmax.py +151 -0
- polystep/solvers/tempered_softmax.py +97 -0
- polystep/subspace.py +694 -0
- polystep/transform.py +365 -0
- polystep-0.7.0.dist-info/METADATA +285 -0
- polystep-0.7.0.dist-info/RECORD +52 -0
- polystep-0.7.0.dist-info/WHEEL +5 -0
- polystep-0.7.0.dist-info/licenses/LICENSE +191 -0
- 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])
|