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
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Named cost-formula vocabulary for fnp.random method-level entries.
|
|
2
|
+
|
|
3
|
+
Each formula resolves to a callable ``(args, kwargs, result) -> int`` that
|
|
4
|
+
computes the FLOP cost from the call arguments and the numpy result.
|
|
5
|
+
The registry's ``cost_formula`` field names which formula a method uses.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import builtins as _builtins
|
|
11
|
+
from collections.abc import Callable
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import numpy as _np
|
|
15
|
+
|
|
16
|
+
from flopscope._flops import sort_cost as _sort_cost
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _numel_output(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
|
|
20
|
+
if isinstance(result, _np.ndarray):
|
|
21
|
+
return _builtins.max(int(result.size), 1)
|
|
22
|
+
return 1
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _numel_input(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
|
|
26
|
+
a = args[0] if args else kwargs.get("x")
|
|
27
|
+
if a is None:
|
|
28
|
+
return 1
|
|
29
|
+
if isinstance(a, _np.ndarray):
|
|
30
|
+
return _builtins.max(int(a.size), 1)
|
|
31
|
+
if hasattr(a, "__len__"):
|
|
32
|
+
return _builtins.max(len(a), 1)
|
|
33
|
+
return 1
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _length(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
|
|
37
|
+
if args:
|
|
38
|
+
n = int(args[0])
|
|
39
|
+
elif "length" in kwargs:
|
|
40
|
+
n = int(kwargs["length"])
|
|
41
|
+
else:
|
|
42
|
+
n = 1
|
|
43
|
+
return _builtins.max(n, 1)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _sort_cost_formula(
|
|
47
|
+
args: tuple[Any, ...], kwargs: dict[str, Any], result: Any
|
|
48
|
+
) -> int:
|
|
49
|
+
a = args[0] if args else kwargs.get("a")
|
|
50
|
+
if a is None:
|
|
51
|
+
return _sort_cost(1)
|
|
52
|
+
if isinstance(a, (int, _np.integer)):
|
|
53
|
+
n = int(a)
|
|
54
|
+
elif isinstance(a, _np.ndarray):
|
|
55
|
+
n = int(a.shape[0]) if a.ndim > 0 else int(a)
|
|
56
|
+
elif hasattr(a, "__len__"):
|
|
57
|
+
n = len(a)
|
|
58
|
+
else:
|
|
59
|
+
n = 1
|
|
60
|
+
return _sort_cost(n)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _shape_axis(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
|
|
64
|
+
"""Cost = shape along the axis being permuted (defaults to axis=0).
|
|
65
|
+
|
|
66
|
+
Used by shuffle/permutation: the algorithm is O(shape[axis]) RNG draws
|
|
67
|
+
regardless of how wide each slice is. For integer input (the
|
|
68
|
+
``permutation(int_n)`` case), cost = ``int(n)``. For ``axis=None`` —
|
|
69
|
+
which numpy interprets as "flatten then operate" — cost = numel.
|
|
70
|
+
"""
|
|
71
|
+
a = args[0] if args else kwargs.get("x")
|
|
72
|
+
if a is None:
|
|
73
|
+
return 1
|
|
74
|
+
if isinstance(a, (int, _np.integer)):
|
|
75
|
+
return _builtins.max(int(a), 1)
|
|
76
|
+
|
|
77
|
+
axis = args[1] if len(args) >= 2 else kwargs.get("axis", 0)
|
|
78
|
+
if axis is None:
|
|
79
|
+
if isinstance(a, _np.ndarray):
|
|
80
|
+
return _builtins.max(int(a.size), 1)
|
|
81
|
+
if hasattr(a, "__len__"):
|
|
82
|
+
return _builtins.max(len(a), 1)
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
if isinstance(a, _np.ndarray):
|
|
86
|
+
if a.ndim == 0:
|
|
87
|
+
# 0-D scalar array; numpy choice/permutation treats as int(a)
|
|
88
|
+
return _builtins.max(int(a), 1)
|
|
89
|
+
return _builtins.max(int(a.shape[int(axis)]), 1)
|
|
90
|
+
if hasattr(a, "__len__"):
|
|
91
|
+
return _builtins.max(len(a), 1)
|
|
92
|
+
return 1
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _choice_cost(args: tuple[Any, ...], kwargs: dict[str, Any], result: Any) -> int:
|
|
96
|
+
# Generator.choice: choice(a, size=None, replace=True, p=None, axis=0, shuffle=True)
|
|
97
|
+
# RandomState.choice: choice(a, size=None, replace=True, p=None)
|
|
98
|
+
# `replace` is the 3rd positional or the `replace` kwarg.
|
|
99
|
+
if len(args) >= 3:
|
|
100
|
+
replace = bool(args[2])
|
|
101
|
+
else:
|
|
102
|
+
replace = bool(kwargs.get("replace", True))
|
|
103
|
+
if replace:
|
|
104
|
+
return _numel_output(args, kwargs, result)
|
|
105
|
+
return _sort_cost_formula(args, kwargs, result)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
COST_FORMULAS: dict[str, Callable[[tuple[Any, ...], dict[str, Any], Any], int]] = {
|
|
109
|
+
"numel(output)": _numel_output,
|
|
110
|
+
"numel(input)": _numel_input,
|
|
111
|
+
"shape[axis]": _shape_axis,
|
|
112
|
+
"length": _length,
|
|
113
|
+
"sort_cost(n)": _sort_cost_formula,
|
|
114
|
+
"choice_cost": _choice_cost,
|
|
115
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""Counted subclasses of numpy.random.Generator and numpy.random.RandomState.
|
|
2
|
+
|
|
3
|
+
The classes use ``__getattribute__`` as a gate that consults registry-derived
|
|
4
|
+
``_COUNTED`` and ``_FREE`` sets. Sampler methods are added at module init by
|
|
5
|
+
``_build_counted_class``, which iterates the registry and emits wrappers via
|
|
6
|
+
``_make_counted_method``.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import functools
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from typing import Any, ClassVar
|
|
14
|
+
|
|
15
|
+
import numpy as _np
|
|
16
|
+
|
|
17
|
+
from flopscope._budget import _counted_wrapper
|
|
18
|
+
from flopscope._ndarray import _asflopscope
|
|
19
|
+
from flopscope._registry import REGISTRY
|
|
20
|
+
from flopscope._validation import require_budget
|
|
21
|
+
from flopscope.errors import UnsupportedFunctionError
|
|
22
|
+
from flopscope.numpy.random._cost_formulas import COST_FORMULAS
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@_counted_wrapper
|
|
26
|
+
def _make_counted_method(
|
|
27
|
+
op_name: str, formula_name: str, base_cls: type, plain_factory: Callable[..., Any]
|
|
28
|
+
) -> Callable[..., Any]:
|
|
29
|
+
"""Build a counted-method wrapper for the parent class's named method.
|
|
30
|
+
|
|
31
|
+
The wrapper calls the parent method on a *plain* base-class instance that
|
|
32
|
+
shares the same RNG state (so internal sibling-method calls do not go
|
|
33
|
+
through the counted gate and double-count). After numpy's C path returns,
|
|
34
|
+
cost is computed via the named formula, deducted from the active
|
|
35
|
+
BudgetContext, and the result is wrapped as FlopscopeArray if it is an
|
|
36
|
+
ndarray.
|
|
37
|
+
"""
|
|
38
|
+
method_name = op_name.split(".")[-1]
|
|
39
|
+
base_method = getattr(base_cls, method_name)
|
|
40
|
+
formula = COST_FORMULAS[formula_name]
|
|
41
|
+
|
|
42
|
+
@functools.wraps(base_method)
|
|
43
|
+
@_counted_wrapper
|
|
44
|
+
def wrapped(self, *args: Any, **kwargs: Any) -> Any:
|
|
45
|
+
budget = require_budget()
|
|
46
|
+
# Dispatch through a plain base-class instance to avoid the counted
|
|
47
|
+
# __getattribute__ gate intercepting any internal sibling calls
|
|
48
|
+
# (e.g. Generator.choice → Generator.integers).
|
|
49
|
+
plain = plain_factory(self)
|
|
50
|
+
result = base_method(plain, *args, **kwargs)
|
|
51
|
+
cost = formula(args, kwargs, result)
|
|
52
|
+
with budget.deduct(op_name, flop_cost=cost, subscripts=None, shapes=()):
|
|
53
|
+
pass # numpy already executed; deduct is post-hoc for output-dependent cost
|
|
54
|
+
if isinstance(result, _np.ndarray):
|
|
55
|
+
return _asflopscope(result)
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
return wrapped
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _build_counted_class(
|
|
62
|
+
base_cls: type,
|
|
63
|
+
op_prefix: str,
|
|
64
|
+
target_cls: type,
|
|
65
|
+
plain_factory: Callable[..., Any],
|
|
66
|
+
) -> None:
|
|
67
|
+
"""Populate target_cls._COUNTED, _FREE, and counted method bindings.
|
|
68
|
+
|
|
69
|
+
Reads REGISTRY entries whose op_name starts with op_prefix. counted_random_method
|
|
70
|
+
entries become wrapped class methods; free_random_method entries are added to
|
|
71
|
+
_FREE so the __getattribute__ gate lets them through unwrapped.
|
|
72
|
+
|
|
73
|
+
plain_factory is called with ``self`` (the counted subclass instance) to
|
|
74
|
+
produce a plain base-class instance sharing the same RNG state, so that
|
|
75
|
+
internal sibling calls inside numpy's C code do not go through the gate.
|
|
76
|
+
"""
|
|
77
|
+
counted: set[str] = set()
|
|
78
|
+
free: set[str] = set()
|
|
79
|
+
for op_name, entry in REGISTRY.items():
|
|
80
|
+
if not op_name.startswith(op_prefix):
|
|
81
|
+
continue
|
|
82
|
+
short = op_name[len(op_prefix) :]
|
|
83
|
+
category = entry.get("category")
|
|
84
|
+
if category == "counted_random_method":
|
|
85
|
+
counted.add(short)
|
|
86
|
+
setattr(
|
|
87
|
+
target_cls,
|
|
88
|
+
short,
|
|
89
|
+
_make_counted_method(
|
|
90
|
+
op_name, entry["cost_formula"], base_cls, plain_factory
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
elif category == "free_random_method":
|
|
94
|
+
free.add(short)
|
|
95
|
+
target_cls._COUNTED = frozenset(counted)
|
|
96
|
+
target_cls._FREE = frozenset(free)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _reconstruct_counted_generator(bit_generator: Any) -> _CountedGenerator:
|
|
100
|
+
"""Pickle helper — reconstruct a _CountedGenerator from its bit_generator."""
|
|
101
|
+
return _CountedGenerator(bit_generator)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _reconstruct_counted_random_state(state: tuple) -> _CountedRandomState:
|
|
105
|
+
"""Pickle helper — reconstruct a _CountedRandomState from its full state tuple."""
|
|
106
|
+
# Construct with default seed; the state we just received will overwrite.
|
|
107
|
+
rs = _CountedRandomState(seed=None)
|
|
108
|
+
rs.set_state(state)
|
|
109
|
+
return rs
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class _CountedGenerator(_np.random.Generator):
|
|
113
|
+
"""numpy ``Generator`` subclass with FLOP-counted sampler methods.
|
|
114
|
+
|
|
115
|
+
Each sampler method (``standard_normal``, ``normal``, ``uniform``,
|
|
116
|
+
``integers``, ``choice``, ``shuffle``, ``permutation``, ``bytes``, ...)
|
|
117
|
+
deducts FLOPs from the active ``BudgetContext`` and returns
|
|
118
|
+
``FlopscopeArray``. Free attribute access (``bit_generator``, ``spawn``)
|
|
119
|
+
is allowed; anything else raises ``UnsupportedFunctionError``.
|
|
120
|
+
|
|
121
|
+
Construct via :func:`flopscope.numpy.random.default_rng` (canonical) or by
|
|
122
|
+
passing a ``BitGenerator`` directly: ``Generator(np.random.PCG64(42))``.
|
|
123
|
+
|
|
124
|
+
Examples
|
|
125
|
+
--------
|
|
126
|
+
>>> import flopscope.numpy as fnp
|
|
127
|
+
>>> from flopscope import BudgetContext
|
|
128
|
+
>>> rng = fnp.random.default_rng(42)
|
|
129
|
+
>>> with BudgetContext(flop_budget=10**6):
|
|
130
|
+
... x = rng.standard_normal((10,))
|
|
131
|
+
>>> type(x).__name__
|
|
132
|
+
'FlopscopeArray'
|
|
133
|
+
|
|
134
|
+
Pickle / ``copy`` round-trips preserve counting:
|
|
135
|
+
|
|
136
|
+
>>> import pickle
|
|
137
|
+
>>> revived = pickle.loads(pickle.dumps(rng))
|
|
138
|
+
>>> isinstance(revived, type(rng))
|
|
139
|
+
True
|
|
140
|
+
|
|
141
|
+
See Also
|
|
142
|
+
--------
|
|
143
|
+
fnp.random.default_rng : canonical constructor.
|
|
144
|
+
"""
|
|
145
|
+
|
|
146
|
+
_COUNTED: ClassVar[frozenset[str]] = frozenset()
|
|
147
|
+
_FREE: ClassVar[frozenset[str]] = frozenset()
|
|
148
|
+
|
|
149
|
+
def __getattribute__(self, name: str) -> Any:
|
|
150
|
+
if name.startswith("_"):
|
|
151
|
+
return super().__getattribute__(name)
|
|
152
|
+
cls = type(self)
|
|
153
|
+
if name in cls._FREE or name in cls._COUNTED:
|
|
154
|
+
return super().__getattribute__(name)
|
|
155
|
+
raise UnsupportedFunctionError(
|
|
156
|
+
f"flopscope does not count Generator.{name}. "
|
|
157
|
+
f"This is either a new numpy method or one not yet wrapped. "
|
|
158
|
+
f"See https://github.com/AIcrowd/flopscope/issues/18"
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
# Wrap spawned children so their methods stay counted.
|
|
162
|
+
# Annotated as list[Generator] to match parent's invariant return type;
|
|
163
|
+
# runtime objects are _CountedGenerator (subclass of Generator).
|
|
164
|
+
def spawn(self, n_children: int) -> list[_np.random.Generator]:
|
|
165
|
+
children = _np.random.Generator.spawn(self, n_children)
|
|
166
|
+
return [_CountedGenerator(c.bit_generator) for c in children]
|
|
167
|
+
|
|
168
|
+
# Round-trip pickling back to _CountedGenerator (numpy's default would
|
|
169
|
+
# reconstruct as np.random.Generator and lose the counting wrappers).
|
|
170
|
+
def __reduce__(self) -> tuple:
|
|
171
|
+
return (_reconstruct_counted_generator, (self.bit_generator,))
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class _CountedRandomState(_np.random.RandomState):
|
|
175
|
+
"""numpy legacy ``RandomState`` subclass with FLOP-counted sampler methods.
|
|
176
|
+
|
|
177
|
+
Mirrors :class:`_CountedGenerator` for the legacy API: each sampler
|
|
178
|
+
(``randn``, ``normal``, ``uniform``, ``randint``, ``choice``,
|
|
179
|
+
``shuffle``, ``permutation``, ``bytes``, ...) deducts FLOPs from the
|
|
180
|
+
active ``BudgetContext`` and returns ``FlopscopeArray``. Free methods
|
|
181
|
+
(``seed``, ``get_state``, ``set_state``) pass through; everything else
|
|
182
|
+
raises ``UnsupportedFunctionError``.
|
|
183
|
+
|
|
184
|
+
Modern code should prefer :func:`flopscope.numpy.random.default_rng`;
|
|
185
|
+
use this only when porting code that relies on the legacy API.
|
|
186
|
+
|
|
187
|
+
Examples
|
|
188
|
+
--------
|
|
189
|
+
>>> import flopscope.numpy as fnp
|
|
190
|
+
>>> from flopscope import BudgetContext
|
|
191
|
+
>>> rs = fnp.random.RandomState(42)
|
|
192
|
+
>>> with BudgetContext(flop_budget=10**6):
|
|
193
|
+
... z = rs.randn(10)
|
|
194
|
+
>>> type(z).__name__
|
|
195
|
+
'FlopscopeArray'
|
|
196
|
+
|
|
197
|
+
See Also
|
|
198
|
+
--------
|
|
199
|
+
fnp.random.default_rng : canonical (modern) RNG constructor.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
_COUNTED: ClassVar[frozenset[str]] = frozenset()
|
|
203
|
+
_FREE: ClassVar[frozenset[str]] = frozenset()
|
|
204
|
+
|
|
205
|
+
def __getattribute__(self, name: str) -> Any:
|
|
206
|
+
if name.startswith("_"):
|
|
207
|
+
return super().__getattribute__(name)
|
|
208
|
+
cls = type(self)
|
|
209
|
+
if name in cls._FREE or name in cls._COUNTED:
|
|
210
|
+
return super().__getattribute__(name)
|
|
211
|
+
raise UnsupportedFunctionError(
|
|
212
|
+
f"flopscope does not count RandomState.{name}. "
|
|
213
|
+
f"This is either a new numpy method or one not yet wrapped. "
|
|
214
|
+
f"See https://github.com/AIcrowd/flopscope/issues/18"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Round-trip pickling back to _CountedRandomState (numpy's default
|
|
218
|
+
# __reduce__ hardcodes __randomstate_ctor which returns plain RandomState,
|
|
219
|
+
# silently losing FLOP counting).
|
|
220
|
+
def __reduce__(self) -> tuple:
|
|
221
|
+
# get_state() is in _FREE so the gate lets it through.
|
|
222
|
+
# set_state() is also in _FREE, used by the helper on the other side.
|
|
223
|
+
return (_reconstruct_counted_random_state, (self.get_state(),))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
# Wire counted methods at import time.
|
|
227
|
+
# plain_factory creates a plain base-class instance sharing the same RNG state;
|
|
228
|
+
# this prevents numpy's internal sibling-method calls from going through the
|
|
229
|
+
# counted __getattribute__ gate and double-counting FLOPs.
|
|
230
|
+
_build_counted_class(
|
|
231
|
+
_np.random.Generator,
|
|
232
|
+
"random.Generator.",
|
|
233
|
+
_CountedGenerator,
|
|
234
|
+
plain_factory=lambda self: _np.random.Generator(self._bit_generator),
|
|
235
|
+
)
|
|
236
|
+
_build_counted_class(
|
|
237
|
+
_np.random.RandomState,
|
|
238
|
+
"random.RandomState.",
|
|
239
|
+
_CountedRandomState,
|
|
240
|
+
plain_factory=lambda self: _np.random.RandomState(self._bit_generator),
|
|
241
|
+
)
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Test utilities re-exported from ``numpy.testing``.
|
|
2
|
+
|
|
3
|
+
These are free operations — they perform assertions and comparisons
|
|
4
|
+
for use in tests and profiling code, and do not count against any
|
|
5
|
+
FLOP budget.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from numpy.testing import assert_allclose, assert_array_equal # noqa: F401
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"assert_allclose",
|
|
12
|
+
"assert_array_equal",
|
|
13
|
+
]
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Type hints re-exported from numpy.typing.
|
|
2
|
+
|
|
3
|
+
Usage::
|
|
4
|
+
|
|
5
|
+
from flopscope.numpy.typing import NDArray, ArrayLike
|
|
6
|
+
|
|
7
|
+
def predict(x: NDArray) -> NDArray:
|
|
8
|
+
...
|
|
9
|
+
|
|
10
|
+
All names are plain aliases to numpy.typing. Because FlopscopeArray
|
|
11
|
+
is a subclass of numpy.ndarray, annotations like `NDArray[float32]`
|
|
12
|
+
accept flopscope arrays without any runtime overhead.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
# Re-export everything numpy.typing publicly exposes
|
|
18
|
+
from numpy import typing as _np_typing
|
|
19
|
+
from numpy.typing import ( # noqa: F401
|
|
20
|
+
ArrayLike,
|
|
21
|
+
DTypeLike,
|
|
22
|
+
NBitBase,
|
|
23
|
+
NDArray,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
__all__ = [name for name in dir(_np_typing) if not name.startswith("_")]
|
|
27
|
+
|
|
28
|
+
for _name in __all__:
|
|
29
|
+
if _name not in globals():
|
|
30
|
+
globals()[_name] = getattr(_np_typing, _name)
|
flopscope/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Continuous probability distributions with analytic FLOP accounting.
|
|
2
|
+
|
|
3
|
+
``flopscope.stats`` provides a focused subset of ``scipy.stats`` continuous
|
|
4
|
+
distributions. Each exported distribution object exposes ``pdf``, ``cdf``,
|
|
5
|
+
and ``ppf`` methods with SciPy-compatible signatures while charging a flat
|
|
6
|
+
FLOP cost per output element to the active budget.
|
|
7
|
+
|
|
8
|
+
Available distributions
|
|
9
|
+
-----------------------
|
|
10
|
+
norm
|
|
11
|
+
Normal (Gaussian) distribution.
|
|
12
|
+
uniform
|
|
13
|
+
Continuous uniform distribution.
|
|
14
|
+
expon
|
|
15
|
+
Exponential distribution.
|
|
16
|
+
cauchy
|
|
17
|
+
Cauchy (Lorentz) distribution.
|
|
18
|
+
logistic
|
|
19
|
+
Logistic distribution.
|
|
20
|
+
laplace
|
|
21
|
+
Laplace (double-exponential) distribution.
|
|
22
|
+
lognorm
|
|
23
|
+
Log-normal distribution.
|
|
24
|
+
truncnorm
|
|
25
|
+
Truncated normal distribution.
|
|
26
|
+
|
|
27
|
+
Notes
|
|
28
|
+
-----
|
|
29
|
+
All distributions use SciPy's ``loc``/``scale`` parameterization. Shape
|
|
30
|
+
parameters, when present, precede ``loc`` and ``scale`` exactly as they do in
|
|
31
|
+
``scipy.stats``. Each public method requires an active
|
|
32
|
+
``flopscope.BudgetContext`` and deducts the documented flat FLOP charge before
|
|
33
|
+
returning a ``FlopscopeArray`` result.
|
|
34
|
+
|
|
35
|
+
import flopscope as flops
|
|
36
|
+
|
|
37
|
+
flops.stats.norm.pdf(0.0) # standard normal PDF at 0
|
|
38
|
+
flops.stats.norm.cdf(1.96, loc=0, scale=1) # ≈ 0.975
|
|
39
|
+
flops.stats.expon.ppf(0.5, scale=2.0) # median of Exp(rate=0.5)
|
|
40
|
+
|
|
41
|
+
FLOP costs
|
|
42
|
+
----------
|
|
43
|
+
Each method deducts a **flat FLOP cost per element** from the active budget.
|
|
44
|
+
Costs are documented in each method's docstring. Internal sub-operations
|
|
45
|
+
(exp, log, etc.) are computed via raw NumPy and do **not** incur additional
|
|
46
|
+
FLOP charges.
|
|
47
|
+
|
|
48
|
+
Compatibility
|
|
49
|
+
-------------
|
|
50
|
+
Outputs are verified against ``scipy.stats`` to within 1e-12 relative
|
|
51
|
+
tolerance across the full input domain. See ``tests/test_stats_*.py``.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
>>> import flopscope as flops
|
|
56
|
+
>>> with flops.BudgetContext(flop_budget=32) as budget:
|
|
57
|
+
... probs = flops.stats.norm.cdf([0.0, 1.96])
|
|
58
|
+
... summary = budget.summary()
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
from flopscope._registry import make_module_getattr as _make_module_getattr
|
|
62
|
+
from flopscope.stats._cauchy import cauchy # noqa: F401
|
|
63
|
+
from flopscope.stats._expon import expon # noqa: F401
|
|
64
|
+
from flopscope.stats._laplace import laplace # noqa: F401
|
|
65
|
+
from flopscope.stats._logistic import logistic # noqa: F401
|
|
66
|
+
from flopscope.stats._lognorm import lognorm # noqa: F401
|
|
67
|
+
from flopscope.stats._norm import norm # noqa: F401
|
|
68
|
+
from flopscope.stats._truncnorm import truncnorm # noqa: F401
|
|
69
|
+
from flopscope.stats._uniform import uniform # noqa: F401
|
|
70
|
+
|
|
71
|
+
__all__ = [
|
|
72
|
+
"cauchy",
|
|
73
|
+
"expon",
|
|
74
|
+
"laplace",
|
|
75
|
+
"logistic",
|
|
76
|
+
"lognorm",
|
|
77
|
+
"norm",
|
|
78
|
+
"truncnorm",
|
|
79
|
+
"uniform",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
__getattr__ = _make_module_getattr(
|
|
83
|
+
module_prefix="stats.", module_label="flopscope.stats"
|
|
84
|
+
)
|
flopscope/stats/_base.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Base support for SciPy-compatible continuous distributions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import numpy as _np
|
|
6
|
+
|
|
7
|
+
from flopscope._budget import _counted_wrapper
|
|
8
|
+
from flopscope._ndarray import _asflopscope
|
|
9
|
+
from flopscope._validation import require_budget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ContinuousDistribution:
|
|
13
|
+
"""Base class for FLOP-counted continuous distributions.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
name : str
|
|
18
|
+
Distribution name used to construct operation labels such as
|
|
19
|
+
``"stats.norm.pdf"`` in the budget log.
|
|
20
|
+
|
|
21
|
+
Notes
|
|
22
|
+
-----
|
|
23
|
+
Subclasses implement ``_compute_pdf``, ``_compute_cdf``, and
|
|
24
|
+
``_compute_ppf`` as pure NumPy kernels. Public ``pdf``, ``cdf``, and
|
|
25
|
+
``ppf`` methods should delegate through :meth:`_deduct_and_call` so that
|
|
26
|
+
budget deduction and ``FlopscopeArray`` wrapping stay consistent across
|
|
27
|
+
the stats surface.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, name: str):
|
|
31
|
+
self._name = name
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def name(self) -> str:
|
|
35
|
+
return self._name
|
|
36
|
+
|
|
37
|
+
@_counted_wrapper
|
|
38
|
+
def _deduct_and_call(self, method: str, cost_per_elem: int, x, *args, **kwargs):
|
|
39
|
+
"""Deduct FLOPs then call the pure-numpy implementation.
|
|
40
|
+
|
|
41
|
+
Parameters
|
|
42
|
+
----------
|
|
43
|
+
method : str
|
|
44
|
+
Method name for budget logging, e.g. ``"pdf"``.
|
|
45
|
+
cost_per_elem : int
|
|
46
|
+
Flat FLOP cost per output element.
|
|
47
|
+
x : array_like
|
|
48
|
+
Primary input array (determines output size).
|
|
49
|
+
*args, **kwargs
|
|
50
|
+
Forwarded to ``_compute_{method}``.
|
|
51
|
+
|
|
52
|
+
Returns
|
|
53
|
+
-------
|
|
54
|
+
FlopscopeArray
|
|
55
|
+
Result returned by the matching ``_compute_{method}``
|
|
56
|
+
implementation after budget deduction.
|
|
57
|
+
|
|
58
|
+
Notes
|
|
59
|
+
-----
|
|
60
|
+
The deducted FLOP charge is ``cost_per_elem * max(numel(x), 1)``.
|
|
61
|
+
"""
|
|
62
|
+
budget = require_budget()
|
|
63
|
+
x = _np.asarray(x, dtype=_np.float64)
|
|
64
|
+
n = max(x.size, 1)
|
|
65
|
+
op_name = f"stats.{self._name}.{method}"
|
|
66
|
+
compute_fn = getattr(self, f"_compute_{method}")
|
|
67
|
+
with budget.deduct(
|
|
68
|
+
op_name,
|
|
69
|
+
flop_cost=cost_per_elem * n,
|
|
70
|
+
subscripts=None,
|
|
71
|
+
shapes=(x.shape,),
|
|
72
|
+
):
|
|
73
|
+
result = compute_fn(x, *args, **kwargs)
|
|
74
|
+
return _asflopscope(result)
|
|
75
|
+
|
|
76
|
+
def __repr__(self) -> str:
|
|
77
|
+
return f"<flopscope.stats.{self._name}>"
|