11"""EdgeFlow Model Benchmarker
22
3- This module provides comprehensive benchmarking and performance measurement
4- for EdgeFlow models, including latency, memory usage, and throughput metrics.
3+ Day 3/4 Implementation (Team B):
4+ --------------------------------
5+ Provides *real* model size and latency benchmarking when TensorFlow Lite is
6+ available, with a graceful fallback to the earlier simulation-based metrics.
57
6- The benchmarker helps prove that EdgeFlow optimizations actually improve
7- performance on edge devices.
8+ Public low-level functions now exposed for the pipeline:
9+ - ``get_model_size(model_path)`` -> float (MB)
10+ - ``benchmark_latency(model_path, runs=100, warmup=1)`` -> float (ms)
11+
12+ If TensorFlow (``tensorflow`` package) is not installed, or a model cannot be
13+ loaded, the module silently falls back to deterministic simulation so tests
14+ remain stable in lightweight environments (CI, dev containers, etc.).
15+
16+ Real benchmarking logic:
17+ * Loads the TFLite model with ``tf.lite.Interpreter``.
18+ * Allocates tensors and inspects the first input tensor to derive shape & dtype.
19+ * Performs one (configurable) warm-up inference.
20+ * Measures average latency across N runs using ``time.perf_counter``.
21+ * Computes an approximate throughput (FPS = 1000 / avg_latency_ms).
22+
23+ The higher-level convenience functions ``benchmark_model`` and
24+ ``compare_models`` retain their original signatures and output schema so the
25+ rest of the codebase (and existing tests) continue to work unchanged.
826"""
927
1028import os
1129import time
12- import json
1330import logging
14- from typing import Dict , Any , List , Optional
15- from pathlib import Path
31+ from typing import Dict , Any , List , Optional , Tuple
32+
33+ try : # Optional heavy dependency
34+ import tensorflow as _tf # type: ignore
35+ _TF_AVAILABLE = True
36+ except Exception : # noqa: BLE001 - optional dependency
37+ _TF_AVAILABLE = False
1638
1739logger = logging .getLogger (__name__ )
1840
41+ def get_model_size (model_path : str ) -> float :
42+ """Return model size in megabytes.
43+
44+ Args:
45+ model_path: Path to a file on disk
46+ Returns:
47+ Size in megabytes (float). Returns 0.0 if file missing.
48+ """
49+ try :
50+ return os .path .getsize (model_path ) / (1024 * 1024 )
51+ except OSError :
52+ return 0.0
53+
54+
55+ def _generate_random_input (shape , dtype ): # type: ignore[no-untyped-def]
56+ """Generate random input tensor matching shape/dtype for benchmarking."""
57+ import numpy as np # Local import to keep global namespace light
58+
59+ if dtype == np .float32 :
60+ return (np .random .random (shape ).astype (np .float32 ) * 1.0 )
61+ if dtype == np .int8 :
62+ return np .random .randint (- 128 , 127 , size = shape , dtype = np .int8 )
63+ if dtype == np .uint8 :
64+ return np .random .randint (0 , 255 , size = shape , dtype = np .uint8 )
65+ # Fallback: float32
66+ return np .random .random (shape ).astype (np .float32 )
67+
68+
69+ def benchmark_latency (model_path : str , runs : int = 100 , warmup : int = 1 ) -> Tuple [float , Optional [Dict [str , Any ]]]:
70+ """Benchmark average inference latency (ms) for a TFLite model.
71+
72+ Performs one or more warm-up runs followed by timed runs.
73+
74+ Args:
75+ model_path: Path to a *.tflite model
76+ runs: Number of timed inference iterations
77+ warmup: Number of warm-up iterations (not timed)
78+ Returns:
79+ (avg_latency_ms, debug_metadata_dict_or_None)
80+ If TensorFlow Lite is unavailable or model can't be loaded, returns (0.0, None)
81+ """
82+ if not _TF_AVAILABLE or not os .path .isfile (model_path ):
83+ return 0.0 , None
84+
85+ try : # Load interpreter
86+ interpreter = _tf .lite .Interpreter (model_path = model_path ) # type: ignore[attr-defined]
87+ interpreter .allocate_tensors ()
88+ input_details = interpreter .get_input_details ()
89+ if not input_details :
90+ return 0.0 , None
91+ first_input = input_details [0 ]
92+ shape = first_input .get ("shape" )
93+ dtype = first_input .get ("dtype" )
94+ index = first_input .get ("index" )
95+ if shape is None or dtype is None or index is None :
96+ return 0.0 , None
97+
98+ # Warm-up
99+ for _ in range (max (warmup , 0 )):
100+ data = _generate_random_input (shape , dtype )
101+ interpreter .set_tensor (index , data )
102+ interpreter .invoke ()
103+
104+ # Timed runs
105+ total = 0.0
106+ for _ in range (max (runs , 1 )):
107+ data = _generate_random_input (shape , dtype )
108+ start = time .perf_counter ()
109+ interpreter .set_tensor (index , data )
110+ interpreter .invoke ()
111+ total += (time .perf_counter () - start ) * 1000.0
112+ avg_ms = total / max (runs , 1 )
113+ metadata = {
114+ 'input_shape' : tuple (int (x ) for x in shape ),
115+ 'dtype' : str (dtype ),
116+ 'runs' : runs ,
117+ 'warmup' : warmup ,
118+ }
119+ return avg_ms , metadata
120+ except Exception : # noqa: BLE001
121+ return 0.0 , None
122+
123+
19124class EdgeFlowBenchmarker :
20- """Comprehensive benchmarking for EdgeFlow models."""
125+ """Comprehensive benchmarking for EdgeFlow models (real + simulated) ."""
21126
22127 def __init__ (self , config : Dict [str , Any ]):
23128 """Initialize benchmarker with EdgeFlow configuration.
@@ -31,31 +136,44 @@ def __init__(self, config: Dict[str, Any]):
31136 self .memory_limit = config .get ('memory_limit' , 64 )
32137
33138 def benchmark_model (self , model_path : str ) -> Dict [str , Any ]:
34- """Benchmark a single model.
35-
36- Args:
37- model_path: Path to the model file
38-
39- Returns:
40- Dictionary with benchmark results
41- """
139+ """Benchmark a single model (real if possible, else simulation)."""
42140 logger .info (f"Benchmarking model: { model_path } " )
43-
141+
44142 if not os .path .exists (model_path ):
45143 logger .warning (f"Model file not found: { model_path } " )
46144 return self ._create_dummy_benchmark (model_path )
47-
48- # Get model size
49- model_size_mb = os .path .getsize (model_path ) / (1024 * 1024 )
50-
51- # Simulate benchmarking based on device and optimization goals
52- results = self ._simulate_benchmark (model_path , model_size_mb )
53-
145+
146+ model_size_mb = get_model_size (model_path )
147+
148+ # Attempt real latency measurement
149+ latency_ms , meta = benchmark_latency (model_path )
150+ used_real = latency_ms > 0.0
151+
152+ if used_real :
153+ throughput_fps = 1000.0 / latency_ms if latency_ms > 0 else 0.0
154+ memory_usage_mb = round (min (model_size_mb * 2 , self .memory_limit ), 2 )
155+ results = {
156+ 'model_path' : model_path ,
157+ 'model_size_mb' : round (model_size_mb , 3 ),
158+ 'device' : self .target_device ,
159+ 'latency_ms' : round (latency_ms , 2 ),
160+ 'throughput_fps' : round (throughput_fps , 2 ),
161+ 'memory_usage_mb' : memory_usage_mb ,
162+ 'optimize_for' : self .optimize_for ,
163+ 'status' : 'success' ,
164+ 'mode' : 'real' ,
165+ }
166+ if meta :
167+ results ['details' ] = meta
168+ else :
169+ # Simulation fallback
170+ results = self ._simulate_benchmark (model_path , model_size_mb )
171+ results ['mode' ] = 'simulation'
172+
54173 logger .info (f"Benchmark complete: { model_path } " )
55- logger .info (f" Latency: { results ['latency_ms' ]:.1f} ms" )
56- logger .info (f" Throughput: { results ['throughput_fps' ]:.1f} FPS" )
57- logger .info (f" Memory: { results ['memory_usage_mb' ]:.1f} MB" )
58-
174+ logger .info (f" Latency: { results ['latency_ms' ]:.2f} ms ({ results .get ('mode' )} )" )
175+ logger .info (f" Throughput: { results ['throughput_fps' ]:.2f} FPS" )
176+ logger .info (f" Memory: { results ['memory_usage_mb' ]:.2f} MB" )
59177 return results
60178
61179 def compare_models (self , original_path : str , optimized_path : str ) -> Dict [str , Any ]:
@@ -209,28 +327,19 @@ def _generate_summary(self, improvements: Dict[str, Any]) -> str:
209327 return "; " .join (summary_parts ) if summary_parts else "No significant improvements"
210328
211329def benchmark_model (model_path : str , config : Dict [str , Any ]) -> Dict [str , Any ]:
212- """Benchmark a single model.
213-
214- Args:
215- model_path: Path to the model file
216- config: EdgeFlow configuration
217-
218- Returns:
219- Benchmark results dictionary
220- """
330+ """Benchmark wrapper retaining original public API."""
221331 benchmarker = EdgeFlowBenchmarker (config )
222332 return benchmarker .benchmark_model (model_path )
223333
224334def compare_models (original_path : str , optimized_path : str , config : Dict [str , Any ]) -> Dict [str , Any ]:
225- """Compare two models.
226-
227- Args:
228- original_path: Path to original model
229- optimized_path: Path to optimized model
230- config: EdgeFlow configuration
231-
232- Returns:
233- Comparison results dictionary
234- """
335+ """Compare two models (original vs optimized)."""
235336 benchmarker = EdgeFlowBenchmarker (config )
236- return benchmarker .compare_models (original_path , optimized_path )
337+ return benchmarker .compare_models (original_path , optimized_path )
338+
339+ __all__ = [
340+ 'get_model_size' ,
341+ 'benchmark_latency' ,
342+ 'benchmark_model' ,
343+ 'compare_models' ,
344+ 'EdgeFlowBenchmarker' ,
345+ ]
0 commit comments