modeldiet 0.1.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.
- modeldiet/__init__.py +6 -0
- modeldiet/api.py +44 -0
- modeldiet/benchmark/__init__.py +5 -0
- modeldiet/benchmark/model.py +143 -0
- modeldiet/benchmark/models/__init__.py +1 -0
- modeldiet/benchmark/models/lr.py +22 -0
- modeldiet/benchmark/models/rf.py +22 -0
- modeldiet/benchmark/models/xgb.py +30 -0
- modeldiet/benchmark/report.py +60 -0
- modeldiet/benchmark/runner.py +75 -0
- modeldiet/benchmark/statistics.py +63 -0
- modeldiet/benchmark/worker.py +211 -0
- modeldiet/driver.py +33 -0
- modeldiet/model.py +5 -0
- modeldiet/models/__init__.py +1 -0
- modeldiet/models/lr.py +5 -0
- modeldiet/models/rf.py +5 -0
- modeldiet/models/xgb.py +5 -0
- modeldiet/profiling/__init__.py +17 -0
- modeldiet/profiling/benchmark.py +7 -0
- modeldiet/profiling/environment.py +51 -0
- modeldiet/profiling/isolated_runner.py +9 -0
- modeldiet/profiling/measurements.py +181 -0
- modeldiet/profiling/orchestrator.py +146 -0
- modeldiet/profiling/statistics.py +5 -0
- modeldiet/report.py +5 -0
- modeldiet/result.py +114 -0
- modeldiet-0.1.0.dist-info/METADATA +228 -0
- modeldiet-0.1.0.dist-info/RECORD +32 -0
- modeldiet-0.1.0.dist-info/WHEEL +5 -0
- modeldiet-0.1.0.dist-info/licenses/LICENSE +21 -0
- modeldiet-0.1.0.dist-info/top_level.txt +1 -0
modeldiet/__init__.py
ADDED
modeldiet/api.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Public Model Diet API."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
from typing import Any, Literal
|
|
7
|
+
|
|
8
|
+
from modeldiet.profiling.measurements import BaseMeasurement, LatencyMeasurement
|
|
9
|
+
from modeldiet.profiling.orchestrator import run_measurements
|
|
10
|
+
from modeldiet.result import ProfileResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def profile(
|
|
14
|
+
model: Any,
|
|
15
|
+
X: Any,
|
|
16
|
+
*,
|
|
17
|
+
measurements: Sequence[BaseMeasurement] | None = None,
|
|
18
|
+
on_error: Literal["raise", "record"] = "raise",
|
|
19
|
+
include_environment: bool = False,
|
|
20
|
+
) -> ProfileResult:
|
|
21
|
+
"""Profile a fitted estimator using the selected measurement strategies."""
|
|
22
|
+
if on_error not in ("raise", "record"):
|
|
23
|
+
raise ValueError("on_error must be 'raise' or 'record'")
|
|
24
|
+
if not callable(getattr(model, "predict", None)):
|
|
25
|
+
raise TypeError("model must provide a callable predict(X) method")
|
|
26
|
+
if X is None:
|
|
27
|
+
raise ValueError("X must contain representative tabular data")
|
|
28
|
+
selected: list[BaseMeasurement]
|
|
29
|
+
if measurements is None:
|
|
30
|
+
selected = [LatencyMeasurement()]
|
|
31
|
+
else:
|
|
32
|
+
selected = list(measurements)
|
|
33
|
+
if not selected:
|
|
34
|
+
raise ValueError("measurements must contain at least one measurement")
|
|
35
|
+
if any(not isinstance(m, BaseMeasurement) for m in selected):
|
|
36
|
+
raise TypeError("each measurement must be a BaseMeasurement instance")
|
|
37
|
+
result = run_measurements(model, X, selected, on_error=on_error)
|
|
38
|
+
if include_environment:
|
|
39
|
+
from dataclasses import replace
|
|
40
|
+
|
|
41
|
+
from modeldiet.profiling.environment import collect_environment
|
|
42
|
+
|
|
43
|
+
return replace(result, environment=collect_environment())
|
|
44
|
+
return result
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Model wrapper that delegates profiling to independent measurements."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import warnings
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from modeldiet import profile
|
|
9
|
+
from modeldiet.profiling.measurements import (
|
|
10
|
+
BaseMeasurement,
|
|
11
|
+
LatencyMeasurement,
|
|
12
|
+
ProcessRSSMeasurement,
|
|
13
|
+
SerializationMeasurement,
|
|
14
|
+
TracedAllocationMeasurement,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class _NamedMeasurement(BaseMeasurement):
|
|
19
|
+
"""Give a legacy strategy slot a stable name for the core profile API."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, name: str, measurement: BaseMeasurement) -> None:
|
|
22
|
+
self.name = name
|
|
23
|
+
self.measurement = measurement
|
|
24
|
+
|
|
25
|
+
def measure(self, model: Any, X: Any = None) -> dict[str, Any]:
|
|
26
|
+
return self.measurement.measure(model, X)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class Model:
|
|
30
|
+
"""Compatibility wrapper around a trained estimator.
|
|
31
|
+
|
|
32
|
+
Estimator-specific subclasses provide ``clf``. Measurement strategies are
|
|
33
|
+
independent objects that can be reused, injected, or subclassed by callers.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(
|
|
37
|
+
self,
|
|
38
|
+
loading_time: float = 0.0,
|
|
39
|
+
mean_inference_latency: float = 0.0,
|
|
40
|
+
p50_latency: float = 0.0,
|
|
41
|
+
p90_latency: float = 0.0,
|
|
42
|
+
p95_latency: float = 0.0,
|
|
43
|
+
p99_latency: float = 0.0,
|
|
44
|
+
traced_peak_memory_usage: float = 0.0,
|
|
45
|
+
processed_rss: float = 0.0,
|
|
46
|
+
serialized_model_size: float = 0.0,
|
|
47
|
+
latency_measurement: BaseMeasurement | None = None,
|
|
48
|
+
allocation_measurement: BaseMeasurement | None = None,
|
|
49
|
+
rss_measurement: BaseMeasurement | None = None,
|
|
50
|
+
serialization_measurement: BaseMeasurement | None = None,
|
|
51
|
+
) -> None:
|
|
52
|
+
warnings.warn(
|
|
53
|
+
f"{type(self).__name__} is a deprecated benchmark compatibility "
|
|
54
|
+
"interface. Use an ordinary fitted estimator with "
|
|
55
|
+
"modeldiet.profile(estimator, X) instead. The legacy model "
|
|
56
|
+
"interfaces will remain available through the 0.x series and "
|
|
57
|
+
"will not be removed before 1.0.0.",
|
|
58
|
+
DeprecationWarning,
|
|
59
|
+
stacklevel=3 if type(self) is not Model else 2,
|
|
60
|
+
)
|
|
61
|
+
self.loading_time = loading_time
|
|
62
|
+
self.mean_inference_latency = mean_inference_latency
|
|
63
|
+
self.p50_latency = p50_latency
|
|
64
|
+
self.p90_latency = p90_latency
|
|
65
|
+
self.p95_latency = p95_latency
|
|
66
|
+
self.p99_latency = p99_latency
|
|
67
|
+
self.traced_peak_inference_allocation_mb = traced_peak_memory_usage
|
|
68
|
+
self.rss_before_inference_mb = processed_rss
|
|
69
|
+
self.rss_after_inference_mb = processed_rss
|
|
70
|
+
self.rss_delta_mb = 0.0
|
|
71
|
+
self.traced_peak_memory_usage = traced_peak_memory_usage # compatibility
|
|
72
|
+
self.processed_rss = processed_rss # compatibility
|
|
73
|
+
self.serialized_model_size = serialized_model_size
|
|
74
|
+
|
|
75
|
+
self.clf: Any = None
|
|
76
|
+
self.latency_measurement = (
|
|
77
|
+
latency_measurement
|
|
78
|
+
if latency_measurement is not None
|
|
79
|
+
else LatencyMeasurement()
|
|
80
|
+
)
|
|
81
|
+
self.allocation_measurement = (
|
|
82
|
+
allocation_measurement
|
|
83
|
+
if allocation_measurement is not None
|
|
84
|
+
else TracedAllocationMeasurement()
|
|
85
|
+
)
|
|
86
|
+
self.rss_measurement = (
|
|
87
|
+
rss_measurement if rss_measurement is not None else ProcessRSSMeasurement()
|
|
88
|
+
)
|
|
89
|
+
self.serialization_measurement = (
|
|
90
|
+
serialization_measurement
|
|
91
|
+
if serialization_measurement is not None
|
|
92
|
+
else SerializationMeasurement()
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def profile_execution(
|
|
96
|
+
self, X_test: Any, load_start_time: float | None = None
|
|
97
|
+
) -> dict[str, dict[str, Any]]:
|
|
98
|
+
"""Run each measurement through the core profiling API.
|
|
99
|
+
|
|
100
|
+
``load_start_time`` remains accepted for backwards compatibility. An
|
|
101
|
+
already-loaded model has no measurable historical load duration.
|
|
102
|
+
"""
|
|
103
|
+
del load_start_time
|
|
104
|
+
named_measurements = [
|
|
105
|
+
_NamedMeasurement("latency", self.latency_measurement),
|
|
106
|
+
_NamedMeasurement("python_traced_allocation", self.allocation_measurement),
|
|
107
|
+
_NamedMeasurement("process_rss", self.rss_measurement),
|
|
108
|
+
_NamedMeasurement("pickle_size", self.serialization_measurement),
|
|
109
|
+
]
|
|
110
|
+
result = profile(self.clf, X_test, measurements=named_measurements)
|
|
111
|
+
|
|
112
|
+
def values(name: str) -> dict[str, Any]:
|
|
113
|
+
return {
|
|
114
|
+
key: metric.value
|
|
115
|
+
for key, metric in result.measurements[name].metrics.items()
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
latency = values("latency")
|
|
119
|
+
allocation = values("python_traced_allocation")
|
|
120
|
+
process_memory = values("process_rss")
|
|
121
|
+
serialization = values("pickle_size")
|
|
122
|
+
|
|
123
|
+
self.mean_inference_latency = latency["mean_ms"]
|
|
124
|
+
self.p50_latency = latency["p50_ms"]
|
|
125
|
+
self.p90_latency = latency["p90_ms"]
|
|
126
|
+
self.p95_latency = latency["p95_ms"]
|
|
127
|
+
self.p99_latency = latency["p99_ms"]
|
|
128
|
+
self.traced_peak_inference_allocation_mb = allocation[
|
|
129
|
+
"traced_peak_inference_allocation_mb"
|
|
130
|
+
]
|
|
131
|
+
self.rss_before_inference_mb = process_memory["rss_before_inference_mb"]
|
|
132
|
+
self.rss_after_inference_mb = process_memory["rss_after_inference_mb"]
|
|
133
|
+
self.rss_delta_mb = process_memory["rss_delta_mb"]
|
|
134
|
+
self.traced_peak_memory_usage = self.traced_peak_inference_allocation_mb
|
|
135
|
+
self.processed_rss = self.rss_after_inference_mb
|
|
136
|
+
self.serialized_model_size = serialization["serialized_model_size_mb"]
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
"latency": latency,
|
|
140
|
+
"allocation": allocation,
|
|
141
|
+
"process_memory": process_memory,
|
|
142
|
+
"serialization": serialization,
|
|
143
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Demonstration estimators used by the optional benchmark worker."""
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from sklearn.linear_model import LogisticRegression
|
|
4
|
+
|
|
5
|
+
from modeldiet.benchmark.model import Model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class LogisticRegressionModel(Model):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
C: float = 1.0,
|
|
12
|
+
max_iter: int = 100,
|
|
13
|
+
solver: str = "lbfgs",
|
|
14
|
+
**kwargs: Any,
|
|
15
|
+
) -> None:
|
|
16
|
+
# Passes all telemetry tracking arguments automatically to the base class
|
|
17
|
+
super().__init__(**kwargs)
|
|
18
|
+
|
|
19
|
+
self.C = C
|
|
20
|
+
self.max_iter = max_iter
|
|
21
|
+
self.solver = solver
|
|
22
|
+
self.clf = LogisticRegression(C=C, max_iter=max_iter, solver=solver)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from sklearn.ensemble import RandomForestClassifier
|
|
4
|
+
|
|
5
|
+
from modeldiet.benchmark.model import Model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class RandomForestModel(Model):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
n_estimators: int = 100,
|
|
12
|
+
max_depth: int | None = None,
|
|
13
|
+
**kwargs: Any,
|
|
14
|
+
) -> None:
|
|
15
|
+
super().__init__(**kwargs)
|
|
16
|
+
|
|
17
|
+
# specific class configurations
|
|
18
|
+
self.n_estimators = n_estimators
|
|
19
|
+
self.max_depth = max_depth
|
|
20
|
+
self.clf = RandomForestClassifier(
|
|
21
|
+
n_estimators=self.n_estimators, max_depth=self.max_depth
|
|
22
|
+
)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from xgboost import XGBClassifier
|
|
4
|
+
|
|
5
|
+
from modeldiet.benchmark.model import Model
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class XGBoostModel(Model):
|
|
9
|
+
def __init__(
|
|
10
|
+
self,
|
|
11
|
+
n_estimators: int = 100,
|
|
12
|
+
max_depth: int = 6,
|
|
13
|
+
learning_rate: float = 0.3,
|
|
14
|
+
subsample: float = 1.0,
|
|
15
|
+
**kwargs: Any,
|
|
16
|
+
) -> None:
|
|
17
|
+
# Passes all telemetry tracking metrics straight to the base class
|
|
18
|
+
super().__init__(**kwargs)
|
|
19
|
+
|
|
20
|
+
self.n_estimators = n_estimators
|
|
21
|
+
self.max_depth = max_depth
|
|
22
|
+
self.learning_rate = learning_rate
|
|
23
|
+
self.subsample = subsample
|
|
24
|
+
|
|
25
|
+
self.clf = XGBClassifier(
|
|
26
|
+
n_estimators=n_estimators,
|
|
27
|
+
max_depth=max_depth,
|
|
28
|
+
learning_rate=learning_rate,
|
|
29
|
+
subsample=subsample,
|
|
30
|
+
)
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import warnings
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from sklearn.metrics import accuracy_score
|
|
5
|
+
|
|
6
|
+
from modeldiet.benchmark.model import Model
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class Report:
|
|
10
|
+
def __init__(self, model: Model, X_test: Any, y_test: Any) -> None:
|
|
11
|
+
warnings.warn(
|
|
12
|
+
"Report is a deprecated benchmark compatibility interface. Use "
|
|
13
|
+
"run_repeated_benchmark() for demonstration benchmark output; use "
|
|
14
|
+
"profile() and ProfileResult exports for general-purpose profiling. "
|
|
15
|
+
"Report will remain available through the 0.x series and will not "
|
|
16
|
+
"be removed before 1.0.0.",
|
|
17
|
+
DeprecationWarning,
|
|
18
|
+
stacklevel=2,
|
|
19
|
+
)
|
|
20
|
+
self.model = model
|
|
21
|
+
self.X_test = X_test
|
|
22
|
+
self.y_test = y_test
|
|
23
|
+
|
|
24
|
+
def print_report(self) -> None:
|
|
25
|
+
y_pred = self.model.clf.predict(self.X_test)
|
|
26
|
+
accuracy = accuracy_score(self.y_test, y_pred)
|
|
27
|
+
model_name = self.model.__class__.__name__
|
|
28
|
+
|
|
29
|
+
print("=" * 60)
|
|
30
|
+
print(f" BENCHMARK REPORT: {model_name.upper()} ".center(60, "="))
|
|
31
|
+
print("=" * 60)
|
|
32
|
+
print("\n [1] MODEL METADATA & ACCURACY")
|
|
33
|
+
print(f" • Model Architecture : {model_name}")
|
|
34
|
+
print(f" • Model Deserialization : {self.model.loading_time:.4f} sec")
|
|
35
|
+
print(f" • Test Dataset Accuracy : {accuracy * 100:.2f}%")
|
|
36
|
+
|
|
37
|
+
print("\n [2] INFERENCE LATENCY PROFILE")
|
|
38
|
+
print(
|
|
39
|
+
f" • Mean Inference Latency : {self.model.mean_inference_latency:.4f} ms"
|
|
40
|
+
)
|
|
41
|
+
print(f" • P50 Latency : {self.model.p50_latency:.4f} ms")
|
|
42
|
+
print(f" • P90 Latency : {self.model.p90_latency:.4f} ms")
|
|
43
|
+
print(f" • P95 Latency : {self.model.p95_latency:.4f} ms")
|
|
44
|
+
print(f" • P99 Latency : {self.model.p99_latency:.4f} ms")
|
|
45
|
+
|
|
46
|
+
print("\n [3] MEMORY & STORAGE FOOTPRINT PROFILE")
|
|
47
|
+
print(
|
|
48
|
+
" • Traced Peak Inference Allocation: "
|
|
49
|
+
f"{self.model.traced_peak_inference_allocation_mb:.2f} MB"
|
|
50
|
+
)
|
|
51
|
+
print(
|
|
52
|
+
" • Process RSS Before / After : "
|
|
53
|
+
f"{self.model.rss_before_inference_mb:.2f} / "
|
|
54
|
+
f"{self.model.rss_after_inference_mb:.2f} MB"
|
|
55
|
+
)
|
|
56
|
+
print(f" • Process RSS Delta : {self.model.rss_delta_mb:.4f} MB")
|
|
57
|
+
print(
|
|
58
|
+
f" • Pickle Serialized Model Size : {self.model.serialized_model_size:.4f} MB"
|
|
59
|
+
)
|
|
60
|
+
print("\n" + "=" * 60 + "\n")
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import subprocess
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from modeldiet.benchmark.statistics import summarize_runs
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_repeated_benchmark(
|
|
11
|
+
model_name: str,
|
|
12
|
+
repetitions: int = 10,
|
|
13
|
+
output_dir: str | Path | None = None,
|
|
14
|
+
) -> dict[str, dict[str, float]]:
|
|
15
|
+
"""Run repeated benchmarks, writing results under output_dir or ./results."""
|
|
16
|
+
if repetitions < 1:
|
|
17
|
+
raise ValueError("repetitions must be at least one")
|
|
18
|
+
|
|
19
|
+
result_dir = Path(output_dir) if output_dir is not None else Path.cwd() / "results"
|
|
20
|
+
raw_dir = result_dir / "raw_runs"
|
|
21
|
+
summary_dir = result_dir / "summaries"
|
|
22
|
+
raw_dir.mkdir(parents=True, exist_ok=True)
|
|
23
|
+
summary_dir.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
runs: list[dict[str, Any]] = []
|
|
25
|
+
|
|
26
|
+
print("\n" + "=" * 60)
|
|
27
|
+
print(f"REPEATED BENCHMARK: {model_name.upper()}")
|
|
28
|
+
print("=" * 60)
|
|
29
|
+
|
|
30
|
+
for iteration in range(1, repetitions + 1):
|
|
31
|
+
print(f"\nRunning {model_name.upper()} - Iteration {iteration}/{repetitions}")
|
|
32
|
+
output_file = raw_dir / f"{model_name}_run_{iteration}.json"
|
|
33
|
+
command = [
|
|
34
|
+
sys.executable,
|
|
35
|
+
"-m",
|
|
36
|
+
"modeldiet.benchmark.worker",
|
|
37
|
+
"--model",
|
|
38
|
+
model_name,
|
|
39
|
+
"--output",
|
|
40
|
+
str(output_file),
|
|
41
|
+
]
|
|
42
|
+
result = subprocess.run(
|
|
43
|
+
command,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
if result.returncode != 0:
|
|
49
|
+
print("Run failed!")
|
|
50
|
+
print(result.stderr)
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
print(result.stdout)
|
|
54
|
+
with output_file.open(encoding="utf-8") as result_file:
|
|
55
|
+
runs.append(json.load(result_file))
|
|
56
|
+
|
|
57
|
+
if not runs:
|
|
58
|
+
raise RuntimeError(f"All benchmark runs failed for {model_name}")
|
|
59
|
+
|
|
60
|
+
summary = summarize_runs(runs)
|
|
61
|
+
summary_file = summary_dir / f"{model_name}_summary.json"
|
|
62
|
+
with summary_file.open("w", encoding="utf-8") as summary_stream:
|
|
63
|
+
json.dump(summary, summary_stream, indent=4)
|
|
64
|
+
|
|
65
|
+
print("\nSTATISTICAL SUMMARY")
|
|
66
|
+
print("=" * 60)
|
|
67
|
+
for metric, stats in summary.items():
|
|
68
|
+
print(f"\n{metric}")
|
|
69
|
+
print(f"Mean : {stats['mean']:.4f}")
|
|
70
|
+
print(f"Std : {stats['std_dev']:.4f}")
|
|
71
|
+
print(f"CV : {stats['coefficient_of_variation']:.2f}%")
|
|
72
|
+
print(f"P95 : {stats['p95']:.4f}")
|
|
73
|
+
|
|
74
|
+
print("\nSaved:", summary_file)
|
|
75
|
+
return summary
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from collections.abc import Mapping, Sequence
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def calculate_statistics(values: Sequence[float]) -> dict[str, float]:
|
|
8
|
+
"""Calculate statistics across independent benchmark runs."""
|
|
9
|
+
measurements = np.asarray(values, dtype=float)
|
|
10
|
+
if len(measurements) == 0:
|
|
11
|
+
raise ValueError("No benchmark measurements provided")
|
|
12
|
+
|
|
13
|
+
mean = float(np.mean(measurements))
|
|
14
|
+
std = float(np.std(measurements, ddof=1)) if len(measurements) > 1 else 0.0
|
|
15
|
+
cv = (std / mean * 100) if mean != 0 else 0.0
|
|
16
|
+
return {
|
|
17
|
+
"mean": mean,
|
|
18
|
+
"median": float(np.median(measurements)),
|
|
19
|
+
"std_dev": std,
|
|
20
|
+
"coefficient_of_variation": float(cv),
|
|
21
|
+
"min": float(np.min(measurements)),
|
|
22
|
+
"max": float(np.max(measurements)),
|
|
23
|
+
"p90": float(np.percentile(measurements, 90)),
|
|
24
|
+
"p95": float(np.percentile(measurements, 95)),
|
|
25
|
+
"p99": float(np.percentile(measurements, 99)),
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def summarize_runs(
|
|
30
|
+
runs: Sequence[Mapping[str, Any]],
|
|
31
|
+
) -> dict[str, dict[str, float]]:
|
|
32
|
+
"""Aggregate measurements from multiple independent benchmark runs."""
|
|
33
|
+
metrics = [
|
|
34
|
+
"training_time",
|
|
35
|
+
"loading_time",
|
|
36
|
+
"mean_inference_latency",
|
|
37
|
+
"p50_latency",
|
|
38
|
+
"p90_latency",
|
|
39
|
+
"p95_latency",
|
|
40
|
+
"p99_latency",
|
|
41
|
+
"traced_peak_inference_allocation_mb",
|
|
42
|
+
"rss_before_inference_mb",
|
|
43
|
+
"rss_after_inference_mb",
|
|
44
|
+
"rss_delta_mb",
|
|
45
|
+
"serialized_model_size",
|
|
46
|
+
]
|
|
47
|
+
legacy_names = {
|
|
48
|
+
"traced_peak_inference_allocation_mb": "traced_peak_memory_usage",
|
|
49
|
+
"rss_after_inference_mb": "processed_rss",
|
|
50
|
+
}
|
|
51
|
+
summary: dict[str, dict[str, float]] = {}
|
|
52
|
+
for metric in metrics:
|
|
53
|
+
legacy_name = legacy_names.get(metric)
|
|
54
|
+
values = []
|
|
55
|
+
for run in runs:
|
|
56
|
+
if metric in run:
|
|
57
|
+
values.append(run[metric])
|
|
58
|
+
elif legacy_name is not None and legacy_name in run:
|
|
59
|
+
values.append(run[legacy_name])
|
|
60
|
+
values = [value for value in values if value is not None]
|
|
61
|
+
if values:
|
|
62
|
+
summary[metric] = calculate_statistics(values)
|
|
63
|
+
return summary
|