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
flopscope/__init__.py
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""Flopscope: FLOP-counting numpy primitives for the Mechanistic Estimation Challenge.
|
|
2
|
+
|
|
3
|
+
The public API is structured JAX-style:
|
|
4
|
+
|
|
5
|
+
- ``flopscope`` (this module) exports flopscope-specific primitives: budget
|
|
6
|
+
contexts, configuration, symmetric tensors, the unified
|
|
7
|
+
:class:`SymmetryGroup`, errors, and the :class:`FlopscopeArray` type.
|
|
8
|
+
- ``flopscope.numpy`` exports the full counted numpy-shaped surface
|
|
9
|
+
(``einsum``, ``array``, ``linspace``, ``linalg``, ``fft``, ``random``, ...).
|
|
10
|
+
Attributes not explicitly implemented there fall back to raw ``numpy``.
|
|
11
|
+
- ``flopscope.accounting`` exposes analytical cost helpers
|
|
12
|
+
(``einsum_cost``, ``pointwise_cost``, ``reduction_cost``, ...).
|
|
13
|
+
- ``flopscope.stats`` hosts statistical-distribution primitives (not a
|
|
14
|
+
numpy submodule; closer in spirit to scipy.stats).
|
|
15
|
+
|
|
16
|
+
Usage::
|
|
17
|
+
|
|
18
|
+
import flopscope as flops
|
|
19
|
+
import flopscope.numpy as fnp
|
|
20
|
+
|
|
21
|
+
flops.configure(symmetry_warnings=False)
|
|
22
|
+
with flops.BudgetContext(flop_budget=1_000_000) as budget:
|
|
23
|
+
W = fnp.array(weight_matrix)
|
|
24
|
+
h = fnp.einsum('ij,j->i', W, x)
|
|
25
|
+
h = fnp.maximum(h, 0)
|
|
26
|
+
print(budget.summary())
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
import importlib as _importlib
|
|
30
|
+
|
|
31
|
+
import numpy as _np
|
|
32
|
+
|
|
33
|
+
from flopscope._registry import REGISTRY_META as _REGISTRY_META
|
|
34
|
+
|
|
35
|
+
__version__ = f"0.2.0+np{_np.__version__}"
|
|
36
|
+
__numpy_version__ = _np.__version__
|
|
37
|
+
__numpy_pinned__ = _REGISTRY_META["numpy_version"]
|
|
38
|
+
__numpy_supported__ = _REGISTRY_META.get("numpy_supported", ">=2.0.0,<2.5.0")
|
|
39
|
+
|
|
40
|
+
from flopscope._version_check import check_numpy_version as _check_numpy_version
|
|
41
|
+
|
|
42
|
+
_check_numpy_version(__numpy_supported__)
|
|
43
|
+
|
|
44
|
+
# --- Symmetry-aware accumulation cost (einsum + reduction) ---
|
|
45
|
+
from flopscope._accumulation import ( # noqa: F401,E402
|
|
46
|
+
AccumulationCost,
|
|
47
|
+
ComponentCost,
|
|
48
|
+
RegimeStep,
|
|
49
|
+
einsum_accumulation_cost,
|
|
50
|
+
reduction_accumulation_cost,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# --- Budget and diagnostics ---
|
|
54
|
+
from flopscope._budget import ( # noqa: F401,E402
|
|
55
|
+
BudgetContext,
|
|
56
|
+
OpRecord,
|
|
57
|
+
budget,
|
|
58
|
+
budget_reset,
|
|
59
|
+
budget_summary_dict,
|
|
60
|
+
namespace,
|
|
61
|
+
)
|
|
62
|
+
from flopscope._config import configure # noqa: F401,E402
|
|
63
|
+
from flopscope._display import budget_live, budget_summary # noqa: F401,E402
|
|
64
|
+
|
|
65
|
+
# --- Array type (flopscope-specific) ---
|
|
66
|
+
from flopscope._ndarray import FlopscopeArray # noqa: F401,E402
|
|
67
|
+
|
|
68
|
+
# --- Path optimization types ---
|
|
69
|
+
from flopscope._opt_einsum import PathInfo, StepInfo # noqa: F401,E402
|
|
70
|
+
|
|
71
|
+
# --- Symmetry (post-#51 unified surface) ---
|
|
72
|
+
from flopscope._perm_group import SymmetryGroup # noqa: F401,E402
|
|
73
|
+
|
|
74
|
+
# --- Symmetric tensor ---
|
|
75
|
+
from flopscope._symmetric import ( # noqa: F401,E402
|
|
76
|
+
SymmetricTensor,
|
|
77
|
+
as_symmetric,
|
|
78
|
+
is_symmetric,
|
|
79
|
+
symmetrize,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# --- Errors ---
|
|
83
|
+
from flopscope.errors import ( # noqa: F401,E402
|
|
84
|
+
BudgetExhaustedError,
|
|
85
|
+
FlopscopeError,
|
|
86
|
+
FlopscopeWarning,
|
|
87
|
+
NoBudgetContextError,
|
|
88
|
+
SymmetryError,
|
|
89
|
+
SymmetryLossWarning,
|
|
90
|
+
TimeExhaustedError,
|
|
91
|
+
UnsupportedFunctionError,
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def einsum_clear_caches() -> None:
|
|
96
|
+
"""Clear all flopscope einsum-related LRU caches.
|
|
97
|
+
|
|
98
|
+
Clears both the einsum path cache (consulted by ``fnp.einsum`` and
|
|
99
|
+
``fnp.einsum_path``) and the einsum accumulation-cost cache (consulted
|
|
100
|
+
by ``fnp.einsum`` and ``flopscope.einsum_accumulation_cost``).
|
|
101
|
+
|
|
102
|
+
Useful when benchmarking cold-call latency. ``fnp.clear_einsum_cache``
|
|
103
|
+
still exists and clears only the path cache.
|
|
104
|
+
"""
|
|
105
|
+
from flopscope._accumulation._cache import _accumulation_cache
|
|
106
|
+
from flopscope._einsum import _path_cache
|
|
107
|
+
|
|
108
|
+
_path_cache.cache_clear()
|
|
109
|
+
_accumulation_cache.cache_clear()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def einsum_cache_info() -> dict:
|
|
113
|
+
"""Return cache statistics for the einsum path + accumulation caches.
|
|
114
|
+
|
|
115
|
+
Returns
|
|
116
|
+
-------
|
|
117
|
+
dict
|
|
118
|
+
``{"path": CacheInfo, "accumulation": CacheInfo}`` where each value
|
|
119
|
+
is a standard ``functools.lru_cache`` info tuple with ``hits``,
|
|
120
|
+
``misses``, ``maxsize``, and ``currsize``.
|
|
121
|
+
"""
|
|
122
|
+
from flopscope._accumulation._cache import _accumulation_cache
|
|
123
|
+
from flopscope._einsum import _path_cache
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
"path": _path_cache.cache_info(),
|
|
127
|
+
"accumulation": _accumulation_cache.cache_info(),
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def reduction_clear_cache() -> None:
|
|
132
|
+
"""Clear the reduction accumulation-cost cache.
|
|
133
|
+
|
|
134
|
+
Consulted by ``flopscope.reduction_accumulation_cost`` and the
|
|
135
|
+
``np.ufunc.reduce`` family wrappers (``fnp.sum``, ``fnp.mean``,
|
|
136
|
+
``fnp.median``, etc.).
|
|
137
|
+
"""
|
|
138
|
+
from flopscope._accumulation._cache import _reduction_cache
|
|
139
|
+
|
|
140
|
+
_reduction_cache.cache_clear()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def reduction_cache_info():
|
|
144
|
+
"""Return cache statistics for the reduction accumulation-cost cache.
|
|
145
|
+
|
|
146
|
+
Returns a standard ``functools.lru_cache`` info tuple with ``hits``,
|
|
147
|
+
``misses``, ``maxsize``, and ``currsize``.
|
|
148
|
+
"""
|
|
149
|
+
from flopscope._accumulation._cache import _reduction_cache
|
|
150
|
+
|
|
151
|
+
return _reduction_cache.cache_info()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def clear_cache() -> None:
|
|
155
|
+
"""Clear all flopscope LRU caches (einsum + reduction).
|
|
156
|
+
|
|
157
|
+
Convenience aggregate over :func:`einsum_clear_caches` and
|
|
158
|
+
:func:`reduction_clear_cache`. Use the per-domain variants if you
|
|
159
|
+
only need to invalidate one cache.
|
|
160
|
+
"""
|
|
161
|
+
einsum_clear_caches()
|
|
162
|
+
reduction_clear_cache()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def tier2_reduction_cost(a, axis=None, *, dense_per_output_cost=None):
|
|
166
|
+
"""Cost for selection-style reductions (median, percentile, quantile).
|
|
167
|
+
|
|
168
|
+
Returns ``num_output_orbits × dense_per_output_cost`` — one partition
|
|
169
|
+
pass per unique output cell. Under symmetry, orbit-shared output
|
|
170
|
+
cells share the partition pass; ``num_output_orbits`` is computed
|
|
171
|
+
from the input's declared symmetry.
|
|
172
|
+
|
|
173
|
+
Parameters
|
|
174
|
+
----------
|
|
175
|
+
a : numpy.ndarray or SymmetricTensor
|
|
176
|
+
Input array. ``SymmetricTensor`` inputs contribute their declared
|
|
177
|
+
symmetry to the orbit count.
|
|
178
|
+
axis : int, tuple of int, or None
|
|
179
|
+
Axes being reduced. ``None`` (default) reduces all axes.
|
|
180
|
+
dense_per_output_cost : int, optional
|
|
181
|
+
The dense per-output cost. If ``None`` (the default), it is set
|
|
182
|
+
to the product of the reduced axes' lengths — i.e., one
|
|
183
|
+
partition pass per output cell, which is how ``fnp.median``,
|
|
184
|
+
``fnp.percentile``, and ``fnp.quantile`` are charged. Pass an
|
|
185
|
+
explicit value to model a custom Tier-2 selection cost.
|
|
186
|
+
|
|
187
|
+
Returns
|
|
188
|
+
-------
|
|
189
|
+
int
|
|
190
|
+
Total flops charged.
|
|
191
|
+
"""
|
|
192
|
+
import math as _math
|
|
193
|
+
|
|
194
|
+
import numpy as _np
|
|
195
|
+
|
|
196
|
+
from flopscope._pointwise import _tier2_reduction_cost
|
|
197
|
+
|
|
198
|
+
if not isinstance(a, _np.ndarray):
|
|
199
|
+
a = _np.asarray(a)
|
|
200
|
+
|
|
201
|
+
if dense_per_output_cost is None:
|
|
202
|
+
if axis is None:
|
|
203
|
+
dense_per_output_cost = _math.prod(a.shape) if a.shape else 1
|
|
204
|
+
else:
|
|
205
|
+
axes = (axis,) if isinstance(axis, int) else tuple(axis)
|
|
206
|
+
dense_per_output_cost = _math.prod(a.shape[i] for i in axes)
|
|
207
|
+
|
|
208
|
+
return _tier2_reduction_cost(a, axis, dense_per_output_cost)
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
_LAZY_SUBMODULES = frozenset({"numpy", "accounting", "stats"})
|
|
212
|
+
|
|
213
|
+
__all__ = [
|
|
214
|
+
"AccumulationCost",
|
|
215
|
+
"BudgetContext",
|
|
216
|
+
"BudgetExhaustedError",
|
|
217
|
+
"ComponentCost",
|
|
218
|
+
"FlopscopeArray",
|
|
219
|
+
"FlopscopeError",
|
|
220
|
+
"FlopscopeWarning",
|
|
221
|
+
"NoBudgetContextError",
|
|
222
|
+
"OpRecord",
|
|
223
|
+
"PathInfo",
|
|
224
|
+
"RegimeStep",
|
|
225
|
+
"StepInfo",
|
|
226
|
+
"SymmetricTensor",
|
|
227
|
+
"SymmetryError",
|
|
228
|
+
"SymmetryGroup",
|
|
229
|
+
"SymmetryLossWarning",
|
|
230
|
+
"TimeExhaustedError",
|
|
231
|
+
"UnsupportedFunctionError",
|
|
232
|
+
"__numpy_pinned__",
|
|
233
|
+
"__numpy_supported__",
|
|
234
|
+
"__numpy_version__",
|
|
235
|
+
"__version__",
|
|
236
|
+
"accounting",
|
|
237
|
+
"as_symmetric",
|
|
238
|
+
"budget",
|
|
239
|
+
"budget_live",
|
|
240
|
+
"budget_reset",
|
|
241
|
+
"budget_summary",
|
|
242
|
+
"budget_summary_dict",
|
|
243
|
+
"clear_cache",
|
|
244
|
+
"configure",
|
|
245
|
+
"einsum_accumulation_cost",
|
|
246
|
+
"einsum_cache_info",
|
|
247
|
+
"einsum_clear_caches",
|
|
248
|
+
"is_symmetric",
|
|
249
|
+
"namespace",
|
|
250
|
+
"numpy",
|
|
251
|
+
"reduction_accumulation_cost",
|
|
252
|
+
"reduction_cache_info",
|
|
253
|
+
"reduction_clear_cache",
|
|
254
|
+
"stats",
|
|
255
|
+
"symmetrize",
|
|
256
|
+
"tier2_reduction_cost",
|
|
257
|
+
]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def __getattr__(name: str):
|
|
261
|
+
if name in _LAZY_SUBMODULES:
|
|
262
|
+
module = _importlib.import_module(f"flopscope.{name}")
|
|
263
|
+
globals()[name] = module
|
|
264
|
+
return module
|
|
265
|
+
raise AttributeError(
|
|
266
|
+
f"flopscope does not provide {name!r}. "
|
|
267
|
+
f"Numpy-shaped operations live under 'flopscope.numpy' "
|
|
268
|
+
f"(try `import flopscope.numpy as fnp; fnp.{name}`)."
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def __dir__() -> list[str]:
|
|
273
|
+
return sorted(set(globals()) | _LAZY_SUBMODULES)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Symmetry-aware einsum accumulation cost — JS-mirrored α/M ladder."""
|
|
2
|
+
|
|
3
|
+
from ._cost import AccumulationCost, ComponentCost
|
|
4
|
+
from ._ladder import RegimeStep
|
|
5
|
+
from ._public import einsum_accumulation_cost, reduction_accumulation_cost
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"AccumulationCost",
|
|
9
|
+
"ComponentCost",
|
|
10
|
+
"RegimeStep",
|
|
11
|
+
"einsum_accumulation_cost",
|
|
12
|
+
"reduction_accumulation_cost",
|
|
13
|
+
]
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"""Bipartite graph + incidence matrix construction for the σ-loop.
|
|
2
|
+
|
|
3
|
+
(buildBipartite + buildIncidenceMatrix only — runSigmaLoop lives in _detection.py).
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from collections.abc import Sequence
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class UVertex:
|
|
14
|
+
op_idx: int
|
|
15
|
+
class_id: int
|
|
16
|
+
labels: frozenset[str]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True)
|
|
20
|
+
class BipartiteGraph:
|
|
21
|
+
u_vertices: tuple[UVertex, ...]
|
|
22
|
+
incidence: tuple[dict[str, int], ...] # one per u-vertex
|
|
23
|
+
u_operand: tuple[int, ...]
|
|
24
|
+
operand_labels: tuple[frozenset[str], ...]
|
|
25
|
+
all_labels: tuple[str, ...] # sorted union
|
|
26
|
+
free_labels: frozenset[str]
|
|
27
|
+
summed_labels: frozenset[str]
|
|
28
|
+
identical_groups: tuple[tuple[int, ...], ...]
|
|
29
|
+
num_operands: int
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class IncidenceMatrix:
|
|
34
|
+
matrix: tuple[tuple[int, ...], ...]
|
|
35
|
+
labels: tuple[str, ...]
|
|
36
|
+
col_fingerprints: dict[str, tuple[int, ...]]
|
|
37
|
+
fp_to_labels: dict[tuple[int, ...], frozenset[str]]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_bipartite(
|
|
41
|
+
*,
|
|
42
|
+
subscripts: Sequence[str],
|
|
43
|
+
output: str,
|
|
44
|
+
operand_names: Sequence[str],
|
|
45
|
+
) -> BipartiteGraph:
|
|
46
|
+
"""Build the bipartite graph: one U-vertex per axis of each operand.
|
|
47
|
+
|
|
48
|
+
Mirrors algorithm.js#buildBipartite. No axis merging — per-operand symmetry
|
|
49
|
+
is handled later by the σ-loop's wreath enumeration, not by collapsing axes here.
|
|
50
|
+
"""
|
|
51
|
+
num_ops = len(subscripts)
|
|
52
|
+
u_vertices: list[UVertex] = []
|
|
53
|
+
incidence: list[dict[str, int]] = []
|
|
54
|
+
u_operand: list[int] = []
|
|
55
|
+
operand_labels: list[frozenset[str]] = []
|
|
56
|
+
|
|
57
|
+
for op_idx, sub in enumerate(subscripts):
|
|
58
|
+
operand_labels.append(frozenset(sub))
|
|
59
|
+
for axis_idx, ch in enumerate(sub):
|
|
60
|
+
u_vertices.append(
|
|
61
|
+
UVertex(op_idx=op_idx, class_id=axis_idx, labels=frozenset({ch}))
|
|
62
|
+
)
|
|
63
|
+
incidence.append({ch: 1})
|
|
64
|
+
u_operand.append(op_idx)
|
|
65
|
+
|
|
66
|
+
all_labels_set: set[str] = set()
|
|
67
|
+
for sub in subscripts:
|
|
68
|
+
all_labels_set.update(sub)
|
|
69
|
+
all_labels = tuple(sorted(all_labels_set))
|
|
70
|
+
output_set = set(output)
|
|
71
|
+
free_labels = frozenset(lbl for lbl in all_labels if lbl in output_set)
|
|
72
|
+
summed_labels = frozenset(lbl for lbl in all_labels if lbl not in output_set)
|
|
73
|
+
|
|
74
|
+
# Group operand positions by name (Python id() equivalent at the einsum-level
|
|
75
|
+
# is "same operand_name").
|
|
76
|
+
name_to_positions: dict[str, list[int]] = {}
|
|
77
|
+
for i, name in enumerate(operand_names):
|
|
78
|
+
name_to_positions.setdefault(name, []).append(i)
|
|
79
|
+
identical_groups = tuple(
|
|
80
|
+
tuple(positions)
|
|
81
|
+
for positions in name_to_positions.values()
|
|
82
|
+
if len(positions) >= 2
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
return BipartiteGraph(
|
|
86
|
+
u_vertices=tuple(u_vertices),
|
|
87
|
+
incidence=tuple(incidence),
|
|
88
|
+
u_operand=tuple(u_operand),
|
|
89
|
+
operand_labels=tuple(operand_labels),
|
|
90
|
+
all_labels=all_labels,
|
|
91
|
+
free_labels=free_labels,
|
|
92
|
+
summed_labels=summed_labels,
|
|
93
|
+
identical_groups=identical_groups,
|
|
94
|
+
num_operands=num_ops,
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_incidence_matrix(graph: BipartiteGraph) -> IncidenceMatrix:
|
|
99
|
+
"""Build the dense incidence matrix and column fingerprints.
|
|
100
|
+
|
|
101
|
+
Mirrors algorithm.js#buildIncidenceMatrix.
|
|
102
|
+
"""
|
|
103
|
+
labels = graph.all_labels
|
|
104
|
+
matrix = tuple(
|
|
105
|
+
tuple(graph.incidence[row_idx].get(lbl, 0) for lbl in labels)
|
|
106
|
+
for row_idx in range(len(graph.u_vertices))
|
|
107
|
+
)
|
|
108
|
+
col_fingerprints: dict[str, tuple[int, ...]] = {}
|
|
109
|
+
fp_to_labels_mut: dict[tuple[int, ...], set[str]] = {}
|
|
110
|
+
for c, label in enumerate(labels):
|
|
111
|
+
fp = tuple(row[c] for row in matrix)
|
|
112
|
+
col_fingerprints[label] = fp
|
|
113
|
+
fp_to_labels_mut.setdefault(fp, set()).add(label)
|
|
114
|
+
fp_to_labels = {fp: frozenset(s) for fp, s in fp_to_labels_mut.items()}
|
|
115
|
+
|
|
116
|
+
return IncidenceMatrix(
|
|
117
|
+
matrix=matrix,
|
|
118
|
+
labels=labels,
|
|
119
|
+
col_fingerprints=col_fingerprints,
|
|
120
|
+
fp_to_labels=fp_to_labels,
|
|
121
|
+
)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Size-aware Burnside lemma for orbit counting under heterogeneous label dimensions.
|
|
2
|
+
|
|
3
|
+
M = (1 / |G|) · Σ_{g ∈ G} ∏_{c ∈ cycles(g)} n_c
|
|
4
|
+
|
|
5
|
+
where n_c is the common size of the labels in cycle c. Within any cycle of a valid
|
|
6
|
+
symmetry, all labels must share a size (asserted at group-construction time elsewhere;
|
|
7
|
+
re-asserted here for safety).
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from collections.abc import Iterable, Sequence
|
|
13
|
+
|
|
14
|
+
from flopscope._perm_group import _Permutation as Permutation
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _common_size_or_throw(cycle: Sequence[int], sizes: Sequence[int]) -> int:
|
|
18
|
+
n0 = sizes[cycle[0]]
|
|
19
|
+
for idx in cycle:
|
|
20
|
+
if sizes[idx] != n0:
|
|
21
|
+
cycle_str = ",".join(str(i) for i in cycle)
|
|
22
|
+
cycle_sizes = ",".join(str(sizes[i]) for i in cycle)
|
|
23
|
+
raise ValueError(
|
|
24
|
+
f"cycle size mismatch: labels {cycle_str} have sizes {cycle_sizes} — "
|
|
25
|
+
f"a permutation can only mix labels of equal size."
|
|
26
|
+
)
|
|
27
|
+
return n0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def size_aware_burnside(elements: Iterable[Permutation], sizes: Sequence[int]) -> int:
|
|
31
|
+
"""Count orbits of `elements` acting on the assignment grid ∏ [sizes].
|
|
32
|
+
|
|
33
|
+
Returns ``M = |X / G|`` where ``X = ∏_ℓ [sizes[ℓ]]``.
|
|
34
|
+
"""
|
|
35
|
+
elements_tuple = tuple(elements)
|
|
36
|
+
if not elements_tuple:
|
|
37
|
+
raise ValueError("size_aware_burnside requires at least one group element")
|
|
38
|
+
|
|
39
|
+
total = 0
|
|
40
|
+
for g in elements_tuple:
|
|
41
|
+
contribution = 1
|
|
42
|
+
for cycle in g.full_cyclic_form:
|
|
43
|
+
contribution *= _common_size_or_throw(cycle, sizes)
|
|
44
|
+
total += contribution
|
|
45
|
+
|
|
46
|
+
if total % len(elements_tuple) != 0:
|
|
47
|
+
raise ValueError(
|
|
48
|
+
f"Burnside sum {total} not divisible by |G|={len(elements_tuple)} — "
|
|
49
|
+
f"group elements probably incomplete or inconsistent."
|
|
50
|
+
)
|
|
51
|
+
return total // len(elements_tuple)
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""LRU cache for compute_accumulation_cost.
|
|
2
|
+
|
|
3
|
+
Lives in _accumulation rather than _einsum.py so that both the public inspection
|
|
4
|
+
function (_public.einsum_accumulation_cost) and the einsum path (_einsum._get_accumulation_cost)
|
|
5
|
+
can share it without circular imports.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import functools
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from ._cost import AccumulationCost, compute_accumulation_cost
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _make_accumulation_cache(maxsize: int):
|
|
17
|
+
@functools.lru_cache(maxsize=maxsize)
|
|
18
|
+
def _compute(
|
|
19
|
+
canonical_subscripts: str,
|
|
20
|
+
input_parts: tuple,
|
|
21
|
+
output_subscript: str,
|
|
22
|
+
shapes: tuple,
|
|
23
|
+
sym_fingerprint: tuple,
|
|
24
|
+
identity_pattern: tuple | None,
|
|
25
|
+
partition_budget: int | None,
|
|
26
|
+
) -> AccumulationCost:
|
|
27
|
+
# Reconstruct per-op symmetries from the fingerprint.
|
|
28
|
+
from flopscope._perm_group import SymmetryGroup
|
|
29
|
+
from flopscope._perm_group import _PermutationCompat as Permutation
|
|
30
|
+
|
|
31
|
+
per_op_symmetries: list[Any] = []
|
|
32
|
+
for fp_entry in sym_fingerprint:
|
|
33
|
+
if fp_entry is None:
|
|
34
|
+
per_op_symmetries.append(None)
|
|
35
|
+
continue
|
|
36
|
+
axes, gen_arrays = fp_entry
|
|
37
|
+
gens = [Permutation(list(g)) for g in gen_arrays]
|
|
38
|
+
group = SymmetryGroup(*gens, axes=axes) if gens else None
|
|
39
|
+
per_op_symmetries.append(group)
|
|
40
|
+
|
|
41
|
+
return compute_accumulation_cost(
|
|
42
|
+
canonical_subscripts=canonical_subscripts,
|
|
43
|
+
input_parts=input_parts,
|
|
44
|
+
output_subscript=output_subscript,
|
|
45
|
+
shapes=shapes,
|
|
46
|
+
per_op_symmetries=tuple(per_op_symmetries),
|
|
47
|
+
identity_pattern=identity_pattern,
|
|
48
|
+
partition_budget=partition_budget,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
return _compute
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
_accumulation_cache = _make_accumulation_cache(4096)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get_accumulation_cost_cached(
|
|
58
|
+
*,
|
|
59
|
+
canonical_subscripts: str,
|
|
60
|
+
input_parts: tuple,
|
|
61
|
+
output_subscript: str,
|
|
62
|
+
shapes: tuple,
|
|
63
|
+
sym_fingerprint: tuple,
|
|
64
|
+
identity_pattern: tuple | None,
|
|
65
|
+
partition_budget: int | None,
|
|
66
|
+
) -> AccumulationCost:
|
|
67
|
+
"""Cached entry point. Routed through by both public and einsum-internal callers."""
|
|
68
|
+
return _accumulation_cache(
|
|
69
|
+
canonical_subscripts,
|
|
70
|
+
tuple(input_parts),
|
|
71
|
+
output_subscript,
|
|
72
|
+
shapes,
|
|
73
|
+
sym_fingerprint,
|
|
74
|
+
identity_pattern,
|
|
75
|
+
partition_budget,
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def rebuild_accumulation_cache(maxsize: int) -> None:
|
|
80
|
+
"""Rebuild the cache with a new maxsize."""
|
|
81
|
+
global _accumulation_cache
|
|
82
|
+
_accumulation_cache = _make_accumulation_cache(maxsize)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _make_reduction_cache(maxsize: int):
|
|
86
|
+
@functools.lru_cache(maxsize=maxsize)
|
|
87
|
+
def _compute(
|
|
88
|
+
input_shape: tuple,
|
|
89
|
+
axes_summed: tuple,
|
|
90
|
+
sym_fingerprint: tuple,
|
|
91
|
+
op_factor: int,
|
|
92
|
+
extra_ops: int,
|
|
93
|
+
partition_budget: int | None,
|
|
94
|
+
) -> AccumulationCost:
|
|
95
|
+
from flopscope._perm_group import SymmetryGroup
|
|
96
|
+
from flopscope._perm_group import _PermutationCompat as Permutation
|
|
97
|
+
|
|
98
|
+
# Reconstruct symmetry from fingerprint.
|
|
99
|
+
if sym_fingerprint == (None,) or sym_fingerprint == ():
|
|
100
|
+
symmetry = None
|
|
101
|
+
else:
|
|
102
|
+
axes, gen_arrays = sym_fingerprint
|
|
103
|
+
gens = [Permutation(list(g)) for g in gen_arrays]
|
|
104
|
+
symmetry = SymmetryGroup(*gens, axes=axes) if gens else None
|
|
105
|
+
|
|
106
|
+
from ._reduction import compute_reduction_accumulation_cost
|
|
107
|
+
|
|
108
|
+
return compute_reduction_accumulation_cost(
|
|
109
|
+
input_shape=input_shape,
|
|
110
|
+
axes_summed=axes_summed,
|
|
111
|
+
symmetry=symmetry,
|
|
112
|
+
op_factor=op_factor,
|
|
113
|
+
extra_ops=extra_ops,
|
|
114
|
+
partition_budget=partition_budget,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
return _compute
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
_reduction_cache = _make_reduction_cache(4096)
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def get_reduction_cost_cached(
|
|
124
|
+
*,
|
|
125
|
+
input_shape: tuple,
|
|
126
|
+
axes_summed: tuple,
|
|
127
|
+
sym_fingerprint: tuple,
|
|
128
|
+
op_factor: int,
|
|
129
|
+
extra_ops: int,
|
|
130
|
+
partition_budget: int | None,
|
|
131
|
+
) -> AccumulationCost:
|
|
132
|
+
"""Cached entry point for reduction-cost lookups."""
|
|
133
|
+
return _reduction_cache(
|
|
134
|
+
input_shape,
|
|
135
|
+
axes_summed,
|
|
136
|
+
sym_fingerprint,
|
|
137
|
+
op_factor,
|
|
138
|
+
extra_ops,
|
|
139
|
+
partition_budget,
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def rebuild_reduction_cache(maxsize: int) -> None:
|
|
144
|
+
"""Rebuild the cache with a new maxsize."""
|
|
145
|
+
global _reduction_cache
|
|
146
|
+
_reduction_cache = _make_reduction_cache(maxsize)
|