optimization.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626
  1. """
  2. Performance optimization utilities for wakeword detection.
  3. This module provides tools for optimizing the wakeword detection system
  4. for real-time performance, including model optimization, memory management,
  5. and latency reduction techniques.
  6. """
  7. import gc
  8. import time
  9. import threading
  10. import psutil
  11. import warnings
  12. from typing import Dict, Any, Optional, List, Tuple
  13. from dataclasses import dataclass
  14. from pathlib import Path
  15. import torch
  16. import torch.nn as nn
  17. import numpy as np
  18. @dataclass
  19. class PerformanceMetrics:
  20. """Performance metrics for monitoring system performance."""
  21. # Latency metrics (milliseconds)
  22. inference_latency_ms: float = 0.0
  23. feature_extraction_latency_ms: float = 0.0
  24. total_processing_latency_ms: float = 0.0
  25. # Throughput metrics
  26. chunks_per_second: float = 0.0
  27. audio_realtime_factor: float = 0.0 # >1.0 means faster than realtime
  28. # Memory metrics (MB)
  29. memory_usage_mb: float = 0.0
  30. peak_memory_mb: float = 0.0
  31. gpu_memory_mb: float = 0.0
  32. # CPU metrics
  33. cpu_usage_percent: float = 0.0
  34. cpu_load_average: float = 0.0
  35. # Accuracy metrics
  36. confidence_scores: List[float] = None
  37. detection_rate: float = 0.0
  38. false_positive_rate: float = 0.0
  39. def __post_init__(self):
  40. if self.confidence_scores is None:
  41. self.confidence_scores = []
  42. class PerformanceMonitor:
  43. """
  44. Real-time performance monitoring for wakeword detection.
  45. Tracks various performance metrics and provides alerts for
  46. performance degradation.
  47. """
  48. def __init__(self, monitoring_interval: float = 1.0,
  49. memory_warning_threshold: float = 512.0, # MB
  50. latency_warning_threshold: float = 100.0): # ms
  51. """
  52. Initialize performance monitor.
  53. Args:
  54. monitoring_interval: How often to collect metrics (seconds)
  55. memory_warning_threshold: Memory usage warning threshold (MB)
  56. latency_warning_threshold: Latency warning threshold (ms)
  57. """
  58. self.monitoring_interval = monitoring_interval
  59. self.memory_warning_threshold = memory_warning_threshold
  60. self.latency_warning_threshold = latency_warning_threshold
  61. self._is_monitoring = False
  62. self._monitor_thread = None
  63. self._metrics_history: List[PerformanceMetrics] = []
  64. self._lock = threading.Lock()
  65. # Performance counters
  66. self._chunk_count = 0
  67. self._total_inference_time = 0.0
  68. self._total_feature_time = 0.0
  69. self._start_time = None
  70. # Warnings tracking
  71. self._warnings_issued = set()
  72. def start_monitoring(self):
  73. """Start performance monitoring."""
  74. if self._is_monitoring:
  75. return
  76. self._is_monitoring = True
  77. self._start_time = time.time()
  78. self._monitor_thread = threading.Thread(
  79. target=self._monitoring_loop,
  80. name="PerformanceMonitor",
  81. daemon=True
  82. )
  83. self._monitor_thread.start()
  84. def stop_monitoring(self):
  85. """Stop performance monitoring."""
  86. self._is_monitoring = False
  87. if self._monitor_thread:
  88. self._monitor_thread.join(timeout=2.0)
  89. def record_inference(self, inference_time_ms: float, feature_time_ms: float):
  90. """Record inference performance metrics."""
  91. with self._lock:
  92. self._chunk_count += 1
  93. self._total_inference_time += inference_time_ms
  94. self._total_feature_time += feature_time_ms
  95. def _monitoring_loop(self):
  96. """Main monitoring loop."""
  97. while self._is_monitoring:
  98. try:
  99. metrics = self._collect_metrics()
  100. with self._lock:
  101. self._metrics_history.append(metrics)
  102. # Keep only recent metrics (last 100 samples)
  103. if len(self._metrics_history) > 100:
  104. self._metrics_history.pop(0)
  105. # Check for performance issues
  106. self._check_performance_warnings(metrics)
  107. time.sleep(self.monitoring_interval)
  108. except Exception as e:
  109. warnings.warn(f"Error in performance monitoring: {e}")
  110. def _collect_metrics(self) -> PerformanceMetrics:
  111. """Collect current performance metrics."""
  112. with self._lock:
  113. # Calculate latencies
  114. if self._chunk_count > 0:
  115. avg_inference_latency = self._total_inference_time / self._chunk_count
  116. avg_feature_latency = self._total_feature_time / self._chunk_count
  117. total_latency = avg_inference_latency + avg_feature_latency
  118. else:
  119. avg_inference_latency = avg_feature_latency = total_latency = 0.0
  120. # Calculate throughput
  121. elapsed_time = time.time() - self._start_time if self._start_time else 1.0
  122. chunks_per_second = self._chunk_count / elapsed_time
  123. # System metrics
  124. process = psutil.Process()
  125. memory_info = process.memory_info()
  126. memory_usage_mb = memory_info.rss / (1024 * 1024)
  127. cpu_percent = process.cpu_percent()
  128. # GPU memory if available
  129. gpu_memory_mb = 0.0
  130. if torch.cuda.is_available():
  131. gpu_memory_mb = torch.cuda.memory_allocated() / (1024 * 1024)
  132. return PerformanceMetrics(
  133. inference_latency_ms=avg_inference_latency,
  134. feature_extraction_latency_ms=avg_feature_latency,
  135. total_processing_latency_ms=total_latency,
  136. chunks_per_second=chunks_per_second,
  137. memory_usage_mb=memory_usage_mb,
  138. gpu_memory_mb=gpu_memory_mb,
  139. cpu_usage_percent=cpu_percent
  140. )
  141. def _check_performance_warnings(self, metrics: PerformanceMetrics):
  142. """Check for performance issues and issue warnings."""
  143. # Memory warning
  144. if metrics.memory_usage_mb > self.memory_warning_threshold:
  145. warning_key = "high_memory"
  146. if warning_key not in self._warnings_issued:
  147. warnings.warn(f"High memory usage: {metrics.memory_usage_mb:.1f}MB")
  148. self._warnings_issued.add(warning_key)
  149. # Latency warning
  150. if metrics.total_processing_latency_ms > self.latency_warning_threshold:
  151. warning_key = "high_latency"
  152. if warning_key not in self._warnings_issued:
  153. warnings.warn(f"High processing latency: {metrics.total_processing_latency_ms:.1f}ms")
  154. self._warnings_issued.add(warning_key)
  155. def get_current_metrics(self) -> Optional[PerformanceMetrics]:
  156. """Get the most recent performance metrics."""
  157. with self._lock:
  158. return self._metrics_history[-1] if self._metrics_history else None
  159. def get_metrics_history(self) -> List[PerformanceMetrics]:
  160. """Get performance metrics history."""
  161. with self._lock:
  162. return self._metrics_history.copy()
  163. def get_performance_summary(self) -> Dict[str, Any]:
  164. """Get performance summary statistics."""
  165. with self._lock:
  166. if not self._metrics_history:
  167. return {}
  168. recent_metrics = self._metrics_history[-10:] # Last 10 samples
  169. return {
  170. 'avg_inference_latency_ms': np.mean([m.inference_latency_ms for m in recent_metrics]),
  171. 'max_inference_latency_ms': max(m.inference_latency_ms for m in recent_metrics),
  172. 'avg_memory_usage_mb': np.mean([m.memory_usage_mb for m in recent_metrics]),
  173. 'max_memory_usage_mb': max(m.memory_usage_mb for m in recent_metrics),
  174. 'avg_cpu_usage_percent': np.mean([m.cpu_usage_percent for m in recent_metrics]),
  175. 'chunks_processed': self._chunk_count,
  176. 'total_runtime_seconds': time.time() - self._start_time if self._start_time else 0
  177. }
  178. class ModelOptimizer:
  179. """
  180. Model optimization utilities for improving inference performance.
  181. Provides various optimization techniques including quantization,
  182. pruning, and compilation.
  183. """
  184. @staticmethod
  185. def optimize_for_inference(model: nn.Module, example_input: torch.Tensor,
  186. optimization_level: str = "moderate") -> nn.Module:
  187. """
  188. Optimize PyTorch model for inference.
  189. Args:
  190. model: PyTorch model to optimize
  191. example_input: Example input tensor for optimization
  192. optimization_level: "light", "moderate", or "aggressive"
  193. Returns:
  194. Optimized model
  195. """
  196. optimized_model = model.eval()
  197. if optimization_level in ["moderate", "aggressive"]:
  198. # Try to use TorchScript
  199. try:
  200. optimized_model = torch.jit.trace(optimized_model, example_input)
  201. except Exception as e:
  202. warnings.warn(f"TorchScript optimization failed: {e}")
  203. if optimization_level == "aggressive":
  204. # Try quantization (CPU only)
  205. if example_input.device.type == "cpu":
  206. try:
  207. optimized_model = torch.quantization.quantize_dynamic(
  208. optimized_model, {nn.Linear, nn.Conv2d}, dtype=torch.qint8
  209. )
  210. except Exception as e:
  211. warnings.warn(f"Quantization failed: {e}")
  212. return optimized_model
  213. @staticmethod
  214. def apply_model_compilation(model: nn.Module) -> nn.Module:
  215. """
  216. Apply PyTorch 2.0+ compilation if available.
  217. Args:
  218. model: Model to compile
  219. Returns:
  220. Compiled model or original if compilation not available
  221. """
  222. try:
  223. if hasattr(torch, 'compile'):
  224. return torch.compile(model, mode="reduce-overhead")
  225. else:
  226. warnings.warn("torch.compile not available in this PyTorch version")
  227. return model
  228. except Exception as e:
  229. warnings.warn(f"Model compilation failed: {e}")
  230. return model
  231. @staticmethod
  232. def estimate_model_flops(model: nn.Module, input_shape: Tuple[int, ...]) -> int:
  233. """
  234. Estimate FLOPs for model inference.
  235. Args:
  236. model: PyTorch model
  237. input_shape: Input tensor shape
  238. Returns:
  239. Estimated FLOPs
  240. """
  241. try:
  242. # Try to use thop if available
  243. import thop
  244. dummy_input = torch.randn(1, *input_shape)
  245. flops, _ = thop.profile(model, inputs=(dummy_input,), verbose=False)
  246. return int(flops)
  247. except ImportError:
  248. # Fallback to simple estimation
  249. total_params = sum(p.numel() for p in model.parameters())
  250. # Rough estimation: assume each parameter is used once
  251. return total_params
  252. @staticmethod
  253. def benchmark_model(model: nn.Module, input_shape: Tuple[int, ...],
  254. device: torch.device, num_runs: int = 100) -> Dict[str, float]:
  255. """
  256. Benchmark model inference performance.
  257. Args:
  258. model: Model to benchmark
  259. input_shape: Input tensor shape
  260. device: Device to run benchmark on
  261. num_runs: Number of benchmark runs
  262. Returns:
  263. Performance metrics dictionary
  264. """
  265. model = model.to(device).eval()
  266. dummy_input = torch.randn(1, *input_shape, device=device)
  267. # Warm up
  268. for _ in range(10):
  269. with torch.no_grad():
  270. _ = model(dummy_input)
  271. if device.type == "cuda":
  272. torch.cuda.synchronize()
  273. # Benchmark
  274. times = []
  275. for _ in range(num_runs):
  276. start_time = time.time()
  277. with torch.no_grad():
  278. _ = model(dummy_input)
  279. if device.type == "cuda":
  280. torch.cuda.synchronize()
  281. end_time = time.time()
  282. times.append((end_time - start_time) * 1000) # ms
  283. return {
  284. 'mean_inference_time_ms': np.mean(times),
  285. 'std_inference_time_ms': np.std(times),
  286. 'min_inference_time_ms': min(times),
  287. 'max_inference_time_ms': max(times),
  288. 'median_inference_time_ms': np.median(times),
  289. 'percentile_95_ms': np.percentile(times, 95),
  290. 'throughput_fps': 1000.0 / np.mean(times)
  291. }
  292. class MemoryManager:
  293. """
  294. Memory management utilities for efficient resource usage.
  295. Helps manage memory allocation and garbage collection for
  296. optimal performance.
  297. """
  298. def __init__(self, gc_frequency: int = 100,
  299. memory_limit_mb: Optional[float] = None):
  300. """
  301. Initialize memory manager.
  302. Args:
  303. gc_frequency: How often to run garbage collection (every N chunks)
  304. memory_limit_mb: Memory limit in MB (None for no limit)
  305. """
  306. self.gc_frequency = gc_frequency
  307. self.memory_limit_mb = memory_limit_mb
  308. self._chunk_counter = 0
  309. self._memory_pool = {}
  310. self._peak_memory = 0.0
  311. def process_chunk(self, func, *args, **kwargs):
  312. """
  313. Process a chunk with memory management.
  314. Args:
  315. func: Function to call
  316. *args, **kwargs: Arguments for the function
  317. Returns:
  318. Function result
  319. """
  320. self._chunk_counter += 1
  321. # Run function
  322. result = func(*args, **kwargs)
  323. # Check memory usage
  324. current_memory = self._get_memory_usage()
  325. self._peak_memory = max(self._peak_memory, current_memory)
  326. # Run garbage collection if needed
  327. if self._chunk_counter % self.gc_frequency == 0:
  328. self._run_garbage_collection()
  329. # Check memory limit
  330. if self.memory_limit_mb and current_memory > self.memory_limit_mb:
  331. warnings.warn(f"Memory usage ({current_memory:.1f}MB) exceeds limit ({self.memory_limit_mb}MB)")
  332. self._emergency_cleanup()
  333. return result
  334. def _get_memory_usage(self) -> float:
  335. """Get current memory usage in MB."""
  336. process = psutil.Process()
  337. return process.memory_info().rss / (1024 * 1024)
  338. def _run_garbage_collection(self):
  339. """Run garbage collection."""
  340. gc.collect()
  341. # Also clear PyTorch cache if on GPU
  342. if torch.cuda.is_available():
  343. torch.cuda.empty_cache()
  344. def _emergency_cleanup(self):
  345. """Emergency memory cleanup."""
  346. # Force garbage collection
  347. self._run_garbage_collection()
  348. # Clear memory pool
  349. self._memory_pool.clear()
  350. # Additional PyTorch cleanup
  351. if torch.cuda.is_available():
  352. torch.cuda.empty_cache()
  353. torch.cuda.ipc_collect()
  354. def get_memory_stats(self) -> Dict[str, float]:
  355. """Get memory usage statistics."""
  356. current_memory = self._get_memory_usage()
  357. stats = {
  358. 'current_memory_mb': current_memory,
  359. 'peak_memory_mb': self._peak_memory,
  360. 'chunks_processed': self._chunk_counter,
  361. 'gc_runs': self._chunk_counter // self.gc_frequency
  362. }
  363. if torch.cuda.is_available():
  364. stats.update({
  365. 'gpu_memory_allocated_mb': torch.cuda.memory_allocated() / (1024 * 1024),
  366. 'gpu_memory_reserved_mb': torch.cuda.memory_reserved() / (1024 * 1024),
  367. 'gpu_memory_cached_mb': torch.cuda.memory_cached() / (1024 * 1024)
  368. })
  369. return stats
  370. class LatencyOptimizer:
  371. """
  372. Latency optimization utilities for reducing processing delays.
  373. Provides techniques for minimizing end-to-end latency in
  374. wakeword detection.
  375. """
  376. @staticmethod
  377. def optimize_audio_buffer_size(target_latency_ms: float,
  378. sample_rate: int,
  379. processing_time_ms: float) -> Tuple[int, float]:
  380. """
  381. Calculate optimal audio buffer size for target latency.
  382. Args:
  383. target_latency_ms: Target total latency
  384. sample_rate: Audio sample rate
  385. processing_time_ms: Expected processing time
  386. Returns:
  387. Tuple of (buffer_samples, actual_latency_ms)
  388. """
  389. # Account for processing overhead
  390. available_latency_ms = target_latency_ms - processing_time_ms
  391. # Calculate buffer size
  392. buffer_duration_s = max(0.010, available_latency_ms / 1000.0) # Minimum 10ms
  393. buffer_samples = int(buffer_duration_s * sample_rate)
  394. # Calculate actual latency
  395. actual_buffer_latency_ms = (buffer_samples / sample_rate) * 1000
  396. actual_total_latency_ms = actual_buffer_latency_ms + processing_time_ms
  397. return buffer_samples, actual_total_latency_ms
  398. @staticmethod
  399. def profile_inference_pipeline(detector, num_samples: int = 100) -> Dict[str, Any]:
  400. """
  401. Profile the complete inference pipeline for latency bottlenecks.
  402. Args:
  403. detector: WakewordDetector instance
  404. num_samples: Number of samples to profile
  405. Returns:
  406. Profiling results
  407. """
  408. # Create dummy audio data
  409. sample_rate = detector.config.audio_config.sample_rate
  410. chunk_duration = detector.config.audio_config.chunk_duration
  411. chunk_samples = int(chunk_duration * sample_rate)
  412. # Profile stages
  413. audio_generation_times = []
  414. feature_extraction_times = []
  415. inference_times = []
  416. total_times = []
  417. for _ in range(num_samples):
  418. # Generate dummy audio
  419. start_time = time.time()
  420. dummy_audio = np.random.randn(chunk_samples).astype(np.float32)
  421. audio_generation_times.append((time.time() - start_time) * 1000)
  422. # Feature extraction
  423. start_time = time.time()
  424. features = detector.feature_extractor.extract_features(
  425. torch.from_numpy(dummy_audio)
  426. )
  427. feature_extraction_times.append((time.time() - start_time) * 1000)
  428. # Inference
  429. start_time = time.time()
  430. with torch.no_grad():
  431. _ = detector.model(features)
  432. inference_times.append((time.time() - start_time) * 1000)
  433. total_times.append(feature_extraction_times[-1] + inference_times[-1])
  434. return {
  435. 'feature_extraction': {
  436. 'mean_ms': np.mean(feature_extraction_times),
  437. 'std_ms': np.std(feature_extraction_times),
  438. 'min_ms': min(feature_extraction_times),
  439. 'max_ms': max(feature_extraction_times)
  440. },
  441. 'inference': {
  442. 'mean_ms': np.mean(inference_times),
  443. 'std_ms': np.std(inference_times),
  444. 'min_ms': min(inference_times),
  445. 'max_ms': max(inference_times)
  446. },
  447. 'total_processing': {
  448. 'mean_ms': np.mean(total_times),
  449. 'std_ms': np.std(total_times),
  450. 'min_ms': min(total_times),
  451. 'max_ms': max(total_times)
  452. },
  453. 'throughput_hz': 1000.0 / np.mean(total_times)
  454. }
  455. def create_performance_monitor(config: Dict[str, Any]) -> PerformanceMonitor:
  456. """
  457. Factory function to create performance monitor.
  458. Args:
  459. config: Configuration dictionary
  460. Returns:
  461. PerformanceMonitor instance
  462. """
  463. return PerformanceMonitor(
  464. monitoring_interval=config.get('monitoring_interval', 1.0),
  465. memory_warning_threshold=config.get('memory_warning_threshold', 512.0),
  466. latency_warning_threshold=config.get('latency_warning_threshold', 100.0)
  467. )
  468. def optimize_for_deployment(model: nn.Module, config: Dict[str, Any],
  469. example_input: torch.Tensor) -> Tuple[nn.Module, Dict[str, Any]]:
  470. """
  471. Comprehensive optimization for deployment.
  472. Args:
  473. model: Model to optimize
  474. config: Optimization configuration
  475. example_input: Example input for optimization
  476. Returns:
  477. Tuple of (optimized_model, optimization_report)
  478. """
  479. optimizer = ModelOptimizer()
  480. # Benchmark original model
  481. original_metrics = optimizer.benchmark_model(
  482. model, example_input.shape[1:], example_input.device
  483. )
  484. # Apply optimizations
  485. optimized_model = model
  486. if config.get('enable_torchscript', True):
  487. optimized_model = optimizer.optimize_for_inference(
  488. optimized_model, example_input,
  489. optimization_level=config.get('optimization_level', 'moderate')
  490. )
  491. if config.get('enable_compilation', True):
  492. optimized_model = optimizer.apply_model_compilation(optimized_model)
  493. # Benchmark optimized model
  494. optimized_metrics = optimizer.benchmark_model(
  495. optimized_model, example_input.shape[1:], example_input.device
  496. )
  497. # Create optimization report
  498. report = {
  499. 'original_performance': original_metrics,
  500. 'optimized_performance': optimized_metrics,
  501. 'improvement_factor': original_metrics['mean_inference_time_ms'] / optimized_metrics['mean_inference_time_ms'],
  502. 'optimizations_applied': list(config.keys())
  503. }
  504. return optimized_model, report