Files
Eric Rollei d6dd6470cf Add complete Eric Image Processing Nodes implementation
Features:
- 40+ image processing algorithms
- Traditional denoising: Wavelet, NLM, BM3D (GPU-accelerated)
- AI models: DnCNN, SwinIR, SCUNet, NAFNet, Noise-DA
- DeepInv diffusion denoising integration
- Advanced film grain detection and processing
- Frequency domain processing
- Richardson-Lucy deconvolution
- Wiener filter
- External DeepInv service with FastAPI
- Comprehensive documentation and guides
2025-11-06 23:33:19 -08:00

255 lines
8.9 KiB
Python

"""
Memory management utilities for advanced image processing
Helps prevent memory issues when working with large models and images
"""
import torch
import gc
import psutil
import numpy as np
from typing import Optional, Any, Dict
import logging
logger = logging.getLogger(__name__)
class MemoryManager:
"""Utility class for managing memory during image processing"""
@staticmethod
def get_memory_info() -> Dict[str, Any]:
"""Get current memory usage information"""
info = {
'system_ram_total': psutil.virtual_memory().total / (1024**3), # GB
'system_ram_available': psutil.virtual_memory().available / (1024**3), # GB
'system_ram_percent': psutil.virtual_memory().percent,
}
if torch.cuda.is_available():
try:
gpu_memory = torch.cuda.get_device_properties(0).total_memory / (1024**3) # GB
gpu_allocated = torch.cuda.memory_allocated(0) / (1024**3) # GB
gpu_cached = torch.cuda.memory_reserved(0) / (1024**3) # GB
info.update({
'gpu_memory_total': gpu_memory,
'gpu_memory_allocated': gpu_allocated,
'gpu_memory_cached': gpu_cached,
'gpu_memory_free': gpu_memory - gpu_cached,
})
except Exception as e:
logger.warning(f"Could not get GPU memory info: {e}")
return info
@staticmethod
def cleanup_memory():
"""Clean up memory (both CPU and GPU)"""
# Python garbage collection
gc.collect()
# PyTorch CUDA cache cleanup
if torch.cuda.is_available():
try:
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
except Exception as e:
logger.warning(f"GPU cleanup failed: {e}")
@staticmethod
def safe_device_selection(preferred_device: str = 'auto',
min_gpu_memory_gb: float = 4.0) -> str:
"""
Safely select device based on available memory
Args:
preferred_device: 'auto', 'cuda', or 'cpu'
min_gpu_memory_gb: Minimum GPU memory required for CUDA
"""
if preferred_device == 'cpu':
return 'cpu'
if not torch.cuda.is_available():
return 'cpu'
try:
gpu_props = torch.cuda.get_device_properties(0)
total_memory_gb = gpu_props.total_memory / (1024**3)
# Check if we have enough free memory
torch.cuda.empty_cache()
allocated_gb = torch.cuda.memory_allocated(0) / (1024**3)
free_memory_gb = total_memory_gb - allocated_gb
if free_memory_gb < min_gpu_memory_gb:
logger.info(f"GPU has {free_memory_gb:.1f}GB free, need {min_gpu_memory_gb}GB. Using CPU.")
return 'cpu'
else:
logger.info(f"GPU has {free_memory_gb:.1f}GB free. Using CUDA.")
return 'cuda'
except Exception as e:
logger.warning(f"Error checking GPU memory: {e}. Using CPU.")
return 'cpu'
@staticmethod
def estimate_memory_requirements(image_shape: tuple,
model_params: int,
processing_factor: float = 4.0) -> Dict[str, float]:
"""
Estimate memory requirements for processing
Args:
image_shape: (H, W, C) shape of input image
model_params: Number of model parameters
processing_factor: Memory multiplier for intermediate tensors
"""
h, w, c = image_shape
# Image memory (input + output + intermediate)
image_memory_mb = (h * w * c * 4 * processing_factor) / (1024**2) # float32
# Model memory
model_memory_mb = (model_params * 4) / (1024**2) # float32 parameters
# Total estimate
total_mb = image_memory_mb + model_memory_mb
return {
'image_memory_mb': image_memory_mb,
'model_memory_mb': model_memory_mb,
'total_memory_mb': total_mb,
'total_memory_gb': total_mb / 1024
}
@staticmethod
def optimize_tile_size(image_shape: tuple,
max_memory_gb: float = 2.0,
min_tile_size: int = 64,
max_tile_size: int = 1024) -> int:
"""
Calculate optimal tile size based on available memory
Args:
image_shape: (H, W, C) shape of input image
max_memory_gb: Maximum memory to use for processing
min_tile_size: Minimum allowed tile size
max_tile_size: Maximum allowed tile size
"""
h, w, c = image_shape
max_memory_mb = max_memory_gb * 1024
# Start with max tile size and reduce if needed
for tile_size in range(max_tile_size, min_tile_size - 1, -64):
# Estimate memory for this tile size
tile_memory_mb = (tile_size * tile_size * c * 4 * 8) / (1024**2) # Processing factor
if tile_memory_mb <= max_memory_mb:
return tile_size
return min_tile_size
class SafeModelLoader:
"""Context manager for safe model loading and cleanup"""
def __init__(self, device: str = 'auto', cleanup_on_exit: bool = True):
self.device = MemoryManager.safe_device_selection(device)
self.cleanup_on_exit = cleanup_on_exit
self.initial_memory = None
def __enter__(self):
self.initial_memory = MemoryManager.get_memory_info()
logger.info(f"Starting model loading on {self.device}")
logger.info(f"Initial memory: {self.initial_memory.get('gpu_memory_free', 'N/A')} GB GPU free")
return self.device
def __exit__(self, exc_type, exc_val, exc_tb):
if self.cleanup_on_exit:
MemoryManager.cleanup_memory()
final_memory = MemoryManager.get_memory_info()
logger.info(f"Final memory: {final_memory.get('gpu_memory_free', 'N/A')} GB GPU free")
if exc_type is not None:
logger.error(f"Model loading failed: {exc_val}")
def safe_torch_operation(func):
"""Decorator for safe PyTorch operations with memory cleanup"""
def wrapper(*args, **kwargs):
try:
# Clean up before operation
MemoryManager.cleanup_memory()
# Run operation
result = func(*args, **kwargs)
# Clean up after operation
MemoryManager.cleanup_memory()
return result
except Exception as e:
# Clean up on error
MemoryManager.cleanup_memory()
logger.error(f"Operation {func.__name__} failed: {e}")
raise
return wrapper
# Convenience functions
def get_safe_batch_size(image_size: int, base_batch_size: int = 4) -> int:
"""Get safe batch size based on image dimensions"""
memory_info = MemoryManager.get_memory_info()
if 'gpu_memory_free' in memory_info:
free_gb = memory_info['gpu_memory_free']
if free_gb < 2:
return 1
elif free_gb < 4:
return max(1, base_batch_size // 2)
# Adjust based on image size
if image_size > 1024:
return 1
elif image_size > 512:
return max(1, base_batch_size // 2)
else:
return base_batch_size
def check_memory_requirements(required_gb: float) -> bool:
"""Check if we have enough memory for an operation"""
memory_info = MemoryManager.get_memory_info()
# Check system RAM
available_ram = memory_info.get('system_ram_available', 0)
if available_ram < required_gb:
return False
# Check GPU memory if using CUDA
if torch.cuda.is_available():
gpu_free = memory_info.get('gpu_memory_free', 0)
if gpu_free < required_gb:
return False
return True
# Example usage
if __name__ == "__main__":
# Test memory utilities
print("Memory Info:")
info = MemoryManager.get_memory_info()
for key, value in info.items():
if 'memory' in key and isinstance(value, float):
print(f" {key}: {value:.2f} GB")
else:
print(f" {key}: {value}")
print(f"\nSafe device: {MemoryManager.safe_device_selection()}")
# Test tile size optimization
tile_size = MemoryManager.optimize_tile_size((2048, 2048, 3))
print(f"Optimal tile size for 2048x2048 image: {tile_size}")
# Test memory requirements estimation
reqs = MemoryManager.estimate_memory_requirements((1024, 1024, 3), 1000000)
print(f"Memory requirements: {reqs['total_memory_gb']:.2f} GB")