Spaces:
Sleeping
Sleeping
| """Model optimization utilities. | |
| This module provides tools for model optimization including quantization, | |
| pruning, and performance optimization techniques. | |
| """ | |
| import torch | |
| import torch.nn as nn | |
| import logging | |
| import time | |
| import psutil | |
| import gc | |
| from typing import Any, Dict, List, Optional, Tuple | |
| logger = logging.getLogger(__name__) | |
| class ModelOptimizer: | |
| """Model optimization utilities for Florence models.""" | |
| def __init__(self, model: nn.Module): | |
| """Initialize model optimizer. | |
| Args: | |
| model: Model to optimize | |
| """ | |
| self.model = model | |
| self.original_state = None | |
| self.optimization_history: List[Dict[str, Any]] = [] | |
| def save_original_state(self) -> None: | |
| """Save original model state for restoration.""" | |
| self.original_state = { | |
| 'state_dict': self.model.state_dict(), | |
| 'model_size': self.get_model_size(), | |
| 'param_count': self.count_parameters() | |
| } | |
| logger.info("Original model state saved") | |
| def restore_original_state(self) -> None: | |
| """Restore model to original state.""" | |
| if self.original_state is None: | |
| logger.warning("No original state saved") | |
| return | |
| self.model.load_state_dict(self.original_state['state_dict']) | |
| logger.info("Model restored to original state") | |
| def quantize_model( | |
| self, | |
| quantization_type: str = "dynamic", | |
| dtype: torch.dtype = torch.qint8, | |
| backend: str = "fbgemm" | |
| ) -> nn.Module: | |
| """Apply quantization to the model. | |
| Args: | |
| quantization_type: Type of quantization ('dynamic', 'static', 'qat') | |
| dtype: Quantization data type | |
| backend: Quantization backend | |
| Returns: | |
| Quantized model | |
| """ | |
| try: | |
| original_size = self.get_model_size() | |
| if quantization_type == "dynamic": | |
| quantized_model = torch.quantization.quantize_dynamic( | |
| self.model, | |
| {nn.Linear, nn.Conv2d}, | |
| dtype=dtype | |
| ) | |
| elif quantization_type == "static": | |
| # Prepare model for static quantization | |
| self.model.qconfig = torch.quantization.get_default_qconfig(backend) | |
| torch.quantization.prepare(self.model, inplace=True) | |
| # Note: In practice, you would need calibration data here | |
| logger.warning("Static quantization requires calibration data") | |
| quantized_model = torch.quantization.convert(self.model, inplace=False) | |
| else: | |
| raise ValueError(f"Unsupported quantization type: {quantization_type}") | |
| quantized_size = self.get_model_size(quantized_model) | |
| # Dynamically quantized layers store packed weights that are not | |
| # exposed through ``parameters()``/``buffers()``, so the measured | |
| # size can be 0. Guard against dividing by zero in that case. | |
| compression_ratio = ( | |
| original_size / quantized_size if quantized_size > 0 else float("inf") | |
| ) | |
| optimization_info = { | |
| 'type': 'quantization', | |
| 'method': quantization_type, | |
| 'original_size_mb': original_size, | |
| 'optimized_size_mb': quantized_size, | |
| 'compression_ratio': compression_ratio, | |
| 'dtype': str(dtype) | |
| } | |
| self.optimization_history.append(optimization_info) | |
| logger.info(f"Model quantized: {original_size:.2f}MB -> {quantized_size:.2f}MB " | |
| f"(compression ratio: {compression_ratio:.2f}x)") | |
| return quantized_model | |
| except Exception as e: | |
| logger.error(f"Quantization failed: {e}") | |
| raise | |
| def prune_model( | |
| self, | |
| pruning_ratio: float = 0.2, | |
| structured: bool = False, | |
| importance_scores: Optional[Dict[str, torch.Tensor]] = None | |
| ) -> nn.Module: | |
| """Apply pruning to the model. | |
| Args: | |
| pruning_ratio: Fraction of parameters to prune | |
| structured: Whether to use structured pruning | |
| importance_scores: Custom importance scores for parameters | |
| Returns: | |
| Pruned model | |
| """ | |
| try: | |
| import torch.nn.utils.prune as prune | |
| original_params = self.count_parameters() | |
| # Apply pruning to linear and convolutional layers | |
| modules_to_prune = [] | |
| for name, module in self.model.named_modules(): | |
| if isinstance(module, (nn.Linear, nn.Conv2d)): | |
| modules_to_prune.append((module, 'weight')) | |
| if structured: | |
| # Structured pruning (remove entire channels/filters) | |
| for module, param_name in modules_to_prune: | |
| if isinstance(module, nn.Conv2d): | |
| prune.ln_structured( | |
| module, param_name, amount=pruning_ratio, n=2, dim=0 | |
| ) | |
| elif isinstance(module, nn.Linear): | |
| prune.ln_structured( | |
| module, param_name, amount=pruning_ratio, n=2, dim=0 | |
| ) | |
| else: | |
| # Unstructured pruning (remove individual weights) | |
| if importance_scores: | |
| # Use custom importance scores | |
| for module, param_name in modules_to_prune: | |
| module_name = None | |
| for name, mod in self.model.named_modules(): | |
| if mod is module: | |
| module_name = name | |
| break | |
| if module_name and module_name in importance_scores: | |
| prune.global_unstructured( | |
| [(module, param_name)], | |
| pruning_method=prune.L1Unstructured, | |
| amount=pruning_ratio, | |
| importance_scores=importance_scores[module_name] | |
| ) | |
| else: | |
| prune.l1_unstructured(module, param_name, amount=pruning_ratio) | |
| else: | |
| # Global magnitude-based pruning | |
| prune.global_unstructured( | |
| modules_to_prune, | |
| pruning_method=prune.L1Unstructured, | |
| amount=pruning_ratio | |
| ) | |
| # Make pruning permanent | |
| for module, param_name in modules_to_prune: | |
| prune.remove(module, param_name) | |
| pruned_params = self.count_parameters() | |
| actual_pruning_ratio = 1 - (pruned_params / original_params) | |
| optimization_info = { | |
| 'type': 'pruning', | |
| 'method': 'structured' if structured else 'unstructured', | |
| 'target_ratio': pruning_ratio, | |
| 'actual_ratio': actual_pruning_ratio, | |
| 'original_params': original_params, | |
| 'pruned_params': pruned_params | |
| } | |
| self.optimization_history.append(optimization_info) | |
| logger.info(f"Model pruned: {original_params:,} -> {pruned_params:,} parameters " | |
| f"(pruning ratio: {actual_pruning_ratio:.2%})") | |
| return self.model | |
| except ImportError: | |
| logger.error("Pruning requires PyTorch >= 1.4.0") | |
| raise | |
| except Exception as e: | |
| logger.error(f"Pruning failed: {e}") | |
| raise | |
| def optimize_for_inference(self) -> nn.Module: | |
| """Optimize model for inference. | |
| Returns: | |
| Optimized model | |
| """ | |
| try: | |
| # Set model to evaluation mode | |
| self.model.eval() | |
| # Disable gradient computation | |
| for param in self.model.parameters(): | |
| param.requires_grad = False | |
| # Fuse operations where possible | |
| if hasattr(torch.quantization, 'fuse_modules'): | |
| # Try to fuse conv-bn-relu patterns | |
| try: | |
| fused_model = torch.quantization.fuse_modules( | |
| self.model, | |
| [['conv', 'bn', 'relu']] if hasattr(self.model, 'conv') else [] | |
| ) | |
| logger.info("Model operations fused for inference") | |
| return fused_model | |
| except Exception as e: | |
| logger.warning(f"Operation fusion failed: {e}") | |
| optimization_info = { | |
| 'type': 'inference_optimization', | |
| 'method': 'eval_mode_no_grad', | |
| 'gradient_disabled': True | |
| } | |
| self.optimization_history.append(optimization_info) | |
| logger.info("Model optimized for inference") | |
| return self.model | |
| except Exception as e: | |
| logger.error(f"Inference optimization failed: {e}") | |
| raise | |
| def get_model_size(self, model: Optional[nn.Module] = None) -> float: | |
| """Get model size in MB. | |
| Args: | |
| model: Model to measure (uses self.model if None) | |
| Returns: | |
| Model size in MB | |
| """ | |
| if model is None: | |
| model = self.model | |
| param_size = 0 | |
| buffer_size = 0 | |
| for param in model.parameters(): | |
| param_size += param.nelement() * param.element_size() | |
| for buffer in model.buffers(): | |
| buffer_size += buffer.nelement() * buffer.element_size() | |
| size_mb = (param_size + buffer_size) / (1024 * 1024) | |
| return size_mb | |
| def count_parameters(self, model: Optional[nn.Module] = None) -> int: | |
| """Count total number of parameters. | |
| Args: | |
| model: Model to count (uses self.model if None) | |
| Returns: | |
| Total parameter count | |
| """ | |
| if model is None: | |
| model = self.model | |
| return sum(p.numel() for p in model.parameters()) | |
| def count_trainable_parameters(self, model: Optional[nn.Module] = None) -> int: | |
| """Count trainable parameters. | |
| Args: | |
| model: Model to count (uses self.model if None) | |
| Returns: | |
| Trainable parameter count | |
| """ | |
| if model is None: | |
| model = self.model | |
| return sum(p.numel() for p in model.parameters() if p.requires_grad) | |
| def benchmark_model( | |
| self, | |
| input_shape: Tuple[int, ...], | |
| num_runs: int = 100, | |
| warmup_runs: int = 10, | |
| device: Optional[torch.device] = None | |
| ) -> Dict[str, float]: | |
| """Benchmark model performance. | |
| Args: | |
| input_shape: Input tensor shape | |
| num_runs: Number of benchmark runs | |
| warmup_runs: Number of warmup runs | |
| device: Device to run benchmark on | |
| Returns: | |
| Benchmark results | |
| """ | |
| if device is None: | |
| device = next(self.model.parameters()).device | |
| self.model.eval() | |
| # Create dummy input | |
| dummy_input = torch.randn(input_shape, device=device) | |
| # Warmup runs | |
| with torch.no_grad(): | |
| for _ in range(warmup_runs): | |
| _ = self.model(dummy_input) | |
| # Benchmark runs | |
| torch.cuda.synchronize() if device.type == 'cuda' else None | |
| start_time = time.time() | |
| start_memory = psutil.Process().memory_info().rss / (1024 * 1024) # MB | |
| with torch.no_grad(): | |
| for _ in range(num_runs): | |
| _ = self.model(dummy_input) | |
| torch.cuda.synchronize() if device.type == 'cuda' else None | |
| end_time = time.time() | |
| end_memory = psutil.Process().memory_info().rss / (1024 * 1024) # MB | |
| total_time = end_time - start_time | |
| avg_time = total_time / num_runs | |
| throughput = num_runs / total_time | |
| memory_usage = end_memory - start_memory | |
| results = { | |
| 'avg_inference_time_ms': avg_time * 1000, | |
| 'throughput_fps': throughput, | |
| 'total_time_s': total_time, | |
| 'memory_usage_mb': memory_usage, | |
| 'model_size_mb': self.get_model_size(), | |
| 'parameter_count': self.count_parameters() | |
| } | |
| logger.info(f"Benchmark results: {avg_time*1000:.2f}ms avg, " | |
| f"{throughput:.2f} FPS, {memory_usage:.2f}MB memory") | |
| return results | |
| def get_optimization_summary(self) -> Dict[str, Any]: | |
| """Get summary of all optimizations applied. | |
| Returns: | |
| Optimization summary | |
| """ | |
| return { | |
| 'optimization_history': self.optimization_history, | |
| 'current_model_size_mb': self.get_model_size(), | |
| 'current_parameter_count': self.count_parameters(), | |
| 'original_state_available': self.original_state is not None | |
| } | |
| class MemoryOptimizer: | |
| """Memory optimization utilities.""" | |
| def clear_cache() -> None: | |
| """Clear GPU and system cache.""" | |
| if torch.cuda.is_available(): | |
| torch.cuda.empty_cache() | |
| gc.collect() | |
| logger.debug("Memory cache cleared") | |
| def get_memory_usage() -> Dict[str, float]: | |
| """Get current memory usage. | |
| Returns: | |
| Memory usage statistics | |
| """ | |
| memory_info = { | |
| 'system_memory_mb': psutil.virtual_memory().used / (1024 * 1024), | |
| 'system_memory_percent': psutil.virtual_memory().percent | |
| } | |
| if torch.cuda.is_available(): | |
| memory_info.update({ | |
| 'gpu_memory_allocated_mb': torch.cuda.memory_allocated() / (1024 * 1024), | |
| 'gpu_memory_reserved_mb': torch.cuda.memory_reserved() / (1024 * 1024), | |
| 'gpu_memory_percent': (torch.cuda.memory_allocated() / torch.cuda.max_memory_allocated()) * 100 | |
| }) | |
| return memory_info | |
| def optimize_batch_size( | |
| model: nn.Module, | |
| input_shape: Tuple[int, ...], | |
| max_memory_mb: float = 8000, | |
| start_batch_size: int = 1 | |
| ) -> int: | |
| """Find optimal batch size for given memory constraint. | |
| Args: | |
| model: Model to test | |
| input_shape: Input shape (without batch dimension) | |
| max_memory_mb: Maximum memory usage in MB | |
| start_batch_size: Starting batch size for search | |
| Returns: | |
| Optimal batch size | |
| """ | |
| model.eval() | |
| device = next(model.parameters()).device | |
| optimal_batch_size = start_batch_size | |
| for batch_size in range(start_batch_size, 128): | |
| try: | |
| # Clear cache before test | |
| MemoryOptimizer.clear_cache() | |
| # Create test input | |
| test_input = torch.randn(batch_size, *input_shape, device=device) | |
| # Test forward pass | |
| with torch.no_grad(): | |
| _ = model(test_input) | |
| # Check memory usage | |
| memory_usage = MemoryOptimizer.get_memory_usage() | |
| current_memory = memory_usage.get('gpu_memory_allocated_mb', | |
| memory_usage['system_memory_mb']) | |
| if current_memory > max_memory_mb: | |
| break | |
| optimal_batch_size = batch_size | |
| except RuntimeError as e: | |
| if "out of memory" in str(e).lower(): | |
| break | |
| raise | |
| logger.info(f"Optimal batch size found: {optimal_batch_size}") | |
| return optimal_batch_size | |
| def create_model_optimizer(model: nn.Module) -> ModelOptimizer: | |
| """Create a model optimizer instance. | |
| Args: | |
| model: Model to optimize | |
| Returns: | |
| ModelOptimizer instance | |
| """ | |
| optimizer = ModelOptimizer(model) | |
| optimizer.save_original_state() | |
| return optimizer | |
| def quick_quantize(model: nn.Module, quantization_type: str = "dynamic") -> nn.Module: | |
| """Quick model quantization. | |
| Args: | |
| model: Model to quantize | |
| quantization_type: Type of quantization | |
| Returns: | |
| Quantized model | |
| """ | |
| optimizer = ModelOptimizer(model) | |
| return optimizer.quantize_model(quantization_type) | |
| def quick_prune(model: nn.Module, pruning_ratio: float = 0.2) -> nn.Module: | |
| """Quick model pruning. | |
| Args: | |
| model: Model to prune | |
| pruning_ratio: Fraction of parameters to prune | |
| Returns: | |
| Pruned model | |
| """ | |
| optimizer = ModelOptimizer(model) | |
| return optimizer.prune_model(pruning_ratio) |