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,16 @@
|
|
|
1
|
+
"""Vmap-compatible PyTorch layers (attention, LSTM).
|
|
2
|
+
|
|
3
|
+
- VmapSafeMultiHeadAttention: avoids SDPA (PyTorch Issue #151558)
|
|
4
|
+
- VmapSafeLSTMCell / VmapSafeLSTM: explicit gate ops bypassing CuDNN
|
|
5
|
+
|
|
6
|
+
Trades some throughput for guaranteed vmap compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .attention import VmapSafeMultiHeadAttention
|
|
10
|
+
from .rnn import VmapSafeLSTMCell, VmapSafeLSTM
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"VmapSafeMultiHeadAttention",
|
|
14
|
+
"VmapSafeLSTMCell",
|
|
15
|
+
"VmapSafeLSTM",
|
|
16
|
+
]
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Vmap-compatible multi-head attention.
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for ``nn.MultiheadAttention`` that uses an
|
|
4
|
+
explicit ``torch.matmul`` path instead of
|
|
5
|
+
``F.scaled_dot_product_attention``. The hand-rolled path sidesteps the
|
|
6
|
+
SDPA mask-validation issues reported under ``torch.vmap`` (PyTorch
|
|
7
|
+
issue #151558 and related) and works the same way on every PyTorch
|
|
8
|
+
build supported by this project.
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> import torch
|
|
12
|
+
>>> from polystep.layers import VmapSafeMultiHeadAttention
|
|
13
|
+
>>> attn = VmapSafeMultiHeadAttention(embed_dim=64, num_heads=4)
|
|
14
|
+
>>> x = torch.randn(2, 10, 64) # (batch, seq, embed)
|
|
15
|
+
>>> out = attn(x, x, x) # self-attention
|
|
16
|
+
>>> out.shape
|
|
17
|
+
torch.Size([2, 10, 64])
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import math
|
|
21
|
+
import warnings
|
|
22
|
+
from typing import Optional
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
import torch.nn as nn
|
|
26
|
+
import torch.nn.functional as F
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class VmapSafeMultiHeadAttention(nn.Module):
|
|
30
|
+
"""Multi-head attention using explicit matmul operations for vmap compatibility.
|
|
31
|
+
|
|
32
|
+
This implementation avoids F.scaled_dot_product_attention which has known
|
|
33
|
+
vmap mask validation bugs (PyTorch Issue #151558). Instead, it uses explicit
|
|
34
|
+
matrix multiplications that work correctly under torch.vmap.
|
|
35
|
+
|
|
36
|
+
Limitations vs nn.MultiheadAttention:
|
|
37
|
+
- No built-in causal masking (pass attn_mask manually)
|
|
38
|
+
- Assumes batch-first layout: (batch, seq, embed_dim)
|
|
39
|
+
- No add_bias_kv or add_zero_attn support
|
|
40
|
+
- kdim/vdim must equal embed_dim (no cross-attention with different dims)
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
embed_dim: Total dimension of the model (must be divisible by num_heads).
|
|
44
|
+
num_heads: Number of parallel attention heads.
|
|
45
|
+
dropout: Dropout probability on attention weights. Default: 0.0.
|
|
46
|
+
bias: Whether to add bias to projection layers. Default: True.
|
|
47
|
+
|
|
48
|
+
Input shapes:
|
|
49
|
+
- query: ``(batch, seq_q, embed_dim)``
|
|
50
|
+
- key: ``(batch, seq_k, embed_dim)``
|
|
51
|
+
- value: ``(batch, seq_k, embed_dim)``
|
|
52
|
+
- attn_mask: ``(seq_q, seq_k)``, ``(batch, seq_q, seq_k)``, or
|
|
53
|
+
``(batch, num_heads, seq_q, seq_k)`` (float additive or bool)
|
|
54
|
+
- key_padding_mask: ``(batch, seq_k)`` bool, ``True`` = padding
|
|
55
|
+
|
|
56
|
+
Output shape:
|
|
57
|
+
- (batch, seq_q, embed_dim)
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> attn = VmapSafeMultiHeadAttention(embed_dim=64, num_heads=4)
|
|
61
|
+
>>> x = torch.randn(2, 10, 64)
|
|
62
|
+
>>> out = attn(x, x, x) # self-attention
|
|
63
|
+
>>> out.shape
|
|
64
|
+
torch.Size([2, 10, 64])
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
embed_dim: int,
|
|
70
|
+
num_heads: int,
|
|
71
|
+
dropout: float = 0.0,
|
|
72
|
+
bias: bool = True,
|
|
73
|
+
# Mirror the upstream nn.MultiheadAttention signature so that
|
|
74
|
+
# callers passing unsupported features get a clear
|
|
75
|
+
# NotImplementedError instead of a generic Python
|
|
76
|
+
# "got an unexpected keyword argument".
|
|
77
|
+
add_bias_kv: bool = False,
|
|
78
|
+
add_zero_attn: bool = False,
|
|
79
|
+
kdim: Optional[int] = None,
|
|
80
|
+
vdim: Optional[int] = None,
|
|
81
|
+
batch_first: bool = True,
|
|
82
|
+
):
|
|
83
|
+
super().__init__()
|
|
84
|
+
|
|
85
|
+
if embed_dim % num_heads != 0:
|
|
86
|
+
raise ValueError(f"embed_dim ({embed_dim}) must be divisible by num_heads ({num_heads})")
|
|
87
|
+
|
|
88
|
+
if kdim is not None and kdim != embed_dim:
|
|
89
|
+
raise NotImplementedError(
|
|
90
|
+
f"VmapSafeMultiHeadAttention only supports kdim == embed_dim, "
|
|
91
|
+
f"got kdim={kdim}, embed_dim={embed_dim}. See LIMITATIONS.md."
|
|
92
|
+
)
|
|
93
|
+
if vdim is not None and vdim != embed_dim:
|
|
94
|
+
raise NotImplementedError(
|
|
95
|
+
f"VmapSafeMultiHeadAttention only supports vdim == embed_dim, "
|
|
96
|
+
f"got vdim={vdim}, embed_dim={embed_dim}. See LIMITATIONS.md."
|
|
97
|
+
)
|
|
98
|
+
if add_bias_kv:
|
|
99
|
+
raise NotImplementedError(
|
|
100
|
+
"VmapSafeMultiHeadAttention does not support add_bias_kv=True. See LIMITATIONS.md."
|
|
101
|
+
)
|
|
102
|
+
if add_zero_attn:
|
|
103
|
+
raise NotImplementedError(
|
|
104
|
+
"VmapSafeMultiHeadAttention does not support add_zero_attn=True. See LIMITATIONS.md."
|
|
105
|
+
)
|
|
106
|
+
if not batch_first:
|
|
107
|
+
raise NotImplementedError(
|
|
108
|
+
"VmapSafeMultiHeadAttention only supports batch_first=True "
|
|
109
|
+
"(input layout (batch, seq, embed_dim)). See LIMITATIONS.md."
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
self.embed_dim = embed_dim
|
|
113
|
+
self.num_heads = num_heads
|
|
114
|
+
self.head_dim = embed_dim // num_heads
|
|
115
|
+
self.scale = 1.0 / math.sqrt(self.head_dim)
|
|
116
|
+
self.dropout = dropout
|
|
117
|
+
|
|
118
|
+
if dropout > 0:
|
|
119
|
+
warnings.warn(
|
|
120
|
+
"VmapSafeMultiHeadAttention with dropout > 0 requires eval mode under vmap. "
|
|
121
|
+
"Call model.eval() before vmap evaluation.",
|
|
122
|
+
stacklevel=2,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Separate projections for Q, K, V (not combined like GPT-2)
|
|
126
|
+
# This makes vmap over parameters cleaner
|
|
127
|
+
self.W_q = nn.Linear(embed_dim, embed_dim, bias=bias)
|
|
128
|
+
self.W_k = nn.Linear(embed_dim, embed_dim, bias=bias)
|
|
129
|
+
self.W_v = nn.Linear(embed_dim, embed_dim, bias=bias)
|
|
130
|
+
|
|
131
|
+
# Output projection
|
|
132
|
+
self.W_o = nn.Linear(embed_dim, embed_dim, bias=bias)
|
|
133
|
+
|
|
134
|
+
# Dropout for attention weights
|
|
135
|
+
self.attn_dropout = nn.Dropout(dropout)
|
|
136
|
+
|
|
137
|
+
def forward(
|
|
138
|
+
self,
|
|
139
|
+
query: torch.Tensor,
|
|
140
|
+
key: torch.Tensor,
|
|
141
|
+
value: torch.Tensor,
|
|
142
|
+
attn_mask: Optional[torch.Tensor] = None,
|
|
143
|
+
key_padding_mask: Optional[torch.Tensor] = None,
|
|
144
|
+
need_weights: bool = False,
|
|
145
|
+
is_causal: bool = False,
|
|
146
|
+
) -> torch.Tensor:
|
|
147
|
+
"""Compute multi-head attention.
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
query: Query tensor of shape ``(batch, seq_q, embed_dim)``.
|
|
151
|
+
key: Key tensor of shape ``(batch, seq_k, embed_dim)``.
|
|
152
|
+
value: Value tensor of shape ``(batch, seq_k, embed_dim)``.
|
|
153
|
+
attn_mask: Attention mask. Accepted shapes (matching
|
|
154
|
+
``nn.MultiheadAttention``):
|
|
155
|
+
``(seq_q, seq_k)``, ``(batch, seq_q, seq_k)``, or
|
|
156
|
+
``(batch, num_heads, seq_q, seq_k)``. Float masks are added
|
|
157
|
+
to attention scores before softmax (use ``-inf`` for hard
|
|
158
|
+
masking). Bool masks are interpreted upstream-style: ``True``
|
|
159
|
+
positions are masked out.
|
|
160
|
+
key_padding_mask: Boolean mask of shape ``(batch, seq_k)`` where
|
|
161
|
+
``True`` marks padding positions.
|
|
162
|
+
need_weights: Not supported. Returning attention weights from
|
|
163
|
+
inside ``vmap`` requires extra reshapes that defeat the
|
|
164
|
+
kernel fusion this layer is here to enable.
|
|
165
|
+
is_causal: Not supported. Pass an explicit triangular
|
|
166
|
+
``attn_mask`` instead.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Output tensor of shape (batch, seq_q, embed_dim).
|
|
170
|
+
"""
|
|
171
|
+
# Reject unsupported forward kwargs up front so the failure
|
|
172
|
+
# mode is loud, not "wrong but plausible-looking output".
|
|
173
|
+
if need_weights:
|
|
174
|
+
raise NotImplementedError(
|
|
175
|
+
"VmapSafeMultiHeadAttention does not support need_weights=True. See LIMITATIONS.md."
|
|
176
|
+
)
|
|
177
|
+
if is_causal:
|
|
178
|
+
raise NotImplementedError(
|
|
179
|
+
"VmapSafeMultiHeadAttention does not support is_causal=True. "
|
|
180
|
+
"Pass an explicit triangular attn_mask instead. See LIMITATIONS.md."
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
batch_size, seq_q, _ = query.shape
|
|
184
|
+
seq_k = key.shape[1]
|
|
185
|
+
|
|
186
|
+
# Project Q, K, V: (batch, seq, embed_dim) -> (batch, seq, embed_dim)
|
|
187
|
+
Q = self.W_q(query)
|
|
188
|
+
K = self.W_k(key)
|
|
189
|
+
V = self.W_v(value)
|
|
190
|
+
|
|
191
|
+
# Reshape to (batch, num_heads, seq, head_dim)
|
|
192
|
+
Q = Q.view(batch_size, seq_q, self.num_heads, self.head_dim).transpose(1, 2)
|
|
193
|
+
K = K.view(batch_size, seq_k, self.num_heads, self.head_dim).transpose(1, 2)
|
|
194
|
+
V = V.view(batch_size, seq_k, self.num_heads, self.head_dim).transpose(1, 2)
|
|
195
|
+
|
|
196
|
+
# Compute attention scores: (batch, num_heads, seq_q, seq_k)
|
|
197
|
+
# Q @ K.T = (batch, num_heads, seq_q, head_dim) @ (batch, num_heads, head_dim, seq_k)
|
|
198
|
+
scores = torch.matmul(Q, K.transpose(-2, -1)) * self.scale
|
|
199
|
+
|
|
200
|
+
# Bool masks are mask-filled with -inf to match
|
|
201
|
+
# ``nn.MultiheadAttention`` semantics. Float masks are additive
|
|
202
|
+
# (use -inf for hard masking, finite values for soft biases like
|
|
203
|
+
# ALiBi or relative-position embeddings).
|
|
204
|
+
if attn_mask is not None:
|
|
205
|
+
if attn_mask.dim() == 2:
|
|
206
|
+
# (seq_q, seq_k) -> (1, 1, seq_q, seq_k)
|
|
207
|
+
attn_mask = attn_mask.unsqueeze(0).unsqueeze(0)
|
|
208
|
+
elif attn_mask.dim() == 3:
|
|
209
|
+
# (batch, seq_q, seq_k) -> (batch, 1, seq_q, seq_k)
|
|
210
|
+
attn_mask = attn_mask.unsqueeze(1)
|
|
211
|
+
elif attn_mask.dim() != 4:
|
|
212
|
+
raise ValueError(
|
|
213
|
+
"attn_mask must have 2, 3, or 4 dimensions, got "
|
|
214
|
+
f"{attn_mask.dim()}D (shape {tuple(attn_mask.shape)})"
|
|
215
|
+
)
|
|
216
|
+
if attn_mask.dtype == torch.bool:
|
|
217
|
+
scores = scores.masked_fill(attn_mask, float("-inf"))
|
|
218
|
+
else:
|
|
219
|
+
scores = scores + attn_mask
|
|
220
|
+
|
|
221
|
+
# Apply key padding mask
|
|
222
|
+
if key_padding_mask is not None:
|
|
223
|
+
# (batch, seq_k) -> (batch, 1, 1, seq_k)
|
|
224
|
+
padding_mask = key_padding_mask.unsqueeze(1).unsqueeze(2)
|
|
225
|
+
# True means padding, so we mask with -inf
|
|
226
|
+
scores = scores.masked_fill(padding_mask, float("-inf"))
|
|
227
|
+
|
|
228
|
+
# Softmax and dropout
|
|
229
|
+
attn_weights = F.softmax(scores, dim=-1)
|
|
230
|
+
attn_weights = self.attn_dropout(attn_weights)
|
|
231
|
+
|
|
232
|
+
# Apply attention to values: (batch, num_heads, seq_q, head_dim)
|
|
233
|
+
context = torch.matmul(attn_weights, V)
|
|
234
|
+
|
|
235
|
+
# Reshape back: (batch, num_heads, seq_q, head_dim) -> (batch, seq_q, embed_dim)
|
|
236
|
+
# Use reshape instead of .contiguous().view() to avoid a full tensor copy
|
|
237
|
+
# per candidate under vmap (N candidates x this copy = ~1.3 GB overhead).
|
|
238
|
+
context = context.transpose(1, 2).reshape(batch_size, seq_q, self.embed_dim)
|
|
239
|
+
|
|
240
|
+
# Output projection
|
|
241
|
+
output = self.W_o(context)
|
|
242
|
+
|
|
243
|
+
return output
|
polystep/layers/rnn.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""Vmap-compatible LSTM bypassing CuDNN.
|
|
2
|
+
|
|
3
|
+
Standard ``nn.LSTM`` (via ``aten::lstm.input``) has no batching rule under
|
|
4
|
+
``torch.vmap`` and fails on both CPU and CUDA -- still the case on PyTorch
|
|
5
|
+
2.12 (verified). This implementation uses explicit per-step gate
|
|
6
|
+
computations, which are vmap-safe but ~2-5x slower than CuDNN.
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> import torch
|
|
10
|
+
>>> from polystep.layers import VmapSafeLSTM
|
|
11
|
+
>>> lstm = VmapSafeLSTM(input_size=32, hidden_size=64, num_layers=2)
|
|
12
|
+
>>> x = torch.randn(4, 10, 32) # (batch, seq, input)
|
|
13
|
+
>>> out, (h_n, c_n) = lstm(x)
|
|
14
|
+
>>> out.shape
|
|
15
|
+
torch.Size([4, 10, 64])
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from typing import Optional, Tuple
|
|
19
|
+
|
|
20
|
+
import torch
|
|
21
|
+
import torch.nn as nn
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VmapSafeLSTMCell(nn.Module):
|
|
25
|
+
"""LSTM cell using explicit gate computations for vmap compatibility.
|
|
26
|
+
|
|
27
|
+
This implementation avoids CuDNN which has known issues under torch.vmap
|
|
28
|
+
with ".data access" errors. Instead, explicit gate computations are used
|
|
29
|
+
that work correctly under vectorization.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
input_size: The number of expected features in the input.
|
|
33
|
+
hidden_size: The number of features in the hidden state.
|
|
34
|
+
bias: Whether to add bias to the linear transformations. Default: True.
|
|
35
|
+
|
|
36
|
+
Input shapes:
|
|
37
|
+
- x: (batch, input_size)
|
|
38
|
+
- state: Tuple of (h, c) where both are (batch, hidden_size)
|
|
39
|
+
|
|
40
|
+
Output shapes:
|
|
41
|
+
- h_new: (batch, hidden_size) - new hidden state
|
|
42
|
+
- (h_new, c_new): Tuple of new states for compatibility with nn.LSTMCell
|
|
43
|
+
|
|
44
|
+
Example:
|
|
45
|
+
>>> cell = VmapSafeLSTMCell(input_size=32, hidden_size=64)
|
|
46
|
+
>>> x = torch.randn(4, 32)
|
|
47
|
+
>>> h = torch.zeros(4, 64)
|
|
48
|
+
>>> c = torch.zeros(4, 64)
|
|
49
|
+
>>> h_new, (h_out, c_out) = cell(x, (h, c))
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
input_size: int,
|
|
55
|
+
hidden_size: int,
|
|
56
|
+
bias: bool = True,
|
|
57
|
+
):
|
|
58
|
+
super().__init__()
|
|
59
|
+
|
|
60
|
+
self.input_size = input_size
|
|
61
|
+
self.hidden_size = hidden_size
|
|
62
|
+
|
|
63
|
+
# Combined input and hidden projections for all 4 gates
|
|
64
|
+
# Gates: input (i), forget (f), cell input (g), output (o)
|
|
65
|
+
self.W_i = nn.Linear(input_size, 4 * hidden_size, bias=bias)
|
|
66
|
+
self.W_h = nn.Linear(hidden_size, 4 * hidden_size, bias=bias)
|
|
67
|
+
|
|
68
|
+
def forward(
|
|
69
|
+
self,
|
|
70
|
+
x: torch.Tensor,
|
|
71
|
+
state: Tuple[torch.Tensor, torch.Tensor],
|
|
72
|
+
) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
|
73
|
+
"""Compute one LSTM step.
|
|
74
|
+
|
|
75
|
+
Args:
|
|
76
|
+
x: Input tensor of shape (batch, input_size).
|
|
77
|
+
state: Tuple of (h, c) hidden and cell states,
|
|
78
|
+
each of shape (batch, hidden_size).
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
Tuple of:
|
|
82
|
+
- h_new: New hidden state of shape (batch, hidden_size)
|
|
83
|
+
- (h_new, c_new): Tuple of new states
|
|
84
|
+
"""
|
|
85
|
+
h, c = state
|
|
86
|
+
|
|
87
|
+
# Compute all gates at once: (batch, 4 * hidden_size)
|
|
88
|
+
gates = self.W_i(x) + self.W_h(h)
|
|
89
|
+
|
|
90
|
+
# Split into 4 gates: each (batch, hidden_size)
|
|
91
|
+
i, f, g, o = gates.chunk(4, dim=-1)
|
|
92
|
+
|
|
93
|
+
# Apply activations
|
|
94
|
+
i = torch.sigmoid(i) # Input gate
|
|
95
|
+
f = torch.sigmoid(f) # Forget gate
|
|
96
|
+
g = torch.tanh(g) # Cell input (candidate)
|
|
97
|
+
o = torch.sigmoid(o) # Output gate
|
|
98
|
+
|
|
99
|
+
# Update cell state
|
|
100
|
+
c_new = f * c + i * g
|
|
101
|
+
|
|
102
|
+
# Compute new hidden state
|
|
103
|
+
h_new = o * torch.tanh(c_new)
|
|
104
|
+
|
|
105
|
+
return h_new, (h_new, c_new)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class VmapSafeLSTM(nn.Module):
|
|
109
|
+
"""Multi-layer LSTM using explicit gate computations for vmap compatibility.
|
|
110
|
+
|
|
111
|
+
This implementation wraps multiple VmapSafeLSTMCell layers to provide
|
|
112
|
+
a drop-in replacement for nn.LSTM that works under torch.vmap.
|
|
113
|
+
|
|
114
|
+
Limitations vs nn.LSTM:
|
|
115
|
+
- No bidirectional support
|
|
116
|
+
- No proj_size support
|
|
117
|
+
- Assumes batch-first layout: (batch, seq_len, input_size)
|
|
118
|
+
- 2-5x slower than CuDNN (explicit gate computation)
|
|
119
|
+
|
|
120
|
+
Note: Expected 2-5x slower than CuDNN but guaranteed vmap-safe.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
input_size: The number of expected features in the input.
|
|
124
|
+
hidden_size: The number of features in the hidden state.
|
|
125
|
+
num_layers: Number of recurrent layers. Default: 1.
|
|
126
|
+
bias: Whether to add bias to the linear transformations. Default: True.
|
|
127
|
+
dropout: Dropout probability between layers (not after last layer).
|
|
128
|
+
Default: 0.0.
|
|
129
|
+
|
|
130
|
+
Input shapes:
|
|
131
|
+
- x: (batch, seq_len, input_size)
|
|
132
|
+
- state: Optional tuple of (h, c) where each is (num_layers, batch, hidden_size).
|
|
133
|
+
If None, zero-initialized states are used.
|
|
134
|
+
|
|
135
|
+
Output shapes:
|
|
136
|
+
- output: (batch, seq_len, hidden_size) - output features from last layer
|
|
137
|
+
- (h_n, c_n): Final states, each (num_layers, batch, hidden_size)
|
|
138
|
+
|
|
139
|
+
Example:
|
|
140
|
+
>>> lstm = VmapSafeLSTM(input_size=32, hidden_size=64, num_layers=2)
|
|
141
|
+
>>> x = torch.randn(4, 10, 32) # (batch, seq, input)
|
|
142
|
+
>>> out, (h_n, c_n) = lstm(x)
|
|
143
|
+
>>> out.shape
|
|
144
|
+
torch.Size([4, 10, 64])
|
|
145
|
+
"""
|
|
146
|
+
|
|
147
|
+
def __init__(
|
|
148
|
+
self,
|
|
149
|
+
input_size: int,
|
|
150
|
+
hidden_size: int,
|
|
151
|
+
num_layers: int = 1,
|
|
152
|
+
bias: bool = True,
|
|
153
|
+
dropout: float = 0.0,
|
|
154
|
+
# Mirror the upstream nn.LSTM signature so that callers passing
|
|
155
|
+
# unsupported features get a clear NotImplementedError instead
|
|
156
|
+
# of a generic Python "got an unexpected keyword argument".
|
|
157
|
+
bidirectional: bool = False,
|
|
158
|
+
proj_size: int = 0,
|
|
159
|
+
batch_first: bool = True,
|
|
160
|
+
):
|
|
161
|
+
super().__init__()
|
|
162
|
+
|
|
163
|
+
if num_layers < 1:
|
|
164
|
+
raise ValueError(f"num_layers must be >= 1, got {num_layers}")
|
|
165
|
+
if bidirectional:
|
|
166
|
+
raise NotImplementedError("VmapSafeLSTM does not support bidirectional=True. See LIMITATIONS.md.")
|
|
167
|
+
if proj_size != 0:
|
|
168
|
+
raise NotImplementedError("VmapSafeLSTM does not support proj_size != 0. See LIMITATIONS.md.")
|
|
169
|
+
if not batch_first:
|
|
170
|
+
raise NotImplementedError(
|
|
171
|
+
"VmapSafeLSTM only supports batch_first=True (input layout "
|
|
172
|
+
"(batch, seq_len, input_size)). See LIMITATIONS.md."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
self.input_size = input_size
|
|
176
|
+
self.hidden_size = hidden_size
|
|
177
|
+
self.num_layers = num_layers
|
|
178
|
+
self.dropout = dropout
|
|
179
|
+
|
|
180
|
+
# Build cells for each layer
|
|
181
|
+
self.cells = nn.ModuleList()
|
|
182
|
+
for layer in range(num_layers):
|
|
183
|
+
layer_input_size = input_size if layer == 0 else hidden_size
|
|
184
|
+
self.cells.append(VmapSafeLSTMCell(layer_input_size, hidden_size, bias=bias))
|
|
185
|
+
|
|
186
|
+
# Dropout between layers (not applied after last layer)
|
|
187
|
+
self.dropout_layer = nn.Dropout(dropout) if dropout > 0 else None
|
|
188
|
+
|
|
189
|
+
def forward(
|
|
190
|
+
self,
|
|
191
|
+
x: torch.Tensor,
|
|
192
|
+
state: Optional[Tuple[torch.Tensor, torch.Tensor]] = None,
|
|
193
|
+
) -> Tuple[torch.Tensor, Tuple[torch.Tensor, torch.Tensor]]:
|
|
194
|
+
"""Process sequence through multi-layer LSTM.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
x: Input tensor of shape (batch, seq_len, input_size).
|
|
198
|
+
state: Optional tuple of (h, c) initial states, each of shape
|
|
199
|
+
(num_layers, batch, hidden_size). If None, zero states are used.
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Tuple of:
|
|
203
|
+
- output: Tensor of shape (batch, seq_len, hidden_size)
|
|
204
|
+
containing output features from the last layer
|
|
205
|
+
- (h_n, c_n): Tuple of final states, each of shape
|
|
206
|
+
(num_layers, batch, hidden_size)
|
|
207
|
+
"""
|
|
208
|
+
# PackedSequence support is not implemented. nn.LSTM accepts
|
|
209
|
+
# one, but our explicit-loop forward would silently treat it as
|
|
210
|
+
# a tensor and crash later with an opaque AttributeError. Catch
|
|
211
|
+
# it up front with a clear message.
|
|
212
|
+
if isinstance(x, nn.utils.rnn.PackedSequence):
|
|
213
|
+
raise NotImplementedError(
|
|
214
|
+
"VmapSafeLSTM does not support PackedSequence input. Pad to a dense tensor first. See LIMITATIONS.md."
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
batch_size, seq_len, _ = x.shape
|
|
218
|
+
device = x.device
|
|
219
|
+
dtype = x.dtype
|
|
220
|
+
|
|
221
|
+
# Initialize states if not provided
|
|
222
|
+
# Use list-based state tracking to avoid in-place updates under vmap
|
|
223
|
+
if state is None:
|
|
224
|
+
h_list = [
|
|
225
|
+
torch.zeros(batch_size, self.hidden_size, device=device, dtype=dtype) for _ in range(self.num_layers)
|
|
226
|
+
]
|
|
227
|
+
c_list = [
|
|
228
|
+
torch.zeros(batch_size, self.hidden_size, device=device, dtype=dtype) for _ in range(self.num_layers)
|
|
229
|
+
]
|
|
230
|
+
else:
|
|
231
|
+
h, c = state
|
|
232
|
+
# Split stacked tensor into list of per-layer states
|
|
233
|
+
h_list = [h[i] for i in range(self.num_layers)]
|
|
234
|
+
c_list = [c[i] for i in range(self.num_layers)]
|
|
235
|
+
|
|
236
|
+
# Process sequence timestep by timestep
|
|
237
|
+
outputs = []
|
|
238
|
+
for t in range(seq_len):
|
|
239
|
+
# Get input for this timestep: (batch, input_size)
|
|
240
|
+
x_t = x[:, t, :]
|
|
241
|
+
|
|
242
|
+
# Process through layers, collecting new states
|
|
243
|
+
new_h_list = []
|
|
244
|
+
new_c_list = []
|
|
245
|
+
for layer_idx, cell in enumerate(self.cells):
|
|
246
|
+
h_layer = h_list[layer_idx]
|
|
247
|
+
c_layer = c_list[layer_idx]
|
|
248
|
+
|
|
249
|
+
# Cell forward pass
|
|
250
|
+
h_new, (h_out, c_out) = cell(x_t, (h_layer, c_layer))
|
|
251
|
+
|
|
252
|
+
# Collect new states (no in-place update)
|
|
253
|
+
new_h_list.append(h_out)
|
|
254
|
+
new_c_list.append(c_out)
|
|
255
|
+
|
|
256
|
+
# Apply dropout between layers (not after last)
|
|
257
|
+
if self.dropout_layer is not None and layer_idx < self.num_layers - 1:
|
|
258
|
+
x_t = self.dropout_layer(h_new)
|
|
259
|
+
else:
|
|
260
|
+
x_t = h_new
|
|
261
|
+
|
|
262
|
+
# Update state lists for next timestep
|
|
263
|
+
h_list = new_h_list
|
|
264
|
+
c_list = new_c_list
|
|
265
|
+
|
|
266
|
+
# Collect output from last layer
|
|
267
|
+
outputs.append(x_t)
|
|
268
|
+
|
|
269
|
+
# Stack outputs: (batch, seq_len, hidden_size)
|
|
270
|
+
output = torch.stack(outputs, dim=1)
|
|
271
|
+
|
|
272
|
+
# Stack final states: (num_layers, batch, hidden_size)
|
|
273
|
+
h_n = torch.stack(h_list, dim=0)
|
|
274
|
+
c_n = torch.stack(c_list, dim=0)
|
|
275
|
+
|
|
276
|
+
return output, (h_n, c_n)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .base import ObjectiveFn
|
|
2
|
+
from .synthetic import (
|
|
3
|
+
Ackley,
|
|
4
|
+
Rosenbrock,
|
|
5
|
+
Rastrigin,
|
|
6
|
+
StyblinskiTang,
|
|
7
|
+
Levy,
|
|
8
|
+
Griewank,
|
|
9
|
+
Beale,
|
|
10
|
+
Branin,
|
|
11
|
+
Sphere,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ObjectiveFn",
|
|
16
|
+
"Ackley",
|
|
17
|
+
"Rosenbrock",
|
|
18
|
+
"Rastrigin",
|
|
19
|
+
"StyblinskiTang",
|
|
20
|
+
"Levy",
|
|
21
|
+
"Griewank",
|
|
22
|
+
"Beale",
|
|
23
|
+
"Branin",
|
|
24
|
+
"Sphere",
|
|
25
|
+
]
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Abstract base class for optimization objective functions."""
|
|
2
|
+
|
|
3
|
+
import abc
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import torch
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ObjectiveFn(abc.ABC):
|
|
10
|
+
"""Base class for optimization objectives.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
dim: Dimensionality of the problem.
|
|
14
|
+
bounds: Search space bounds of shape ``(dim, 2)`` as
|
|
15
|
+
``[(min, max), ...]``.
|
|
16
|
+
optimizers: Known global optimizer locations of shape
|
|
17
|
+
``(num_opts, dim)``.
|
|
18
|
+
optimal_value: Known global optimum value.
|
|
19
|
+
noise_std: Standard deviation of additive Gaussian observation noise.
|
|
20
|
+
``None`` or ``0`` disables noise.
|
|
21
|
+
negate: If ``True``, negate the output (turn a maximization problem
|
|
22
|
+
into a minimization problem).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
dim: int,
|
|
28
|
+
bounds: Optional[torch.Tensor] = None,
|
|
29
|
+
optimizers: Optional[torch.Tensor] = None,
|
|
30
|
+
optimal_value: Optional[float] = None,
|
|
31
|
+
noise_std: Optional[float] = None,
|
|
32
|
+
negate: bool = False,
|
|
33
|
+
):
|
|
34
|
+
self.dim = dim
|
|
35
|
+
self.bounds = bounds
|
|
36
|
+
self.optimizers = optimizers
|
|
37
|
+
self.optimal_value = optimal_value
|
|
38
|
+
self.noise_std = noise_std
|
|
39
|
+
self.negate = negate
|
|
40
|
+
|
|
41
|
+
@abc.abstractmethod
|
|
42
|
+
def evaluate(self, X: torch.Tensor) -> torch.Tensor:
|
|
43
|
+
"""Compute the raw objective value.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
X: Input array of shape ``(..., dim)``.
|
|
47
|
+
|
|
48
|
+
Returns:
|
|
49
|
+
Cost array of shape ``(...)``.
|
|
50
|
+
"""
|
|
51
|
+
pass
|
|
52
|
+
|
|
53
|
+
def __call__(
|
|
54
|
+
self,
|
|
55
|
+
X: torch.Tensor,
|
|
56
|
+
generator: Optional[torch.Generator] = None,
|
|
57
|
+
) -> torch.Tensor:
|
|
58
|
+
"""Compute the final cost, applying noise and negation if configured.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
X: Input points of shape ``(..., dim)``.
|
|
62
|
+
generator: Optional ``torch.Generator`` for reproducible noise.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
Cost values of shape ``(...)``.
|
|
66
|
+
"""
|
|
67
|
+
cost = self.evaluate(X)
|
|
68
|
+
if self.noise_std is not None and self.noise_std > 0.0:
|
|
69
|
+
noise = torch.empty_like(cost).normal_(
|
|
70
|
+
mean=0.0,
|
|
71
|
+
std=float(self.noise_std),
|
|
72
|
+
generator=generator,
|
|
73
|
+
)
|
|
74
|
+
cost = cost + noise
|
|
75
|
+
if self.negate:
|
|
76
|
+
return -cost
|
|
77
|
+
return cost
|