flopscope 0.2.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.
- benchmarks/__init__.py +1 -0
- benchmarks/__main__.py +6 -0
- benchmarks/_baseline.py +171 -0
- benchmarks/_bitwise.py +231 -0
- benchmarks/_complex.py +176 -0
- benchmarks/_contractions.py +291 -0
- benchmarks/_fft.py +198 -0
- benchmarks/_impl_urls.py +139 -0
- benchmarks/_linalg.py +197 -0
- benchmarks/_linalg_delegates.py +407 -0
- benchmarks/_metadata.py +141 -0
- benchmarks/_misc.py +653 -0
- benchmarks/_perf.py +321 -0
- benchmarks/_perm_group_calibration.py +175 -0
- benchmarks/_pointwise.py +372 -0
- benchmarks/_polynomial.py +193 -0
- benchmarks/_random.py +209 -0
- benchmarks/_reductions.py +136 -0
- benchmarks/_sorting.py +289 -0
- benchmarks/_stats.py +137 -0
- benchmarks/_window.py +92 -0
- benchmarks/accumulation/__init__.py +0 -0
- benchmarks/accumulation/bench_cost_compute.py +138 -0
- benchmarks/dashboard.py +312 -0
- benchmarks/runner.py +636 -0
- flopscope/__init__.py +273 -0
- flopscope/_accumulation/__init__.py +13 -0
- flopscope/_accumulation/_bipartite.py +121 -0
- flopscope/_accumulation/_burnside.py +51 -0
- flopscope/_accumulation/_cache.py +146 -0
- flopscope/_accumulation/_components.py +153 -0
- flopscope/_accumulation/_cost.py +1414 -0
- flopscope/_accumulation/_cost_descriptions.py +63 -0
- flopscope/_accumulation/_detection.py +318 -0
- flopscope/_accumulation/_ladder.py +191 -0
- flopscope/_accumulation/_output_orbit.py +104 -0
- flopscope/_accumulation/_partition.py +290 -0
- flopscope/_accumulation/_path_info.py +211 -0
- flopscope/_accumulation/_public.py +169 -0
- flopscope/_accumulation/_reduction.py +310 -0
- flopscope/_accumulation/_regimes.py +303 -0
- flopscope/_accumulation/_shape.py +33 -0
- flopscope/_accumulation/_wreath.py +209 -0
- flopscope/_budget.py +1027 -0
- flopscope/_config.py +118 -0
- flopscope/_counting_ops.py +451 -0
- flopscope/_display.py +478 -0
- flopscope/_docstrings.py +59 -0
- flopscope/_dtypes.py +20 -0
- flopscope/_einsum.py +717 -0
- flopscope/_errstate.py +25 -0
- flopscope/_flops.py +282 -0
- flopscope/_free_ops.py +2654 -0
- flopscope/_ndarray.py +1126 -0
- flopscope/_opt_einsum/LICENSE +21 -0
- flopscope/_opt_einsum/NOTICE +59 -0
- flopscope/_opt_einsum/__init__.py +209 -0
- flopscope/_opt_einsum/_contract.py +1478 -0
- flopscope/_opt_einsum/_helpers.py +164 -0
- flopscope/_opt_einsum/_hsluv.py +273 -0
- flopscope/_opt_einsum/_path_random.py +462 -0
- flopscope/_opt_einsum/_paths.py +1653 -0
- flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
- flopscope/_opt_einsum/_symmetry.py +140 -0
- flopscope/_opt_einsum/_typing.py +37 -0
- flopscope/_perm_group.py +717 -0
- flopscope/_pointwise.py +2522 -0
- flopscope/_polynomial.py +278 -0
- flopscope/_registry.py +3216 -0
- flopscope/_sorting_ops.py +571 -0
- flopscope/_symmetric.py +812 -0
- flopscope/_symmetry_transport.py +510 -0
- flopscope/_symmetry_utils.py +669 -0
- flopscope/_type_info.py +12 -0
- flopscope/_unwrap.py +70 -0
- flopscope/_validation.py +83 -0
- flopscope/_version_check.py +46 -0
- flopscope/_weights.py +195 -0
- flopscope/_window.py +177 -0
- flopscope/accounting.py +565 -0
- flopscope/data/default_weights.json +462 -0
- flopscope/data/weights.csv +509 -0
- flopscope/errors.py +197 -0
- flopscope/numpy/__init__.py +878 -0
- flopscope/numpy/fft/__init__.py +55 -0
- flopscope/numpy/fft/_free.py +51 -0
- flopscope/numpy/fft/_transforms.py +695 -0
- flopscope/numpy/linalg/__init__.py +105 -0
- flopscope/numpy/linalg/_aliases.py +126 -0
- flopscope/numpy/linalg/_compound.py +161 -0
- flopscope/numpy/linalg/_decompositions.py +353 -0
- flopscope/numpy/linalg/_properties.py +533 -0
- flopscope/numpy/linalg/_solvers.py +444 -0
- flopscope/numpy/linalg/_svd.py +122 -0
- flopscope/numpy/random/__init__.py +684 -0
- flopscope/numpy/random/_cost_formulas.py +115 -0
- flopscope/numpy/random/_counted_classes.py +241 -0
- flopscope/numpy/testing/__init__.py +13 -0
- flopscope/numpy/typing/__init__.py +30 -0
- flopscope/py.typed +0 -0
- flopscope/stats/__init__.py +84 -0
- flopscope/stats/_base.py +77 -0
- flopscope/stats/_cauchy.py +146 -0
- flopscope/stats/_erf.py +190 -0
- flopscope/stats/_expon.py +146 -0
- flopscope/stats/_laplace.py +150 -0
- flopscope/stats/_logistic.py +148 -0
- flopscope/stats/_lognorm.py +160 -0
- flopscope/stats/_ndtri.py +133 -0
- flopscope/stats/_norm.py +149 -0
- flopscope/stats/_truncnorm.py +186 -0
- flopscope/stats/_uniform.py +141 -0
- flopscope-0.2.0.dist-info/METADATA +23 -0
- flopscope-0.2.0.dist-info/RECORD +115 -0
- flopscope-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""Support for random optimizers, including the random-greedy path."""
|
|
2
|
+
|
|
3
|
+
import functools
|
|
4
|
+
import heapq
|
|
5
|
+
import math
|
|
6
|
+
import time
|
|
7
|
+
from collections import deque
|
|
8
|
+
from collections.abc import Generator, Iterable
|
|
9
|
+
from decimal import Decimal
|
|
10
|
+
from random import choices as random_choices
|
|
11
|
+
from random import seed as random_seed
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from . import _paths as paths
|
|
15
|
+
from ._helpers import compute_size_by_dict
|
|
16
|
+
from ._subgraph_symmetry import SubgraphSymmetryOracle
|
|
17
|
+
from ._typing import ArrayIndexType, ArrayType, PathType
|
|
18
|
+
|
|
19
|
+
__all__ = ["RandomGreedy", "random_greedy", "random_greedy_128"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class RandomOptimizer(paths.PathOptimizer):
|
|
23
|
+
"""Base class for running any random path finder that benefits
|
|
24
|
+
from repeated calling, possibly in a parallel fashion. Custom random
|
|
25
|
+
optimizers should subclass this, and the `setup` method should be
|
|
26
|
+
implemented with the following signature:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
def setup(self, inputs, output, size_dict):
|
|
30
|
+
# custom preparation here ...
|
|
31
|
+
return trial_fn, trial_args
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Where `trial_fn` itself should have the signature::
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
def trial_fn(r, *trial_args):
|
|
38
|
+
# custom computation of path here
|
|
39
|
+
return ssa_path, cost, size
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Where `r` is the run number and could for example be used to seed a
|
|
43
|
+
random number generator. See `RandomGreedy` for an example.
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
Parameters:
|
|
47
|
+
max_repeats: The maximum number of repeat trials to have.
|
|
48
|
+
max_time: The maximum amount of time to run the algorithm for.
|
|
49
|
+
minimize: Whether to favour paths that minimize the total estimated flop-count or
|
|
50
|
+
the size of the largest intermediate created.
|
|
51
|
+
parallel: Whether to parallelize the random trials, by default `False`. If
|
|
52
|
+
`True`, use a `concurrent.futures.ProcessPoolExecutor` with the same
|
|
53
|
+
number of processes as cores. If an integer is specified, use that many
|
|
54
|
+
processes instead. Finally, you can supply a custom executor-pool which
|
|
55
|
+
should have an API matching that of the python 3 standard library
|
|
56
|
+
module `concurrent.futures`. Namely, a `submit` method that returns
|
|
57
|
+
`Future` objects, themselves with `result` and `cancel` methods.
|
|
58
|
+
pre_dispatch: If running in parallel, how many jobs to pre-dispatch so as to avoid
|
|
59
|
+
submitting all jobs at once. Should also be more than twice the number
|
|
60
|
+
of workers to avoid under-subscription. Default: 128.
|
|
61
|
+
|
|
62
|
+
Attributes:
|
|
63
|
+
path: The best path found so far.
|
|
64
|
+
costs: The list of each trial's costs found so far.
|
|
65
|
+
sizes: The list of each trial's largest intermediate size so far.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def __init__(
|
|
69
|
+
self,
|
|
70
|
+
max_repeats: int = 32,
|
|
71
|
+
max_time: float | None = None,
|
|
72
|
+
minimize: str = "flops",
|
|
73
|
+
parallel: bool | Decimal | int = False,
|
|
74
|
+
pre_dispatch: int = 128,
|
|
75
|
+
):
|
|
76
|
+
if minimize not in ("flops", "size"):
|
|
77
|
+
raise ValueError("`minimize` should be one of {'flops', 'size'}.")
|
|
78
|
+
|
|
79
|
+
self.max_repeats = max_repeats
|
|
80
|
+
self.max_time = max_time
|
|
81
|
+
self.minimize = minimize
|
|
82
|
+
self.better = paths.get_better_fn(minimize)
|
|
83
|
+
self._parallel: bool | Decimal | int = False
|
|
84
|
+
self.parallel = parallel
|
|
85
|
+
self.pre_dispatch = pre_dispatch
|
|
86
|
+
|
|
87
|
+
self.costs: list[int] = []
|
|
88
|
+
self.sizes: list[int] = []
|
|
89
|
+
self.best: dict[str, Any] = {"flops": float("inf"), "size": float("inf")}
|
|
90
|
+
|
|
91
|
+
self._repeats_start = 0
|
|
92
|
+
self._executor: Any
|
|
93
|
+
self._futures: Any
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def path(self) -> PathType:
|
|
97
|
+
"""The best path found so far."""
|
|
98
|
+
return paths.ssa_to_linear(self.best["ssa_path"])
|
|
99
|
+
|
|
100
|
+
@property
|
|
101
|
+
def parallel(self) -> bool | Decimal | int:
|
|
102
|
+
return self._parallel
|
|
103
|
+
|
|
104
|
+
@parallel.setter
|
|
105
|
+
def parallel(self, parallel: bool | Decimal | int) -> None:
|
|
106
|
+
# shutdown any previous executor if we are managing it
|
|
107
|
+
if getattr(self, "_managing_executor", False):
|
|
108
|
+
self._executor.shutdown()
|
|
109
|
+
|
|
110
|
+
self._parallel = parallel
|
|
111
|
+
self._managing_executor = False
|
|
112
|
+
|
|
113
|
+
if parallel is False:
|
|
114
|
+
self._executor = None
|
|
115
|
+
return
|
|
116
|
+
|
|
117
|
+
if parallel is True:
|
|
118
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
119
|
+
|
|
120
|
+
self._executor = ProcessPoolExecutor()
|
|
121
|
+
self._managing_executor = True
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
if isinstance(parallel, (int, Decimal)):
|
|
125
|
+
from concurrent.futures import ProcessPoolExecutor
|
|
126
|
+
|
|
127
|
+
self._executor = ProcessPoolExecutor(int(parallel))
|
|
128
|
+
self._managing_executor = True
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
# assume a pool-executor has been supplied
|
|
132
|
+
self._executor = parallel
|
|
133
|
+
|
|
134
|
+
def _gen_results_parallel(
|
|
135
|
+
self, repeats: Iterable[int], trial_fn: Any, args: Any
|
|
136
|
+
) -> Generator[Any, None, None]:
|
|
137
|
+
"""Lazily generate results from an executor without submitting all jobs at once."""
|
|
138
|
+
self._futures = deque()
|
|
139
|
+
|
|
140
|
+
# the idea here is to submit at least ``pre_dispatch`` jobs *before* we
|
|
141
|
+
# yield any results, then do both in tandem, before draining the queue
|
|
142
|
+
for r in repeats:
|
|
143
|
+
if len(self._futures) < self.pre_dispatch:
|
|
144
|
+
self._futures.append(self._executor.submit(trial_fn, r, *args))
|
|
145
|
+
continue
|
|
146
|
+
yield self._futures.popleft().result()
|
|
147
|
+
|
|
148
|
+
while self._futures:
|
|
149
|
+
yield self._futures.popleft().result()
|
|
150
|
+
|
|
151
|
+
def _cancel_futures(self) -> None:
|
|
152
|
+
if self._executor is not None:
|
|
153
|
+
for f in self._futures:
|
|
154
|
+
f.cancel()
|
|
155
|
+
|
|
156
|
+
def setup(
|
|
157
|
+
self,
|
|
158
|
+
inputs: list[ArrayIndexType],
|
|
159
|
+
output: ArrayIndexType,
|
|
160
|
+
size_dict: dict[str, int],
|
|
161
|
+
**kwargs: Any,
|
|
162
|
+
) -> tuple[Any, Any]:
|
|
163
|
+
raise NotImplementedError
|
|
164
|
+
|
|
165
|
+
def __call__(
|
|
166
|
+
self,
|
|
167
|
+
inputs: list[ArrayIndexType],
|
|
168
|
+
output: ArrayIndexType,
|
|
169
|
+
size_dict: dict[str, int],
|
|
170
|
+
memory_limit: int | None = None,
|
|
171
|
+
symmetry_oracle: "SubgraphSymmetryOracle | None" = None,
|
|
172
|
+
**kwargs: Any,
|
|
173
|
+
) -> PathType:
|
|
174
|
+
self._check_args_against_first_call(inputs, output, size_dict)
|
|
175
|
+
|
|
176
|
+
self._symmetry_oracle = symmetry_oracle
|
|
177
|
+
|
|
178
|
+
# start a timer?
|
|
179
|
+
t0 = 0.0 # assigned below if max_time is set
|
|
180
|
+
if self.max_time is not None:
|
|
181
|
+
t0 = time.time()
|
|
182
|
+
|
|
183
|
+
trial_fn, trial_args = self.setup(
|
|
184
|
+
inputs, output, size_dict, symmetry_oracle=symmetry_oracle, **kwargs
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
r_start = self._repeats_start + len(self.costs)
|
|
188
|
+
r_stop = r_start + self.max_repeats
|
|
189
|
+
repeats = range(r_start, r_stop)
|
|
190
|
+
|
|
191
|
+
# create the trials lazily
|
|
192
|
+
if self._executor is not None:
|
|
193
|
+
trials = self._gen_results_parallel(repeats, trial_fn, trial_args)
|
|
194
|
+
else:
|
|
195
|
+
trials = (trial_fn(r, *trial_args) for r in repeats)
|
|
196
|
+
|
|
197
|
+
# assess the trials
|
|
198
|
+
for ssa_path, cost, size in trials:
|
|
199
|
+
# keep track of all costs and sizes
|
|
200
|
+
self.costs.append(cost)
|
|
201
|
+
self.sizes.append(size)
|
|
202
|
+
|
|
203
|
+
# check if we have found a new best
|
|
204
|
+
found_new_best = self.better(
|
|
205
|
+
cost, size, self.best["flops"], self.best["size"]
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
if found_new_best:
|
|
209
|
+
self.best["flops"] = cost
|
|
210
|
+
self.best["size"] = size
|
|
211
|
+
self.best["ssa_path"] = ssa_path
|
|
212
|
+
|
|
213
|
+
# check if we have run out of time
|
|
214
|
+
if (self.max_time is not None) and (time.time() > t0 + self.max_time):
|
|
215
|
+
break
|
|
216
|
+
|
|
217
|
+
self._cancel_futures()
|
|
218
|
+
return self.path
|
|
219
|
+
|
|
220
|
+
def __del__(self):
|
|
221
|
+
# if we created the parallel pool-executor, shut it down
|
|
222
|
+
if getattr(self, "_managing_executor", False):
|
|
223
|
+
self._executor.shutdown()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def thermal_chooser(queue, remaining, nbranch=8, temperature=1, rel_temperature=True):
|
|
227
|
+
"""A contraction 'chooser' that weights possible contractions using a
|
|
228
|
+
Boltzmann distribution. Explicitly, given costs `c_i` (with `c_0` the
|
|
229
|
+
smallest), the relative weights, `w_i`, are computed as:
|
|
230
|
+
|
|
231
|
+
$$w_i = exp( -(c_i - c_0) / temperature)$$
|
|
232
|
+
|
|
233
|
+
Additionally, if `rel_temperature` is set, scale `temperature` by
|
|
234
|
+
`abs(c_0)` to account for likely fluctuating cost magnitudes during the
|
|
235
|
+
course of a contraction.
|
|
236
|
+
|
|
237
|
+
Parameters:
|
|
238
|
+
queue: The heapified list of candidate contractions.
|
|
239
|
+
remaining: Mapping of remaining inputs' indices to the ssa id.
|
|
240
|
+
temperature: When choosing a possible contraction, its relative probability will be
|
|
241
|
+
proportional to `exp(-cost / temperature)`. Thus the larger
|
|
242
|
+
`temperature` is, the further random paths will stray from the normal
|
|
243
|
+
'greedy' path. Conversely, if set to zero, only paths with exactly the
|
|
244
|
+
same cost as the best at each step will be explored.
|
|
245
|
+
rel_temperature: Whether to normalize the `temperature` at each step to the scale of
|
|
246
|
+
the best cost. This is generally beneficial as the magnitude of costs
|
|
247
|
+
can vary significantly throughout a contraction.
|
|
248
|
+
nbranch: How many potential paths to calculate probability for and choose from at each step.
|
|
249
|
+
|
|
250
|
+
Returns:
|
|
251
|
+
cost
|
|
252
|
+
k1
|
|
253
|
+
k2
|
|
254
|
+
k3
|
|
255
|
+
"""
|
|
256
|
+
n = 0
|
|
257
|
+
choices = []
|
|
258
|
+
while queue and n < nbranch:
|
|
259
|
+
cost, k1, k2, k12 = heapq.heappop(queue)
|
|
260
|
+
if k1 not in remaining or k2 not in remaining:
|
|
261
|
+
continue # candidate is obsolete
|
|
262
|
+
choices.append((cost, k1, k2, k12))
|
|
263
|
+
n += 1
|
|
264
|
+
|
|
265
|
+
if n == 0:
|
|
266
|
+
return None
|
|
267
|
+
if n == 1:
|
|
268
|
+
return choices[0]
|
|
269
|
+
|
|
270
|
+
costs = [choice[0][0] for choice in choices]
|
|
271
|
+
cmin = costs[0]
|
|
272
|
+
|
|
273
|
+
# adjust by the overall scale to account for fluctuating absolute costs
|
|
274
|
+
if rel_temperature:
|
|
275
|
+
temperature *= max(1, abs(cmin))
|
|
276
|
+
|
|
277
|
+
# compute relative probability for each potential contraction
|
|
278
|
+
if temperature == 0.0:
|
|
279
|
+
energies = [1 if c == cmin else 0 for c in costs]
|
|
280
|
+
else:
|
|
281
|
+
# shift by cmin for numerical reasons
|
|
282
|
+
energies = [math.exp(-(c - cmin) / temperature) for c in costs]
|
|
283
|
+
|
|
284
|
+
# randomly choose a contraction based on energies
|
|
285
|
+
(chosen,) = random_choices(range(n), weights=energies)
|
|
286
|
+
cost, k1, k2, k12 = choices.pop(chosen)
|
|
287
|
+
|
|
288
|
+
# put the other choice back in the heap
|
|
289
|
+
for other in choices:
|
|
290
|
+
heapq.heappush(queue, other)
|
|
291
|
+
|
|
292
|
+
return cost, k1, k2, k12
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def ssa_path_compute_cost(
|
|
296
|
+
ssa_path: PathType,
|
|
297
|
+
inputs: list[ArrayIndexType],
|
|
298
|
+
output: ArrayIndexType,
|
|
299
|
+
size_dict: dict[str, int],
|
|
300
|
+
symmetry_oracle: "SubgraphSymmetryOracle | None" = None,
|
|
301
|
+
) -> tuple[int, int]:
|
|
302
|
+
"""Compute the flops and max size of an ssa path, using the symmetry oracle
|
|
303
|
+
if provided."""
|
|
304
|
+
inputs = list(map(frozenset, inputs))
|
|
305
|
+
output = frozenset(output)
|
|
306
|
+
remaining = set(range(len(inputs)))
|
|
307
|
+
num_operands = len(inputs)
|
|
308
|
+
ssa_to_subset: dict[int, frozenset[int]] = {
|
|
309
|
+
k: frozenset({k}) for k in range(num_operands)
|
|
310
|
+
}
|
|
311
|
+
total_cost = 0
|
|
312
|
+
max_size = 0
|
|
313
|
+
|
|
314
|
+
for i, j in ssa_path:
|
|
315
|
+
k12, flops12, _sym12 = paths.calc_k12_flops(
|
|
316
|
+
inputs, # type: ignore[arg-type]
|
|
317
|
+
output,
|
|
318
|
+
remaining, # type: ignore[arg-type]
|
|
319
|
+
i,
|
|
320
|
+
j,
|
|
321
|
+
size_dict,
|
|
322
|
+
oracle=symmetry_oracle,
|
|
323
|
+
ssa_to_subset=ssa_to_subset,
|
|
324
|
+
)
|
|
325
|
+
# Update ssa_to_subset for the new intermediate
|
|
326
|
+
new_ssa_id = len(inputs)
|
|
327
|
+
ssa_to_subset[new_ssa_id] = ssa_to_subset[i] | ssa_to_subset[j]
|
|
328
|
+
|
|
329
|
+
remaining.discard(i)
|
|
330
|
+
remaining.discard(j)
|
|
331
|
+
remaining.add(new_ssa_id)
|
|
332
|
+
inputs.append(k12)
|
|
333
|
+
total_cost += flops12
|
|
334
|
+
max_size = max(max_size, compute_size_by_dict(k12, size_dict))
|
|
335
|
+
|
|
336
|
+
return total_cost, max_size
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def _trial_greedy_ssa_path_and_cost(
|
|
340
|
+
r: int,
|
|
341
|
+
inputs: list[ArrayIndexType],
|
|
342
|
+
output: ArrayIndexType,
|
|
343
|
+
size_dict: dict[str, int],
|
|
344
|
+
choose_fn: Any,
|
|
345
|
+
cost_fn: Any,
|
|
346
|
+
symmetry_oracle: "SubgraphSymmetryOracle | None" = None,
|
|
347
|
+
) -> tuple[PathType, int, int]:
|
|
348
|
+
"""A single, repeatable, greedy trial run. **Returns:** ``ssa_path`` and cost."""
|
|
349
|
+
if r == 0:
|
|
350
|
+
# always start with the standard greedy approach
|
|
351
|
+
choose_fn = None
|
|
352
|
+
|
|
353
|
+
random_seed(r)
|
|
354
|
+
|
|
355
|
+
num_operands = len(inputs)
|
|
356
|
+
ssa_to_subset = {k: frozenset({k}) for k in range(num_operands)}
|
|
357
|
+
|
|
358
|
+
ssa_path = paths.ssa_greedy_optimize(
|
|
359
|
+
inputs,
|
|
360
|
+
output,
|
|
361
|
+
size_dict,
|
|
362
|
+
choose_fn,
|
|
363
|
+
cost_fn,
|
|
364
|
+
symmetry_oracle=symmetry_oracle,
|
|
365
|
+
ssa_to_subset=ssa_to_subset,
|
|
366
|
+
)
|
|
367
|
+
cost, size = ssa_path_compute_cost(
|
|
368
|
+
ssa_path, inputs, output, size_dict, symmetry_oracle=symmetry_oracle
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
return ssa_path, cost, size
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
class RandomGreedy(RandomOptimizer):
|
|
375
|
+
def __init__(
|
|
376
|
+
self,
|
|
377
|
+
cost_fn: str = "memory-removed-jitter",
|
|
378
|
+
temperature: float = 1.0,
|
|
379
|
+
rel_temperature: bool = True,
|
|
380
|
+
nbranch: int = 8,
|
|
381
|
+
**kwargs: Any,
|
|
382
|
+
):
|
|
383
|
+
"""Parameters:
|
|
384
|
+
cost_fn: A function that returns a heuristic 'cost' of a potential contraction
|
|
385
|
+
with which to sort candidates. Should have signature
|
|
386
|
+
`cost_fn(size12, size1, size2, k12, k1, k2)`.
|
|
387
|
+
temperature: When choosing a possible contraction, its relative probability will be
|
|
388
|
+
proportional to `exp(-cost / temperature)`. Thus the larger
|
|
389
|
+
`temperature` is, the further random paths will stray from the normal
|
|
390
|
+
'greedy' path. Conversely, if set to zero, only paths with exactly the
|
|
391
|
+
same cost as the best at each step will be explored.
|
|
392
|
+
rel_temperature: Whether to normalize the ``temperature`` at each step to the scale of
|
|
393
|
+
the best cost. This is generally beneficial as the magnitude of costs
|
|
394
|
+
can vary significantly throughout a contraction. If False, the
|
|
395
|
+
algorithm will end up branching when the absolute cost is low, but
|
|
396
|
+
stick to the 'greedy' path when the cost is high - this can also be
|
|
397
|
+
beneficial.
|
|
398
|
+
nbranch: How many potential paths to calculate probability for and choose from at each step.
|
|
399
|
+
kwargs: Supplied to RandomOptimizer.
|
|
400
|
+
"""
|
|
401
|
+
self.cost_fn = cost_fn
|
|
402
|
+
self.temperature = temperature
|
|
403
|
+
self.rel_temperature = rel_temperature
|
|
404
|
+
self.nbranch = nbranch
|
|
405
|
+
super().__init__(**kwargs)
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def choose_fn(self) -> Any:
|
|
409
|
+
"""The function that chooses which contraction to take - make this a
|
|
410
|
+
property so that ``temperature`` and ``nbranch`` etc. can be updated
|
|
411
|
+
between runs.
|
|
412
|
+
"""
|
|
413
|
+
if self.nbranch == 1:
|
|
414
|
+
return None
|
|
415
|
+
|
|
416
|
+
return functools.partial(
|
|
417
|
+
thermal_chooser,
|
|
418
|
+
temperature=self.temperature, # type: ignore[arg-type]
|
|
419
|
+
nbranch=self.nbranch,
|
|
420
|
+
rel_temperature=self.rel_temperature,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
def setup(
|
|
424
|
+
self,
|
|
425
|
+
inputs: list[ArrayIndexType],
|
|
426
|
+
output: ArrayIndexType,
|
|
427
|
+
size_dict: dict[str, int],
|
|
428
|
+
symmetry_oracle: "SubgraphSymmetryOracle | None" = None,
|
|
429
|
+
**kwargs: Any,
|
|
430
|
+
) -> tuple[Any, Any]:
|
|
431
|
+
fn = _trial_greedy_ssa_path_and_cost
|
|
432
|
+
args = (
|
|
433
|
+
inputs,
|
|
434
|
+
output,
|
|
435
|
+
size_dict,
|
|
436
|
+
self.choose_fn,
|
|
437
|
+
self.cost_fn,
|
|
438
|
+
symmetry_oracle,
|
|
439
|
+
)
|
|
440
|
+
return fn, args
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def random_greedy(
|
|
444
|
+
inputs: list[ArrayIndexType],
|
|
445
|
+
output: ArrayIndexType,
|
|
446
|
+
idx_dict: dict[str, int],
|
|
447
|
+
memory_limit: int | None = None,
|
|
448
|
+
symmetry_oracle: "SubgraphSymmetryOracle | None" = None,
|
|
449
|
+
**optimizer_kwargs: Any,
|
|
450
|
+
) -> ArrayType:
|
|
451
|
+
"""A simple wrapper around the RandomGreedy optimizer."""
|
|
452
|
+
optimizer = RandomGreedy(**optimizer_kwargs)
|
|
453
|
+
return optimizer(
|
|
454
|
+
inputs,
|
|
455
|
+
output,
|
|
456
|
+
idx_dict,
|
|
457
|
+
memory_limit,
|
|
458
|
+
symmetry_oracle=symmetry_oracle,
|
|
459
|
+
)
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
random_greedy_128 = functools.partial(random_greedy, max_repeats=128)
|