counted-float 1.0.1__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.
Files changed (40) hide show
  1. counted_float/__init__.py +39 -0
  2. counted_float/_core/__init__.py +0 -0
  3. counted_float/_core/_backwards_compat.py +6 -0
  4. counted_float/_core/_optional_deps.py +49 -0
  5. counted_float/_core/benchmarking/__init__.py +10 -0
  6. counted_float/_core/benchmarking/_flops_benchmark_suite.py +189 -0
  7. counted_float/_core/benchmarking/_flops_micro_benchmark.py +64 -0
  8. counted_float/_core/benchmarking/_micro_benchmark.py +113 -0
  9. counted_float/_core/benchmarking/_models.py +31 -0
  10. counted_float/_core/benchmarking/_time_utils.py +70 -0
  11. counted_float/_core/counting/__init__.py +3 -0
  12. counted_float/_core/counting/_builtin_data.py +25 -0
  13. counted_float/_core/counting/_context_managers.py +95 -0
  14. counted_float/_core/counting/_counted_float.py +201 -0
  15. counted_float/_core/counting/_global_counter.py +98 -0
  16. counted_float/_core/counting/config/__init__.py +6 -0
  17. counted_float/_core/counting/config/_config.py +54 -0
  18. counted_float/_core/counting/config/_defaults.py +49 -0
  19. counted_float/_core/counting/models/__init__.py +13 -0
  20. counted_float/_core/counting/models/_base.py +6 -0
  21. counted_float/_core/counting/models/_flop_counts.py +75 -0
  22. counted_float/_core/counting/models/_flop_type.py +47 -0
  23. counted_float/_core/counting/models/_flop_weights.py +85 -0
  24. counted_float/_core/counting/models/_flops_benchmark_result.py +69 -0
  25. counted_float/_core/counting/models/_fpu_instruction.py +22 -0
  26. counted_float/_core/counting/models/_fpu_specs.py +94 -0
  27. counted_float/_core/data/__init__.py +0 -0
  28. counted_float/_core/data/benchmarks/__init__.py +0 -0
  29. counted_float/_core/data/benchmarks/apple_m3_max.json +103 -0
  30. counted_float/_core/data/benchmarks/intel_i5_7200u.json +103 -0
  31. counted_float/_core/data/benchmarks/intel_i7_1265u.json +103 -0
  32. counted_float/_core/data/specs/__init__.py +0 -0
  33. counted_float/_core/data/specs/amd_zen4_r9_7900x.json +52 -0
  34. counted_float/_core/data/specs/intel_gen11_tiger_lake.json +52 -0
  35. counted_float/benchmarking/__init__.py +14 -0
  36. counted_float/config/__init__.py +15 -0
  37. counted_float-1.0.1.dist-info/METADATA +289 -0
  38. counted_float-1.0.1.dist-info/RECORD +40 -0
  39. counted_float-1.0.1.dist-info/WHEEL +4 -0
  40. counted_float-1.0.1.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,39 @@
