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/_random.py
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
"""Benchmark random number generation operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import statistics
|
|
6
|
+
|
|
7
|
+
from benchmarks._perf import measure_flops
|
|
8
|
+
|
|
9
|
+
RANDOM_OPS: list[str] = [
|
|
10
|
+
"random.standard_normal",
|
|
11
|
+
"random.uniform",
|
|
12
|
+
"random.standard_exponential",
|
|
13
|
+
"random.standard_cauchy",
|
|
14
|
+
"random.standard_gamma",
|
|
15
|
+
"random.standard_t",
|
|
16
|
+
"random.poisson",
|
|
17
|
+
"random.binomial",
|
|
18
|
+
"random.permutation",
|
|
19
|
+
"random.shuffle",
|
|
20
|
+
# --- added in Step 2.5 ---
|
|
21
|
+
"random.beta",
|
|
22
|
+
"random.chisquare",
|
|
23
|
+
"random.choice",
|
|
24
|
+
"random.dirichlet",
|
|
25
|
+
"random.exponential",
|
|
26
|
+
"random.f",
|
|
27
|
+
"random.gamma",
|
|
28
|
+
"random.geometric",
|
|
29
|
+
"random.gumbel",
|
|
30
|
+
"random.hypergeometric",
|
|
31
|
+
"random.laplace",
|
|
32
|
+
"random.logistic",
|
|
33
|
+
"random.lognormal",
|
|
34
|
+
"random.logseries",
|
|
35
|
+
"random.multinomial",
|
|
36
|
+
"random.multivariate_normal",
|
|
37
|
+
"random.negative_binomial",
|
|
38
|
+
"random.noncentral_chisquare",
|
|
39
|
+
"random.noncentral_f",
|
|
40
|
+
"random.normal",
|
|
41
|
+
"random.pareto",
|
|
42
|
+
"random.power",
|
|
43
|
+
"random.rand",
|
|
44
|
+
"random.randint",
|
|
45
|
+
"random.randn",
|
|
46
|
+
"random.random",
|
|
47
|
+
"random.random_sample",
|
|
48
|
+
"random.rayleigh",
|
|
49
|
+
"random.triangular",
|
|
50
|
+
"random.vonmises",
|
|
51
|
+
"random.wald",
|
|
52
|
+
"random.weibull",
|
|
53
|
+
"random.zipf",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
# Ops that need extra arguments beyond size.
|
|
57
|
+
_EXTRA_ARGS: dict[str, str] = {
|
|
58
|
+
"random.uniform": "0.0, 1.0, ",
|
|
59
|
+
"random.standard_gamma": "1.0, ",
|
|
60
|
+
"random.standard_t": "3, ",
|
|
61
|
+
"random.poisson": "5.0, ",
|
|
62
|
+
"random.binomial": "10, 0.5, ",
|
|
63
|
+
# --- added in Step 2.5 ---
|
|
64
|
+
"random.beta": "2.0, 5.0, ",
|
|
65
|
+
"random.chisquare": "2, ",
|
|
66
|
+
"random.exponential": "1.0, ",
|
|
67
|
+
"random.f": "5, 10, ",
|
|
68
|
+
"random.gamma": "2.0, 1.0, ",
|
|
69
|
+
"random.geometric": "0.5, ",
|
|
70
|
+
"random.gumbel": "0.0, 1.0, ",
|
|
71
|
+
"random.hypergeometric": "100, 50, 20, ",
|
|
72
|
+
"random.laplace": "0.0, 1.0, ",
|
|
73
|
+
"random.logistic": "0.0, 1.0, ",
|
|
74
|
+
"random.lognormal": "0.0, 1.0, ",
|
|
75
|
+
"random.logseries": "0.5, ",
|
|
76
|
+
"random.negative_binomial": "10, 0.5, ",
|
|
77
|
+
"random.noncentral_chisquare": "2, 1.0, ",
|
|
78
|
+
"random.noncentral_f": "5, 10, 1.0, ",
|
|
79
|
+
"random.normal": "0.0, 1.0, ",
|
|
80
|
+
"random.pareto": "3.0, ",
|
|
81
|
+
"random.power": "5.0, ",
|
|
82
|
+
"random.rayleigh": "1.0, ",
|
|
83
|
+
"random.triangular": "-1.0, 0.0, 1.0, ",
|
|
84
|
+
"random.vonmises": "0.0, 1.0, ",
|
|
85
|
+
"random.wald": "1.0, 1.0, ",
|
|
86
|
+
"random.weibull": "2.0, ",
|
|
87
|
+
"random.zipf": "2.0, ",
|
|
88
|
+
"random.randint": "0, 1000, ",
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
# Ops that need completely custom setup/bench code.
|
|
92
|
+
_CUSTOM_OPS = frozenset(
|
|
93
|
+
{
|
|
94
|
+
"random.choice",
|
|
95
|
+
"random.dirichlet",
|
|
96
|
+
"random.multinomial",
|
|
97
|
+
"random.multivariate_normal",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _custom_bench(op: str, n: int, dtype: str, seed: int) -> tuple[str, str]:
|
|
103
|
+
"""Return (setup, bench) for ops with non-standard patterns."""
|
|
104
|
+
base_setup = f"import numpy as np; np.random.seed({seed})"
|
|
105
|
+
|
|
106
|
+
if op == "random.choice":
|
|
107
|
+
setup = base_setup + "; _pool = np.arange(1000)"
|
|
108
|
+
bench = f"np.random.choice(_pool, {n})"
|
|
109
|
+
elif op == "random.dirichlet":
|
|
110
|
+
setup = base_setup
|
|
111
|
+
bench = f"np.random.dirichlet([1.0, 2.0, 3.0], {n})"
|
|
112
|
+
elif op == "random.multinomial":
|
|
113
|
+
setup = base_setup
|
|
114
|
+
bench = f"np.random.multinomial(10, [0.2, 0.3, 0.5], {n})"
|
|
115
|
+
elif op == "random.multivariate_normal":
|
|
116
|
+
setup = base_setup + "; _mean = np.zeros(10); _cov = np.eye(10)"
|
|
117
|
+
bench = f"np.random.multivariate_normal(_mean, _cov, {n})"
|
|
118
|
+
else:
|
|
119
|
+
raise ValueError(f"Unknown custom op: {op}")
|
|
120
|
+
|
|
121
|
+
return setup, bench
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def benchmark_random(
|
|
125
|
+
n: int = 10_000_000,
|
|
126
|
+
dtype: str = "float64",
|
|
127
|
+
repeats: int = 10,
|
|
128
|
+
) -> tuple[dict[str, float], dict[str, dict]]:
|
|
129
|
+
"""Benchmark random ops, returning FP ops per element.
|
|
130
|
+
|
|
131
|
+
Parameters
|
|
132
|
+
----------
|
|
133
|
+
n : int
|
|
134
|
+
Array size.
|
|
135
|
+
dtype : str
|
|
136
|
+
NumPy dtype string.
|
|
137
|
+
repeats : int
|
|
138
|
+
Number of repetitions per measurement.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
tuple[dict[str, float], dict[str, dict]]
|
|
143
|
+
A pair of (alphas, details). ``alphas`` maps op name to median
|
|
144
|
+
measurement per element. ``details`` maps op name to a dict of
|
|
145
|
+
raw benchmark metadata.
|
|
146
|
+
"""
|
|
147
|
+
results: dict[str, float] = {}
|
|
148
|
+
details: dict[str, dict] = {}
|
|
149
|
+
seeds = [42, 123, 999]
|
|
150
|
+
|
|
151
|
+
for op in RANDOM_OPS:
|
|
152
|
+
dist_values: list[float] = []
|
|
153
|
+
dist_raw_totals: list[int] = []
|
|
154
|
+
extra = _EXTRA_ARGS.get(op, "")
|
|
155
|
+
bench = ""
|
|
156
|
+
|
|
157
|
+
for seed in seeds:
|
|
158
|
+
if op in _CUSTOM_OPS:
|
|
159
|
+
setup, bench = _custom_bench(op, n, dtype, seed)
|
|
160
|
+
elif op == "random.shuffle":
|
|
161
|
+
setup = (
|
|
162
|
+
f"import numpy as np; np.random.seed({seed})"
|
|
163
|
+
f"; x = np.arange({n}, dtype=np.{dtype})"
|
|
164
|
+
)
|
|
165
|
+
bench = "np.random.shuffle(x)"
|
|
166
|
+
elif op == "random.permutation":
|
|
167
|
+
setup = f"import numpy as np; np.random.seed({seed})"
|
|
168
|
+
bench = f"np.random.permutation({n})"
|
|
169
|
+
else:
|
|
170
|
+
setup = f"import numpy as np; np.random.seed({seed})"
|
|
171
|
+
bench = f"np.{op}({extra}{n})"
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
result = measure_flops(setup, bench, repeats=repeats)
|
|
175
|
+
except RuntimeError:
|
|
176
|
+
continue
|
|
177
|
+
dist_values.append(result.total_flops / (n * repeats))
|
|
178
|
+
dist_raw_totals.append(result.total_flops)
|
|
179
|
+
|
|
180
|
+
if dist_values:
|
|
181
|
+
results[op] = statistics.median(dist_values)
|
|
182
|
+
# Build explicit benchmark_size per op
|
|
183
|
+
if op == "random.shuffle":
|
|
184
|
+
bm_size = f"x: ({n},)"
|
|
185
|
+
elif op == "random.permutation":
|
|
186
|
+
bm_size = f"n={n}"
|
|
187
|
+
elif op == "random.choice":
|
|
188
|
+
bm_size = f"pool: (1000,), size={n}"
|
|
189
|
+
elif op == "random.multinomial":
|
|
190
|
+
bm_size = f"n_trials=10, pvals: (3,), size={n}"
|
|
191
|
+
elif op == "random.multivariate_normal":
|
|
192
|
+
bm_size = f"mean: (10,), cov: (10,10), size={n}"
|
|
193
|
+
elif op == "random.dirichlet":
|
|
194
|
+
bm_size = f"alpha: (3,), size={n}"
|
|
195
|
+
else:
|
|
196
|
+
bm_size = f"output: ({n},)"
|
|
197
|
+
details[op] = {
|
|
198
|
+
"category": "counted_custom",
|
|
199
|
+
"measurement_mode": "custom",
|
|
200
|
+
"analytical_formula": "numel(output)",
|
|
201
|
+
"analytical_flops": n,
|
|
202
|
+
"benchmark_size": bm_size,
|
|
203
|
+
"bench_code": bench,
|
|
204
|
+
"repeats": repeats,
|
|
205
|
+
"perf_instructions_total": dist_raw_totals,
|
|
206
|
+
"distribution_alphas": dist_values,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return results, details
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""Benchmark reduction operations.
|
|
2
|
+
|
|
3
|
+
Cumulative ops (cumsum, cumprod) use pre-allocated output via ``out=``
|
|
4
|
+
to match the pointwise measurement methodology.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import statistics
|
|
10
|
+
|
|
11
|
+
import numpy as np
|
|
12
|
+
|
|
13
|
+
from benchmarks._perf import measure_flops
|
|
14
|
+
|
|
15
|
+
REDUCTION_OPS: list[str] = [
|
|
16
|
+
"sum",
|
|
17
|
+
"prod",
|
|
18
|
+
"mean",
|
|
19
|
+
"std",
|
|
20
|
+
"var",
|
|
21
|
+
"max",
|
|
22
|
+
"min",
|
|
23
|
+
"argmax",
|
|
24
|
+
"argmin",
|
|
25
|
+
"any",
|
|
26
|
+
"all",
|
|
27
|
+
"cumsum",
|
|
28
|
+
"cumprod",
|
|
29
|
+
"nansum",
|
|
30
|
+
"nanmean",
|
|
31
|
+
"nanstd",
|
|
32
|
+
"nanvar",
|
|
33
|
+
"nanmax",
|
|
34
|
+
"nanmin",
|
|
35
|
+
"nanprod",
|
|
36
|
+
"median",
|
|
37
|
+
"nanmedian",
|
|
38
|
+
"percentile",
|
|
39
|
+
"nanpercentile",
|
|
40
|
+
"quantile",
|
|
41
|
+
"nanquantile",
|
|
42
|
+
"count_nonzero",
|
|
43
|
+
"average",
|
|
44
|
+
# --- added in Step 2.4 ---
|
|
45
|
+
"nanargmax",
|
|
46
|
+
"nanargmin",
|
|
47
|
+
"ptp",
|
|
48
|
+
"nancumprod",
|
|
49
|
+
"nancumsum",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
# Add NumPy 2.x array API cumulative ops if available.
|
|
53
|
+
if hasattr(np, "cumulative_sum"):
|
|
54
|
+
REDUCTION_OPS.extend(["cumulative_sum", "cumulative_prod"])
|
|
55
|
+
|
|
56
|
+
# Ops that require an extra argument.
|
|
57
|
+
_SPECIAL_ARGS: dict[str, str] = {
|
|
58
|
+
"percentile": ", 50",
|
|
59
|
+
"nanpercentile": ", 50",
|
|
60
|
+
"quantile": ", 0.5",
|
|
61
|
+
"nanquantile": ", 0.5",
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Cumulative ops return same-size array — pre-allocate output.
|
|
65
|
+
_CUMULATIVE_OPS = {"cumsum", "cumprod", "nancumprod", "nancumsum"}
|
|
66
|
+
if hasattr(np, "cumulative_sum"):
|
|
67
|
+
_CUMULATIVE_OPS |= {"cumulative_sum", "cumulative_prod"}
|
|
68
|
+
_CUMULATIVE_OPS = frozenset(_CUMULATIVE_OPS)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def benchmark_reductions(
|
|
72
|
+
n: int = 10_000_000,
|
|
73
|
+
dtype: str = "float64",
|
|
74
|
+
repeats: int = 10,
|
|
75
|
+
) -> tuple[dict[str, float], dict[str, dict]]:
|
|
76
|
+
"""Benchmark all reduction ops, returning raw measurement per element.
|
|
77
|
+
|
|
78
|
+
Parameters
|
|
79
|
+
----------
|
|
80
|
+
n : int
|
|
81
|
+
Array size.
|
|
82
|
+
dtype : str
|
|
83
|
+
NumPy dtype string.
|
|
84
|
+
repeats : int
|
|
85
|
+
Number of repetitions per measurement.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
tuple[dict[str, float], dict[str, dict]]
|
|
90
|
+
A pair of (alphas, details). ``alphas`` maps op name to median
|
|
91
|
+
measurement per element. ``details`` maps op name to a dict of
|
|
92
|
+
raw benchmark metadata.
|
|
93
|
+
"""
|
|
94
|
+
results: dict[str, float] = {}
|
|
95
|
+
details: dict[str, dict] = {}
|
|
96
|
+
|
|
97
|
+
base_setups = [
|
|
98
|
+
f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
|
|
99
|
+
f"import numpy as np; x = np.random.default_rng(42).uniform(0.01, 100, size={n}).astype(np.{dtype})",
|
|
100
|
+
f"import numpy as np; x = np.random.default_rng(42).uniform(-1000, 1000, size={n}).astype(np.{dtype})",
|
|
101
|
+
]
|
|
102
|
+
|
|
103
|
+
for op in REDUCTION_OPS:
|
|
104
|
+
dist_values: list[float] = []
|
|
105
|
+
dist_raw_totals: list[int] = []
|
|
106
|
+
extra = _SPECIAL_ARGS.get(op, "")
|
|
107
|
+
bench = ""
|
|
108
|
+
|
|
109
|
+
for base_setup in base_setups:
|
|
110
|
+
if op in _CUMULATIVE_OPS:
|
|
111
|
+
setup = base_setup + f"; _out = np.empty({n}, dtype=np.{dtype})"
|
|
112
|
+
bench = f"np.{op}(x, out=_out)"
|
|
113
|
+
else:
|
|
114
|
+
setup = base_setup
|
|
115
|
+
bench = f"np.{op}(x{extra})"
|
|
116
|
+
try:
|
|
117
|
+
result = measure_flops(setup, bench, repeats=repeats)
|
|
118
|
+
except RuntimeError:
|
|
119
|
+
continue
|
|
120
|
+
dist_values.append(result.total_flops / (n * repeats))
|
|
121
|
+
dist_raw_totals.append(result.total_flops)
|
|
122
|
+
if dist_values:
|
|
123
|
+
results[op] = statistics.median(dist_values)
|
|
124
|
+
details[op] = {
|
|
125
|
+
"category": "counted_reduction",
|
|
126
|
+
"measurement_mode": "ufunc_reduction",
|
|
127
|
+
"analytical_formula": "numel(input)",
|
|
128
|
+
"analytical_flops": n,
|
|
129
|
+
"benchmark_size": f"x: ({n},)",
|
|
130
|
+
"bench_code": bench,
|
|
131
|
+
"repeats": repeats,
|
|
132
|
+
"perf_instructions_total": dist_raw_totals,
|
|
133
|
+
"distribution_alphas": dist_values,
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return results, details
|
benchmarks/_sorting.py
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"""Benchmark sorting and related operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
import statistics
|
|
7
|
+
|
|
8
|
+
from benchmarks._perf import measure_flops
|
|
9
|
+
|
|
10
|
+
SORTING_OPS: list[str] = [
|
|
11
|
+
"sort",
|
|
12
|
+
"argsort",
|
|
13
|
+
"lexsort",
|
|
14
|
+
"partition",
|
|
15
|
+
"argpartition",
|
|
16
|
+
"searchsorted",
|
|
17
|
+
"unique",
|
|
18
|
+
# Set operations
|
|
19
|
+
"in1d",
|
|
20
|
+
"isin",
|
|
21
|
+
"intersect1d",
|
|
22
|
+
"setdiff1d",
|
|
23
|
+
"setxor1d",
|
|
24
|
+
"union1d",
|
|
25
|
+
# NumPy 2.x unique variants
|
|
26
|
+
"unique_all",
|
|
27
|
+
"unique_counts",
|
|
28
|
+
"unique_inverse",
|
|
29
|
+
"unique_values",
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
_FORMULA_STRINGS: dict[str, str] = {
|
|
33
|
+
"sort": "n * ceil(log2(n))",
|
|
34
|
+
"argsort": "n * ceil(log2(n))",
|
|
35
|
+
"unique": "n * ceil(log2(n))",
|
|
36
|
+
"unique_all": "n * ceil(log2(n))",
|
|
37
|
+
"unique_counts": "n * ceil(log2(n))",
|
|
38
|
+
"unique_inverse": "n * ceil(log2(n))",
|
|
39
|
+
"unique_values": "n * ceil(log2(n))",
|
|
40
|
+
"lexsort": "k * n * ceil(log2(n))",
|
|
41
|
+
"searchsorted": "m * ceil(log2(n))",
|
|
42
|
+
"partition": "n",
|
|
43
|
+
"argpartition": "n",
|
|
44
|
+
"in1d": "(n+m) * ceil(log2(n+m))",
|
|
45
|
+
"isin": "(n+m) * ceil(log2(n+m))",
|
|
46
|
+
"intersect1d": "(n+m) * ceil(log2(n+m))",
|
|
47
|
+
"setdiff1d": "(n+m) * ceil(log2(n+m))",
|
|
48
|
+
"setxor1d": "(n+m) * ceil(log2(n+m))",
|
|
49
|
+
"union1d": "(n+m) * ceil(log2(n+m))",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _analytical_cost(op: str, n: int, **kwargs: int) -> int:
|
|
54
|
+
"""Return the analytical operation count for a sorting-related op.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
op : str
|
|
59
|
+
Operation name (e.g. ``"sort"``, ``"lexsort"``).
|
|
60
|
+
n : int
|
|
61
|
+
Primary array size.
|
|
62
|
+
**kwargs
|
|
63
|
+
Extra parameters: ``k`` for lexsort key count, ``m`` for second
|
|
64
|
+
array size in set operations / searchsorted queries.
|
|
65
|
+
|
|
66
|
+
Returns
|
|
67
|
+
-------
|
|
68
|
+
int
|
|
69
|
+
Analytical FP-operation count used as the normalization denominator.
|
|
70
|
+
"""
|
|
71
|
+
nlogn = n * math.ceil(math.log2(n)) if n > 1 else n
|
|
72
|
+
|
|
73
|
+
if op in ("sort", "argsort", "unique"):
|
|
74
|
+
return nlogn
|
|
75
|
+
|
|
76
|
+
if op in ("unique_all", "unique_counts", "unique_inverse", "unique_values"):
|
|
77
|
+
return nlogn
|
|
78
|
+
|
|
79
|
+
if op == "lexsort":
|
|
80
|
+
k = kwargs.get("k", 2)
|
|
81
|
+
return k * nlogn
|
|
82
|
+
|
|
83
|
+
if op == "searchsorted":
|
|
84
|
+
m = kwargs.get("m", n)
|
|
85
|
+
return m * math.ceil(math.log2(n)) if n > 1 else m
|
|
86
|
+
|
|
87
|
+
if op in ("partition", "argpartition"):
|
|
88
|
+
return n
|
|
89
|
+
|
|
90
|
+
# Set operations: in1d, isin, intersect1d, setdiff1d, setxor1d, union1d
|
|
91
|
+
if op in ("in1d", "isin", "intersect1d", "setdiff1d", "setxor1d", "union1d"):
|
|
92
|
+
m = kwargs.get("m", n)
|
|
93
|
+
total = n + m
|
|
94
|
+
return total * math.ceil(math.log2(total)) if total > 1 else total
|
|
95
|
+
|
|
96
|
+
# Fallback: per-element
|
|
97
|
+
return n
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def benchmark_sorting(
|
|
101
|
+
n: int = 10_000_000,
|
|
102
|
+
dtype: str = "float64",
|
|
103
|
+
repeats: int = 10,
|
|
104
|
+
) -> tuple[dict[str, float], dict[str, dict]]:
|
|
105
|
+
"""Benchmark sorting ops, returning FP ops per analytical operation.
|
|
106
|
+
|
|
107
|
+
Parameters
|
|
108
|
+
----------
|
|
109
|
+
n : int
|
|
110
|
+
Array size.
|
|
111
|
+
dtype : str
|
|
112
|
+
NumPy dtype string.
|
|
113
|
+
repeats : int
|
|
114
|
+
Number of repetitions per measurement.
|
|
115
|
+
|
|
116
|
+
Returns
|
|
117
|
+
-------
|
|
118
|
+
tuple[dict[str, float], dict[str, dict]]
|
|
119
|
+
``(alphas, details)`` where *alphas* maps op name to median alpha
|
|
120
|
+
and *details* maps op name to a dict of per-op measurement metadata.
|
|
121
|
+
"""
|
|
122
|
+
results: dict[str, float] = {}
|
|
123
|
+
details: dict[str, dict] = {}
|
|
124
|
+
|
|
125
|
+
for op in SORTING_OPS:
|
|
126
|
+
dist_values: list[float] = []
|
|
127
|
+
perf_instructions: list[int] = []
|
|
128
|
+
|
|
129
|
+
if op == "lexsort":
|
|
130
|
+
setups = [
|
|
131
|
+
(
|
|
132
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
133
|
+
f"x = rng.standard_normal({n}).astype(np.{dtype}); "
|
|
134
|
+
f"y = rng.standard_normal({n}).astype(np.{dtype})"
|
|
135
|
+
),
|
|
136
|
+
(
|
|
137
|
+
f"import numpy as np; "
|
|
138
|
+
f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
|
|
139
|
+
f"y = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
|
|
140
|
+
),
|
|
141
|
+
(
|
|
142
|
+
f"import numpy as np; "
|
|
143
|
+
f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
|
|
144
|
+
f"y = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
|
|
145
|
+
),
|
|
146
|
+
]
|
|
147
|
+
bench = "np.lexsort((x, y))"
|
|
148
|
+
analytical = _analytical_cost(op, n, k=2)
|
|
149
|
+
elif op == "searchsorted":
|
|
150
|
+
setups = [
|
|
151
|
+
(
|
|
152
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
153
|
+
f"x = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
|
|
154
|
+
f"q = rng.standard_normal({n}).astype(np.{dtype})"
|
|
155
|
+
),
|
|
156
|
+
(
|
|
157
|
+
f"import numpy as np; "
|
|
158
|
+
f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
|
|
159
|
+
f"q = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
|
|
160
|
+
),
|
|
161
|
+
(
|
|
162
|
+
f"import numpy as np; "
|
|
163
|
+
f"x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})); "
|
|
164
|
+
f"q = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
|
|
165
|
+
),
|
|
166
|
+
]
|
|
167
|
+
bench = "np.searchsorted(x, q)"
|
|
168
|
+
analytical = _analytical_cost(op, n, m=n)
|
|
169
|
+
elif op in ("partition", "argpartition"):
|
|
170
|
+
kth = n // 2
|
|
171
|
+
# Use only random input — sorted/reverse-sorted is pathological
|
|
172
|
+
# for introselect (O(n^2) worst case), which inflates the weight.
|
|
173
|
+
# The analytical formula O(n) reflects average-case performance.
|
|
174
|
+
setups = [
|
|
175
|
+
f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
|
|
176
|
+
f"import numpy as np; x = np.random.default_rng(43).standard_normal({n}).astype(np.{dtype})",
|
|
177
|
+
f"import numpy as np; x = np.random.default_rng(99).standard_normal({n}).astype(np.{dtype})",
|
|
178
|
+
]
|
|
179
|
+
bench = f"np.{op}(x, {kth})"
|
|
180
|
+
analytical = _analytical_cost(op, n)
|
|
181
|
+
elif op in ("in1d", "isin"):
|
|
182
|
+
setups = [
|
|
183
|
+
(
|
|
184
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
185
|
+
f"a = rng.standard_normal({n}).astype(np.{dtype}); "
|
|
186
|
+
f"b = rng.standard_normal({n}).astype(np.{dtype})"
|
|
187
|
+
),
|
|
188
|
+
(
|
|
189
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
190
|
+
f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
|
|
191
|
+
f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
195
|
+
f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
|
|
196
|
+
f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
|
|
197
|
+
),
|
|
198
|
+
]
|
|
199
|
+
bench = f"np.{op}(a, b)"
|
|
200
|
+
analytical = _analytical_cost(op, n, m=n)
|
|
201
|
+
elif op in ("intersect1d", "setdiff1d", "setxor1d", "union1d"):
|
|
202
|
+
setups = [
|
|
203
|
+
(
|
|
204
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
205
|
+
f"a = rng.standard_normal({n}).astype(np.{dtype}); "
|
|
206
|
+
f"b = rng.standard_normal({n}).astype(np.{dtype})"
|
|
207
|
+
),
|
|
208
|
+
(
|
|
209
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
210
|
+
f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype})); "
|
|
211
|
+
f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))"
|
|
212
|
+
),
|
|
213
|
+
(
|
|
214
|
+
f"import numpy as np; rng = np.random.default_rng(42); "
|
|
215
|
+
f"a = np.sort(rng.standard_normal({n}).astype(np.{dtype}))[::-1].copy(); "
|
|
216
|
+
f"b = np.sort(np.random.default_rng(43).standard_normal({n}).astype(np.{dtype}))[::-1].copy()"
|
|
217
|
+
),
|
|
218
|
+
]
|
|
219
|
+
bench = f"np.{op}(a, b)"
|
|
220
|
+
analytical = _analytical_cost(op, n, m=n)
|
|
221
|
+
elif op in ("unique_all", "unique_counts", "unique_inverse", "unique_values"):
|
|
222
|
+
setups = [
|
|
223
|
+
f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
|
|
224
|
+
f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))",
|
|
225
|
+
f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy()",
|
|
226
|
+
]
|
|
227
|
+
bench = f"np.{op}(x)"
|
|
228
|
+
analytical = _analytical_cost(op, n)
|
|
229
|
+
else:
|
|
230
|
+
# sort, argsort, unique
|
|
231
|
+
setups = [
|
|
232
|
+
f"import numpy as np; x = np.random.default_rng(42).standard_normal({n}).astype(np.{dtype})",
|
|
233
|
+
f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))",
|
|
234
|
+
f"import numpy as np; x = np.sort(np.random.default_rng(42).standard_normal({n}).astype(np.{dtype}))[::-1].copy()",
|
|
235
|
+
]
|
|
236
|
+
bench = f"np.{op}(x)"
|
|
237
|
+
analytical = _analytical_cost(op, n)
|
|
238
|
+
|
|
239
|
+
for setup in setups:
|
|
240
|
+
try:
|
|
241
|
+
result = measure_flops(setup, bench, repeats=repeats)
|
|
242
|
+
except RuntimeError:
|
|
243
|
+
continue
|
|
244
|
+
perf_instructions.append(result.total_flops)
|
|
245
|
+
dist_values.append(result.total_flops / (analytical * repeats))
|
|
246
|
+
|
|
247
|
+
if dist_values:
|
|
248
|
+
results[op] = statistics.median(dist_values)
|
|
249
|
+
# Build explicit benchmark_size per op
|
|
250
|
+
if op in (
|
|
251
|
+
"sort",
|
|
252
|
+
"argsort",
|
|
253
|
+
"unique",
|
|
254
|
+
"unique_all",
|
|
255
|
+
"unique_counts",
|
|
256
|
+
"unique_inverse",
|
|
257
|
+
"unique_values",
|
|
258
|
+
):
|
|
259
|
+
bm_size = f"x: ({n},)"
|
|
260
|
+
elif op in ("partition", "argpartition"):
|
|
261
|
+
bm_size = f"x: ({n},), kth={n // 2}"
|
|
262
|
+
elif op == "searchsorted":
|
|
263
|
+
bm_size = f"x: ({n},), q: ({n},)"
|
|
264
|
+
elif op == "lexsort":
|
|
265
|
+
bm_size = f"x: ({n},), y: ({n},), k=2"
|
|
266
|
+
elif op in (
|
|
267
|
+
"in1d",
|
|
268
|
+
"isin",
|
|
269
|
+
"intersect1d",
|
|
270
|
+
"setdiff1d",
|
|
271
|
+
"setxor1d",
|
|
272
|
+
"union1d",
|
|
273
|
+
):
|
|
274
|
+
bm_size = f"a: ({n},), b: ({n},)"
|
|
275
|
+
else:
|
|
276
|
+
bm_size = f"x: ({n},)"
|
|
277
|
+
details[op] = {
|
|
278
|
+
"category": "counted_custom",
|
|
279
|
+
"measurement_mode": "custom",
|
|
280
|
+
"analytical_formula": _FORMULA_STRINGS.get(op, "n"),
|
|
281
|
+
"analytical_flops": analytical,
|
|
282
|
+
"benchmark_size": bm_size,
|
|
283
|
+
"bench_code": bench,
|
|
284
|
+
"repeats": repeats,
|
|
285
|
+
"perf_instructions_total": perf_instructions,
|
|
286
|
+
"distribution_alphas": dist_values,
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return results, details
|