Files
numz-ComfyUI-SeedVR2_VideoU…/src/optimization/memory_manager.py
T

401 lines
15 KiB
Python

"""
Memory management module for SeedVR2
Handles VRAM usage, cache management, and memory optimization
Extracted from: seedvr2.py (lines 373-405, 607-626, 1016-1044)
"""
import os
import torch
import gc
import time
import platform
import psutil
from typing import Tuple, Optional
from src.common.cache import Cache
from src.models.dit_v2.rope import RotaryEmbeddingBase
try:
from comfy import model_management as mm
COMFYUI_AVAILABLE = True
except:
COMFYUI_AVAILABLE = False
pass
def get_basic_vram_info():
if platform.system() == "Darwin":
mem = psutil.virtual_memory()
free_memory = mem.total - mem.used
total_memory = mem.total
else:
"""🔍 Méthode basique avec PyTorch natif"""
if not torch.cuda.is_available():
return {"error": "CUDA not available"}
# Mémoire libre et totale (en bytes)
free_memory, total_memory = torch.cuda.mem_get_info()
# Conversion en GB
free_gb = free_memory / (1024**3)
total_gb = total_memory / (1024**3)
return {
"free_gb": free_gb,
"total_gb": total_gb
}
# Initial VRAM check at module load
vram_info = get_basic_vram_info()
if "error" not in vram_info:
print(f"📊 Initial VRAM status: {vram_info['free_gb']:.2f}GB free / {vram_info['total_gb']:.2f}GB total")
else:
print(f"⚠️ VRAM check: {vram_info['error']} - SeedVR2 requires an NVIDIA GPU")
def get_vram_usage() -> Tuple[float, float, float]:
"""
Get current VRAM usage (allocated, reserved, peak)
Returns:
tuple: (allocated_gb, reserved_gb, max_allocated_gb)
Returns (0, 0, 0) if CUDA not available
"""
if platform.system() == "Darwin":
allocated = torch.mps.current_allocated_memory() / (1024**3)
reserved = torch.mps.driver_allocated_memory() / (1024**3)
max_allocated = 0
return allocated, reserved, max_allocated
if torch.cuda.is_available():
allocated = torch.cuda.memory_allocated() / (1024**3)
reserved = torch.cuda.memory_reserved() / (1024**3)
max_allocated = torch.cuda.max_memory_allocated() / (1024**3)
return allocated, reserved, max_allocated
return 0, 0, 0
def clear_vram_cache(debug) -> None:
"""Clear VRAM cache and run garbage collection"""
debug.log("Clearing VRAM cache...", category="cleanup")
if platform.system() == "Darwin":
torch.mps.empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
gc.collect()
def reset_vram_peak(debug) -> None:
"""
Reset VRAM peak counter for new tracking
"""
debug.log("Resetting VRAM peak memory statistics", category="memory")
if torch.cuda.is_available():
torch.cuda.reset_peak_memory_stats()
def preinitialize_rope_cache(runner, debug) -> None:
"""
🚀 Pre-initialize RoPE cache to avoid OOM at first launch
Args:
runner: The model runner containing DiT and VAE models
debug: Optional Debug instance
"""
debug.log("Pre-initializing RoPE cache to avoid OOM...", category="setup")
try:
# Create dummy tensors to simulate common shapes
# Format: [batch, channels, frames, height, width] for vid_shape
# Format: [batch, seq_len] for txt_shape
common_shapes = [
# Common video resolutions
(torch.tensor([[1, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 1 frame, 77 tokens
(torch.tensor([[4, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 4 frames
(torch.tensor([[5, 3, 3]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # 5 frames (4n+1 format)
(torch.tensor([[1, 4, 4]], dtype=torch.long), torch.tensor([[77]], dtype=torch.long)), # Higher resolution
]
# Create mock cache for pre-initialization
temp_cache = Cache()
# Access RoPE modules in DiT (recursive search)
def find_rope_modules(module):
rope_modules = []
for name, child in module.named_modules():
if hasattr(child, 'get_freqs') and callable(getattr(child, 'get_freqs')):
rope_modules.append((name, child))
return rope_modules
rope_modules = find_rope_modules(runner.dit)
# Pre-calculate for each RoPE module found
for name, rope_module in rope_modules:
# Temporarily move module to CPU if necessary
original_device = next(rope_module.parameters()).device if list(rope_module.parameters()) else torch.device('cpu')
rope_module.to('cpu')
try:
for vid_shape, txt_shape in common_shapes:
cache_key = f"720pswin_by_size_bysize_{tuple(vid_shape[0].tolist())}_sd3.mmrope_freqs_3d"
def compute_freqs():
try:
# Calculate with reduced dimensions to avoid OOM
with torch.no_grad():
# Detect RoPE module type
module_type = type(rope_module).__name__
if module_type == 'NaRotaryEmbedding3d':
# NaRotaryEmbedding3d: only takes shape (vid_shape)
return rope_module.get_freqs(vid_shape.cpu())
else:
# Standard RoPE: takes vid_shape and txt_shape
return rope_module.get_freqs(vid_shape.cpu(), txt_shape.cpu())
except Exception as e:
debug.log(f"Failed for {cache_key}: {e}", level="WARNING", category="cache")
# Return empty tensors as fallback
time.sleep(1)
clear_vram_cache(debug)
return torch.zeros(1, 64)
# Store in cache
temp_cache(cache_key, compute_freqs)
except Exception as e:
debug.log(f"Error in module {name}: {e}", level="ERROR", category="cache")
finally:
# Restore to original device
rope_module.to(original_device)
# Copy temporary cache to runner cache
if hasattr(runner, 'cache'):
runner.cache.cache.update(temp_cache.cache)
else:
runner.cache = temp_cache
except Exception as e:
debug.log(f"Error during RoPE pre-init: {e}", level="WARNING", category="setup", force=True)
debug.log("Model will work but could have OOM at first launch", level="WARNING", category="setup", force=True)
def clear_rope_lru_caches(model) -> int:
"""Clear ALL LRU caches from RoPE modules"""
cleared_count = 0
for name, module in model.named_modules():
if hasattr(module, 'get_axial_freqs') and hasattr(module.get_axial_freqs, 'cache_clear'):
module.get_axial_freqs.cache_clear()
cleared_count += 1
return cleared_count
def fast_model_cleanup(model):
"""Fast model cleanup without logs"""
if model is None:
return
# Move to CPU
model.to("cpu")
# Clear parameters and buffers recursively
def clear_recursive(m):
for child in m.children():
clear_recursive(child)
for param in m.parameters():
if param is not None:
param.data = param.data.cpu()
param.grad = None
for buffer in m.buffers():
if buffer is not None:
buffer.data = buffer.data.cpu()
clear_recursive(model)
def fast_ram_cleanup():
"""Fast RAM cleanup without excessive logging"""
# Garbage collection
gc.collect()
# Clear MPS cache
if platform.system() == "Darwin":
torch.mps.empty_cache()
# Clear CUDA cache
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
torch.cuda.reset_peak_memory_stats()
# Clear PyTorch internal caches
try:
torch._C._clear_cache()
except:
pass
def clear_all_caches(runner, debug, offload_vae=False) -> int:
"""
Aggressively clear all caches from runner and model.
Optimized to only process what's necessary.
Args:
runner: The runner instance to clear caches from
debug: Optional Debug instance for logging
offload_vae: If True, also moves VAE to CPU and clears its caches
"""
if not runner:
return 0
cleaned_items = 0
# Early exit if no caches to clear
has_cache = hasattr(runner, 'cache') and hasattr(runner.cache, 'cache')
if not has_cache and not hasattr(runner, 'dit') and not (offload_vae and hasattr(runner, 'vae')):
return 0
# Clear main runner cache efficiently
if has_cache and runner.cache.cache:
cache_entries = len(runner.cache.cache)
# Process all cache items to properly free memory
for key, value in list(runner.cache.cache.items()):
if torch.is_tensor(value):
# Force deallocation of tensor storage
if value.is_cuda or value.is_mps:
value.data = value.data.cpu()
value.grad = None
if value.numel() > 0:
value.data.set_() # Release underlying storage
elif isinstance(value, (list, tuple)):
for item in value:
if torch.is_tensor(item):
if item.is_cuda or item.is_mps:
item.data = item.data.cpu()
item.grad = None
if item.numel() > 0:
item.data.set_()
# Clear the cache after processing
runner.cache.cache.clear()
cleaned_items += cache_entries
debug.log(f"Cleared {cache_entries} cache entries", category="success")
# Clear any accumulated state in blocks
if hasattr(runner, 'dit'):
model = runner.dit
if hasattr(model, 'dit_model'):
model = model.dit_model
# Clear RoPE LRU caches
rope_caches_cleared = clear_rope_lru_caches(model)
cleaned_items += rope_caches_cleared
if rope_caches_cleared > 0:
debug.log(f"Cleared {rope_caches_cleared} RoPE LRU caches", category="success")
# Clear block attributes if needed
if hasattr(model, 'blocks'):
block_attrs_cleared = 0
# Define PyTorch's essential attributes that must NOT be deleted
essential_attrs = {
'_modules', '_parameters', '_buffers',
'_forward_hooks', '_forward_pre_hooks',
'_backward_hooks', '_backward_pre_hooks',
'_state_dict_hooks', '_state_dict_pre_hooks',
'_load_state_dict_pre_hooks', '_load_state_dict_post_hooks',
'_non_persistent_buffers_set', '_version',
'_is_full_backward_hook', 'training',
'_original_forward', # BlockSwap attribute
'_is_io_wrapped', # BlockSwap attribute
'_block_idx', # BlockSwap attribute
}
for idx, block in enumerate(model.blocks):
# Get all attributes that look like caches
attrs_to_remove = []
for attr_name in list(block.__dict__.keys()):
# Only remove cache-like attributes, not essential PyTorch attributes
if (attr_name not in essential_attrs and
('cache' in attr_name or
'temp' in attr_name or
(attr_name.startswith('_') and
not attr_name.startswith('__') and
attr_name not in essential_attrs))):
attrs_to_remove.append(attr_name)
# Remove the identified attributes
for attr_name in attrs_to_remove:
try:
delattr(block, attr_name)
block_attrs_cleared += 1
except AttributeError:
pass # Already deleted or doesn't exist
if block_attrs_cleared > 0:
debug.log(f"Cleared {block_attrs_cleared} temporary attributes from blocks", category="success")
# Clear any temporary attributes that might accumulate
temp_attrs = ['_temp_cache', '_block_cache', '_swap_cache', '_generation_cache',
'_rope_cache', '_intermediate_cache', '_backward_cache']
# Check both runner and model for these attributes
for obj in [runner, getattr(runner, 'dit', None)]:
if obj is None:
continue
# Handle wrapped models
if hasattr(obj, 'dit_model'):
obj = obj.dit_model
for attr in temp_attrs:
if hasattr(obj, attr):
delattr(obj, attr)
cleaned_items += 1
debug.log(f"Cleared {attr} from {type(obj).__name__}", category="success")
# Handle VAE offloading if requested
if offload_vae and hasattr(runner, 'vae') and runner.vae is not None:
debug.log("Moving VAE to CPU and clearing intermediate tensors", category="cleanup")
# Clear any intermediate tensors/buffers in VAE
vae_caches_cleared = 0
for module in runner.vae.modules():
# Clear module-specific caches
for cache_attr in ['_temp_cache', '_intermediate_cache']:
if hasattr(module, cache_attr):
delattr(module, cache_attr)
vae_caches_cleared += 1
# Clear any CUDA tensors in module attributes
for attr_name in list(vars(module).keys()):
attr = getattr(module, attr_name, None)
if torch.is_tensor(attr) and attr.is_cuda:
# Move tensor to CPU if it's not a parameter/buffer
if attr_name not in module._parameters and attr_name not in module._buffers:
setattr(module, attr_name, attr.cpu())
vae_caches_cleared += 1
if vae_caches_cleared > 0:
debug.log(f"Cleared {vae_caches_cleared} VAE caches", category="success")
# Move entire VAE to CPU (preserves model for reuse)
runner.vae = runner.vae.to('cpu')
debug.log("VAE moved to CPU, intermediate tensors cleared", category="success")
cleaned_items += vae_caches_cleared
# Force garbage collection
gc.collect(2) # Collect all generations
# Clear MPS cache
if platform.system() == "Darwin":
torch.mps.empty_cache()
# Clear CUDA cache
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()
return cleaned_items