Skip to content

Commit 456311d

Browse files
added real benchmarking + quantization and pipeline reordering
1 parent 8d718ae commit 456311d

5 files changed

Lines changed: 304 additions & 144 deletions

File tree

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,3 +60,7 @@ Thumbs.db
6060
test_results/
6161
benchmark_results/
6262
optimization_results/
63+
64+
# Models
65+
baseline_model*
66+
simple_model*

benchmarker.py

Lines changed: 157 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,128 @@
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

1028
import os
1129
import time
12-
import json
1330
import 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

1739
logger = 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+
19124
class 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

211329
def 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

224334
def 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+
]

edgeflowc.py

Lines changed: 32 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,11 @@
2323
from typing import Any, Dict
2424

2525
# Import our modules
26-
from parser import parse_ef
26+
from parser import parse_ef # Backward-compatible name
27+
try: # Prefer Day 2 API if present
28+
from parser import parse_edgeflow_file as _parse_edgeflow_file
29+
except Exception: # noqa: BLE001
30+
_parse_edgeflow_file = None
2731
from edgeflow_ast import create_program_from_dict
2832
from code_generator import CodeGenerator, generate_code
2933

@@ -172,8 +176,11 @@ def load_config(file_path: str) -> Dict[str, Any]:
172176
Dict[str, Any]: Parsed configuration dictionary.
173177
"""
174178
try:
175-
# Use our working parser directly
176-
config = parse_ef(file_path)
179+
# Prefer modern parser API if available
180+
if _parse_edgeflow_file:
181+
config = _parse_edgeflow_file(file_path)
182+
else:
183+
config = parse_ef(file_path)
177184

178185
# Basic validation
179186
if not config.get('model'):
@@ -189,60 +196,48 @@ def load_config(file_path: str) -> Dict[str, Any]:
189196

190197

191198
def optimize_model(config: Dict[str, Any]) -> Dict[str, Any]:
192-
"""Run the complete EdgeFlow optimization pipeline.
199+
"""Run the full Day 3/4 pipeline: benchmark -> optimize -> benchmark.
193200
194-
Args:
195-
config: Parsed configuration dictionary produced by ``load_config``.
196-
197-
Returns:
198-
Dictionary with optimization results
201+
This reorders the earlier logic so we always capture baseline metrics
202+
prior to optimization (as required by Phase II tasks). It then performs
203+
optimization and captures post-optimization metrics plus a comparison.
199204
"""
200205
try:
201206
from optimizer import optimize
202207
from benchmarker import benchmark_model, compare_models
203208

204-
# Get model path
205209
model_path = config.get('model', 'model.tflite')
206210
if not os.path.exists(model_path):
207-
logging.warning(f"Model file not found: {model_path}, creating dummy model")
208-
209-
# Run optimization
210-
logging.info("Starting EdgeFlow optimization pipeline...")
211-
optimized_path, opt_results = optimize(config)
211+
logging.warning("Model file not found: %s (a test model may be generated by optimizer)", model_path)
212212

213-
# Benchmark original model
214-
logging.info("Benchmarking original model...")
213+
logging.info("=== BASELINE BENCHMARK (Pre-Optimization) ===")
215214
original_benchmark = benchmark_model(model_path, config)
216215

217-
# Benchmark optimized model
218-
logging.info("Benchmarking optimized model...")
216+
logging.info("=== OPTIMIZATION PHASE ===")
217+
optimized_path, opt_results = optimize(config)
218+
219+
logging.info("=== POST-OPTIMIZATION BENCHMARK ===")
219220
optimized_benchmark = benchmark_model(optimized_path, config)
220221

221-
# Compare models
222-
logging.info("Comparing models...")
222+
logging.info("=== COMPARISON ===")
223223
comparison = compare_models(model_path, optimized_path, config)
224+
improvements = comparison.get('improvements', {})
224225

225-
# Combine results
226-
results = {
226+
logging.info("=== EDGEFLOW OPTIMIZATION SUMMARY ===")
227+
logging.info("Model size reduction: %.1f%%", improvements.get('size_reduction_percent', 0.0))
228+
logging.info("Latency improvement: %.1f%%", improvements.get('latency_improvement_percent', 0.0))
229+
logging.info("Throughput improvement: %.1f%%", improvements.get('throughput_improvement_percent', 0.0))
230+
logging.info("Memory improvement: %.1f%%", improvements.get('memory_improvement_percent', 0.0))
231+
logging.info("Optimized model saved to: %s", optimized_path)
232+
233+
return {
227234
'optimization': opt_results,
228235
'original_benchmark': original_benchmark,
229236
'optimized_benchmark': optimized_benchmark,
230-
'comparison': comparison
237+
'comparison': comparison,
231238
}
232-
233-
# Print summary
234-
improvements = comparison.get('improvements', {})
235-
logging.info("=== EDGEFLOW OPTIMIZATION SUMMARY ===")
236-
logging.info(f"Model size reduction: {improvements.get('size_reduction_percent', 0):.1f}%")
237-
logging.info(f"Latency improvement: {improvements.get('latency_improvement_percent', 0):.1f}%")
238-
logging.info(f"Throughput improvement: {improvements.get('throughput_improvement_percent', 0):.1f}%")
239-
logging.info(f"Memory improvement: {improvements.get('memory_improvement_percent', 0):.1f}%")
240-
logging.info(f"Optimized model saved to: {optimized_path}")
241-
242-
return results
243-
244239
except Exception as e: # noqa: BLE001
245-
logging.error(f"Optimization failed: {e}")
240+
logging.error("Optimization pipeline failed: %s", e)
246241
return {'error': str(e)}
247242

248243

0 commit comments

Comments
 (0)