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/__init__.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
"""polystep: PyTorch PolyStep Optimizer."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.7.0"
|
|
4
|
+
|
|
5
|
+
__all__ = [
|
|
6
|
+
# Core solver
|
|
7
|
+
"Solver",
|
|
8
|
+
"SolverResult",
|
|
9
|
+
"SinkhornSolver",
|
|
10
|
+
"SinkhornResult",
|
|
11
|
+
"SoftmaxSolver",
|
|
12
|
+
"SoftmaxResult",
|
|
13
|
+
"KLSoftmaxSolver",
|
|
14
|
+
"TemperedSoftmaxSolver",
|
|
15
|
+
# Geometry
|
|
16
|
+
"get_orthoplex_vertices",
|
|
17
|
+
"get_simplex_vertices",
|
|
18
|
+
"get_cube_vertices",
|
|
19
|
+
"get_sampled_polytope_vertices",
|
|
20
|
+
"get_random_rotation_matrices",
|
|
21
|
+
"POLYTOPE_MAP",
|
|
22
|
+
"POLYTOPE_NUM_VERTICES_MAP",
|
|
23
|
+
# Cost & epsilon
|
|
24
|
+
"compute_cost_matrix",
|
|
25
|
+
"scale_cost_matrix",
|
|
26
|
+
"LinearEpsilon",
|
|
27
|
+
"CosineEpsilon",
|
|
28
|
+
"ProgressiveEpsilon",
|
|
29
|
+
# Solver
|
|
30
|
+
"PolyStep",
|
|
31
|
+
"SolverState",
|
|
32
|
+
# Transform
|
|
33
|
+
"ParamEntry",
|
|
34
|
+
"ParamLayout",
|
|
35
|
+
"get_device",
|
|
36
|
+
"create_generator",
|
|
37
|
+
# NN cost evaluation
|
|
38
|
+
"NNCostEvaluator",
|
|
39
|
+
"compute_nn_cost_matrix",
|
|
40
|
+
"auto_detect_chunk_size",
|
|
41
|
+
# Compilation
|
|
42
|
+
"CompiledFunctions",
|
|
43
|
+
"try_compile",
|
|
44
|
+
# Subspace
|
|
45
|
+
"FactorSpec",
|
|
46
|
+
"LowRankSubspace",
|
|
47
|
+
"LinearSubspace",
|
|
48
|
+
"ProjectionSpec",
|
|
49
|
+
"AdaptiveSubspace",
|
|
50
|
+
"CMAAdaptiveSubspace",
|
|
51
|
+
# CMA
|
|
52
|
+
"compute_cma_hyperparameters",
|
|
53
|
+
"update_step_size_csa",
|
|
54
|
+
# Blockwise
|
|
55
|
+
"BlockConfig",
|
|
56
|
+
"create_per_layer_blocks",
|
|
57
|
+
"create_grouped_blocks",
|
|
58
|
+
# Dynamics
|
|
59
|
+
"apply_momentum",
|
|
60
|
+
"update_adaptive_radius",
|
|
61
|
+
"compute_momentum_coefficient",
|
|
62
|
+
# Optimizer
|
|
63
|
+
"PolyStepOptimizer",
|
|
64
|
+
"RankSchedule",
|
|
65
|
+
# Ask/tell adapter
|
|
66
|
+
"PolyStepES",
|
|
67
|
+
"minimize",
|
|
68
|
+
# High-level API
|
|
69
|
+
"train",
|
|
70
|
+
"TrainConfig",
|
|
71
|
+
"TrainCallback",
|
|
72
|
+
"LoggingCallback",
|
|
73
|
+
"EarlyStoppingCallback",
|
|
74
|
+
"get_diagnostics",
|
|
75
|
+
# Objectives
|
|
76
|
+
"ObjectiveFn",
|
|
77
|
+
"Ackley",
|
|
78
|
+
"Rosenbrock",
|
|
79
|
+
"Rastrigin",
|
|
80
|
+
"StyblinskiTang",
|
|
81
|
+
"Levy",
|
|
82
|
+
"Griewank",
|
|
83
|
+
"Beale",
|
|
84
|
+
"Branin",
|
|
85
|
+
"Sphere",
|
|
86
|
+
# Layers
|
|
87
|
+
"VmapSafeMultiHeadAttention",
|
|
88
|
+
"VmapSafeLSTMCell",
|
|
89
|
+
"VmapSafeLSTM",
|
|
90
|
+
# Projection
|
|
91
|
+
"SparseRandomProjection",
|
|
92
|
+
# Hybrid subspace
|
|
93
|
+
"HybridSubspace",
|
|
94
|
+
"LayerProjectionSpec",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
from .solvers import (
|
|
98
|
+
Solver,
|
|
99
|
+
SolverResult,
|
|
100
|
+
SinkhornSolver,
|
|
101
|
+
SinkhornResult,
|
|
102
|
+
SoftmaxSolver,
|
|
103
|
+
SoftmaxResult,
|
|
104
|
+
KLSoftmaxSolver,
|
|
105
|
+
TemperedSoftmaxSolver,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
from .geometry import (
|
|
109
|
+
get_orthoplex_vertices,
|
|
110
|
+
get_simplex_vertices,
|
|
111
|
+
get_cube_vertices,
|
|
112
|
+
get_sampled_polytope_vertices,
|
|
113
|
+
get_random_rotation_matrices,
|
|
114
|
+
POLYTOPE_MAP,
|
|
115
|
+
POLYTOPE_NUM_VERTICES_MAP,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
from .costs import compute_cost_matrix, scale_cost_matrix
|
|
119
|
+
from .epsilon import LinearEpsilon, CosineEpsilon, ProgressiveEpsilon
|
|
120
|
+
|
|
121
|
+
from .solver import PolyStep, SolverState
|
|
122
|
+
|
|
123
|
+
from .transform import ParamEntry, ParamLayout, get_device, create_generator
|
|
124
|
+
|
|
125
|
+
from .cost_nn import NNCostEvaluator, compute_nn_cost_matrix, auto_detect_chunk_size
|
|
126
|
+
|
|
127
|
+
from ._compiled import CompiledFunctions, try_compile
|
|
128
|
+
|
|
129
|
+
from .subspace import FactorSpec, LowRankSubspace, LinearSubspace, ProjectionSpec
|
|
130
|
+
|
|
131
|
+
from .adaptive_subspace import AdaptiveSubspace
|
|
132
|
+
|
|
133
|
+
from .cma_subspace import CMAAdaptiveSubspace
|
|
134
|
+
from .cma import (
|
|
135
|
+
compute_cma_hyperparameters,
|
|
136
|
+
update_step_size_csa,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
from .blockwise import BlockConfig, create_per_layer_blocks, create_grouped_blocks
|
|
140
|
+
|
|
141
|
+
from .dynamics import apply_momentum, update_adaptive_radius, compute_momentum_coefficient
|
|
142
|
+
|
|
143
|
+
from .optimizer import PolyStepOptimizer, RankSchedule
|
|
144
|
+
from .ask_tell import PolyStepES, minimize
|
|
145
|
+
|
|
146
|
+
from .api import (
|
|
147
|
+
train,
|
|
148
|
+
TrainConfig,
|
|
149
|
+
TrainCallback,
|
|
150
|
+
LoggingCallback,
|
|
151
|
+
EarlyStoppingCallback,
|
|
152
|
+
get_diagnostics,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
# Objectives
|
|
156
|
+
from .objectives import (
|
|
157
|
+
ObjectiveFn,
|
|
158
|
+
Ackley,
|
|
159
|
+
Rosenbrock,
|
|
160
|
+
Rastrigin,
|
|
161
|
+
StyblinskiTang,
|
|
162
|
+
Levy,
|
|
163
|
+
Griewank,
|
|
164
|
+
Beale,
|
|
165
|
+
Branin,
|
|
166
|
+
Sphere,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Vmap-safe layers
|
|
170
|
+
from .layers import VmapSafeMultiHeadAttention, VmapSafeLSTMCell, VmapSafeLSTM
|
|
171
|
+
|
|
172
|
+
# Sparse projection for large-scale models
|
|
173
|
+
from .projection import SparseRandomProjection
|
|
174
|
+
|
|
175
|
+
# Hybrid subspace
|
|
176
|
+
from .hybrid_subspace import HybridSubspace, LayerProjectionSpec
|
polystep/_compiled.py
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
"""Compiled function variants for hot paths in the Sinkhorn Step solver.
|
|
2
|
+
|
|
3
|
+
These are the compiled hot paths. ``torch.compile`` is applied to individual
|
|
4
|
+
loop body functions (not the outer convergence loop) to avoid graph breaks
|
|
5
|
+
from Python control flow (iteration counting, convergence checks, early
|
|
6
|
+
stopping). Each function is a pure tensor operation with no Python side
|
|
7
|
+
effects, making it safe for ``fullgraph=True`` compilation.
|
|
8
|
+
|
|
9
|
+
Compilation strategy:
|
|
10
|
+
- Per-function compilation using torch.compile with 'default' mode.
|
|
11
|
+
The 'default' mode compiles with Inductor for operator fusion while
|
|
12
|
+
avoiding CUDA graph tensor ownership conflicts between chained functions.
|
|
13
|
+
- Each function is compiled independently so one failure does not block others.
|
|
14
|
+
- Compilation is only attempted when compile=True AND CUDA is available;
|
|
15
|
+
on CPU, all functions use eager mode (torch.compile overhead exceeds benefit).
|
|
16
|
+
- If torch.compile raises an exception for a specific function, that function
|
|
17
|
+
silently falls back to its eager implementation with a one-time UserWarning.
|
|
18
|
+
- compile=False disables all compilation attempts, using raw eager functions.
|
|
19
|
+
|
|
20
|
+
Usage:
|
|
21
|
+
cf = CompiledFunctions(compile=True) # compiled on CUDA, eager on CPU
|
|
22
|
+
cf = CompiledFunctions(compile=False) # always eager
|
|
23
|
+
|
|
24
|
+
# Pre-compile all functions to exclude JIT warmup from benchmarks:
|
|
25
|
+
cf.warm_start(dim=10, batch=5, device=torch.device('cuda'))
|
|
26
|
+
|
|
27
|
+
Provides pure tensor functions suitable for torch.compile, plus a try_compile
|
|
28
|
+
helper that gracefully falls back to eager mode on failure. The CompiledFunctions
|
|
29
|
+
registry holds either compiled or eager versions depending on the compile flag.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import warnings
|
|
33
|
+
from typing import Callable, Optional, Tuple
|
|
34
|
+
|
|
35
|
+
import torch
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# Note: "reduce-overhead" uses CUDA graphs which cause tensor ownership conflicts
|
|
39
|
+
# when chaining multiple compiled functions (rotate->probe->barycentric) within one step.
|
|
40
|
+
# "default" mode still compiles with Inductor but avoids CUDA graph issues.
|
|
41
|
+
DEFAULT_MODE = "default"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def try_compile(
|
|
45
|
+
fn: Callable,
|
|
46
|
+
*,
|
|
47
|
+
fullgraph: bool = True,
|
|
48
|
+
mode: str = DEFAULT_MODE,
|
|
49
|
+
name: Optional[str] = None,
|
|
50
|
+
) -> Callable:
|
|
51
|
+
"""Attempt to compile a function with torch.compile, falling back to eager.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
fn: Pure tensor function to compile.
|
|
55
|
+
fullgraph: Whether to require a single graph (no graph breaks).
|
|
56
|
+
mode: Compilation mode ('default', 'reduce-overhead', 'max-autotune').
|
|
57
|
+
name: Label for warning messages. Defaults to fn.__name__.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Compiled function, or the original fn if compilation fails.
|
|
61
|
+
"""
|
|
62
|
+
label = name if name is not None else getattr(fn, "__name__", repr(fn))
|
|
63
|
+
try:
|
|
64
|
+
return torch.compile(fn, fullgraph=fullgraph, mode=mode)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
warnings.warn(
|
|
67
|
+
f"torch.compile failed for '{label}': {e}. Falling back to eager mode.",
|
|
68
|
+
stacklevel=2,
|
|
69
|
+
)
|
|
70
|
+
return fn
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Pure compiled functions (no .item(), no list ops, no shape-dependent branches)
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _sinkhorn_iteration(
|
|
79
|
+
f: torch.Tensor,
|
|
80
|
+
g: torch.Tensor,
|
|
81
|
+
log_K: torch.Tensor,
|
|
82
|
+
log_a: torch.Tensor,
|
|
83
|
+
log_b: torch.Tensor,
|
|
84
|
+
eps: float,
|
|
85
|
+
omega: float = 1.0,
|
|
86
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
87
|
+
"""Single log-domain Sinkhorn iteration with optional overrelaxation (pure tensor ops).
|
|
88
|
+
|
|
89
|
+
When omega=1.0 (default), this is standard Sinkhorn. When omega>1.0,
|
|
90
|
+
overrelaxation accelerates convergence from O(1/t) to O(1/t^2).
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
f: First dual potential of shape (n,).
|
|
94
|
+
g: Second dual potential of shape (m,).
|
|
95
|
+
log_K: Log kernel matrix (-C / eps) of shape (n, m).
|
|
96
|
+
log_a: Log source marginal of shape (n,).
|
|
97
|
+
log_b: Log target marginal of shape (m,).
|
|
98
|
+
eps: Entropic regularization strength.
|
|
99
|
+
omega: Overrelaxation parameter in [0.5, 1.95]. Default 1.0 (no overrelaxation).
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
Tuple of updated (f, g) dual potentials.
|
|
103
|
+
"""
|
|
104
|
+
f_target = eps * (log_a - torch.logsumexp(log_K + g.unsqueeze(0) / eps, dim=1))
|
|
105
|
+
f_new = (1 - omega) * f + omega * f_target
|
|
106
|
+
|
|
107
|
+
g_target = eps * (log_b - torch.logsumexp(log_K + f_new.unsqueeze(1) / eps, dim=0))
|
|
108
|
+
g_new = (1 - omega) * g + omega * g_target
|
|
109
|
+
return f_new, g_new
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _rotate_and_translate(
|
|
113
|
+
rot_mats: torch.Tensor,
|
|
114
|
+
polytope_vertices: torch.Tensor,
|
|
115
|
+
origin: torch.Tensor,
|
|
116
|
+
step_radius: float,
|
|
117
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
118
|
+
"""Rotate polytope vertices and translate to particle positions (pure tensor ops).
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
rot_mats: Rotation matrices of shape (batch, dim, dim).
|
|
122
|
+
polytope_vertices: Template vertices of shape (num_verts, dim).
|
|
123
|
+
origin: Particle positions of shape (batch, dim).
|
|
124
|
+
step_radius: Step distance multiplier.
|
|
125
|
+
|
|
126
|
+
Returns:
|
|
127
|
+
Tuple of (step_points, rotated_vertices):
|
|
128
|
+
- step_points: (batch, num_verts, dim) vertex positions after rotation + translation.
|
|
129
|
+
- rotated_vertices: (batch, num_verts, dim) rotated vertices before translation.
|
|
130
|
+
"""
|
|
131
|
+
rotated = torch.einsum("bji, ni -> bnj", rot_mats, polytope_vertices)
|
|
132
|
+
step_points = rotated * step_radius + origin.unsqueeze(1)
|
|
133
|
+
return step_points, rotated
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _barycentric_projection(
|
|
137
|
+
transport_matrix: torch.Tensor,
|
|
138
|
+
a: torch.Tensor,
|
|
139
|
+
X_vertices: torch.Tensor,
|
|
140
|
+
) -> torch.Tensor:
|
|
141
|
+
"""Barycentric projection: weighted average of vertices by transport plan (pure tensor ops).
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
transport_matrix: Transport plan of shape (batch, num_vertices).
|
|
145
|
+
a: Source marginal weights of shape (batch,).
|
|
146
|
+
X_vertices: Vertex positions of shape (batch, num_vertices, dim).
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
Updated particle positions of shape (batch, dim).
|
|
150
|
+
"""
|
|
151
|
+
# Divide by the realized row sum (the OT barycenter), not the target marginal
|
|
152
|
+
# a. Equal for softmax rows; correct for unconverged Sinkhorn (a kept for API).
|
|
153
|
+
row_sum = transport_matrix.sum(dim=-1, keepdim=True).clamp(min=1e-12)
|
|
154
|
+
weights = transport_matrix / row_sum
|
|
155
|
+
# Cast to vertex dtype so the matmul works in mixed precision (no fp32/bf16 promote).
|
|
156
|
+
X_new = torch.einsum("bkd,bk->bd", X_vertices, weights.to(X_vertices.dtype))
|
|
157
|
+
return X_new
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _compute_probe_points(
|
|
161
|
+
origin: torch.Tensor,
|
|
162
|
+
directions: torch.Tensor,
|
|
163
|
+
scales: torch.Tensor,
|
|
164
|
+
probe_radius: float,
|
|
165
|
+
) -> torch.Tensor:
|
|
166
|
+
"""Generate probe points at fixed scale intervals along directions (pure tensor ops).
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
origin: Particle positions of shape (batch, dim).
|
|
170
|
+
directions: Direction vectors of shape (batch, num_points, dim).
|
|
171
|
+
scales: Scalar coefficients of shape (num_probe,).
|
|
172
|
+
probe_radius: Maximum distance multiplier.
|
|
173
|
+
|
|
174
|
+
Returns:
|
|
175
|
+
Probe points of shape (batch, num_points, num_probe, dim).
|
|
176
|
+
"""
|
|
177
|
+
# origin: (batch, 1, 1, dim)
|
|
178
|
+
origin_exp = origin[:, None, None, :]
|
|
179
|
+
# directions: (batch, num_points, 1, dim)
|
|
180
|
+
directions_exp = directions[:, :, None, :]
|
|
181
|
+
# scales: (1, 1, num_probe, 1)
|
|
182
|
+
scales_exp = scales[None, None, :, None]
|
|
183
|
+
|
|
184
|
+
return origin_exp + (directions_exp * probe_radius) * scales_exp
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def _fused_softmax_project(
|
|
188
|
+
cost_matrix: torch.Tensor,
|
|
189
|
+
epsilon: float,
|
|
190
|
+
a: torch.Tensor,
|
|
191
|
+
polytope_verts: torch.Tensor,
|
|
192
|
+
rot_mats: torch.Tensor,
|
|
193
|
+
step_radius: float,
|
|
194
|
+
X: torch.Tensor,
|
|
195
|
+
scale_cost_mean: bool = True,
|
|
196
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
197
|
+
"""Fused softmax weighting + vertex-free barycentric projection (pure tensor ops).
|
|
198
|
+
|
|
199
|
+
Combines softmax OT solve and barycentric projection into a single compiled
|
|
200
|
+
pipeline. Eliminates the intermediate O(P*V*dim) rotated vertex tensor by
|
|
201
|
+
computing the weighted centroid in template space then rotating, reducing
|
|
202
|
+
the projection step to O(P*dim).
|
|
203
|
+
|
|
204
|
+
Args:
|
|
205
|
+
cost_matrix: Cost matrix C of shape (P, V).
|
|
206
|
+
epsilon: Temperature parameter (entropic regularization strength).
|
|
207
|
+
a: Source marginal of shape (P,).
|
|
208
|
+
polytope_verts: Template polytope vertices of shape (V, dim) -- NOT rotated.
|
|
209
|
+
rot_mats: Per-particle rotation matrices of shape (P, dim, dim).
|
|
210
|
+
step_radius: Step distance multiplier.
|
|
211
|
+
X: Current particle positions of shape (P, dim).
|
|
212
|
+
scale_cost_mean: If True, normalize cost matrix by its mean absolute value.
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Tuple of (X_new, transport):
|
|
216
|
+
- X_new: Updated particle positions of shape (P, dim).
|
|
217
|
+
- transport: Transport matrix of shape (P, V) with row sums equal to a.
|
|
218
|
+
"""
|
|
219
|
+
# Cost scaling (inline for compile safety, avoids cross-module string dispatch)
|
|
220
|
+
if scale_cost_mean:
|
|
221
|
+
s = torch.clamp(cost_matrix.abs().mean(), min=1e-10)
|
|
222
|
+
C = cost_matrix / s
|
|
223
|
+
else:
|
|
224
|
+
C = cost_matrix
|
|
225
|
+
|
|
226
|
+
# Softmax weights: PyTorch's softmax subtracts row-max internally for stability
|
|
227
|
+
W = torch.softmax(-C / epsilon, dim=-1) # (P, V)
|
|
228
|
+
|
|
229
|
+
# Transport matrix: row sums equal source marginal a
|
|
230
|
+
transport = W * a.unsqueeze(-1) # (P, V)
|
|
231
|
+
|
|
232
|
+
# Vertex-free centroid: O(P*dim), not O(P*V*dim) rotated vertices. Cast W to the
|
|
233
|
+
# geometry dtype so the matmul works in mixed precision (W is fp32, geometry bf16).
|
|
234
|
+
w_centroid = W.to(polytope_verts.dtype) @ polytope_verts # (P, dim)
|
|
235
|
+
rot_centroid = torch.einsum("bij,bj->bi", rot_mats, w_centroid) # (P, dim)
|
|
236
|
+
|
|
237
|
+
# Barycentric projection
|
|
238
|
+
X_new = X + step_radius * rot_centroid # (P, dim)
|
|
239
|
+
|
|
240
|
+
return X_new, transport
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
# ---------------------------------------------------------------------------
|
|
244
|
+
# Registry: holds compiled or eager versions of each function
|
|
245
|
+
# ---------------------------------------------------------------------------
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
class CompiledFunctions:
|
|
249
|
+
"""Registry of compiled (or eager) pure tensor functions.
|
|
250
|
+
|
|
251
|
+
When compile=True and CUDA is available, wraps each function with
|
|
252
|
+
torch.compile via try_compile. Otherwise uses the eager versions directly.
|
|
253
|
+
|
|
254
|
+
Attributes:
|
|
255
|
+
sinkhorn_iter: Compiled or eager _sinkhorn_iteration.
|
|
256
|
+
rotate_and_translate: Compiled or eager _rotate_and_translate.
|
|
257
|
+
barycentric_projection: Compiled or eager _barycentric_projection.
|
|
258
|
+
compute_probe_points: Compiled or eager _compute_probe_points.
|
|
259
|
+
fused_softmax_project: Compiled or eager _fused_softmax_project.
|
|
260
|
+
"""
|
|
261
|
+
|
|
262
|
+
DEFAULT_MODE = DEFAULT_MODE
|
|
263
|
+
|
|
264
|
+
def __init__(self, compile: bool = True) -> None:
|
|
265
|
+
self.compile = compile and torch.cuda.is_available()
|
|
266
|
+
if self.compile:
|
|
267
|
+
self.sinkhorn_iter = try_compile(_sinkhorn_iteration, name="sinkhorn_iteration")
|
|
268
|
+
self.rotate_and_translate = try_compile(_rotate_and_translate, name="rotate_and_translate")
|
|
269
|
+
self.barycentric_projection = try_compile(_barycentric_projection, name="barycentric_projection")
|
|
270
|
+
self.compute_probe_points = try_compile(_compute_probe_points, name="compute_probe_points")
|
|
271
|
+
self.fused_softmax_project = try_compile(_fused_softmax_project, name="fused_softmax_project")
|
|
272
|
+
else:
|
|
273
|
+
self.sinkhorn_iter = _sinkhorn_iteration
|
|
274
|
+
self.rotate_and_translate = _rotate_and_translate
|
|
275
|
+
self.barycentric_projection = _barycentric_projection
|
|
276
|
+
self.compute_probe_points = _compute_probe_points
|
|
277
|
+
self.fused_softmax_project = _fused_softmax_project
|
|
278
|
+
|
|
279
|
+
def warm_start(self, dim: int = 10, batch: int = 5, device: Optional[torch.device] = None) -> None:
|
|
280
|
+
"""Pre-compile all hot paths by running dummy inputs.
|
|
281
|
+
|
|
282
|
+
Triggers JIT compilation warmup so that the first real call does not
|
|
283
|
+
include compilation overhead. Call before benchmarking to get accurate
|
|
284
|
+
timing measurements.
|
|
285
|
+
|
|
286
|
+
No-ops when compilation is disabled (``compile=False``).
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
dim: Dimensionality for dummy tensors. For best results, match
|
|
290
|
+
the actual ``particle_dim`` of your problem.
|
|
291
|
+
batch: Batch size for dummy tensors. For best results, match
|
|
292
|
+
the actual ``num_particles`` of your problem.
|
|
293
|
+
device: Device for dummy tensors. Defaults to CPU if None.
|
|
294
|
+
"""
|
|
295
|
+
if not self.compile:
|
|
296
|
+
return # No JIT compilation to warm up
|
|
297
|
+
|
|
298
|
+
if device is None:
|
|
299
|
+
device = torch.device("cpu")
|
|
300
|
+
num_verts = 2 * dim
|
|
301
|
+
|
|
302
|
+
with torch.inference_mode():
|
|
303
|
+
# Warm sinkhorn_iter
|
|
304
|
+
f = torch.zeros(batch, device=device)
|
|
305
|
+
g = torch.zeros(batch, device=device)
|
|
306
|
+
log_K = torch.zeros(batch, batch, device=device)
|
|
307
|
+
log_a = torch.zeros(batch, device=device)
|
|
308
|
+
log_b = torch.zeros(batch, device=device)
|
|
309
|
+
self.sinkhorn_iter(f, g, log_K, log_a, log_b, 0.1)
|
|
310
|
+
|
|
311
|
+
# Warm rotate_and_translate
|
|
312
|
+
rot_mats = torch.eye(dim, device=device).unsqueeze(0).expand(batch, -1, -1).contiguous()
|
|
313
|
+
polytope_verts = torch.randn(num_verts, dim, device=device)
|
|
314
|
+
origin = torch.zeros(batch, dim, device=device)
|
|
315
|
+
self.rotate_and_translate(rot_mats, polytope_verts, origin, 1.0)
|
|
316
|
+
|
|
317
|
+
# Warm barycentric_projection
|
|
318
|
+
transport = torch.ones(batch, num_verts, device=device) / num_verts
|
|
319
|
+
a = torch.ones(batch, device=device) / batch
|
|
320
|
+
vertices = torch.randn(batch, num_verts, dim, device=device)
|
|
321
|
+
self.barycentric_projection(transport, a, vertices)
|
|
322
|
+
|
|
323
|
+
# Warm compute_probe_points
|
|
324
|
+
directions = torch.randn(batch, num_verts, dim, device=device)
|
|
325
|
+
scales = torch.linspace(0.2, 0.8, 3, device=device)
|
|
326
|
+
self.compute_probe_points(origin, directions, scales, 1.0)
|
|
327
|
+
|
|
328
|
+
# Warm fused_softmax_project
|
|
329
|
+
C_warm = torch.randn(batch, num_verts, device=device)
|
|
330
|
+
a_warm = torch.ones(batch, device=device) / batch
|
|
331
|
+
verts_warm = torch.randn(num_verts, dim, device=device)
|
|
332
|
+
rot_warm = torch.eye(dim, device=device).unsqueeze(0).expand(batch, -1, -1).contiguous()
|
|
333
|
+
X_warm = torch.randn(batch, dim, device=device)
|
|
334
|
+
self.fused_softmax_project(C_warm, 0.1, a_warm, verts_warm, rot_warm, 1.0, X_warm)
|