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,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