1
+ import counted_float.config as config
2
+
3
+ from ._core.counting import BuiltInData, CountedFloat, FlopCountingContext, PauseFlopCounting
4
+ from ._core.counting.models import (
5
+ FlopCounts,
6
+ FlopsBenchmarkDurations,
7
+ FlopsBenchmarkResults,
8
+ FlopType,
9
+ FlopWeights,
10
+ FPUInstruction,
11
+ SystemInfo,
12
+ )
13
+
14
+ __all__ = [
15
+ "config",
16
+ "CountedFloat",
17
+ "FlopCountingContext",
18
+ "FlopCounts",
19
+ "FlopsBenchmarkDurations",
20
+ "FlopsBenchmarkResults",
21
+ "FlopType",
22
+ "FlopWeights",
23
+ "FPUInstruction",
24
+ "PauseFlopCounting",
25
+ "SystemInfo",
26
+ ]
27
+
28
+ # -------------------------------------------------------------------------
29
+ # Optional [benchmarking] dependencies
30
+ # -------------------------------------------------------------------------
31
+ from ._core._optional_deps import FLAG_BENCHMARK_DEPS
32
+
33
+ if FLAG_BENCHMARK_DEPS:
34
+ import counted_float.benchmarking as benchmarking
35
+
36
+ __all__.append("benchmarking")
37
+
38
+ # delete variable again, we don't want to expose this
39
+ del FLAG_BENCHMARK_DEPS
File without changes
@@ -0,0 +1,6 @@
1
+ import sys
2
+
3
+ if sys.version_info >= (3, 11):
4
+ from enum import StrEnum
5
+ else:
6
+ from backports.strenum import StrEnum
@@ -0,0 +1,49 @@
1
+ """
2
+ Module implementing boolean flags indicating whether optional dependencies are available & decorators to
3
+ safeguard execution of functions that require these dependencies.
4
+ """
5
+
6
+ import functools
7
+ from typing import Callable
8
+
9
+ # -------------------------------------------------------------------------
10
+ # Flags
11
+ # -------------------------------------------------------------------------
12
+ try:
13
+ import numba
14
+ import numpy
15
+ import psutil
16
+
17
+ FLAG_BENCHMARK_DEPS = True
18
+ except ImportError:
19
+ FLAG_BENCHMARK_DEPS = False
20
+
21
+
22
+ # -------------------------------------------------------------------------
23
+ # Decorators
24
+ # -------------------------------------------------------------------------
25
+ def requires_benchmark_deps(fun: Callable) -> Callable:
26
+ """
27
+ Decorator to mark a function that requires some deps to be installed.
28
+
29
+ USAGE:
30
+
31
+ @requires_benchmark_deps
32
+ def fun_that_needs_numba_or_numpy_or_psutil():
33
+ pass
34
+
35
+ This will ensure that the function - when it is called for the first time - will check if the required
36
+ dependencies are available. If not, an ImportError will be raised with an informative message.
37
+ """
38
+
39
+ @functools.wraps(fun)
40
+ def wrapped_fun(*args, **kwargs):
41
+ if not FLAG_BENCHMARK_DEPS:
42
+ raise ImportError(
43
+ "This function requires counted_float to be installed with benchmark optional dependencies. "
44
+ + "Please install this package as counted_float[benchmark]."
45
+ )
46
+ else:
47
+ return fun(*args, **kwargs)
48
+
49
+ return wrapped_fun
@@ -0,0 +1,10 @@
1
+ from counted_float._core._optional_deps import requires_benchmark_deps
2
+ from counted_float._core.counting.models import FlopsBenchmarkResults
3
+
4
+ from ._flops_benchmark_suite import FlopsBenchmarkSuite
5
+
6
+
7
+ @requires_benchmark_deps
8
+ def run_flops_benchmark() -> FlopsBenchmarkResults:
9
+ """Run the flops benchmark suite with default settings returns a FlopsBenchmarkResults object."""
10
+ return FlopsBenchmarkSuite().run()
@@ -0,0 +1,189 @@
1
+ import platform
2
+
3
+ import psutil
4
+
5
+ from counted_float._core._optional_deps import requires_benchmark_deps
6
+ from counted_float._core.counting.models import (
7
+ BenchmarkSettings,
8
+ FlopsBenchmarkDurations,
9
+ FlopsBenchmarkResults,
10
+ FlopType,
11
+ Quantiles,
12
+ SystemInfo,
13
+ )
14
+
15
+ from ._flops_micro_benchmark import FlopsMicroBenchmark
16
+
17
+
18
+ class FlopsBenchmarkSuite:
19
+ # -------------------------------------------------------------------------
20
+ # Main API
21
+ # -------------------------------------------------------------------------
22
+ def run(
23
+ self,
24
+ array_size: int = 1000,
25
+ n_runs_total: int = 30,
26
+ n_runs_warmup: int = 10,
27
+ n_seconds_per_run_target: float = 0.5,
28
+ ) -> FlopsBenchmarkResults:
29
+ """
30
+ Run entire flops benchmarking suite and return the results as a FlopsBenchmarkResults object.
31
+ """
32
+
33
+ # run actual benchmarks
34
+ benchmarks = self.get_flops_benchmarking_suite(size=array_size)
35
+ results_dict: dict[FlopType | None, Quantiles] = {
36
+ flop_type: benchmark.run_many(
37
+ n_runs_total=n_runs_total,
38
+ n_runs_warmup=n_runs_warmup,
39
+ n_seconds_per_run_target=n_seconds_per_run_target,
40
+ ).summary_stats()
41
+ for flop_type, benchmark in benchmarks.items()
42
+ }
43
+
44
+ # put results in appropriate format & return
45
+ return FlopsBenchmarkResults(
46
+ system_info=SystemInfo(
47
+ platform_processor=platform.processor(),
48
+ platform_machine=platform.machine(),
49
+ platform_system=platform.system(),
50
+ platform_release=platform.release(),
51
+ platform_python_version=platform.python_version(),
52
+ platform_python_implementation=platform.python_implementation(),
53
+ platform_python_compiler=platform.python_compiler(),
54
+ psutil_cpu_count_logical=psutil.cpu_count(logical=True),
55
+ psutil_cpu_count_physical=psutil.cpu_count(logical=False),
56
+ ),
57
+ benchmark_settings=BenchmarkSettings(
58
+ array_size=array_size,
59
+ n_runs_total=n_runs_total,
60
+ n_runs_warmup=n_runs_warmup,
61
+ n_seconds_per_run_target=n_seconds_per_run_target,
62
+ ),
63
+ results_ns=FlopsBenchmarkDurations(
64
+ baseline=results_dict[None],
65
+ flops={flop_type: results_dict[flop_type] for flop_type in FlopType},
66
+ ),
67
+ )
68
+
69
+ # -------------------------------------------------------------------------
70
+ # Static methods
71
+ # -------------------------------------------------------------------------
72
+ @staticmethod
73
+ @requires_benchmark_deps
74
+ def get_flops_benchmarking_suite(size: int) -> dict[FlopType | None, FlopsMicroBenchmark]:
75
+ """
76
+ Returns a benchmark for each FlopType + None (=baseline test), of requested array size.
77
+ """
78
+
79
+ # --- late import of optional dependencies --------
80
+ import numba
81
+ import numpy as np
82
+
83
+ # --- define all test functions -------------------
84
+ @numba.njit(parallel=False)
85
+ def baseline(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
86
+ """baseline benchmark to measure the overhead of the benchmarking framework + iteration"""
87
+ for i in range(n):
88
+ pass
89
+
90
+ @numba.njit(parallel=False)
91
+ def flop_abs(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
92
+ for i in range(n):
93
+ out_f[i] = abs(in_f1[i])
94
+
95
+ @numba.njit(parallel=False)
96
+ def flop_minus(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
97
+ for i in range(n):
98
+ out_f[i] = -in_f1[i]
99
+
100
+ @numba.njit(parallel=False)
101
+ def flop_equals(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
102
+ for i in range(n):
103
+ out_i[i] = in_f1[i] == in_f2[i] # assign to integer output, to avoid unnecessary conversion overhead
104
+
105
+ @numba.njit(parallel=False)
106
+ def flop_gte(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
107
+ for i in range(n):
108
+ out_i[i] = in_f1[i] >= in_f2[i] # assign to integer output, to avoid unnecessary conversion overhead
109
+
110
+ @numba.njit(parallel=False)
111
+ def flop_lte(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
112
+ for i in range(n):
113
+ out_i[i] = in_f1[i] <= in_f2[i] # assign to integer output, to avoid unnecessary conversion overhead
114
+
115
+ @numba.njit(parallel=False)
116
+ def flop_gte_zero(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
117
+ for i in range(n):
118
+ out_i[i] = in_f1[i] >= 0.0 # assign to integer output, to avoid unnecessary conversion overhead
119
+
120
+ @numba.njit(parallel=False)
121
+ def flop_rnd(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
122
+ for i in range(n):
123
+ out_i[i] = np.ceil(in_f1[i])
124
+
125
+ @numba.njit(parallel=False)
126
+ def flop_add(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
127
+ for i in range(n):
128
+ out_f[i] = in_f1[i] + in_f2[i]
129
+
130
+ @numba.njit(parallel=False)
131
+ def flop_sub(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
132
+ for i in range(n):
133
+ out_f[i] = in_f1[i] - in_f2[i]
134
+
135
+ @numba.njit(parallel=False)
136
+ def flop_mul(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
137
+ for i in range(n):
138
+ out_f[i] = in_f1[i] * in_f2[i]
139
+
140
+ @numba.njit(parallel=False)
141
+ def flop_div(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
142
+ for i in range(n):
143
+ out_f[i] = in_f1[i] / in_f2[i]
144
+
145
+ @numba.njit(parallel=False)
146
+ def flop_sqrt(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
147
+ for i in range(n):
148
+ out_f[i] = np.sqrt(in_f1[i])
149
+
150
+ @numba.njit(parallel=False)
151
+ def flop_pow2(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
152
+ for i in range(n):
153
+ out_f[i] = 2 ** in_f1[i]
154
+
155
+ @numba.njit(parallel=False)
156
+ def flop_log2(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
157
+ for i in range(n):
158
+ out_f[i] = np.log2(in_f1[i])
159
+
160
+ @numba.njit(parallel=False)
161
+ def flop_pow(n: int, in_f1: np.ndarray, in_f2: np.ndarray, out_f: np.ndarray, out_i: np.ndarray):
162
+ for i in range(n):
163
+ out_f[i] = in_f1[i] ** in_f2[i]
164
+
165
+ # --- return in appropriate format ----------------
166
+ return {
167
+ key: FlopsMicroBenchmark(name=name, f=f, size=size)
168
+ for key, name, f in [
169
+ (key, key.long_name() if key else "baseline", f)
170
+ for key, f in [
171
+ (None, baseline),
172
+ (FlopType.ABS, flop_abs),
173
+ (FlopType.CMP_ZERO, flop_gte_zero),
174
+ (FlopType.RND, flop_rnd),
175
+ (FlopType.MINUS, flop_minus),
176
+ (FlopType.EQUALS, flop_equals),
177
+ (FlopType.GTE, flop_gte),
178
+ (FlopType.LTE, flop_lte),
179
+ (FlopType.ADD, flop_add),
180
+ (FlopType.SUB, flop_sub),
181
+ (FlopType.MUL, flop_mul),
182
+ (FlopType.SQRT, flop_sqrt),
183
+ (FlopType.DIV, flop_div),
184
+ (FlopType.POW2, flop_pow2),
185
+ (FlopType.LOG2, flop_log2),
186
+ (FlopType.POW, flop_pow),
187
+ ]
188
+ ]
189
+ }
@@ -0,0 +1,64 @@
1
+ from typing import Callable
2
+
3
+ from counted_float._core._optional_deps import requires_benchmark_deps
4
+
5
+ from ._micro_benchmark import MicroBenchmark
6
+
7
+
8
+ class FlopsMicroBenchmark(MicroBenchmark):
9
+ """
10
+ Base class for benchmark that checks speed of a certain type of floating point operation.
11
+
12
+ This is set up as follows:
13
+ - we configure the benchmark with a 'size' and a function 'f'
14
+ - we prepare the inputs: 2 1D numpy arrays of size 'size': in_f1, in_f2
15
+ - initialized as random floating point numbers in range [0, 10]
16
+ - we prepare the output arrays: of size 'size': out_f, out_i
17
+ - 1 output array per type of result: float, int
18
+ - initialized with zeros
19
+ - the function f will loop over a&b and write the result to c
20
+ - the function should be implemented such that it does not use vectorized operations, we want to avoid
21
+ using vectorized CPU instructions (AVX, etc...): we want to measure the speed of the regular, scalar
22
+ operations
23
+ - we numba.jit the function, to make sure it is compiled to machine code, to avoid Python overhead to dominate
24
+ the benchmark
25
+ - 'size' should be chosen large enough to avoid the overhead of the benchmarking framework to be noticeable
26
+ - 'size' should be chosen small enough to fit in the CPU cache, to avoid being RAM-bandwidth limited
27
+
28
+ Despite all these measures, it is not expected that the benchmark will be fully accurate in absolute terms.
29
+ In real-life the speed of execution of floating point operations on a CPU will also be influenced by branching,
30
+ memory access patterns, etc...
31
+ The main goal is to find a reasonably accurate estimate of the relative cost of the main types of floating point
32
+ operations, so we can make representative estimates of the number of FLOPS executed by instrumented algorithms.
33
+ """
34
+
35
+ @requires_benchmark_deps
36
+ def __init__(self, name: str, f: Callable, size: int):
37
+ import numpy as np # late import of optional dependency
38
+
39
+ super().__init__(name=name)
40
+ self.size = size
41
+ self.f = f
42
+ self.n_operations = 0
43
+ # input arrays
44
+ self.in_f1: np.ndarray = np.zeros(size, dtype=float)
45
+ self.in_f2: np.ndarray = np.zeros(size, dtype=float)
46
+ # output arrays
47
+ self.out_f: np.ndarray = np.zeros(size, dtype=float)
48
+ self.out_i: np.ndarray = np.zeros(size, dtype=int)
49
+
50
+ def _prepare_benchmark(self, n_operations: int):
51
+ import numpy as np # late import of optional dependency
52
+
53
+ self.n_operations = n_operations
54
+ # input arrays
55
+ self.in_f1 = 10 * np.random.rand(self.size)
56
+ self.in_f2 = 10 * np.random.rand(self.size)
57
+ # output arrays
58
+ self.out_f: np.ndarray = np.full(self.size, 0.0, dtype=float)
59
+ self.out_i: np.ndarray = np.full(self.size, 0, dtype=int)
60
+
61
+ def _run_benchmark(self):
62
+ # repeat 'f' n_operations times, each time on the same data
63
+ for _ in range(self.n_operations):
64
+ self.f(self.size, self.in_f1, self.in_f2, self.out_f, self.out_i)
@@ -0,0 +1,113 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from counted_float._core._optional_deps import requires_benchmark_deps
4
+
5
+ from ._models import MicroBenchmarkResult, SingleRunResult
6
+ from ._time_utils import Timer, format_time_durations
7
+
8
+
9
+ # =================================================================================================
10
+ # Base class for micro-benchmarks
11
+ # =================================================================================================
12
+ class MicroBenchmark(ABC):
13
+ """
14
+ Base class for micro-benchmarks, where a child class needs to implement the following methods:
15
+ _prepare_benchmark --> prepares the benchmark_runs (e.g. sets up data); is called once before each _run_benchmark
16
+ and time spent here is not counted.
17
+ _run_benchmark --> runs the benchmark_runs; is called multiple times and time spent here is counted.
18
+
19
+ NOTE: this essentially offers similar functionality as the python-builtin timeit module, but with some benefits:
20
+ - automatic resizing of the benchmark_runs to achieve a target time per run
21
+ - automatic warmup_runs runs
22
+ - robust computation of median run time and ± range based on quantiles, rather than mean & std.
23
+ (=more robust to outliers)
24
+ """
25
+
26
+ MAX_N_OPERATIONS_FACTOR = 10 # never adjust n_operations by more than this factor (up or down)
27
+
28
+ @requires_benchmark_deps
29
+ def __init__(self, name: str):
30
+ self.name = name
31
+
32
+ def run_many(
33
+ self, n_runs_total: int = 20, n_runs_warmup: int = 5, n_seconds_per_run_target: float = 0.5
34
+ ) -> MicroBenchmarkResult:
35
+ """
36
+ Runs the MicroBenchmark multiple times (warmup_runs & actual test runs), with provided parameters and returns
37
+ (q25, q50, q75) quantiles of run times in nanoseconds.
38
+ :param n_runs_total: (int, default=20) total number of benchmark_runs runs
39
+ :param n_runs_warmup: (int, default=5) number of warmup_runs runs
40
+ - the first n_runs_warmup of n_runs_total are not included in timing stats
41
+ - warmup_runs runs serve to initialize n_operations & get processor, cache, ...
42
+ in a stable, representative state
43
+ :param n_seconds_per_run_target: (float, default=0.5) target time (sec) per benchmark_runs run (prepare + run).
44
+ n_operations will be iteratively adjusted to achieve this target time.
45
+ """
46
+
47
+ print(f"{self.name.ljust(35)}: ", end="")
48
+
49
+ # repeat benchmark_runs n_runs_total times
50
+ n_operations = 1 # start with a benchmark_runs of 1 operation and scale up as needed
51
+ warmup_runs: list[SingleRunResult] = []
52
+ benchmark_runs: list[SingleRunResult] = []
53
+ for i in range(n_runs_total):
54
+ # --- run benchmark_runs ---
55
+ with Timer() as t:
56
+ single_run_result = self.run_once(n_operations)
57
+ t_tot_seconds = t.t_elapsed_sec() # total time (sec) of prepare + run
58
+
59
+ # --- capture result ---
60
+ if i < n_runs_warmup:
61
+ # warmup run that doesn't count
62
+ print("w", end="", flush=True)
63
+ warmup_runs.append(single_run_result)
64
+ else:
65
+ # benchmark run that does count
66
+ print(".", end="", flush=True)
67
+ benchmark_runs.append(single_run_result)
68
+
69
+ # --- adjust n_ops ---
70
+ n_ops_min = max(1, int(n_operations / self.MAX_N_OPERATIONS_FACTOR))
71
+ n_ops_max = int(n_operations * self.MAX_N_OPERATIONS_FACTOR)
72
+ n_operations = max(n_ops_min, min(n_ops_max, int(n_operations * n_seconds_per_run_target / t_tot_seconds)))
73
+
74
+ # final results
75
+ benchmark_result = MicroBenchmarkResult(
76
+ warmup_runs=warmup_runs,
77
+ benchmark_runs=benchmark_runs,
78
+ )
79
+ stats = benchmark_result.summary_stats()
80
+ print(f" {format_time_durations(nsec_q25=stats.q25, nsec_q50=stats.q50, nsec_q75=stats.q75)}")
81
+
82
+ # return quantiles
83
+ return benchmark_result
84
+
85
+ def run_once(self, n_operations: int) -> SingleRunResult:
86
+ """Runs benchmark_runs once for a given # of operations and returns time in nanoseconds per operation"""
87
+
88
+ # prepare
89
+ self._prepare_benchmark(n_operations)
90
+
91
+ # run
92
+ with Timer() as t:
93
+ self._run_benchmark()
94
+
95
+ # report
96
+ return SingleRunResult(
97
+ n_operations=n_operations,
98
+ t_nsecs=t.t_elapsed_nsec(),
99
+ )
100
+
101
+ @abstractmethod
102
+ def _prepare_benchmark(self, n_operations: int):
103
+ """
104
+ Prepare benchmark_runs (e.g. set up data) based on requested number of operations. This argument is adjusted each
105
+ run by the MicroBenchmarkRunner class to ensure that the benchmark_runs runs for a reasonable amount of time
106
+ (e.g. 1 second per run).
107
+ """
108
+ raise NotImplementedError()
109
+
110
+ @abstractmethod
111
+ def _run_benchmark(self):
112
+ """Run benchmark_runs. This method is called multiple times and the time spent here is measured."""
113
+ raise NotImplementedError()
@@ -0,0 +1,31 @@
1
+ from counted_float._core.counting.models import MyBaseModel, Quantiles
2
+
3
+
4
+ class SingleRunResult(MyBaseModel):
5
+ """Result of a single run of our micro-benchmark_runs for a give # of operations."""
6
+
7
+ n_operations: int
8
+ t_nsecs: float
9
+
10
+ def nsecs_per_op(self) -> float:
11
+ return self.t_nsecs / self.n_operations
12
+
13
+
14
+ class MicroBenchmarkResult(MyBaseModel):
15
+ """Results of all runs in the micro-benchmark_runs (warmup_runs + actual benchmark_runs runs)."""
16
+
17
+ warmup_runs: list[SingleRunResult]
18
+ benchmark_runs: list[SingleRunResult]
19
+
20
+ def get_nsec_per_op_quantile(self, q: float) -> float:
21
+ """Returns a specific quantile of all results in the 'benchmark_runs' category expressed as nsec/op."""
22
+ import numpy as np # late import of optional dependency
23
+
24
+ return float(np.quantile([el.nsecs_per_op() for el in self.benchmark_runs], q))
25
+
26
+ def summary_stats(self) -> Quantiles:
27
+ return Quantiles(
28
+ q25=self.get_nsec_per_op_quantile(q=0.25),
29
+ q50=self.get_nsec_per_op_quantile(q=0.50),
30
+ q75=self.get_nsec_per_op_quantile(q=0.75),
31
+ )
@@ -0,0 +1,70 @@
1
+ import time
2
+
3
+
4
+ # =================================================================================================
5
+ # Formatting
6
+ # =================================================================================================
7
+ def format_time_durations(nsec_q25: float, nsec_q50: float, nsec_q75: float) -> str:
8
+ if nsec_q50 < 1e3:
9
+ formatter = _format_nsec_as_ns
10
+ elif nsec_q50 < 1e6:
11
+ formatter = _format_nsec_as_us
12
+ elif nsec_q50 < 1e9:
13
+ formatter = _format_nsec_as_ms
14
+ else:
15
+ formatter = _format_nsec_as_s
16
+
17
+ return f"{formatter(nsec_q50)} ± {formatter((nsec_q75 - nsec_q25) / 2)} / operation"
18
+
19
+
20
+ def _format_nsec_as_ns(nsec: float) -> str:
21
+ return f"{nsec:7.2f} ns"
22
+
23
+
24
+ def _format_nsec_as_us(nsec: float) -> str:
25
+ return f"{nsec / 1e3:7.2f} µs"
26
+
27
+
28
+ def _format_nsec_as_ms(nsec: float) -> str:
29
+ return f"{nsec / 1e6:7.2f} ms"
30
+
31
+
32
+ def _format_nsec_as_s(nsec: float) -> str:
33
+ return f"{nsec / 1e9:7.2f} s"
34
+
35
+
36
+ # =================================================================================================
37
+ # Timing
38
+ # =================================================================================================
39
+ class Timer:
40
+ """
41
+ Class acting as a context manager to measure time elapsed. Elapsed time can be retrieved using t_elapsed().
42
+ """
43
+
44
+ # --- constructor -------------------------------------
45
+ def __init__(self):
46
+ self._start: float | None = None
47
+ self._end: float | None = None
48
+
49
+ # --- context manager ---------------------------------
50
+ def __enter__(self):
51
+ self._start = time.perf_counter_ns()
52
+ return self
53
+
54
+ def __exit__(self, exc_type, exc_val, exc_tb):
55
+ self._end = time.perf_counter_ns()
56
+
57
+ # --- extract results ---------------------------------
58
+ def t_elapsed_nsec(self) -> float:
59
+ if self._start is None:
60
+ raise RuntimeError("Timer has not been started.")
61
+
62
+ if self._end is None:
63
+ # timer still running
64
+ return time.perf_counter_ns() - self._start
65
+ else:
66
+ # timer finished
67
+ return self._end - self._start
68
+
69
+ def t_elapsed_sec(self) -> float:
70
+ return self.t_elapsed_nsec() / 1e9
@@ -0,0 +1,3 @@
1
+ from ._builtin_data import BuiltInData
2
+ from ._context_managers import FlopCountingContext, PauseFlopCounting
3
+ from ._counted_float import CountedFloat
@@ -0,0 +1,25 @@
1
+ from importlib.resources import files
2
+
3
+ from counted_float._core.counting.models import FlopsBenchmarkResults, InstructionLatencies
4
+
5
+
6
+ class BuiltInData:
7
+ """
8
+ A class that provides access to built-in data for the counted_float package.
9
+ """
10
+
11
+ @classmethod
12
+ def benchmarks(cls) -> dict[str, FlopsBenchmarkResults]:
13
+ return {
14
+ file.stem: FlopsBenchmarkResults.model_validate_json(file.read_text())
15
+ for file in files("counted_float._core.data.benchmarks").iterdir()
16
+ if file.is_file() and file.name.endswith(".json")
17
+ }
18
+
19
+ @classmethod
20
+ def specs(cls) -> dict[str, InstructionLatencies]:
21
+ return {
22
+ file.stem: InstructionLatencies.model_validate_json(file.read_text())
23
+ for file in files("counted_float._core.data.specs").iterdir()
24
+ if file.is_file() and file.name.endswith(".json")
25
+ }