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 ADDED
@@ -0,0 +1,6 @@
1
+ """Model Diet public API."""
2
+
3
+ from modeldiet.api import profile
4
+ from modeldiet.result import ProfileResult
5
+
6
+ __all__ = ["profile", "ProfileResult"]
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,5 @@
1
+ """Optional repeated benchmark tooling for bundled demonstration models."""
2
+
3
+ from modeldiet.benchmark.runner import run_repeated_benchmark
4
+
5
+ __all__ = ["run_repeated_benchmark"]
@@ -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