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
benchmarks/_perf.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""Measure floating-point work for benchmark operations.
|
|
2
|
+
|
|
3
|
+
Primary method: Linux ``perf stat`` hardware counters (exact FP op counts).
|
|
4
|
+
Fallback: wall-clock time measurement (relative proxy, works everywhere).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
import tempfile
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
PERF_EVENTS = [
|
|
18
|
+
"fp_arith_inst_retired.scalar_double",
|
|
19
|
+
"fp_arith_inst_retired.128b_packed_double",
|
|
20
|
+
"fp_arith_inst_retired.256b_packed_double",
|
|
21
|
+
"fp_arith_inst_retired.512b_packed_double",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
# Mapping from event name suffix to SIMD width multiplier.
|
|
25
|
+
_WIDTH = {
|
|
26
|
+
"scalar_double": 1,
|
|
27
|
+
"128b_packed_double": 2,
|
|
28
|
+
"256b_packed_double": 4,
|
|
29
|
+
"512b_packed_double": 8,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class PerfResult:
|
|
35
|
+
"""Counts of retired floating-point arithmetic instructions."""
|
|
36
|
+
|
|
37
|
+
scalar_double: int
|
|
38
|
+
packed_128_double: int
|
|
39
|
+
packed_256_double: int
|
|
40
|
+
packed_512_double: int
|
|
41
|
+
|
|
42
|
+
@property
|
|
43
|
+
def total_flops(self) -> int:
|
|
44
|
+
"""Total double-precision FLOPs, weighted by SIMD width."""
|
|
45
|
+
return (
|
|
46
|
+
self.scalar_double * 1
|
|
47
|
+
+ self.packed_128_double * 2
|
|
48
|
+
+ self.packed_256_double * 4
|
|
49
|
+
+ self.packed_512_double * 8
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class TimingResult:
|
|
55
|
+
"""Wall-clock timing result used as fallback when perf is unavailable.
|
|
56
|
+
|
|
57
|
+
Stores elapsed nanoseconds. Consumers use ``total_flops`` which returns
|
|
58
|
+
the raw nanosecond value — the normalization step (op_time / add_time)
|
|
59
|
+
in the runner cancels units, producing valid relative weights.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
elapsed_ns: int
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def total_flops(self) -> int:
|
|
66
|
+
"""Return elapsed nanoseconds as a proxy for FP work.
|
|
67
|
+
|
|
68
|
+
This is intentionally named ``total_flops`` so that all benchmark
|
|
69
|
+
modules can use the same interface regardless of measurement mode.
|
|
70
|
+
The values are only meaningful as ratios (normalized against the
|
|
71
|
+
baseline ``np.add`` measurement).
|
|
72
|
+
"""
|
|
73
|
+
return self.elapsed_ns
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass(frozen=True)
|
|
77
|
+
class InstructionsResult:
|
|
78
|
+
"""Total retired instructions measured via ``perf stat -e instructions``.
|
|
79
|
+
|
|
80
|
+
Used as a hardware-counter fallback for integer/bitwise operations
|
|
81
|
+
where ``fp_arith_inst_retired`` reads 0. More stable than wall-clock
|
|
82
|
+
timing because it is deterministic and independent of system load.
|
|
83
|
+
|
|
84
|
+
Like ``TimingResult``, the ``total_flops`` property returns the raw
|
|
85
|
+
instruction count — normalization against the baseline (``np.add``)
|
|
86
|
+
in the runner cancels units, producing valid relative weights.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
instructions: int
|
|
90
|
+
|
|
91
|
+
@property
|
|
92
|
+
def total_flops(self) -> int:
|
|
93
|
+
"""Return total retired instructions as a proxy for work."""
|
|
94
|
+
return self.instructions
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# Union type for all measurement modes
|
|
98
|
+
MeasureResult = PerfResult | TimingResult | InstructionsResult
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def has_perf() -> bool:
|
|
102
|
+
"""Return True if the ``perf`` binary is on PATH.
|
|
103
|
+
|
|
104
|
+
The check can be overridden by setting the environment variable
|
|
105
|
+
``FLOPSCOPE_FORCE_TIMING=1``, which forces timing mode regardless
|
|
106
|
+
of whether ``perf`` is available. This is used by the dual-mode
|
|
107
|
+
validation loop to re-run benchmarks in timing mode.
|
|
108
|
+
"""
|
|
109
|
+
if os.environ.get("FLOPSCOPE_FORCE_TIMING") == "1":
|
|
110
|
+
return False
|
|
111
|
+
return shutil.which("perf") is not None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def measurement_mode() -> str:
|
|
115
|
+
"""Return the active measurement mode: ``'perf'`` or ``'timing'``."""
|
|
116
|
+
return "perf" if has_perf() else "timing"
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _parse_perf_csv(output: str) -> PerfResult:
|
|
120
|
+
"""Parse the CSV output produced by ``perf stat -x ,``."""
|
|
121
|
+
counts: dict[str, int] = {}
|
|
122
|
+
for line in output.splitlines():
|
|
123
|
+
line = line.strip()
|
|
124
|
+
if not line or line.startswith("#"):
|
|
125
|
+
continue
|
|
126
|
+
parts = line.split(",")
|
|
127
|
+
if len(parts) < 3:
|
|
128
|
+
continue
|
|
129
|
+
raw_count = parts[0].strip()
|
|
130
|
+
event_name = parts[2].strip()
|
|
131
|
+
# Match against known events.
|
|
132
|
+
for evt in PERF_EVENTS:
|
|
133
|
+
if evt == event_name:
|
|
134
|
+
try:
|
|
135
|
+
counts[evt] = int(raw_count)
|
|
136
|
+
except (ValueError, TypeError):
|
|
137
|
+
# <not supported> or similar
|
|
138
|
+
counts[evt] = 0
|
|
139
|
+
break
|
|
140
|
+
|
|
141
|
+
return PerfResult(
|
|
142
|
+
scalar_double=counts.get(PERF_EVENTS[0], 0),
|
|
143
|
+
packed_128_double=counts.get(PERF_EVENTS[1], 0),
|
|
144
|
+
packed_256_double=counts.get(PERF_EVENTS[2], 0),
|
|
145
|
+
packed_512_double=counts.get(PERF_EVENTS[3], 0),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _build_script(setup_code: str, bench_code: str, repeats: int) -> str:
|
|
150
|
+
"""Build the benchmark Python script content."""
|
|
151
|
+
return (
|
|
152
|
+
"import numpy as np\n"
|
|
153
|
+
f"{setup_code}\n"
|
|
154
|
+
f"for _i in range({repeats}):\n"
|
|
155
|
+
f" {bench_code}\n"
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
def _measure_perf(setup_code: str, bench_code: str, repeats: int) -> PerfResult:
|
|
160
|
+
"""Measure using Linux perf stat hardware counters."""
|
|
161
|
+
script = _build_script(setup_code, bench_code, repeats)
|
|
162
|
+
|
|
163
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False)
|
|
164
|
+
try:
|
|
165
|
+
tmp.write(script)
|
|
166
|
+
tmp.close()
|
|
167
|
+
|
|
168
|
+
events_arg = ",".join(PERF_EVENTS)
|
|
169
|
+
proc = subprocess.run(
|
|
170
|
+
[
|
|
171
|
+
"perf",
|
|
172
|
+
"stat",
|
|
173
|
+
"-e",
|
|
174
|
+
events_arg,
|
|
175
|
+
"-x",
|
|
176
|
+
",",
|
|
177
|
+
sys.executable,
|
|
178
|
+
tmp.name,
|
|
179
|
+
],
|
|
180
|
+
capture_output=True,
|
|
181
|
+
text=True,
|
|
182
|
+
)
|
|
183
|
+
return _parse_perf_csv(proc.stderr)
|
|
184
|
+
finally:
|
|
185
|
+
Path(tmp.name).unlink(missing_ok=True)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def _measure_timing(setup_code: str, bench_code: str, repeats: int) -> TimingResult:
|
|
189
|
+
"""Measure using wall-clock time in a subprocess."""
|
|
190
|
+
script = (
|
|
191
|
+
"import time\n"
|
|
192
|
+
"import numpy as np\n"
|
|
193
|
+
f"{setup_code}\n"
|
|
194
|
+
"# Warmup\n"
|
|
195
|
+
f"for _i in range(2):\n"
|
|
196
|
+
f" {bench_code}\n"
|
|
197
|
+
"# Timed run\n"
|
|
198
|
+
"_t0 = time.perf_counter_ns()\n"
|
|
199
|
+
f"for _i in range({repeats}):\n"
|
|
200
|
+
f" {bench_code}\n"
|
|
201
|
+
"_t1 = time.perf_counter_ns()\n"
|
|
202
|
+
"print(_t1 - _t0)\n"
|
|
203
|
+
)
|
|
204
|
+
|
|
205
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False)
|
|
206
|
+
try:
|
|
207
|
+
tmp.write(script)
|
|
208
|
+
tmp.close()
|
|
209
|
+
|
|
210
|
+
proc = subprocess.run(
|
|
211
|
+
[sys.executable, tmp.name],
|
|
212
|
+
capture_output=True,
|
|
213
|
+
text=True,
|
|
214
|
+
timeout=300,
|
|
215
|
+
)
|
|
216
|
+
if proc.returncode != 0:
|
|
217
|
+
raise RuntimeError(
|
|
218
|
+
f"Benchmark subprocess failed (exit {proc.returncode}):\n"
|
|
219
|
+
f"stderr: {proc.stderr}"
|
|
220
|
+
)
|
|
221
|
+
elapsed_ns = int(proc.stdout.strip())
|
|
222
|
+
return TimingResult(elapsed_ns=elapsed_ns)
|
|
223
|
+
finally:
|
|
224
|
+
Path(tmp.name).unlink(missing_ok=True)
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _measure_instructions(
|
|
228
|
+
setup_code: str, bench_code: str, repeats: int
|
|
229
|
+
) -> InstructionsResult:
|
|
230
|
+
"""Measure total retired instructions via ``perf stat -e instructions``.
|
|
231
|
+
|
|
232
|
+
This is a hardware counter that counts all retired instructions (integer,
|
|
233
|
+
FP, branch, load/store). It is deterministic and independent of system
|
|
234
|
+
load, making it a better fallback than wall-clock timing for integer
|
|
235
|
+
and bitwise operations where ``fp_arith_inst_retired`` reads 0.
|
|
236
|
+
"""
|
|
237
|
+
script = _build_script(setup_code, bench_code, repeats)
|
|
238
|
+
|
|
239
|
+
tmp = tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False)
|
|
240
|
+
try:
|
|
241
|
+
tmp.write(script)
|
|
242
|
+
tmp.close()
|
|
243
|
+
|
|
244
|
+
proc = subprocess.run(
|
|
245
|
+
[
|
|
246
|
+
"perf",
|
|
247
|
+
"stat",
|
|
248
|
+
"-e",
|
|
249
|
+
"instructions",
|
|
250
|
+
"-x",
|
|
251
|
+
",",
|
|
252
|
+
sys.executable,
|
|
253
|
+
tmp.name,
|
|
254
|
+
],
|
|
255
|
+
capture_output=True,
|
|
256
|
+
text=True,
|
|
257
|
+
)
|
|
258
|
+
# Parse CSV: first field is count, third is event name
|
|
259
|
+
instructions = 0
|
|
260
|
+
for line in proc.stderr.splitlines():
|
|
261
|
+
line = line.strip()
|
|
262
|
+
if not line or line.startswith("#"):
|
|
263
|
+
continue
|
|
264
|
+
parts = line.split(",")
|
|
265
|
+
if len(parts) >= 3 and "instructions" in parts[2]:
|
|
266
|
+
try:
|
|
267
|
+
instructions = int(parts[0].strip())
|
|
268
|
+
except (ValueError, TypeError):
|
|
269
|
+
pass
|
|
270
|
+
break
|
|
271
|
+
return InstructionsResult(instructions=instructions)
|
|
272
|
+
finally:
|
|
273
|
+
Path(tmp.name).unlink(missing_ok=True)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def measure_flops(
|
|
277
|
+
setup_code: str,
|
|
278
|
+
bench_code: str,
|
|
279
|
+
repeats: int = 10,
|
|
280
|
+
) -> MeasureResult:
|
|
281
|
+
"""Measure FP work for a benchmark operation.
|
|
282
|
+
|
|
283
|
+
Uses ``perf stat`` hardware counters when available (Linux). Falls back
|
|
284
|
+
to wall-clock time measurement on other platforms. Both return an object
|
|
285
|
+
with a ``total_flops`` property — for perf mode this is actual FP ops,
|
|
286
|
+
for timing mode it is elapsed nanoseconds (valid as a relative proxy
|
|
287
|
+
when normalized against the baseline).
|
|
288
|
+
|
|
289
|
+
Parameters
|
|
290
|
+
----------
|
|
291
|
+
setup_code:
|
|
292
|
+
Python code executed once before the hot loop (numpy is
|
|
293
|
+
already imported as ``np``).
|
|
294
|
+
bench_code:
|
|
295
|
+
Python code executed *repeats* times inside the hot loop.
|
|
296
|
+
repeats:
|
|
297
|
+
Number of iterations of the hot loop.
|
|
298
|
+
"""
|
|
299
|
+
if has_perf():
|
|
300
|
+
return _measure_perf(setup_code, bench_code, repeats)
|
|
301
|
+
return _measure_timing(setup_code, bench_code, repeats)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def measure_instructions(
|
|
305
|
+
setup_code: str,
|
|
306
|
+
bench_code: str,
|
|
307
|
+
repeats: int = 10,
|
|
308
|
+
) -> InstructionsResult:
|
|
309
|
+
"""Measure total retired instructions for a benchmark operation.
|
|
310
|
+
|
|
311
|
+
Uses ``perf stat -e instructions`` hardware counter. This is the
|
|
312
|
+
preferred fallback for integer/bitwise operations where
|
|
313
|
+
``fp_arith_inst_retired`` reads 0. Requires ``perf`` to be available.
|
|
314
|
+
|
|
315
|
+
Falls back to timing if ``perf`` is not available.
|
|
316
|
+
"""
|
|
317
|
+
if shutil.which("perf") is not None:
|
|
318
|
+
return _measure_instructions(setup_code, bench_code, repeats)
|
|
319
|
+
# If perf isn't available at all, fall back to timing
|
|
320
|
+
timing = _measure_timing(setup_code, bench_code, repeats)
|
|
321
|
+
return InstructionsResult(instructions=timing.elapsed_ns)
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Calibration script for the dimino_budget setting.
|
|
2
|
+
|
|
3
|
+
Measures cold ``_dimino`` enumeration time plus ``burnside_unique_count``
|
|
4
|
+
time across representative permutation groups, then prints a
|
|
5
|
+
recommendation for ``dimino_budget``.
|
|
6
|
+
|
|
7
|
+
Run with::
|
|
8
|
+
|
|
9
|
+
python -m benchmarks._perm_group_calibration
|
|
10
|
+
python -m benchmarks._perm_group_calibration --budget-ms 50 \\
|
|
11
|
+
--output benchmarks/_perm_group_calibration.json
|
|
12
|
+
|
|
13
|
+
Output is informational. Users on faster/slower machines run this and
|
|
14
|
+
``flops.configure(dimino_budget=<recommended>)``.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import platform
|
|
22
|
+
import time
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
|
|
25
|
+
from flopscope._perm_group import SymmetryGroup, _dimino
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class Measurement:
|
|
30
|
+
label: str
|
|
31
|
+
group_order: int
|
|
32
|
+
degree: int
|
|
33
|
+
dimino_ms: float
|
|
34
|
+
burnside_ms: float
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
def total_ms(self) -> float:
|
|
38
|
+
return self.dimino_ms + self.burnside_ms
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _measure_one(group: SymmetryGroup, label: str) -> Measurement:
|
|
42
|
+
"""Time cold ``_dimino`` + Burnside enumeration on one group."""
|
|
43
|
+
# Cold dimino: bypass the cache by calling _dimino directly.
|
|
44
|
+
t = time.perf_counter()
|
|
45
|
+
_dimino(group._generators)
|
|
46
|
+
dimino_ms = (time.perf_counter() - t) * 1000.0
|
|
47
|
+
|
|
48
|
+
# Burnside: realistic size_dict (uniform 4-dim across all axes).
|
|
49
|
+
size_dict = dict.fromkeys(range(group.degree), 4)
|
|
50
|
+
t = time.perf_counter()
|
|
51
|
+
group.burnside_unique_count(size_dict)
|
|
52
|
+
burnside_ms = (time.perf_counter() - t) * 1000.0
|
|
53
|
+
|
|
54
|
+
return Measurement(
|
|
55
|
+
label=label,
|
|
56
|
+
group_order=group.order(),
|
|
57
|
+
degree=group.degree,
|
|
58
|
+
dimino_ms=dimino_ms,
|
|
59
|
+
burnside_ms=burnside_ms,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _sample_groups() -> list[tuple[str, SymmetryGroup]]:
|
|
64
|
+
"""Return the representative group sample for calibration."""
|
|
65
|
+
samples: list[tuple[str, SymmetryGroup]] = []
|
|
66
|
+
for n in (3, 4, 5, 6, 7, 8, 9):
|
|
67
|
+
samples.append((f"S_{n}", SymmetryGroup.symmetric(axes=tuple(range(n)))))
|
|
68
|
+
for n in (4, 8, 16, 32, 64):
|
|
69
|
+
samples.append((f"C_{n}", SymmetryGroup.cyclic(axes=tuple(range(n)))))
|
|
70
|
+
for n in (4, 8, 16, 32, 64):
|
|
71
|
+
samples.append((f"D_{n}", SymmetryGroup.dihedral(axes=tuple(range(n)))))
|
|
72
|
+
samples.append(
|
|
73
|
+
(
|
|
74
|
+
"S_3 x S_3",
|
|
75
|
+
SymmetryGroup.direct_product(
|
|
76
|
+
SymmetryGroup.symmetric(axes=(0, 1, 2)),
|
|
77
|
+
SymmetryGroup.symmetric(axes=(3, 4, 5)),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
samples.append(
|
|
82
|
+
(
|
|
83
|
+
"S_4 x S_4",
|
|
84
|
+
SymmetryGroup.direct_product(
|
|
85
|
+
SymmetryGroup.symmetric(axes=(0, 1, 2, 3)),
|
|
86
|
+
SymmetryGroup.symmetric(axes=(4, 5, 6, 7)),
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
samples.append(
|
|
91
|
+
(
|
|
92
|
+
"C_5 x C_5 x C_5",
|
|
93
|
+
SymmetryGroup.direct_product(
|
|
94
|
+
SymmetryGroup.cyclic(axes=(0, 1, 2, 3, 4)),
|
|
95
|
+
SymmetryGroup.cyclic(axes=(5, 6, 7, 8, 9)),
|
|
96
|
+
SymmetryGroup.cyclic(axes=(10, 11, 12, 13, 14)),
|
|
97
|
+
),
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
return samples
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _print_table(measurements: list[Measurement]) -> None:
|
|
104
|
+
header = f"{'group':<22}{'|G|':>12}{'dimino':>12}{'burnside':>12}{'total':>12}"
|
|
105
|
+
print(header)
|
|
106
|
+
print("-" * len(header))
|
|
107
|
+
for m in measurements:
|
|
108
|
+
print(
|
|
109
|
+
f"{m.label:<22}"
|
|
110
|
+
f"{m.group_order:>12}"
|
|
111
|
+
f"{m.dimino_ms:>10.2f}ms"
|
|
112
|
+
f"{m.burnside_ms:>10.2f}ms"
|
|
113
|
+
f"{m.total_ms:>10.2f}ms"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _recommend_budget(measurements: list[Measurement], budget_ms: float) -> int:
|
|
118
|
+
"""Largest |G| whose total cold cost stays under ``budget_ms``."""
|
|
119
|
+
under_budget = [m for m in measurements if m.total_ms <= budget_ms]
|
|
120
|
+
if not under_budget:
|
|
121
|
+
return 1 # nothing fits; degenerate machine
|
|
122
|
+
return max(m.group_order for m in under_budget)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main() -> None:
|
|
126
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
127
|
+
parser.add_argument(
|
|
128
|
+
"--budget-ms",
|
|
129
|
+
type=float,
|
|
130
|
+
default=100.0,
|
|
131
|
+
help="Wall-clock budget per group construction (default: 100ms)",
|
|
132
|
+
)
|
|
133
|
+
parser.add_argument(
|
|
134
|
+
"--output",
|
|
135
|
+
type=str,
|
|
136
|
+
default=None,
|
|
137
|
+
help="Optional JSON output path",
|
|
138
|
+
)
|
|
139
|
+
args = parser.parse_args()
|
|
140
|
+
|
|
141
|
+
samples = _sample_groups()
|
|
142
|
+
measurements = [_measure_one(g, label) for label, g in samples]
|
|
143
|
+
|
|
144
|
+
_print_table(measurements)
|
|
145
|
+
recommended = _recommend_budget(measurements, args.budget_ms)
|
|
146
|
+
print()
|
|
147
|
+
print(
|
|
148
|
+
f"Recommended dimino_budget: {recommended} "
|
|
149
|
+
f"(group_order_budget_ms={args.budget_ms}, machine: {platform.platform()})"
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
if args.output:
|
|
153
|
+
payload = {
|
|
154
|
+
"machine": platform.platform(),
|
|
155
|
+
"budget_ms": args.budget_ms,
|
|
156
|
+
"recommended_dimino_budget": recommended,
|
|
157
|
+
"measurements": [
|
|
158
|
+
{
|
|
159
|
+
"label": m.label,
|
|
160
|
+
"group_order": m.group_order,
|
|
161
|
+
"degree": m.degree,
|
|
162
|
+
"dimino_ms": m.dimino_ms,
|
|
163
|
+
"burnside_ms": m.burnside_ms,
|
|
164
|
+
"total_ms": m.total_ms,
|
|
165
|
+
}
|
|
166
|
+
for m in measurements
|
|
167
|
+
],
|
|
168
|
+
}
|
|
169
|
+
with open(args.output, "w") as f:
|
|
170
|
+
json.dump(payload, f, indent=2)
|
|
171
|
+
print(f"Wrote calibration data to {args.output}")
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
if __name__ == "__main__":
|
|
175
|
+
main()
|