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/_display.py
ADDED
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
"""Budget display rendering with Rich (optional) and plain-text fallback."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from flopscope._budget import _snapshot_records, budget_summary_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _format_flops(n: int) -> str:
|
|
9
|
+
"""Format a FLOP count with thousands separators."""
|
|
10
|
+
return f"{n:,}"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _pct(used: int | float, total: int | float) -> str:
|
|
14
|
+
"""Return a percentage string."""
|
|
15
|
+
if total == 0:
|
|
16
|
+
return "0.0%"
|
|
17
|
+
return f"{100 * used / total:.1f}%"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _usage_color(used: int, total: int) -> str:
|
|
21
|
+
"""Return a Rich color name based on usage percentage."""
|
|
22
|
+
if total == 0:
|
|
23
|
+
return "white"
|
|
24
|
+
ratio = used / total
|
|
25
|
+
if ratio < 0.5:
|
|
26
|
+
return "green"
|
|
27
|
+
if ratio < 0.8:
|
|
28
|
+
return "yellow"
|
|
29
|
+
return "red"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _namespace_label(ns: str | None) -> str:
|
|
33
|
+
return ns if ns is not None else "(unlabeled)"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _call_label(calls: int) -> str:
|
|
37
|
+
return f"{calls} call{'s' if calls != 1 else ''}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _sorted_namespace_rows(
|
|
41
|
+
by_namespace: dict[str | None, dict],
|
|
42
|
+
) -> list[tuple[str | None, dict]]:
|
|
43
|
+
return sorted(
|
|
44
|
+
by_namespace.items(),
|
|
45
|
+
key=lambda item: (-item[1]["flops_used"], _namespace_label(item[0])),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format_budget_summary_text(
|
|
50
|
+
data: dict,
|
|
51
|
+
*,
|
|
52
|
+
by_namespace: bool = False,
|
|
53
|
+
header: str = "flopscope FLOP Budget Summary",
|
|
54
|
+
) -> str:
|
|
55
|
+
"""Render structured budget data as plain text."""
|
|
56
|
+
if data["flops_used"] == 0 and not data.get("operations"):
|
|
57
|
+
return "No budget data recorded yet."
|
|
58
|
+
|
|
59
|
+
lines = [
|
|
60
|
+
header,
|
|
61
|
+
"=" * len(header),
|
|
62
|
+
f" Total budget: {_format_flops(data['flop_budget']):>20}",
|
|
63
|
+
f" Used: {_format_flops(data['flops_used']):>20} ({_pct(data['flops_used'], data['flop_budget'])})",
|
|
64
|
+
f" Remaining: {_format_flops(data['flops_remaining']):>20} ({_pct(data['flops_remaining'], data['flop_budget'])})",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
if by_namespace and data.get("by_namespace"):
|
|
68
|
+
lines += ["", " By namespace:"]
|
|
69
|
+
for namespace, bucket in _sorted_namespace_rows(data["by_namespace"]):
|
|
70
|
+
lines.append(
|
|
71
|
+
f" {_namespace_label(namespace):<24} {_format_flops(bucket['flops_used']):>12} ({_pct(bucket['flops_used'], data['flops_used']):>6}) [{_call_label(bucket['calls'])}] Backend {bucket['flopscope_backend_time_s']:.3f}s Overhead {bucket['flopscope_overhead_time_s']:.3f}s"
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
ops = data.get("operations", {})
|
|
75
|
+
if ops:
|
|
76
|
+
lines += ["", " By operation:"]
|
|
77
|
+
for op_name, op_info in sorted(
|
|
78
|
+
ops.items(), key=lambda item: -item[1]["flop_cost"]
|
|
79
|
+
):
|
|
80
|
+
lines.append(
|
|
81
|
+
f" {op_name:<20} {_format_flops(op_info['flop_cost']):>12} ({_pct(op_info['flop_cost'], data['flops_used']):>6}) [{_call_label(op_info['calls'])}]"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
wall_time = data.get("wall_time_s")
|
|
85
|
+
backend_time = data.get("flopscope_backend_time_s", 0.0)
|
|
86
|
+
overhead_time = data.get("flopscope_overhead_time_s", 0.0)
|
|
87
|
+
residual_wall_time = data.get("residual_wall_time_s")
|
|
88
|
+
if wall_time is not None and residual_wall_time is not None:
|
|
89
|
+
lines += [
|
|
90
|
+
"",
|
|
91
|
+
f" Total Wall Time: {wall_time:.3f}s",
|
|
92
|
+
f" Flopscope Backend: {backend_time:.3f}s ({_pct(backend_time, wall_time)})",
|
|
93
|
+
f" Flopscope Overhead: {overhead_time:.3f}s ({_pct(overhead_time, wall_time)})",
|
|
94
|
+
f" Residual Wall Time: {residual_wall_time:.3f}s ({_pct(residual_wall_time, wall_time)})",
|
|
95
|
+
]
|
|
96
|
+
|
|
97
|
+
op_backend_times = {
|
|
98
|
+
op_name: op_info["flopscope_backend_time_s"]
|
|
99
|
+
for op_name, op_info in ops.items()
|
|
100
|
+
if op_info.get("flopscope_backend_time_s", 0.0) > 0
|
|
101
|
+
}
|
|
102
|
+
if backend_time > 0 and op_backend_times:
|
|
103
|
+
lines += ["", " By operation (time):"]
|
|
104
|
+
for op_name, op_backend_time in sorted(
|
|
105
|
+
op_backend_times.items(), key=lambda item: -item[1]
|
|
106
|
+
):
|
|
107
|
+
lines.append(
|
|
108
|
+
f" {op_name:<20} {op_backend_time:.3f}s ({_pct(op_backend_time, backend_time):>6}) [{_call_label(ops[op_name]['calls'])}]"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
return "\n".join(lines)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _plain_text_summary(by_namespace: bool = False) -> str:
|
|
115
|
+
"""Render a plain-text budget summary."""
|
|
116
|
+
return _format_budget_summary_text(
|
|
117
|
+
budget_summary_dict(by_namespace=by_namespace),
|
|
118
|
+
by_namespace=by_namespace,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
_GLOBAL_DEFAULT_BUDGET = int(1e15)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_global_default_ns(ns, ns_data: dict) -> bool:
|
|
126
|
+
"""Check if a namespace record is the implicit global default."""
|
|
127
|
+
return ns is None and ns_data.get("flop_budget", 0) >= _GLOBAL_DEFAULT_BUDGET
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _display_totals(data: dict | None = None) -> dict:
|
|
131
|
+
"""Compute user-facing totals, hiding the implicit global default budget."""
|
|
132
|
+
if data is None:
|
|
133
|
+
data = budget_summary_dict()
|
|
134
|
+
|
|
135
|
+
explicit_budget = 0
|
|
136
|
+
explicit_used = 0
|
|
137
|
+
for record in _snapshot_records():
|
|
138
|
+
if record.namespace is None and record.flop_budget >= _GLOBAL_DEFAULT_BUDGET:
|
|
139
|
+
explicit_used += record.flops_used
|
|
140
|
+
else:
|
|
141
|
+
explicit_budget += record.flop_budget
|
|
142
|
+
explicit_used += record.flops_used
|
|
143
|
+
|
|
144
|
+
has_explicit_budget = explicit_budget > 0
|
|
145
|
+
if has_explicit_budget:
|
|
146
|
+
return {
|
|
147
|
+
"has_explicit_budget": True,
|
|
148
|
+
"budget": explicit_budget,
|
|
149
|
+
"used": explicit_used,
|
|
150
|
+
"remaining": explicit_budget - explicit_used,
|
|
151
|
+
"color": _usage_color(explicit_used, explicit_budget),
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
"has_explicit_budget": False,
|
|
156
|
+
"budget": data["flop_budget"],
|
|
157
|
+
"used": data["flops_used"],
|
|
158
|
+
"remaining": data["flops_remaining"],
|
|
159
|
+
"color": "green",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _rich_namespace_table(label: str, ns_data: dict, color: str):
|
|
164
|
+
"""Build a Rich Table for a single namespace bucket."""
|
|
165
|
+
from rich.table import Table
|
|
166
|
+
from rich.text import Text
|
|
167
|
+
|
|
168
|
+
table = Table(
|
|
169
|
+
show_header=True,
|
|
170
|
+
header_style="bold",
|
|
171
|
+
title_style="bold",
|
|
172
|
+
expand=True,
|
|
173
|
+
padding=(0, 1),
|
|
174
|
+
)
|
|
175
|
+
table.add_column("Operation", style="dim")
|
|
176
|
+
table.add_column("FLOPs", justify="right")
|
|
177
|
+
table.add_column("%", justify="right")
|
|
178
|
+
table.add_column("Backend", justify="right", style="dim")
|
|
179
|
+
table.add_column("Overhead", justify="right", style="dim")
|
|
180
|
+
table.add_column("Calls", justify="right", style="dim")
|
|
181
|
+
|
|
182
|
+
ops = ns_data.get("operations", {})
|
|
183
|
+
total = ns_data.get("flop_budget", ns_data.get("flops_used", 0))
|
|
184
|
+
for op_name, op_info in sorted(ops.items(), key=lambda item: -item[1]["flop_cost"]):
|
|
185
|
+
backend = op_info.get("flopscope_backend_time_s", 0.0)
|
|
186
|
+
overhead = op_info.get("flopscope_overhead_time_s", 0.0)
|
|
187
|
+
table.add_row(
|
|
188
|
+
op_name,
|
|
189
|
+
_format_flops(op_info["flop_cost"]),
|
|
190
|
+
_pct(op_info["flop_cost"], total),
|
|
191
|
+
f"{backend:.3f}s" if backend > 0 else "",
|
|
192
|
+
f"{overhead:.3f}s" if overhead > 0 else "",
|
|
193
|
+
_call_label(op_info["calls"]),
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
table.add_section()
|
|
197
|
+
table.add_row(
|
|
198
|
+
Text("Used", style="bold"),
|
|
199
|
+
Text(_format_flops(ns_data.get("flops_used", 0)), style=f"bold {color}"),
|
|
200
|
+
Text(_pct(ns_data.get("flops_used", 0), total), style=color),
|
|
201
|
+
"",
|
|
202
|
+
"",
|
|
203
|
+
"",
|
|
204
|
+
)
|
|
205
|
+
if "flop_budget" in ns_data:
|
|
206
|
+
remaining = ns_data["flop_budget"] - ns_data.get("flops_used", 0)
|
|
207
|
+
table.add_row(
|
|
208
|
+
Text("Remaining", style="bold"),
|
|
209
|
+
Text(_format_flops(remaining), style="bold"),
|
|
210
|
+
"",
|
|
211
|
+
"",
|
|
212
|
+
"",
|
|
213
|
+
"",
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return table
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _rich_totals_table(data: dict):
|
|
220
|
+
from rich.table import Table
|
|
221
|
+
from rich.text import Text
|
|
222
|
+
|
|
223
|
+
table = Table(show_header=False, expand=True, padding=(0, 1), box=None)
|
|
224
|
+
table.add_column("label", style="bold")
|
|
225
|
+
table.add_column("value", justify="right")
|
|
226
|
+
|
|
227
|
+
totals = _display_totals(data)
|
|
228
|
+
if totals["has_explicit_budget"]:
|
|
229
|
+
table.add_row("Budget", _format_flops(totals["budget"]))
|
|
230
|
+
table.add_row(
|
|
231
|
+
"Used",
|
|
232
|
+
Text(
|
|
233
|
+
(
|
|
234
|
+
f"{_format_flops(totals['used'])} ({_pct(totals['used'], totals['budget'])})"
|
|
235
|
+
if totals["has_explicit_budget"]
|
|
236
|
+
else _format_flops(totals["used"])
|
|
237
|
+
),
|
|
238
|
+
style=totals["color"],
|
|
239
|
+
),
|
|
240
|
+
)
|
|
241
|
+
if totals["has_explicit_budget"]:
|
|
242
|
+
table.add_row(
|
|
243
|
+
"Remaining",
|
|
244
|
+
f"{_format_flops(totals['remaining'])} ({_pct(totals['remaining'], totals['budget'])})",
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
wall_time = data.get("wall_time_s")
|
|
248
|
+
backend_time = data.get("flopscope_backend_time_s", 0.0)
|
|
249
|
+
overhead_time = data.get("flopscope_overhead_time_s", 0.0)
|
|
250
|
+
residual_wall_time = data.get("residual_wall_time_s")
|
|
251
|
+
if wall_time is not None and residual_wall_time is not None:
|
|
252
|
+
table.add_section()
|
|
253
|
+
table.add_row("Total Wall Time", f"{wall_time:.3f}s")
|
|
254
|
+
table.add_row(
|
|
255
|
+
"Flopscope Backend",
|
|
256
|
+
Text(
|
|
257
|
+
f"{backend_time:.3f}s ({_pct(backend_time, wall_time)})",
|
|
258
|
+
style="dim",
|
|
259
|
+
),
|
|
260
|
+
)
|
|
261
|
+
table.add_row(
|
|
262
|
+
"Flopscope Overhead",
|
|
263
|
+
Text(
|
|
264
|
+
f"{overhead_time:.3f}s ({_pct(overhead_time, wall_time)})",
|
|
265
|
+
style="dim",
|
|
266
|
+
),
|
|
267
|
+
)
|
|
268
|
+
table.add_row(
|
|
269
|
+
"Residual Wall Time",
|
|
270
|
+
Text(
|
|
271
|
+
f"{residual_wall_time:.3f}s ({_pct(residual_wall_time, wall_time)})",
|
|
272
|
+
style="dim",
|
|
273
|
+
),
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
return table
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _rich_attribution_table(by_ns: dict[str | None, dict], total_flops_used: int):
|
|
280
|
+
from rich.table import Table
|
|
281
|
+
|
|
282
|
+
table = Table(
|
|
283
|
+
title="By namespace",
|
|
284
|
+
show_header=True,
|
|
285
|
+
header_style="bold",
|
|
286
|
+
expand=True,
|
|
287
|
+
padding=(0, 1),
|
|
288
|
+
)
|
|
289
|
+
table.add_column("Namespace")
|
|
290
|
+
table.add_column("FLOPs", justify="right")
|
|
291
|
+
table.add_column("%", justify="right")
|
|
292
|
+
table.add_column("Calls", justify="right")
|
|
293
|
+
table.add_column("Backend", justify="right")
|
|
294
|
+
table.add_column("Overhead", justify="right")
|
|
295
|
+
|
|
296
|
+
for namespace, bucket in _sorted_namespace_rows(by_ns):
|
|
297
|
+
table.add_row(
|
|
298
|
+
_namespace_label(namespace),
|
|
299
|
+
_format_flops(bucket["flops_used"]),
|
|
300
|
+
_pct(bucket["flops_used"], total_flops_used),
|
|
301
|
+
str(bucket["calls"]),
|
|
302
|
+
f"{bucket['flopscope_backend_time_s']:.3f}s",
|
|
303
|
+
f"{bucket['flopscope_overhead_time_s']:.3f}s",
|
|
304
|
+
)
|
|
305
|
+
|
|
306
|
+
return table
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def _rich_operations_table(ops: dict[str, dict], total_flops_used: int):
|
|
310
|
+
from rich.table import Table
|
|
311
|
+
|
|
312
|
+
table = Table(
|
|
313
|
+
title="By operation",
|
|
314
|
+
show_header=True,
|
|
315
|
+
header_style="bold",
|
|
316
|
+
expand=True,
|
|
317
|
+
padding=(0, 1),
|
|
318
|
+
)
|
|
319
|
+
table.add_column("Operation", style="dim")
|
|
320
|
+
table.add_column("FLOPs", justify="right")
|
|
321
|
+
table.add_column("%", justify="right")
|
|
322
|
+
table.add_column("Backend", justify="right", style="dim")
|
|
323
|
+
table.add_column("Overhead", justify="right", style="dim")
|
|
324
|
+
table.add_column("Calls", justify="right", style="dim")
|
|
325
|
+
|
|
326
|
+
for op_name, op_info in sorted(ops.items(), key=lambda item: -item[1]["flop_cost"]):
|
|
327
|
+
backend = op_info.get("flopscope_backend_time_s", 0.0)
|
|
328
|
+
overhead = op_info.get("flopscope_overhead_time_s", 0.0)
|
|
329
|
+
table.add_row(
|
|
330
|
+
op_name,
|
|
331
|
+
_format_flops(op_info["flop_cost"]),
|
|
332
|
+
_pct(op_info["flop_cost"], total_flops_used),
|
|
333
|
+
f"{backend:.3f}s" if backend > 0 else "",
|
|
334
|
+
f"{overhead:.3f}s" if overhead > 0 else "",
|
|
335
|
+
_call_label(op_info["calls"]),
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
return table
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _rich_summary(by_namespace: bool = False):
|
|
342
|
+
"""Render a Rich-formatted budget summary."""
|
|
343
|
+
from rich.console import Group
|
|
344
|
+
from rich.panel import Panel
|
|
345
|
+
|
|
346
|
+
data = budget_summary_dict(by_namespace=by_namespace)
|
|
347
|
+
if data["flops_used"] == 0 and not data.get("operations"):
|
|
348
|
+
return Panel("No budget data recorded yet.", title="flopscope Budget")
|
|
349
|
+
|
|
350
|
+
renderables = [_rich_totals_table(data)]
|
|
351
|
+
if by_namespace and data.get("by_namespace"):
|
|
352
|
+
renderables.append(
|
|
353
|
+
_rich_attribution_table(data["by_namespace"], data["flops_used"])
|
|
354
|
+
)
|
|
355
|
+
if data.get("operations"):
|
|
356
|
+
renderables.append(
|
|
357
|
+
_rich_operations_table(data["operations"], data["flops_used"])
|
|
358
|
+
)
|
|
359
|
+
|
|
360
|
+
return Panel(
|
|
361
|
+
Group(*renderables),
|
|
362
|
+
title="[bold cyan]flopscope FLOP Budget Summary[/bold cyan]",
|
|
363
|
+
border_style="cyan",
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def render_budget_summary(by_namespace: bool = False):
|
|
368
|
+
"""Return a Rich renderable if Rich is installed, otherwise plain text."""
|
|
369
|
+
try:
|
|
370
|
+
import rich # noqa: F401
|
|
371
|
+
|
|
372
|
+
return _rich_summary(by_namespace=by_namespace)
|
|
373
|
+
except ImportError:
|
|
374
|
+
return _plain_text_summary(by_namespace=by_namespace)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
class _PlainTextLive:
|
|
378
|
+
"""Fallback live display that prints summary on exit."""
|
|
379
|
+
|
|
380
|
+
def __init__(self, by_namespace: bool = False):
|
|
381
|
+
self._by_namespace = by_namespace
|
|
382
|
+
|
|
383
|
+
def __enter__(self):
|
|
384
|
+
return self
|
|
385
|
+
|
|
386
|
+
def __exit__(self, *args):
|
|
387
|
+
print(_plain_text_summary(by_namespace=self._by_namespace))
|
|
388
|
+
return None
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
def budget_live(by_namespace: bool = False):
|
|
392
|
+
"""Return a live-updating budget summary context manager.
|
|
393
|
+
|
|
394
|
+
Parameters
|
|
395
|
+
----------
|
|
396
|
+
by_namespace : bool, optional
|
|
397
|
+
If ``True``, include the namespace breakdown in the live display.
|
|
398
|
+
Default ``False``.
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
object
|
|
403
|
+
A context manager that refreshes the session-wide budget summary while
|
|
404
|
+
the ``with`` block is active. When Rich is installed this is a
|
|
405
|
+
live-rendered display; otherwise it falls back to a plain-text summary
|
|
406
|
+
printed when the block exits.
|
|
407
|
+
|
|
408
|
+
Examples
|
|
409
|
+
--------
|
|
410
|
+
>>> import flopscope as flops
|
|
411
|
+
>>> import flopscope.numpy as fnp
|
|
412
|
+
>>> with flops.budget_live():
|
|
413
|
+
... with flops.BudgetContext(flop_budget=100):
|
|
414
|
+
... _ = fnp.add(fnp.array([1.0]), fnp.array([2.0]))
|
|
415
|
+
"""
|
|
416
|
+
try:
|
|
417
|
+
from rich.live import Live
|
|
418
|
+
|
|
419
|
+
class _RichBudgetLive:
|
|
420
|
+
def __init__(self, by_namespace: bool):
|
|
421
|
+
self._by_namespace = by_namespace
|
|
422
|
+
self._live = None
|
|
423
|
+
|
|
424
|
+
def __enter__(self):
|
|
425
|
+
self._live = Live(
|
|
426
|
+
_rich_summary(by_namespace=self._by_namespace),
|
|
427
|
+
refresh_per_second=2,
|
|
428
|
+
)
|
|
429
|
+
self._live.__enter__()
|
|
430
|
+
return self
|
|
431
|
+
|
|
432
|
+
def __exit__(self, *args):
|
|
433
|
+
if self._live is not None:
|
|
434
|
+
self._live.update(_rich_summary(by_namespace=self._by_namespace))
|
|
435
|
+
self._live.__exit__(*args)
|
|
436
|
+
return None
|
|
437
|
+
|
|
438
|
+
return _RichBudgetLive(by_namespace)
|
|
439
|
+
except ImportError:
|
|
440
|
+
return _PlainTextLive(by_namespace=by_namespace)
|
|
441
|
+
|
|
442
|
+
|
|
443
|
+
def budget_summary(by_namespace: bool = False):
|
|
444
|
+
"""Render the session-wide budget summary.
|
|
445
|
+
|
|
446
|
+
Parameters
|
|
447
|
+
----------
|
|
448
|
+
by_namespace : bool, optional
|
|
449
|
+
If ``True``, include the namespace breakdown in the rendered summary.
|
|
450
|
+
Default ``False``.
|
|
451
|
+
|
|
452
|
+
Returns
|
|
453
|
+
-------
|
|
454
|
+
object or None
|
|
455
|
+
In notebook-style environments, returns the Rich renderable or plain
|
|
456
|
+
text summary object. In a standard terminal, prints the summary and
|
|
457
|
+
returns ``None``.
|
|
458
|
+
|
|
459
|
+
Examples
|
|
460
|
+
--------
|
|
461
|
+
>>> import flopscope as flops
|
|
462
|
+
>>> import flopscope.numpy as fnp
|
|
463
|
+
>>> with flops.BudgetContext(flop_budget=100):
|
|
464
|
+
... _ = fnp.add(fnp.array([1.0]), fnp.array([2.0]))
|
|
465
|
+
>>> flops.budget_summary()
|
|
466
|
+
"""
|
|
467
|
+
result = render_budget_summary(by_namespace=by_namespace)
|
|
468
|
+
try:
|
|
469
|
+
_ = get_ipython # type: ignore[name-defined] # noqa: F821
|
|
470
|
+
return result
|
|
471
|
+
except NameError:
|
|
472
|
+
if isinstance(result, str):
|
|
473
|
+
print(result)
|
|
474
|
+
else:
|
|
475
|
+
from rich.console import Console
|
|
476
|
+
|
|
477
|
+
Console().print(result)
|
|
478
|
+
return None
|
flopscope/_docstrings.py
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Docstring inheritance helper for flopscope wrappers."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def attach_docstring(wrapper, np_func, category: str, cost_description: str) -> None:
|
|
7
|
+
"""Attach NumPy's docstring to a flopscope wrapper with a FLOP Cost section.
|
|
8
|
+
|
|
9
|
+
Inserts a dedicated "FLOP Cost" section after the summary line(s),
|
|
10
|
+
before the Parameters section. This keeps cost info prominent and
|
|
11
|
+
separate from NumPy's own Notes.
|
|
12
|
+
"""
|
|
13
|
+
np_doc = getattr(np_func, "__doc__", None) or ""
|
|
14
|
+
|
|
15
|
+
cost_section = f"FLOP Cost\n---------\n{cost_description}\n"
|
|
16
|
+
|
|
17
|
+
if not np_doc:
|
|
18
|
+
wrapper.__doc__ = (
|
|
19
|
+
f"Counted wrapper for ``numpy.{np_func.__name__}``.\n\n{cost_section}"
|
|
20
|
+
)
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
# Find the first standard section header (Parameters, Returns, etc.)
|
|
24
|
+
# and insert the cost section before it.
|
|
25
|
+
lines = np_doc.split("\n")
|
|
26
|
+
section_headers = {
|
|
27
|
+
"Parameters",
|
|
28
|
+
"Returns",
|
|
29
|
+
"Raises",
|
|
30
|
+
"See Also",
|
|
31
|
+
"Notes",
|
|
32
|
+
"References",
|
|
33
|
+
"Examples",
|
|
34
|
+
"Yields",
|
|
35
|
+
"Warns",
|
|
36
|
+
"Other Parameters",
|
|
37
|
+
"Attributes",
|
|
38
|
+
"Methods",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
insert_idx = None
|
|
42
|
+
for i, line in enumerate(lines):
|
|
43
|
+
stripped = line.strip()
|
|
44
|
+
if (
|
|
45
|
+
stripped in section_headers
|
|
46
|
+
and i + 1 < len(lines)
|
|
47
|
+
and lines[i + 1].strip().startswith("---")
|
|
48
|
+
):
|
|
49
|
+
insert_idx = i
|
|
50
|
+
break
|
|
51
|
+
|
|
52
|
+
if insert_idx is not None:
|
|
53
|
+
# Insert cost section before the first standard section
|
|
54
|
+
before = "\n".join(lines[:insert_idx]).rstrip()
|
|
55
|
+
after = "\n".join(lines[insert_idx:])
|
|
56
|
+
wrapper.__doc__ = f"{before}\n\n{cost_section}\n{after}"
|
|
57
|
+
else:
|
|
58
|
+
# No standard sections found — append cost section
|
|
59
|
+
wrapper.__doc__ = f"{np_doc.rstrip()}\n\n{cost_section}"
|
flopscope/_dtypes.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""NumPy dtype types re-exported as free (0-FLOP) attributes.
|
|
2
|
+
|
|
3
|
+
These are plain aliases — they perform no FLOP tracking because they
|
|
4
|
+
are type objects, not operations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import numpy as _np
|
|
10
|
+
|
|
11
|
+
# Abstract dtype hierarchy
|
|
12
|
+
floating = _np.floating
|
|
13
|
+
integer = _np.integer
|
|
14
|
+
number = _np.number
|
|
15
|
+
dtype = _np.dtype
|
|
16
|
+
|
|
17
|
+
# Missing unsigned integer dtypes (uint8 is already exported from __init__.py)
|
|
18
|
+
uint16 = _np.uint16
|
|
19
|
+
uint32 = _np.uint32
|
|
20
|
+
uint64 = _np.uint64
|