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/blockwise.py
ADDED
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
"""Block-wise Sinkhorn decomposition for scaling to large parameter spaces.
|
|
2
|
+
|
|
3
|
+
Instead of solving a single OT problem over all particles, decomposes into
|
|
4
|
+
independent smaller OT problems per block (e.g. per layer). Each block has
|
|
5
|
+
its own polytope, cost matrix, and OT solve.
|
|
6
|
+
|
|
7
|
+
Sinkhorn is O(n^2) in particle count. Splitting M particles into L blocks
|
|
8
|
+
of M/L reduces total cost from O(M^2) to O(L * (M/L)^2) = O(M^2/L).
|
|
9
|
+
|
|
10
|
+
Usage::
|
|
11
|
+
|
|
12
|
+
optimizer = PolyStepOptimizer(model, block_strategy='per_layer')
|
|
13
|
+
|
|
14
|
+
See Also:
|
|
15
|
+
``PolyStepOptimizer`` for the ``block_strategy`` parameter.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import warnings
|
|
21
|
+
from dataclasses import dataclass
|
|
22
|
+
from typing import List, Optional, Tuple, TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
import torch
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING:
|
|
27
|
+
from .transform import ParamLayout
|
|
28
|
+
from .cost_nn import NNCostEvaluator
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class BlockConfig:
|
|
33
|
+
"""Configuration for a single block in block-wise Sinkhorn.
|
|
34
|
+
|
|
35
|
+
Attributes:
|
|
36
|
+
name: Human-readable name for the block (e.g. 'fc1.weight').
|
|
37
|
+
leaf_indices: Indices into ParamLayout.entries belonging to this block.
|
|
38
|
+
flat_start: Start offset in the block-wise flat particle vector.
|
|
39
|
+
flat_end: End offset in the block-wise flat particle vector.
|
|
40
|
+
num_particles: Number of particle rows in this block.
|
|
41
|
+
particle_dim: Dimension of each particle in this block.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
name: str
|
|
45
|
+
leaf_indices: Tuple[int, ...]
|
|
46
|
+
flat_start: int
|
|
47
|
+
flat_end: int
|
|
48
|
+
num_particles: int
|
|
49
|
+
particle_dim: int
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def create_per_layer_blocks(
|
|
53
|
+
layout: ParamLayout,
|
|
54
|
+
particle_dim: int = 2,
|
|
55
|
+
) -> List[BlockConfig]:
|
|
56
|
+
"""Create one block per parameter entry (layer) in the layout.
|
|
57
|
+
|
|
58
|
+
Each block computes its own padding independently so that
|
|
59
|
+
``num_params`` is divisible by ``particle_dim``.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
layout: ParamLayout describing model parameter structure.
|
|
63
|
+
particle_dim: Number of elements per particle row.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
List of BlockConfig, one per layout entry.
|
|
67
|
+
"""
|
|
68
|
+
blocks: List[BlockConfig] = []
|
|
69
|
+
offset = 0
|
|
70
|
+
|
|
71
|
+
for i, entry in enumerate(layout.entries):
|
|
72
|
+
num_params = entry.numel
|
|
73
|
+
padded = num_params + (-num_params % particle_dim)
|
|
74
|
+
num_particles = padded // particle_dim
|
|
75
|
+
blocks.append(
|
|
76
|
+
BlockConfig(
|
|
77
|
+
name=entry.key,
|
|
78
|
+
leaf_indices=(i,),
|
|
79
|
+
flat_start=offset,
|
|
80
|
+
flat_end=offset + padded,
|
|
81
|
+
num_particles=num_particles,
|
|
82
|
+
particle_dim=particle_dim,
|
|
83
|
+
)
|
|
84
|
+
)
|
|
85
|
+
offset += padded
|
|
86
|
+
|
|
87
|
+
return blocks
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def create_grouped_blocks(
|
|
91
|
+
layout: ParamLayout,
|
|
92
|
+
group_size: int = 2,
|
|
93
|
+
particle_dim: int = 2,
|
|
94
|
+
) -> List[BlockConfig]:
|
|
95
|
+
"""Create blocks by grouping consecutive layout entries.
|
|
96
|
+
|
|
97
|
+
Bundles consecutive entries (e.g. weight+bias pairs) into blocks.
|
|
98
|
+
Each group's total element count is padded independently.
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
layout: ParamLayout describing model parameter structure.
|
|
102
|
+
group_size: Number of consecutive entries per block.
|
|
103
|
+
particle_dim: Number of elements per particle row.
|
|
104
|
+
|
|
105
|
+
Returns:
|
|
106
|
+
List of BlockConfig.
|
|
107
|
+
"""
|
|
108
|
+
blocks: List[BlockConfig] = []
|
|
109
|
+
offset = 0
|
|
110
|
+
entries = layout.entries
|
|
111
|
+
|
|
112
|
+
for g_start in range(0, len(entries), group_size):
|
|
113
|
+
g_end = min(g_start + group_size, len(entries))
|
|
114
|
+
group_entries = entries[g_start:g_end]
|
|
115
|
+
leaf_indices = tuple(range(g_start, g_end))
|
|
116
|
+
num_params = sum(e.numel for e in group_entries)
|
|
117
|
+
padded = num_params + (-num_params % particle_dim)
|
|
118
|
+
num_particles = padded // particle_dim
|
|
119
|
+
blocks.append(
|
|
120
|
+
BlockConfig(
|
|
121
|
+
name=f"block_{g_start}_{g_end}",
|
|
122
|
+
leaf_indices=leaf_indices,
|
|
123
|
+
flat_start=offset,
|
|
124
|
+
flat_end=offset + padded,
|
|
125
|
+
num_particles=num_particles,
|
|
126
|
+
particle_dim=particle_dim,
|
|
127
|
+
)
|
|
128
|
+
)
|
|
129
|
+
offset += padded
|
|
130
|
+
|
|
131
|
+
return blocks
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def split_particles(
|
|
135
|
+
particles: torch.Tensor,
|
|
136
|
+
blocks: List[BlockConfig],
|
|
137
|
+
) -> List[torch.Tensor]:
|
|
138
|
+
"""Slice full flat particle vector into per-block particle tensors.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
particles: 2D tensor of shape ``(total_particles, particle_dim)`` or
|
|
142
|
+
1D flat tensor. Internally flattened and sliced by block offsets.
|
|
143
|
+
blocks: List of BlockConfig with flat_start/flat_end offsets.
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of tensors, each ``(block.num_particles, block.particle_dim)``.
|
|
147
|
+
"""
|
|
148
|
+
flat = particles.reshape(-1)
|
|
149
|
+
result: List[torch.Tensor] = []
|
|
150
|
+
|
|
151
|
+
for block in blocks:
|
|
152
|
+
block_flat = flat[block.flat_start : block.flat_end]
|
|
153
|
+
result.append(block_flat.reshape(block.num_particles, block.particle_dim))
|
|
154
|
+
|
|
155
|
+
return result
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
@torch.inference_mode()
|
|
159
|
+
def reassemble_blocks(
|
|
160
|
+
block_particles: List[torch.Tensor],
|
|
161
|
+
blocks: List[BlockConfig],
|
|
162
|
+
total_flat_size: int,
|
|
163
|
+
) -> torch.Tensor:
|
|
164
|
+
"""Reconstruct full flat vector from per-block particle tensors.
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
block_particles: List of tensors, each
|
|
168
|
+
``(block.num_particles, block.particle_dim)``.
|
|
169
|
+
blocks: List of BlockConfig with flat_start/flat_end offsets.
|
|
170
|
+
total_flat_size: Total size of the reassembled flat vector
|
|
171
|
+
(sum of all block padded sizes).
|
|
172
|
+
|
|
173
|
+
Returns:
|
|
174
|
+
1D tensor of shape ``(total_flat_size,)``.
|
|
175
|
+
"""
|
|
176
|
+
if not block_particles:
|
|
177
|
+
return torch.zeros(total_flat_size)
|
|
178
|
+
|
|
179
|
+
full_flat = torch.zeros(total_flat_size, dtype=block_particles[0].dtype, device=block_particles[0].device)
|
|
180
|
+
|
|
181
|
+
for block_X, block in zip(block_particles, blocks):
|
|
182
|
+
block_flat = block_X.reshape(-1)
|
|
183
|
+
block_size = block.flat_end - block.flat_start
|
|
184
|
+
full_flat[block.flat_start : block.flat_end] = block_flat[:block_size]
|
|
185
|
+
|
|
186
|
+
return full_flat
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@torch.inference_mode()
|
|
190
|
+
def blocks_to_layout_flat(
|
|
191
|
+
block_flat: torch.Tensor,
|
|
192
|
+
blocks: List[BlockConfig],
|
|
193
|
+
layout: "ParamLayout",
|
|
194
|
+
) -> torch.Tensor:
|
|
195
|
+
"""Map block-indexed flat vector to layout-indexed flat vector.
|
|
196
|
+
|
|
197
|
+
Per-layer (and grouped) blocks pad each block independently, creating a
|
|
198
|
+
different offset scheme than ``ParamLayout`` (which concatenates all
|
|
199
|
+
entries contiguously and pads once at the end). This function copies
|
|
200
|
+
actual parameter data from block offsets to the correct layout offsets,
|
|
201
|
+
producing a vector suitable for ``layout.batch_unflatten``.
|
|
202
|
+
|
|
203
|
+
Handles both per-layer blocks (one entry per block) and grouped blocks
|
|
204
|
+
(multiple entries per block) via ``block.leaf_indices``.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
block_flat: 1D tensor in block-indexed layout (from ``reassemble_blocks``).
|
|
208
|
+
blocks: List of BlockConfig with ``leaf_indices`` into ``layout.entries``.
|
|
209
|
+
layout: ParamLayout for the model.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
1D tensor of shape ``(layout.padded_size,)`` in layout-indexed layout.
|
|
213
|
+
"""
|
|
214
|
+
layout_flat = torch.zeros(
|
|
215
|
+
layout.padded_size,
|
|
216
|
+
dtype=block_flat.dtype,
|
|
217
|
+
device=block_flat.device,
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
for block in blocks:
|
|
221
|
+
internal_offset = 0
|
|
222
|
+
for leaf_idx in block.leaf_indices:
|
|
223
|
+
entry = layout.entries[leaf_idx]
|
|
224
|
+
numel = entry.numel
|
|
225
|
+
layout_flat[entry.offset : entry.offset + numel] = block_flat[
|
|
226
|
+
block.flat_start + internal_offset : block.flat_start + internal_offset + numel
|
|
227
|
+
]
|
|
228
|
+
internal_offset += numel
|
|
229
|
+
|
|
230
|
+
return layout_flat
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@torch.inference_mode()
|
|
234
|
+
def layout_flat_to_block_flat(
|
|
235
|
+
layout_flat: torch.Tensor,
|
|
236
|
+
blocks: List[BlockConfig],
|
|
237
|
+
layout: "ParamLayout",
|
|
238
|
+
) -> torch.Tensor:
|
|
239
|
+
"""Map layout-indexed flat vector to block-indexed flat vector.
|
|
240
|
+
|
|
241
|
+
Inverse of ``blocks_to_layout_flat``. Extracts actual parameter data
|
|
242
|
+
from layout entry offsets and places it at the correct block offsets,
|
|
243
|
+
with per-block padding zeros.
|
|
244
|
+
|
|
245
|
+
Args:
|
|
246
|
+
layout_flat: 1D tensor of shape ``(layout.padded_size,)`` in
|
|
247
|
+
layout-indexed format (e.g. from ``layout.flatten(model)``).
|
|
248
|
+
blocks: List of BlockConfig with ``leaf_indices`` into ``layout.entries``.
|
|
249
|
+
layout: ParamLayout for the model.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
1D tensor of shape ``(total_block_flat_size,)`` in block-indexed layout.
|
|
253
|
+
"""
|
|
254
|
+
total_block_flat_size = blocks[-1].flat_end if blocks else 0
|
|
255
|
+
block_flat = torch.zeros(
|
|
256
|
+
total_block_flat_size,
|
|
257
|
+
dtype=layout_flat.dtype,
|
|
258
|
+
device=layout_flat.device,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
for block in blocks:
|
|
262
|
+
internal_offset = 0
|
|
263
|
+
for leaf_idx in block.leaf_indices:
|
|
264
|
+
entry = layout.entries[leaf_idx]
|
|
265
|
+
numel = entry.numel
|
|
266
|
+
block_flat[block.flat_start + internal_offset : block.flat_start + internal_offset + numel] = layout_flat[
|
|
267
|
+
entry.offset : entry.offset + numel
|
|
268
|
+
]
|
|
269
|
+
internal_offset += numel
|
|
270
|
+
|
|
271
|
+
return block_flat
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
@torch.inference_mode()
|
|
275
|
+
def blocks_to_layout_flat_batch(
|
|
276
|
+
block_flat_batch: torch.Tensor,
|
|
277
|
+
blocks: List[BlockConfig],
|
|
278
|
+
layout: "ParamLayout",
|
|
279
|
+
) -> torch.Tensor:
|
|
280
|
+
"""Batched version of ``blocks_to_layout_flat``.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
block_flat_batch: 2D tensor of shape ``(N, total_block_flat_size)``.
|
|
284
|
+
blocks: List of BlockConfig with ``leaf_indices`` into ``layout.entries``.
|
|
285
|
+
layout: ParamLayout for the model.
|
|
286
|
+
|
|
287
|
+
Returns:
|
|
288
|
+
2D tensor of shape ``(N, layout.padded_size)``.
|
|
289
|
+
"""
|
|
290
|
+
N = block_flat_batch.shape[0]
|
|
291
|
+
layout_batch = torch.zeros(
|
|
292
|
+
N,
|
|
293
|
+
layout.padded_size,
|
|
294
|
+
dtype=block_flat_batch.dtype,
|
|
295
|
+
device=block_flat_batch.device,
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
for block in blocks:
|
|
299
|
+
internal_offset = 0
|
|
300
|
+
for leaf_idx in block.leaf_indices:
|
|
301
|
+
entry = layout.entries[leaf_idx]
|
|
302
|
+
numel = entry.numel
|
|
303
|
+
layout_batch[:, entry.offset : entry.offset + numel] = block_flat_batch[
|
|
304
|
+
:, block.flat_start + internal_offset : block.flat_start + internal_offset + numel
|
|
305
|
+
]
|
|
306
|
+
internal_offset += numel
|
|
307
|
+
|
|
308
|
+
return layout_batch
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
# ------------------------------------------------------------------
|
|
312
|
+
# Subspace-aware block functions
|
|
313
|
+
# ------------------------------------------------------------------
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def create_subspace_blocks(
|
|
317
|
+
subspace_dim: int,
|
|
318
|
+
num_blocks: int,
|
|
319
|
+
subspace_particle_dim: int = 8,
|
|
320
|
+
) -> List[BlockConfig]:
|
|
321
|
+
"""Create blocks that divide the subspace coordinate space.
|
|
322
|
+
|
|
323
|
+
For combined subspace + block-wise mode, the OT decomposition operates
|
|
324
|
+
in the projected subspace coordinates, NOT in full parameter space.
|
|
325
|
+
This function divides the subspace_dim coordinates into num_blocks
|
|
326
|
+
equal-sized blocks, each with subspace_particle_dim as the particle
|
|
327
|
+
dimension.
|
|
328
|
+
|
|
329
|
+
Key design insight:
|
|
330
|
+
- Block operation space is PROJECTED SUBSPACE, not full parameter space
|
|
331
|
+
- Blocks slice the subspace_dim coordinates
|
|
332
|
+
- Global projection P is shared across all blocks (applied once at start/end)
|
|
333
|
+
|
|
334
|
+
Args:
|
|
335
|
+
subspace_dim: Total subspace dimension (e.g., 256).
|
|
336
|
+
num_blocks: Number of blocks to create.
|
|
337
|
+
subspace_particle_dim: Particle dimension within each block (default 8).
|
|
338
|
+
Higher values give more polytope vertices but fewer particles per block.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
List of BlockConfig for subspace-aware block decomposition.
|
|
342
|
+
|
|
343
|
+
Example::
|
|
344
|
+
|
|
345
|
+
# 256-dim subspace split into 4 blocks with 8-dim particles
|
|
346
|
+
blocks = create_subspace_blocks(256, num_blocks=4, subspace_particle_dim=8)
|
|
347
|
+
# Each block: 64 subspace coords -> 8 particles of dim 8
|
|
348
|
+
|
|
349
|
+
"""
|
|
350
|
+
# Pad subspace_dim to be divisible by subspace_particle_dim
|
|
351
|
+
padded_subspace = subspace_dim + (-subspace_dim % subspace_particle_dim)
|
|
352
|
+
total_particles = padded_subspace // subspace_particle_dim
|
|
353
|
+
|
|
354
|
+
if num_blocks > total_particles:
|
|
355
|
+
warnings.warn(
|
|
356
|
+
f"num_blocks ({num_blocks}) exceeds total_particles ({total_particles}). Clamping to total_particles.",
|
|
357
|
+
stacklevel=2,
|
|
358
|
+
)
|
|
359
|
+
num_blocks = total_particles
|
|
360
|
+
|
|
361
|
+
# Divide particles evenly across blocks
|
|
362
|
+
base_particles_per_block = total_particles // num_blocks
|
|
363
|
+
remainder = total_particles % num_blocks
|
|
364
|
+
|
|
365
|
+
blocks: List[BlockConfig] = []
|
|
366
|
+
offset = 0
|
|
367
|
+
|
|
368
|
+
for i in range(num_blocks):
|
|
369
|
+
# Distribute remainder: first 'remainder' blocks get one extra particle
|
|
370
|
+
num_particles = base_particles_per_block + (1 if i < remainder else 0)
|
|
371
|
+
flat_size = num_particles * subspace_particle_dim
|
|
372
|
+
blocks.append(
|
|
373
|
+
BlockConfig(
|
|
374
|
+
name=f"subspace_block_{i}",
|
|
375
|
+
leaf_indices=(), # Not used for subspace blocks
|
|
376
|
+
flat_start=offset,
|
|
377
|
+
flat_end=offset + flat_size,
|
|
378
|
+
num_particles=num_particles,
|
|
379
|
+
particle_dim=subspace_particle_dim,
|
|
380
|
+
)
|
|
381
|
+
)
|
|
382
|
+
offset += flat_size
|
|
383
|
+
|
|
384
|
+
return blocks
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def split_subspace_to_blocks(
|
|
388
|
+
subspace_coords: torch.Tensor,
|
|
389
|
+
blocks: List[BlockConfig],
|
|
390
|
+
) -> List[torch.Tensor]:
|
|
391
|
+
"""Split 1D subspace coordinate vector into per-block particle tensors.
|
|
392
|
+
|
|
393
|
+
Takes a flattened subspace coordinate vector and slices it according to
|
|
394
|
+
the block configuration. Each slice is reshaped to (num_particles, particle_dim).
|
|
395
|
+
|
|
396
|
+
This is the subspace-aware equivalent of split_particles(), operating
|
|
397
|
+
on subspace coordinates rather than full parameter flat vectors.
|
|
398
|
+
|
|
399
|
+
Args:
|
|
400
|
+
subspace_coords: 1D tensor of shape (subspace_dim,) or 2D tensor of
|
|
401
|
+
shape (num_particles, particle_dim) that will be flattened.
|
|
402
|
+
blocks: List of BlockConfig from create_subspace_blocks().
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List of tensors, each of shape (block.num_particles, block.particle_dim).
|
|
406
|
+
|
|
407
|
+
Example::
|
|
408
|
+
|
|
409
|
+
coords = torch.randn(256) # subspace coordinates
|
|
410
|
+
blocks = create_subspace_blocks(256, 4, 8)
|
|
411
|
+
block_particles = split_subspace_to_blocks(coords, blocks)
|
|
412
|
+
# block_particles[0].shape == (8, 8) # 64 coords -> 8 particles x 8 dim
|
|
413
|
+
|
|
414
|
+
"""
|
|
415
|
+
flat = subspace_coords.reshape(-1)
|
|
416
|
+
|
|
417
|
+
# Pad to total block size if subspace_dim isn't divisible by particle_dim
|
|
418
|
+
total_block_size = blocks[-1].flat_end if blocks else 0
|
|
419
|
+
if flat.shape[0] < total_block_size:
|
|
420
|
+
flat = torch.nn.functional.pad(flat, (0, total_block_size - flat.shape[0]))
|
|
421
|
+
|
|
422
|
+
result: List[torch.Tensor] = []
|
|
423
|
+
|
|
424
|
+
for block in blocks:
|
|
425
|
+
block_flat = flat[block.flat_start : block.flat_end]
|
|
426
|
+
result.append(block_flat.reshape(block.num_particles, block.particle_dim))
|
|
427
|
+
|
|
428
|
+
return result
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
def reassemble_blocks_to_subspace(
|
|
432
|
+
block_particles: List[torch.Tensor],
|
|
433
|
+
blocks: List[BlockConfig],
|
|
434
|
+
subspace_dim: int,
|
|
435
|
+
) -> torch.Tensor:
|
|
436
|
+
"""Reconstruct 1D subspace coordinate vector from per-block particle tensors.
|
|
437
|
+
|
|
438
|
+
Inverse of split_subspace_to_blocks(). Takes the list of per-block particle
|
|
439
|
+
tensors and reassembles them into a contiguous subspace coordinate vector.
|
|
440
|
+
|
|
441
|
+
Args:
|
|
442
|
+
block_particles: List of tensors, each of shape
|
|
443
|
+
(block.num_particles, block.particle_dim).
|
|
444
|
+
blocks: List of BlockConfig from create_subspace_blocks().
|
|
445
|
+
subspace_dim: Original subspace dimension (for trimming padding).
|
|
446
|
+
|
|
447
|
+
Returns:
|
|
448
|
+
1D tensor of shape (subspace_dim,).
|
|
449
|
+
|
|
450
|
+
Example::
|
|
451
|
+
|
|
452
|
+
# After per-block OT updates
|
|
453
|
+
updated_coords = reassemble_blocks_to_subspace(
|
|
454
|
+
block_particles, blocks, subspace_dim=256
|
|
455
|
+
)
|
|
456
|
+
|
|
457
|
+
"""
|
|
458
|
+
if not block_particles:
|
|
459
|
+
return torch.zeros(subspace_dim)
|
|
460
|
+
|
|
461
|
+
# Total padded size from blocks
|
|
462
|
+
total_padded = sum(b.flat_end - b.flat_start for b in blocks)
|
|
463
|
+
device = block_particles[0].device
|
|
464
|
+
dtype = block_particles[0].dtype
|
|
465
|
+
|
|
466
|
+
full_flat = torch.zeros(total_padded, dtype=dtype, device=device)
|
|
467
|
+
|
|
468
|
+
for block_X, block in zip(block_particles, blocks):
|
|
469
|
+
block_flat = block_X.reshape(-1)
|
|
470
|
+
full_flat[block.flat_start : block.flat_end] = block_flat
|
|
471
|
+
|
|
472
|
+
# Trim to actual subspace_dim (remove padding)
|
|
473
|
+
return full_flat[:subspace_dim]
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def compute_block_cost_matrix(
|
|
477
|
+
block_idx: int,
|
|
478
|
+
X_probe_block: torch.Tensor,
|
|
479
|
+
all_block_particles: List[torch.Tensor],
|
|
480
|
+
blocks: List[BlockConfig],
|
|
481
|
+
layout: ParamLayout,
|
|
482
|
+
evaluator: NNCostEvaluator,
|
|
483
|
+
inputs: torch.Tensor,
|
|
484
|
+
targets: Optional[torch.Tensor] = None,
|
|
485
|
+
) -> torch.Tensor:
|
|
486
|
+
"""Compute cost matrix for one block: full forward, perturb only this block.
|
|
487
|
+
|
|
488
|
+
For each probe point (p, v, k), constructs full model parameters by
|
|
489
|
+
taking the current particles from all blocks and replacing particle row p
|
|
490
|
+
in block_idx with the probe value. This captures cross-block interactions
|
|
491
|
+
through the full model forward pass.
|
|
492
|
+
|
|
493
|
+
Args:
|
|
494
|
+
block_idx: Index of the block being evaluated.
|
|
495
|
+
X_probe_block: Probe points for this block, shape
|
|
496
|
+
``(P, V, K, block_particle_dim)``.
|
|
497
|
+
all_block_particles: Current particles for all blocks (base values).
|
|
498
|
+
blocks: List of all BlockConfig.
|
|
499
|
+
layout: ParamLayout for converting flat vectors to param dicts.
|
|
500
|
+
evaluator: NNCostEvaluator for model evaluation.
|
|
501
|
+
inputs: Input data batch.
|
|
502
|
+
targets: Optional target labels.
|
|
503
|
+
|
|
504
|
+
Returns:
|
|
505
|
+
Cost matrix of shape ``(P, V)``.
|
|
506
|
+
"""
|
|
507
|
+
P, V, K, D = X_probe_block.shape
|
|
508
|
+
N = P * V * K
|
|
509
|
+
total_flat_size = sum(b.flat_end - b.flat_start for b in blocks)
|
|
510
|
+
|
|
511
|
+
# Build base full flat vector from current block particles
|
|
512
|
+
base_flat = reassemble_blocks(all_block_particles, blocks, total_flat_size)
|
|
513
|
+
|
|
514
|
+
# Expand base flat to batch: (N, total_flat_size)
|
|
515
|
+
base_batch = base_flat.unsqueeze(0).expand(N, -1).clone()
|
|
516
|
+
|
|
517
|
+
# For each probe point (p, v, k), replace particle row p in block_idx
|
|
518
|
+
block = blocks[block_idx]
|
|
519
|
+
flat_probes = X_probe_block.reshape(P * V * K, D) # (N, D)
|
|
520
|
+
|
|
521
|
+
# Each probe n corresponds to particle p = n // (V*K)
|
|
522
|
+
# The flat offset for particle p in the block is:
|
|
523
|
+
# block.flat_start + p * D
|
|
524
|
+
# Vectorized scatter: compute all row offsets at once
|
|
525
|
+
indices = torch.arange(N, device=flat_probes.device)
|
|
526
|
+
p_indices = indices // (V * K)
|
|
527
|
+
row_starts = block.flat_start + p_indices * D
|
|
528
|
+
# Build column indices for each of the D elements per probe
|
|
529
|
+
col_offsets = torch.arange(D, device=flat_probes.device) # (D,)
|
|
530
|
+
# (N, D) matrix of column indices into base_batch
|
|
531
|
+
col_indices = row_starts.unsqueeze(1) + col_offsets.unsqueeze(0)
|
|
532
|
+
# Scatter flat_probes into base_batch using advanced indexing
|
|
533
|
+
row_idx = indices.unsqueeze(1).expand(-1, D) # (N, D)
|
|
534
|
+
base_batch[row_idx, col_indices] = flat_probes
|
|
535
|
+
|
|
536
|
+
# Per-layer blocks pad each entry independently, creating different
|
|
537
|
+
# offsets from ParamLayout (which concatenates entries contiguously).
|
|
538
|
+
# Map from block offsets to layout offsets for correct batch_unflatten.
|
|
539
|
+
batch_for_layout = blocks_to_layout_flat_batch(base_batch, blocks, layout)
|
|
540
|
+
|
|
541
|
+
# Convert to stacked param dicts
|
|
542
|
+
stacked_params = layout.batch_unflatten(batch_for_layout)
|
|
543
|
+
|
|
544
|
+
# Evaluate all probe points
|
|
545
|
+
losses = evaluator.evaluate(stacked_params, inputs, targets) # (N,)
|
|
546
|
+
|
|
547
|
+
# Reshape and average over probe dimension K
|
|
548
|
+
cost_matrix = losses.reshape(P, V, K).mean(dim=-1) # (P, V)
|
|
549
|
+
return cost_matrix
|