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