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
polystep/subspace.py
ADDED
|
@@ -0,0 +1,694 @@
|
|
|
1
|
+
"""Subspace compression for neural network parameters.
|
|
2
|
+
|
|
3
|
+
Reduces the OT problem from ``N`` parameters to a ``K``-dimensional
|
|
4
|
+
subspace (``K << N``) and projects back to full parameter space, dropping
|
|
5
|
+
the OT solver's memory from O(N^2) to O(K^2).
|
|
6
|
+
|
|
7
|
+
Two factorisations are provided:
|
|
8
|
+
|
|
9
|
+
- :class:`LowRankSubspace` - ``delta_W = B @ A``, bilinear in the subspace
|
|
10
|
+
coords. Near zero this bilinearity flattens the cost landscape and
|
|
11
|
+
often gives uniform OT transport; useful for scalability diagnostics.
|
|
12
|
+
- :class:`LinearSubspace` - ``delta_W = P @ coords`` with a fixed random
|
|
13
|
+
projection ``P``. Linear in the subspace coords, so probing
|
|
14
|
+
``coords + alpha * direction`` produces a proportional cost change.
|
|
15
|
+
Recommended for training.
|
|
16
|
+
|
|
17
|
+
All factors/coords are packed into a single flat subspace vector with
|
|
18
|
+
per-layer offsets. That vector *is* the particle array for the OT solver
|
|
19
|
+
(reshaped to ``(num_particles, particle_dim)``).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import math
|
|
25
|
+
import zlib
|
|
26
|
+
from dataclasses import dataclass
|
|
27
|
+
from typing import Dict, Optional, Tuple, TYPE_CHECKING
|
|
28
|
+
|
|
29
|
+
import torch
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _stable_entry_seed(*parts: object) -> int:
|
|
33
|
+
"""Hash a tuple of seed components to a 31-bit integer, deterministically
|
|
34
|
+
across processes (unlike Python's salted ``hash``).
|
|
35
|
+
"""
|
|
36
|
+
key = "|".join(str(p) for p in parts).encode("utf-8")
|
|
37
|
+
return int(zlib.adler32(key)) & 0x7FFFFFFF
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
if TYPE_CHECKING:
|
|
41
|
+
from .transform import ParamLayout
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass(frozen=True)
|
|
45
|
+
class FactorSpec:
|
|
46
|
+
"""Describes the low-rank decomposition of a single parameter entry.
|
|
47
|
+
|
|
48
|
+
Attributes:
|
|
49
|
+
entry_key: ParamLayout entry key (e.g., "fc1.weight").
|
|
50
|
+
original_shape: Original parameter shape.
|
|
51
|
+
is_lowrank: True for 2D+ params (B@A compression), False for 1D (full).
|
|
52
|
+
b_shape: Shape of B factor (d_out, r), or () for 1D params.
|
|
53
|
+
a_shape: Shape of A factor (r, d_in), or () for 1D params.
|
|
54
|
+
flat_start: Start offset into the flat subspace vector.
|
|
55
|
+
flat_end: End offset into the flat subspace vector.
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
entry_key: str
|
|
59
|
+
original_shape: Tuple[int, ...]
|
|
60
|
+
is_lowrank: bool
|
|
61
|
+
b_shape: Tuple[int, ...]
|
|
62
|
+
a_shape: Tuple[int, ...]
|
|
63
|
+
flat_start: int
|
|
64
|
+
flat_end: int
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class LowRankSubspace:
|
|
69
|
+
"""Low-rank subspace compression for neural network parameters.
|
|
70
|
+
|
|
71
|
+
Packs per-layer B and A factors into a single flat subspace vector.
|
|
72
|
+
Supports both fixed-rank and auto-rank (per-layer) modes.
|
|
73
|
+
|
|
74
|
+
Attributes:
|
|
75
|
+
specs: Per-parameter decomposition specs.
|
|
76
|
+
rank: Fixed rank used (0 for auto mode with per-layer ranks).
|
|
77
|
+
subspace_dim: Total flat subspace vector size (sum of all factor elements).
|
|
78
|
+
compression_ratio: subspace_dim / total_params for diagnostics.
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
specs: Tuple[FactorSpec, ...]
|
|
82
|
+
rank: int
|
|
83
|
+
subspace_dim: int
|
|
84
|
+
compression_ratio: float
|
|
85
|
+
|
|
86
|
+
# ------------------------------------------------------------------
|
|
87
|
+
# Factory methods
|
|
88
|
+
# ------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_layout(cls, layout: ParamLayout, rank: int) -> LowRankSubspace:
|
|
92
|
+
"""Create a LowRankSubspace with fixed rank from a ParamLayout.
|
|
93
|
+
|
|
94
|
+
For each 2D+ parameter entry, computes effective_rank = min(rank, d_in, d_out)
|
|
95
|
+
and creates B=(d_out, effective_rank), A=(effective_rank, d_in) factors.
|
|
96
|
+
1D parameters are stored as full perturbations (no compression).
|
|
97
|
+
|
|
98
|
+
Args:
|
|
99
|
+
layout: ParamLayout describing the model's parameter structure.
|
|
100
|
+
rank: Fixed rank for all low-rank decompositions.
|
|
101
|
+
|
|
102
|
+
Returns:
|
|
103
|
+
LowRankSubspace with per-layer FactorSpecs and flat offsets.
|
|
104
|
+
"""
|
|
105
|
+
specs = []
|
|
106
|
+
offset = 0
|
|
107
|
+
|
|
108
|
+
for entry in layout.entries:
|
|
109
|
+
shape = entry.shape
|
|
110
|
+
if len(shape) >= 2:
|
|
111
|
+
# 2D+ param: low-rank B@A decomposition
|
|
112
|
+
# Conv: (C_out, C_in, H, W) -> d_out=C_out, d_in=C_in*H*W
|
|
113
|
+
d_out = shape[0]
|
|
114
|
+
d_in = math.prod(shape[1:])
|
|
115
|
+
effective_rank = min(rank, d_in, d_out)
|
|
116
|
+
b_shape = (d_out, effective_rank)
|
|
117
|
+
a_shape = (effective_rank, d_in)
|
|
118
|
+
num_elements = d_out * effective_rank + effective_rank * d_in
|
|
119
|
+
specs.append(
|
|
120
|
+
FactorSpec(
|
|
121
|
+
entry_key=entry.key,
|
|
122
|
+
original_shape=shape,
|
|
123
|
+
is_lowrank=True,
|
|
124
|
+
b_shape=b_shape,
|
|
125
|
+
a_shape=a_shape,
|
|
126
|
+
flat_start=offset,
|
|
127
|
+
flat_end=offset + num_elements,
|
|
128
|
+
)
|
|
129
|
+
)
|
|
130
|
+
offset += num_elements
|
|
131
|
+
else:
|
|
132
|
+
# 1D param (bias, LayerNorm): full perturbation
|
|
133
|
+
num_elements = entry.numel
|
|
134
|
+
specs.append(
|
|
135
|
+
FactorSpec(
|
|
136
|
+
entry_key=entry.key,
|
|
137
|
+
original_shape=shape,
|
|
138
|
+
is_lowrank=False,
|
|
139
|
+
b_shape=(),
|
|
140
|
+
a_shape=(),
|
|
141
|
+
flat_start=offset,
|
|
142
|
+
flat_end=offset + num_elements,
|
|
143
|
+
)
|
|
144
|
+
)
|
|
145
|
+
offset += num_elements
|
|
146
|
+
|
|
147
|
+
total_params = layout.total_params
|
|
148
|
+
compression = offset / total_params if total_params > 0 else 0.0
|
|
149
|
+
|
|
150
|
+
return cls(
|
|
151
|
+
specs=tuple(specs),
|
|
152
|
+
rank=rank,
|
|
153
|
+
subspace_dim=offset,
|
|
154
|
+
compression_ratio=compression,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
@classmethod
|
|
158
|
+
def auto_from_layout(
|
|
159
|
+
cls,
|
|
160
|
+
layout: ParamLayout,
|
|
161
|
+
compression_ratio: int = 16,
|
|
162
|
+
min_rank: int = 4,
|
|
163
|
+
max_rank: int = 64,
|
|
164
|
+
) -> LowRankSubspace:
|
|
165
|
+
"""Create a LowRankSubspace with auto per-layer rank selection.
|
|
166
|
+
|
|
167
|
+
Computes per-layer rank proportional to min(d_in, d_out) without
|
|
168
|
+
user tuning: rank = max(min_rank, min(max_rank, min(d_in, d_out) // compression_ratio)).
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
layout: ParamLayout describing the model's parameter structure.
|
|
172
|
+
compression_ratio: Divisor for rank computation (default 16).
|
|
173
|
+
min_rank: Minimum rank per layer (default 4).
|
|
174
|
+
max_rank: Maximum rank per layer (default 64).
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
LowRankSubspace with per-layer auto-selected ranks (rank=0).
|
|
178
|
+
"""
|
|
179
|
+
specs = []
|
|
180
|
+
offset = 0
|
|
181
|
+
|
|
182
|
+
for entry in layout.entries:
|
|
183
|
+
shape = entry.shape
|
|
184
|
+
if len(shape) >= 2:
|
|
185
|
+
d_out = shape[0]
|
|
186
|
+
d_in = math.prod(shape[1:])
|
|
187
|
+
min_dim = min(d_in, d_out)
|
|
188
|
+
auto_rank = max(min_rank, min(max_rank, min_dim // compression_ratio))
|
|
189
|
+
effective_rank = min(auto_rank, d_in, d_out)
|
|
190
|
+
b_shape = (d_out, effective_rank)
|
|
191
|
+
a_shape = (effective_rank, d_in)
|
|
192
|
+
num_elements = d_out * effective_rank + effective_rank * d_in
|
|
193
|
+
specs.append(
|
|
194
|
+
FactorSpec(
|
|
195
|
+
entry_key=entry.key,
|
|
196
|
+
original_shape=shape,
|
|
197
|
+
is_lowrank=True,
|
|
198
|
+
b_shape=b_shape,
|
|
199
|
+
a_shape=a_shape,
|
|
200
|
+
flat_start=offset,
|
|
201
|
+
flat_end=offset + num_elements,
|
|
202
|
+
)
|
|
203
|
+
)
|
|
204
|
+
offset += num_elements
|
|
205
|
+
else:
|
|
206
|
+
num_elements = entry.numel
|
|
207
|
+
specs.append(
|
|
208
|
+
FactorSpec(
|
|
209
|
+
entry_key=entry.key,
|
|
210
|
+
original_shape=shape,
|
|
211
|
+
is_lowrank=False,
|
|
212
|
+
b_shape=(),
|
|
213
|
+
a_shape=(),
|
|
214
|
+
flat_start=offset,
|
|
215
|
+
flat_end=offset + num_elements,
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
offset += num_elements
|
|
219
|
+
|
|
220
|
+
total_params = layout.total_params
|
|
221
|
+
compression = offset / total_params if total_params > 0 else 0.0
|
|
222
|
+
|
|
223
|
+
return cls(
|
|
224
|
+
specs=tuple(specs),
|
|
225
|
+
rank=0, # 0 signals auto mode
|
|
226
|
+
subspace_dim=offset,
|
|
227
|
+
compression_ratio=compression,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
# ------------------------------------------------------------------
|
|
231
|
+
# Core methods
|
|
232
|
+
# ------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
def apply_perturbation(
|
|
235
|
+
self,
|
|
236
|
+
base_sd: Dict[str, torch.Tensor],
|
|
237
|
+
flat_subspace: torch.Tensor,
|
|
238
|
+
) -> Dict[str, torch.Tensor]:
|
|
239
|
+
"""Reconstruct full state_dict from base params + flat subspace vector.
|
|
240
|
+
|
|
241
|
+
For low-rank entries, slices out B and A factors, computes delta = B @ A,
|
|
242
|
+
reshapes to original shape, and adds to base. For 1D entries, adds chunk
|
|
243
|
+
directly.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
base_sd: Base state_dict with original parameter values.
|
|
247
|
+
flat_subspace: 1D tensor of shape (subspace_dim,).
|
|
248
|
+
|
|
249
|
+
Returns:
|
|
250
|
+
New state_dict with perturbed parameters.
|
|
251
|
+
"""
|
|
252
|
+
result = {}
|
|
253
|
+
for spec in self.specs:
|
|
254
|
+
chunk = flat_subspace[spec.flat_start : spec.flat_end]
|
|
255
|
+
base = base_sd[spec.entry_key]
|
|
256
|
+
if spec.is_lowrank:
|
|
257
|
+
b_size = spec.b_shape[0] * spec.b_shape[1]
|
|
258
|
+
B = chunk[:b_size].reshape(spec.b_shape)
|
|
259
|
+
A = chunk[b_size:].reshape(spec.a_shape)
|
|
260
|
+
delta = (B @ A).reshape(spec.original_shape)
|
|
261
|
+
result[spec.entry_key] = base + delta
|
|
262
|
+
else:
|
|
263
|
+
result[spec.entry_key] = base + chunk.reshape(spec.original_shape)
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
def reconstruct_batch(
|
|
267
|
+
self,
|
|
268
|
+
base_sd: Dict[str, torch.Tensor],
|
|
269
|
+
flat_subspace_batch: torch.Tensor,
|
|
270
|
+
) -> Dict[str, torch.Tensor]:
|
|
271
|
+
"""Vectorized reconstruction for N probe points using torch.bmm.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
base_sd: Base state_dict with original parameter values.
|
|
275
|
+
flat_subspace_batch: 2D tensor of shape (N, subspace_dim).
|
|
276
|
+
|
|
277
|
+
Returns:
|
|
278
|
+
Dict ``{key: (N, *original_shape)}`` with batched perturbed params.
|
|
279
|
+
"""
|
|
280
|
+
N = flat_subspace_batch.shape[0]
|
|
281
|
+
result = {}
|
|
282
|
+
for spec in self.specs:
|
|
283
|
+
chunk = flat_subspace_batch[:, spec.flat_start : spec.flat_end]
|
|
284
|
+
base = base_sd[spec.entry_key]
|
|
285
|
+
if spec.is_lowrank:
|
|
286
|
+
b_size = spec.b_shape[0] * spec.b_shape[1]
|
|
287
|
+
B = chunk[:, :b_size].reshape(N, *spec.b_shape)
|
|
288
|
+
A = chunk[:, b_size:].reshape(N, *spec.a_shape)
|
|
289
|
+
delta_2d = torch.bmm(B, A) # (N, d_out, d_in)
|
|
290
|
+
delta = delta_2d.reshape(N, *spec.original_shape)
|
|
291
|
+
result[spec.entry_key] = base.unsqueeze(0) + delta
|
|
292
|
+
else:
|
|
293
|
+
delta = chunk.reshape(N, *spec.original_shape)
|
|
294
|
+
result[spec.entry_key] = base.unsqueeze(0) + delta
|
|
295
|
+
return result
|
|
296
|
+
|
|
297
|
+
def absorb(
|
|
298
|
+
self,
|
|
299
|
+
base_sd: Dict[str, torch.Tensor],
|
|
300
|
+
flat_subspace: torch.Tensor,
|
|
301
|
+
) -> Tuple[Dict[str, torch.Tensor], torch.Tensor]:
|
|
302
|
+
"""Fold subspace perturbation into base weights and zero the subspace.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
base_sd: Base state_dict to absorb perturbation into.
|
|
306
|
+
flat_subspace: Current subspace vector of shape (subspace_dim,).
|
|
307
|
+
|
|
308
|
+
Returns:
|
|
309
|
+
Tuple of (new_base_sd, zeroed_subspace_vector).
|
|
310
|
+
"""
|
|
311
|
+
new_sd = self.apply_perturbation(base_sd, flat_subspace)
|
|
312
|
+
return new_sd, torch.zeros_like(flat_subspace)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
# ======================================================================
|
|
316
|
+
# LinearSubspace: random projection (linear mapping)
|
|
317
|
+
# ======================================================================
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
@dataclass(frozen=True)
|
|
321
|
+
class ProjectionSpec:
|
|
322
|
+
"""Describes the random projection mapping for a single parameter entry.
|
|
323
|
+
|
|
324
|
+
Attributes:
|
|
325
|
+
entry_key: ParamLayout entry key (e.g., "fc1.weight").
|
|
326
|
+
original_shape: Original parameter shape.
|
|
327
|
+
is_projected: True for 2D+ params (random projection), False for 1D (full).
|
|
328
|
+
num_params: Total elements in this parameter (d_out * d_in).
|
|
329
|
+
num_coords: Number of subspace coordinates allocated.
|
|
330
|
+
flat_start: Start offset into the flat subspace vector.
|
|
331
|
+
flat_end: End offset into the flat subspace vector.
|
|
332
|
+
"""
|
|
333
|
+
|
|
334
|
+
entry_key: str
|
|
335
|
+
original_shape: Tuple[int, ...]
|
|
336
|
+
is_projected: bool
|
|
337
|
+
num_params: int
|
|
338
|
+
num_coords: int
|
|
339
|
+
flat_start: int
|
|
340
|
+
flat_end: int
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
class LinearSubspace:
|
|
344
|
+
"""Linear subspace compression via fixed random projection matrices.
|
|
345
|
+
|
|
346
|
+
For each 2D+ parameter, weight perturbation is:
|
|
347
|
+
delta_W = (P @ coords).reshape(original_shape)
|
|
348
|
+
where P is a fixed random projection matrix of shape (num_params, num_coords).
|
|
349
|
+
|
|
350
|
+
Key property: This is LINEAR in coords. When the OT solver probes at
|
|
351
|
+
coords + alpha * direction, the weight change is P @ (coords + alpha * direction)
|
|
352
|
+
= P @ coords + alpha * (P @ direction), which is proportional. The OT solver
|
|
353
|
+
sees meaningful cost differences, enabling convergence.
|
|
354
|
+
|
|
355
|
+
Unlike LowRankSubspace (B@A, bilinear), this avoids the near-zero gradient
|
|
356
|
+
problem that causes uniform OT transport.
|
|
357
|
+
|
|
358
|
+
Note on radius scaling: LinearSubspace requires larger ``step_radius``
|
|
359
|
+
(typically 30x the default) because the random projection matrix has
|
|
360
|
+
1/sqrt(N) scaling, which dilutes the perturbation magnitude in full
|
|
361
|
+
parameter space.
|
|
362
|
+
|
|
363
|
+
Example::
|
|
364
|
+
|
|
365
|
+
from polystep import LinearSubspace, PolyStepOptimizer
|
|
366
|
+
|
|
367
|
+
subspace = LinearSubspace.from_layout(optimizer.layout, rank=8)
|
|
368
|
+
optimizer = PolyStepOptimizer(
|
|
369
|
+
model, subspace=subspace, step_radius=4.5,
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
See Also:
|
|
373
|
+
``LowRankSubspace`` for bilinear B@A compression (scalability only).
|
|
374
|
+
``PolyStepOptimizer`` for subspace integration.
|
|
375
|
+
|
|
376
|
+
Attributes:
|
|
377
|
+
specs: Per-parameter projection specs.
|
|
378
|
+
subspace_dim: Total flat subspace vector size.
|
|
379
|
+
compression_ratio: subspace_dim / total_params for diagnostics.
|
|
380
|
+
seed: Seed for deterministic projection matrix generation.
|
|
381
|
+
"""
|
|
382
|
+
|
|
383
|
+
def __init__(
|
|
384
|
+
self,
|
|
385
|
+
specs: Tuple[ProjectionSpec, ...],
|
|
386
|
+
subspace_dim: int,
|
|
387
|
+
compression_ratio: float,
|
|
388
|
+
seed: int = 0,
|
|
389
|
+
) -> None:
|
|
390
|
+
self.specs = specs
|
|
391
|
+
self.subspace_dim = subspace_dim
|
|
392
|
+
self.compression_ratio = compression_ratio
|
|
393
|
+
self.seed = seed
|
|
394
|
+
self._projections: Dict[str, torch.Tensor] = {}
|
|
395
|
+
|
|
396
|
+
# ------------------------------------------------------------------
|
|
397
|
+
# Factory methods
|
|
398
|
+
# ------------------------------------------------------------------
|
|
399
|
+
|
|
400
|
+
@classmethod
|
|
401
|
+
def from_layout(
|
|
402
|
+
cls,
|
|
403
|
+
layout: ParamLayout,
|
|
404
|
+
rank: int,
|
|
405
|
+
seed: int = 0,
|
|
406
|
+
max_subspace_dim: Optional[int] = None,
|
|
407
|
+
) -> LinearSubspace:
|
|
408
|
+
"""Create a LinearSubspace with fixed rank from a ParamLayout.
|
|
409
|
+
|
|
410
|
+
Uses the SAME formula as LowRankSubspace for num_coords per layer:
|
|
411
|
+
num_coords = d_out * effective_rank + effective_rank * d_in.
|
|
412
|
+
This makes subspace_dim identical, so switching between the two
|
|
413
|
+
is a drop-in replacement.
|
|
414
|
+
|
|
415
|
+
Args:
|
|
416
|
+
layout: ParamLayout describing the model's parameter structure.
|
|
417
|
+
rank: Rank parameter (controls compression level).
|
|
418
|
+
seed: Seed for deterministic projection matrices.
|
|
419
|
+
max_subspace_dim: Optional cap on total subspace dimension.
|
|
420
|
+
|
|
421
|
+
Returns:
|
|
422
|
+
LinearSubspace with per-layer ProjectionSpecs and flat offsets.
|
|
423
|
+
"""
|
|
424
|
+
specs = []
|
|
425
|
+
offset = 0
|
|
426
|
+
|
|
427
|
+
for entry in layout.entries:
|
|
428
|
+
shape = entry.shape
|
|
429
|
+
if len(shape) >= 2:
|
|
430
|
+
d_out = shape[0]
|
|
431
|
+
d_in = math.prod(shape[1:])
|
|
432
|
+
effective_rank = min(rank, d_in, d_out)
|
|
433
|
+
# Same formula as LowRankSubspace for drop-in compatibility
|
|
434
|
+
num_coords = d_out * effective_rank + effective_rank * d_in
|
|
435
|
+
num_params = math.prod(shape)
|
|
436
|
+
specs.append(
|
|
437
|
+
ProjectionSpec(
|
|
438
|
+
entry_key=entry.key,
|
|
439
|
+
original_shape=shape,
|
|
440
|
+
is_projected=True,
|
|
441
|
+
num_params=num_params,
|
|
442
|
+
num_coords=num_coords,
|
|
443
|
+
flat_start=offset,
|
|
444
|
+
flat_end=offset + num_coords,
|
|
445
|
+
)
|
|
446
|
+
)
|
|
447
|
+
offset += num_coords
|
|
448
|
+
else:
|
|
449
|
+
num_elements = entry.numel
|
|
450
|
+
specs.append(
|
|
451
|
+
ProjectionSpec(
|
|
452
|
+
entry_key=entry.key,
|
|
453
|
+
original_shape=shape,
|
|
454
|
+
is_projected=False,
|
|
455
|
+
num_params=num_elements,
|
|
456
|
+
num_coords=num_elements,
|
|
457
|
+
flat_start=offset,
|
|
458
|
+
flat_end=offset + num_elements,
|
|
459
|
+
)
|
|
460
|
+
)
|
|
461
|
+
offset += num_elements
|
|
462
|
+
|
|
463
|
+
# Apply budget cap if specified
|
|
464
|
+
from .hybrid_subspace import _scale_specs_to_budget
|
|
465
|
+
|
|
466
|
+
specs, offset = _scale_specs_to_budget(specs, max_subspace_dim, offset)
|
|
467
|
+
|
|
468
|
+
total_params = layout.total_params
|
|
469
|
+
compression = offset / total_params if total_params > 0 else 0.0
|
|
470
|
+
|
|
471
|
+
inst = cls(
|
|
472
|
+
specs=tuple(specs),
|
|
473
|
+
subspace_dim=offset,
|
|
474
|
+
compression_ratio=compression,
|
|
475
|
+
seed=seed,
|
|
476
|
+
)
|
|
477
|
+
inst._max_subspace_dim = max_subspace_dim
|
|
478
|
+
return inst
|
|
479
|
+
|
|
480
|
+
@classmethod
|
|
481
|
+
def auto_from_layout(
|
|
482
|
+
cls,
|
|
483
|
+
layout: ParamLayout,
|
|
484
|
+
compression_ratio: int = 16,
|
|
485
|
+
min_rank: int = 4,
|
|
486
|
+
max_rank: int = 64,
|
|
487
|
+
seed: int = 0,
|
|
488
|
+
max_subspace_dim: Optional[int] = None,
|
|
489
|
+
) -> LinearSubspace:
|
|
490
|
+
"""Create a LinearSubspace with auto per-layer rank selection.
|
|
491
|
+
|
|
492
|
+
Same auto-rank logic as LowRankSubspace: per-layer rank proportional
|
|
493
|
+
to min(d_in, d_out) without user tuning.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
layout: ParamLayout describing the model's parameter structure.
|
|
497
|
+
compression_ratio: Divisor for rank computation (default 16).
|
|
498
|
+
min_rank: Minimum rank per layer (default 4).
|
|
499
|
+
max_rank: Maximum rank per layer (default 64).
|
|
500
|
+
seed: Seed for deterministic projection matrices.
|
|
501
|
+
|
|
502
|
+
Returns:
|
|
503
|
+
LinearSubspace with per-layer auto-selected coordinate counts.
|
|
504
|
+
"""
|
|
505
|
+
specs = []
|
|
506
|
+
offset = 0
|
|
507
|
+
|
|
508
|
+
for entry in layout.entries:
|
|
509
|
+
shape = entry.shape
|
|
510
|
+
if len(shape) >= 2:
|
|
511
|
+
d_out = shape[0]
|
|
512
|
+
d_in = math.prod(shape[1:])
|
|
513
|
+
min_dim = min(d_in, d_out)
|
|
514
|
+
auto_rank = max(min_rank, min(max_rank, min_dim // compression_ratio))
|
|
515
|
+
effective_rank = min(auto_rank, d_in, d_out)
|
|
516
|
+
num_coords = d_out * effective_rank + effective_rank * d_in
|
|
517
|
+
num_params = math.prod(shape)
|
|
518
|
+
specs.append(
|
|
519
|
+
ProjectionSpec(
|
|
520
|
+
entry_key=entry.key,
|
|
521
|
+
original_shape=shape,
|
|
522
|
+
is_projected=True,
|
|
523
|
+
num_params=num_params,
|
|
524
|
+
num_coords=num_coords,
|
|
525
|
+
flat_start=offset,
|
|
526
|
+
flat_end=offset + num_coords,
|
|
527
|
+
)
|
|
528
|
+
)
|
|
529
|
+
offset += num_coords
|
|
530
|
+
else:
|
|
531
|
+
num_elements = entry.numel
|
|
532
|
+
specs.append(
|
|
533
|
+
ProjectionSpec(
|
|
534
|
+
entry_key=entry.key,
|
|
535
|
+
original_shape=shape,
|
|
536
|
+
is_projected=False,
|
|
537
|
+
num_params=num_elements,
|
|
538
|
+
num_coords=num_elements,
|
|
539
|
+
flat_start=offset,
|
|
540
|
+
flat_end=offset + num_elements,
|
|
541
|
+
)
|
|
542
|
+
)
|
|
543
|
+
offset += num_elements
|
|
544
|
+
|
|
545
|
+
# Apply budget cap if specified
|
|
546
|
+
from .hybrid_subspace import _scale_specs_to_budget
|
|
547
|
+
|
|
548
|
+
specs, offset = _scale_specs_to_budget(specs, max_subspace_dim, offset)
|
|
549
|
+
|
|
550
|
+
total_params = layout.total_params
|
|
551
|
+
compression = offset / total_params if total_params > 0 else 0.0
|
|
552
|
+
|
|
553
|
+
inst = cls(
|
|
554
|
+
specs=tuple(specs),
|
|
555
|
+
subspace_dim=offset,
|
|
556
|
+
compression_ratio=compression,
|
|
557
|
+
seed=seed,
|
|
558
|
+
)
|
|
559
|
+
inst._max_subspace_dim = max_subspace_dim
|
|
560
|
+
return inst
|
|
561
|
+
|
|
562
|
+
# ------------------------------------------------------------------
|
|
563
|
+
# Projection matrix generation
|
|
564
|
+
# ------------------------------------------------------------------
|
|
565
|
+
|
|
566
|
+
def _get_projection(
|
|
567
|
+
self,
|
|
568
|
+
spec: ProjectionSpec,
|
|
569
|
+
device: torch.device,
|
|
570
|
+
dtype: torch.dtype,
|
|
571
|
+
) -> torch.Tensor:
|
|
572
|
+
"""Get or generate the projection matrix for a parameter entry.
|
|
573
|
+
|
|
574
|
+
Generates P of shape (num_params, num_coords) deterministically from
|
|
575
|
+
self.seed and spec.entry_key. Columns are scaled by 1/sqrt(num_coords)
|
|
576
|
+
for unit-variance output (standard random projection scaling).
|
|
577
|
+
|
|
578
|
+
Args:
|
|
579
|
+
spec: ProjectionSpec for this parameter.
|
|
580
|
+
device: Target device.
|
|
581
|
+
dtype: Target dtype.
|
|
582
|
+
|
|
583
|
+
Returns:
|
|
584
|
+
Projection matrix P of shape (num_params, num_coords).
|
|
585
|
+
"""
|
|
586
|
+
cache_key = (spec.entry_key, str(device), str(dtype))
|
|
587
|
+
if cache_key in self._projections:
|
|
588
|
+
cached = self._projections[cache_key]
|
|
589
|
+
if cached.device == device and cached.dtype == dtype:
|
|
590
|
+
return cached
|
|
591
|
+
|
|
592
|
+
# Deterministic seed from global seed + entry key
|
|
593
|
+
entry_seed = _stable_entry_seed(self.seed, spec.entry_key)
|
|
594
|
+
gen = torch.Generator(device="cpu")
|
|
595
|
+
gen.manual_seed(entry_seed)
|
|
596
|
+
|
|
597
|
+
# Generate on CPU then move (Generator doesn't support CUDA)
|
|
598
|
+
P = torch.randn(
|
|
599
|
+
spec.num_params,
|
|
600
|
+
spec.num_coords,
|
|
601
|
+
generator=gen,
|
|
602
|
+
dtype=dtype,
|
|
603
|
+
device="cpu",
|
|
604
|
+
)
|
|
605
|
+
# Scale for unit-variance output
|
|
606
|
+
P = P * (1.0 / math.sqrt(spec.num_coords))
|
|
607
|
+
P = P.to(device=device)
|
|
608
|
+
|
|
609
|
+
self._projections[cache_key] = P
|
|
610
|
+
return P
|
|
611
|
+
|
|
612
|
+
# ------------------------------------------------------------------
|
|
613
|
+
# Core methods
|
|
614
|
+
# ------------------------------------------------------------------
|
|
615
|
+
|
|
616
|
+
def apply_perturbation(
|
|
617
|
+
self,
|
|
618
|
+
base_sd: Dict[str, torch.Tensor],
|
|
619
|
+
flat_subspace: torch.Tensor,
|
|
620
|
+
) -> Dict[str, torch.Tensor]:
|
|
621
|
+
"""Reconstruct full state_dict from base params + flat subspace vector.
|
|
622
|
+
|
|
623
|
+
For projected entries, slices coords, matrix-multiplies with projection P,
|
|
624
|
+
reshapes to original shape, and adds to base. For 1D entries, adds directly.
|
|
625
|
+
|
|
626
|
+
Args:
|
|
627
|
+
base_sd: Base state_dict with original parameter values.
|
|
628
|
+
flat_subspace: 1D tensor of shape (subspace_dim,).
|
|
629
|
+
|
|
630
|
+
Returns:
|
|
631
|
+
New state_dict with perturbed parameters.
|
|
632
|
+
"""
|
|
633
|
+
result = {}
|
|
634
|
+
for spec in self.specs:
|
|
635
|
+
chunk = flat_subspace[spec.flat_start : spec.flat_end]
|
|
636
|
+
base = base_sd[spec.entry_key]
|
|
637
|
+
if spec.is_projected:
|
|
638
|
+
P = self._get_projection(spec, base.device, base.dtype)
|
|
639
|
+
delta = (P @ chunk).reshape(spec.original_shape)
|
|
640
|
+
result[spec.entry_key] = base + delta
|
|
641
|
+
else:
|
|
642
|
+
result[spec.entry_key] = base + chunk.reshape(spec.original_shape)
|
|
643
|
+
return result
|
|
644
|
+
|
|
645
|
+
def reconstruct_batch(
|
|
646
|
+
self,
|
|
647
|
+
base_sd: Dict[str, torch.Tensor],
|
|
648
|
+
flat_subspace_batch: torch.Tensor,
|
|
649
|
+
) -> Dict[str, torch.Tensor]:
|
|
650
|
+
"""Vectorized reconstruction for N probe points.
|
|
651
|
+
|
|
652
|
+
For projected entries: coords @ P.T gives (N, num_params), then reshape.
|
|
653
|
+
This avoids bmm -- simple matmul.
|
|
654
|
+
|
|
655
|
+
Args:
|
|
656
|
+
base_sd: Base state_dict with original parameter values.
|
|
657
|
+
flat_subspace_batch: 2D tensor of shape (N, subspace_dim).
|
|
658
|
+
|
|
659
|
+
Returns:
|
|
660
|
+
Dict ``{key: (N, *original_shape)}`` with batched perturbed params.
|
|
661
|
+
"""
|
|
662
|
+
N = flat_subspace_batch.shape[0]
|
|
663
|
+
result = {}
|
|
664
|
+
for spec in self.specs:
|
|
665
|
+
chunk = flat_subspace_batch[:, spec.flat_start : spec.flat_end]
|
|
666
|
+
base = base_sd[spec.entry_key]
|
|
667
|
+
if spec.is_projected:
|
|
668
|
+
P = self._get_projection(spec, base.device, base.dtype)
|
|
669
|
+
# chunk: (N, num_coords), P: (num_params, num_coords)
|
|
670
|
+
# delta_flat = chunk @ P.T -> (N, num_params)
|
|
671
|
+
delta_flat = chunk @ P.t()
|
|
672
|
+
delta = delta_flat.reshape(N, *spec.original_shape)
|
|
673
|
+
result[spec.entry_key] = base.unsqueeze(0) + delta
|
|
674
|
+
else:
|
|
675
|
+
delta = chunk.reshape(N, *spec.original_shape)
|
|
676
|
+
result[spec.entry_key] = base.unsqueeze(0) + delta
|
|
677
|
+
return result
|
|
678
|
+
|
|
679
|
+
def absorb(
|
|
680
|
+
self,
|
|
681
|
+
base_sd: Dict[str, torch.Tensor],
|
|
682
|
+
flat_subspace: torch.Tensor,
|
|
683
|
+
) -> Tuple[Dict[str, torch.Tensor], torch.Tensor]:
|
|
684
|
+
"""Fold subspace perturbation into base weights and zero the subspace.
|
|
685
|
+
|
|
686
|
+
Args:
|
|
687
|
+
base_sd: Base state_dict to absorb perturbation into.
|
|
688
|
+
flat_subspace: Current subspace vector of shape (subspace_dim,).
|
|
689
|
+
|
|
690
|
+
Returns:
|
|
691
|
+
Tuple of (new_base_sd, zeroed_subspace_vector).
|
|
692
|
+
"""
|
|
693
|
+
new_sd = self.apply_perturbation(base_sd, flat_subspace)
|
|
694
|
+
return new_sd, torch.zeros_like(flat_subspace)
|