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.
Files changed (115) hide show
  1. benchmarks/__init__.py +1 -0
  2. benchmarks/__main__.py +6 -0
  3. benchmarks/_baseline.py +171 -0
  4. benchmarks/_bitwise.py +231 -0
  5. benchmarks/_complex.py +176 -0
  6. benchmarks/_contractions.py +291 -0
  7. benchmarks/_fft.py +198 -0
  8. benchmarks/_impl_urls.py +139 -0
  9. benchmarks/_linalg.py +197 -0
  10. benchmarks/_linalg_delegates.py +407 -0
  11. benchmarks/_metadata.py +141 -0
  12. benchmarks/_misc.py +653 -0
  13. benchmarks/_perf.py +321 -0
  14. benchmarks/_perm_group_calibration.py +175 -0
  15. benchmarks/_pointwise.py +372 -0
  16. benchmarks/_polynomial.py +193 -0
  17. benchmarks/_random.py +209 -0
  18. benchmarks/_reductions.py +136 -0
  19. benchmarks/_sorting.py +289 -0
  20. benchmarks/_stats.py +137 -0
  21. benchmarks/_window.py +92 -0
  22. benchmarks/accumulation/__init__.py +0 -0
  23. benchmarks/accumulation/bench_cost_compute.py +138 -0
  24. benchmarks/dashboard.py +312 -0
  25. benchmarks/runner.py +636 -0
  26. flopscope/__init__.py +273 -0
  27. flopscope/_accumulation/__init__.py +13 -0
  28. flopscope/_accumulation/_bipartite.py +121 -0
  29. flopscope/_accumulation/_burnside.py +51 -0
  30. flopscope/_accumulation/_cache.py +146 -0
  31. flopscope/_accumulation/_components.py +153 -0
  32. flopscope/_accumulation/_cost.py +1414 -0
  33. flopscope/_accumulation/_cost_descriptions.py +63 -0
  34. flopscope/_accumulation/_detection.py +318 -0
  35. flopscope/_accumulation/_ladder.py +191 -0
  36. flopscope/_accumulation/_output_orbit.py +104 -0
  37. flopscope/_accumulation/_partition.py +290 -0
  38. flopscope/_accumulation/_path_info.py +211 -0
  39. flopscope/_accumulation/_public.py +169 -0
  40. flopscope/_accumulation/_reduction.py +310 -0
  41. flopscope/_accumulation/_regimes.py +303 -0
  42. flopscope/_accumulation/_shape.py +33 -0
  43. flopscope/_accumulation/_wreath.py +209 -0
  44. flopscope/_budget.py +1027 -0
  45. flopscope/_config.py +118 -0
  46. flopscope/_counting_ops.py +451 -0
  47. flopscope/_display.py +478 -0
  48. flopscope/_docstrings.py +59 -0
  49. flopscope/_dtypes.py +20 -0
  50. flopscope/_einsum.py +717 -0
  51. flopscope/_errstate.py +25 -0
  52. flopscope/_flops.py +282 -0
  53. flopscope/_free_ops.py +2654 -0
  54. flopscope/_ndarray.py +1126 -0
  55. flopscope/_opt_einsum/LICENSE +21 -0
  56. flopscope/_opt_einsum/NOTICE +59 -0
  57. flopscope/_opt_einsum/__init__.py +209 -0
  58. flopscope/_opt_einsum/_contract.py +1478 -0
  59. flopscope/_opt_einsum/_helpers.py +164 -0
  60. flopscope/_opt_einsum/_hsluv.py +273 -0
  61. flopscope/_opt_einsum/_path_random.py +462 -0
  62. flopscope/_opt_einsum/_paths.py +1653 -0
  63. flopscope/_opt_einsum/_subgraph_symmetry.py +544 -0
  64. flopscope/_opt_einsum/_symmetry.py +140 -0
  65. flopscope/_opt_einsum/_typing.py +37 -0
  66. flopscope/_perm_group.py +717 -0
  67. flopscope/_pointwise.py +2522 -0
  68. flopscope/_polynomial.py +278 -0
  69. flopscope/_registry.py +3216 -0
  70. flopscope/_sorting_ops.py +571 -0
  71. flopscope/_symmetric.py +812 -0
  72. flopscope/_symmetry_transport.py +510 -0
  73. flopscope/_symmetry_utils.py +669 -0
  74. flopscope/_type_info.py +12 -0
  75. flopscope/_unwrap.py +70 -0
  76. flopscope/_validation.py +83 -0
  77. flopscope/_version_check.py +46 -0
  78. flopscope/_weights.py +195 -0
  79. flopscope/_window.py +177 -0
  80. flopscope/accounting.py +565 -0
  81. flopscope/data/default_weights.json +462 -0
  82. flopscope/data/weights.csv +509 -0
  83. flopscope/errors.py +197 -0
  84. flopscope/numpy/__init__.py +878 -0
  85. flopscope/numpy/fft/__init__.py +55 -0
  86. flopscope/numpy/fft/_free.py +51 -0
  87. flopscope/numpy/fft/_transforms.py +695 -0
  88. flopscope/numpy/linalg/__init__.py +105 -0
  89. flopscope/numpy/linalg/_aliases.py +126 -0
  90. flopscope/numpy/linalg/_compound.py +161 -0
  91. flopscope/numpy/linalg/_decompositions.py +353 -0
  92. flopscope/numpy/linalg/_properties.py +533 -0
  93. flopscope/numpy/linalg/_solvers.py +444 -0
  94. flopscope/numpy/linalg/_svd.py +122 -0
  95. flopscope/numpy/random/__init__.py +684 -0
  96. flopscope/numpy/random/_cost_formulas.py +115 -0
  97. flopscope/numpy/random/_counted_classes.py +241 -0
  98. flopscope/numpy/testing/__init__.py +13 -0
  99. flopscope/numpy/typing/__init__.py +30 -0
  100. flopscope/py.typed +0 -0
  101. flopscope/stats/__init__.py +84 -0
  102. flopscope/stats/_base.py +77 -0
  103. flopscope/stats/_cauchy.py +146 -0
  104. flopscope/stats/_erf.py +190 -0
  105. flopscope/stats/_expon.py +146 -0
  106. flopscope/stats/_laplace.py +150 -0
  107. flopscope/stats/_logistic.py +148 -0
  108. flopscope/stats/_lognorm.py +160 -0
  109. flopscope/stats/_ndtri.py +133 -0
  110. flopscope/stats/_norm.py +149 -0
  111. flopscope/stats/_truncnorm.py +186 -0
  112. flopscope/stats/_uniform.py +141 -0
  113. flopscope-0.2.0.dist-info/METADATA +23 -0
  114. flopscope-0.2.0.dist-info/RECORD +115 -0
  115. 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)