- ROCMSamplerCustomAdvanced: V3 ComfyNode drop-in for stock SamplerCustomAdvanced with architecture detection, ROCm backend tuning, memory defrag for high-memory models (LTX 128ch), enhanced per-step callback with ETA - ROCMSamplerCustomAdvancedBenchmark: A/B comparison node for stock vs ROCm - docs/SAMPLER_CUSTOM_ADVANCED.md: full documentation for agents and users - Category: ROCm Ninodes/Sampling - Bump version to 2.2.0 across all files
2664 lines
125 KiB
Python
2664 lines
125 KiB
Python
"""
|
|
ROCM Optimized VAE Nodes for AMD GPUs
|
|
Specifically optimized for gfx1151 architecture with ROCm 6.4+
|
|
"""
|
|
|
|
import os
|
|
import gc
|
|
import time
|
|
from typing import Dict, Any, Tuple, Optional
|
|
|
|
# Environment variables are now managed by main.py and run_comfy.ps1
|
|
|
|
# Now import PyTorch and other modules
|
|
import torch
|
|
import torch.nn.functional as F
|
|
import comfy.model_management as model_management
|
|
import comfy.utils
|
|
import comfy.sample
|
|
import comfy.samplers
|
|
import latent_preview
|
|
import logging
|
|
import folder_paths
|
|
|
|
# Diagnostic logging for ROCm memory management
|
|
def log_rocm_diagnostics():
|
|
"""Log ROCm memory management configuration for debugging"""
|
|
try:
|
|
print("ROCm Memory Management Diagnostics:")
|
|
print(f" PYTORCH_CUDA_ALLOC_CONF: {os.environ.get('PYTORCH_CUDA_ALLOC_CONF', 'NOT SET')}")
|
|
print(f" PYTORCH_HIP_ALLOC_CONF: {os.environ.get('PYTORCH_HIP_ALLOC_CONF', 'NOT SET')}")
|
|
|
|
if torch.cuda.is_available():
|
|
print(f" CUDA Available: {torch.cuda.is_available()}")
|
|
print(f" CUDA Device: {torch.cuda.get_device_name(0)}")
|
|
|
|
# Get detailed memory info
|
|
total_memory = torch.cuda.get_device_properties(0).total_memory
|
|
allocated_memory = torch.cuda.memory_allocated(0)
|
|
reserved_memory = torch.cuda.memory_reserved(0)
|
|
free_memory = total_memory - allocated_memory
|
|
fragmentation = reserved_memory - allocated_memory
|
|
|
|
print(f" Total Memory: {total_memory / 1024**3:.2f}GB")
|
|
print(f" Allocated Memory: {allocated_memory / 1024**3:.2f}GB")
|
|
print(f" Reserved Memory: {reserved_memory / 1024**3:.2f}GB")
|
|
print(f" Free Memory: {free_memory / 1024**3:.2f}GB")
|
|
print(f" Fragmentation: {fragmentation / 1024**3:.2f}GB")
|
|
|
|
# Check fragmentation level
|
|
if fragmentation > 500 * 1024**2: # 500MB
|
|
print(f" WARNING: High fragmentation detected! This will cause OOM errors.")
|
|
print(f" Solution: Force memory defragmentation before operations.")
|
|
|
|
# Check if PyTorch is using the right allocator
|
|
try:
|
|
# Try to get allocator info (this might not work on all PyTorch versions)
|
|
allocator_info = torch.cuda.memory_stats(0)
|
|
print(f" Memory Allocator: Active (stats available)")
|
|
except:
|
|
print(f" Memory Allocator: Unknown (stats not available)")
|
|
else:
|
|
print(" CUDA Not Available - using CPU")
|
|
|
|
except Exception as e:
|
|
print(f" Diagnostic Error: {e}")
|
|
|
|
def simple_memory_cleanup():
|
|
"""Simple memory cleanup for ROCm"""
|
|
try:
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
return True
|
|
return False
|
|
except Exception as e:
|
|
print(f" Memory cleanup error: {e}")
|
|
return False
|
|
|
|
def gentle_memory_cleanup():
|
|
"""Gentle memory cleanup - less aggressive for mature ROCm drivers"""
|
|
try:
|
|
if not torch.cuda.is_available():
|
|
return False
|
|
|
|
# Single cache clear with synchronization
|
|
torch.cuda.empty_cache()
|
|
torch.cuda.synchronize()
|
|
|
|
# Light garbage collection
|
|
gc.collect()
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f" Memory cleanup error: {e}")
|
|
return False
|
|
|
|
# Diagnostics will be run when nodes are actually used, not at module import
|
|
|
|
|
|
# from debug_config import DEBUG_MODE, save_debug_data, capture_timing, capture_memory_usage, log_debug
|
|
DEBUG_MODE = False
|
|
def save_debug_data(*args, **kwargs): pass
|
|
def capture_timing(*args, **kwargs): pass
|
|
def capture_memory_usage(*args, **kwargs): pass
|
|
def log_debug(*args, **kwargs): pass
|
|
|
|
def get_gpu_memory_info():
|
|
"""Get accurate GPU memory information"""
|
|
if not torch.cuda.is_available():
|
|
return None, None, None, None
|
|
|
|
total_memory = torch.cuda.get_device_properties(0).total_memory
|
|
allocated_memory = torch.cuda.memory_allocated(0)
|
|
reserved_memory = torch.cuda.memory_reserved(0)
|
|
free_memory = total_memory - reserved_memory
|
|
|
|
return total_memory, allocated_memory, reserved_memory, free_memory
|
|
|
|
def check_memory_safety(required_memory_gb=2.0):
|
|
"""Check if there's enough memory for the operation"""
|
|
if not torch.cuda.is_available():
|
|
return True, "CUDA not available"
|
|
|
|
total_memory, allocated_memory, reserved_memory, free_memory = get_gpu_memory_info()
|
|
|
|
if total_memory is None:
|
|
return True, "Memory info unavailable"
|
|
|
|
free_gb = free_memory / (1024**3)
|
|
required_gb = required_memory_gb
|
|
|
|
if free_gb < required_gb:
|
|
return False, f"Only {free_gb:.2f}GB free, need {required_gb:.2f}GB"
|
|
|
|
return True, f"Memory OK: {free_gb:.2f}GB free"
|
|
|
|
def emergency_memory_cleanup():
|
|
"""Emergency memory cleanup - gentle approach for mature ROCm"""
|
|
try:
|
|
if not torch.cuda.is_available():
|
|
return False
|
|
|
|
# Gentle cleanup - single pass
|
|
torch.cuda.synchronize()
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
return True
|
|
except Exception as e:
|
|
logging.error(f"Emergency memory cleanup failed: {e}")
|
|
return False
|
|
|
|
|
|
"""
|
|
Quantization-aware memory cleanup - less aggressive for quantized models
|
|
"""
|
|
try:
|
|
if not torch.cuda.is_available():
|
|
return False
|
|
|
|
# Different cleanup strategies based on quantization type
|
|
if quantization_type in ["fp8", "int8", "int4"]:
|
|
# Quantized models are already memory-efficient, minimal cleanup
|
|
torch.cuda.empty_cache()
|
|
print(f"🧹 Light memory cleanup for {quantization_type} model")
|
|
else:
|
|
# Standard cleanup for non-quantized models
|
|
gentle_memory_cleanup()
|
|
print(f"🧹 Standard memory cleanup for {quantization_type} model")
|
|
|
|
return True
|
|
except Exception as e:
|
|
print(f" Quantized memory cleanup error: {e}")
|
|
return False
|
|
|
|
def check_quantized_memory_safety(model_size, quantization_type="fp8"):
|
|
"""
|
|
Check memory safety with quantization-aware estimation
|
|
"""
|
|
if not torch.cuda.is_available():
|
|
return True, "CUDA not available"
|
|
|
|
total_memory, allocated_memory, reserved_memory, free_memory = get_gpu_memory_info()
|
|
|
|
if total_memory is None:
|
|
return True, "Memory info unavailable"
|
|
|
|
# Calculate memory requirements based on quantization
|
|
if quantization_type == "fp8":
|
|
required_memory = model_size * 0.5 # 50% of FP32
|
|
safety_margin = 1.2 # Less aggressive for quantized
|
|
elif quantization_type in ["int8", "int4"]:
|
|
required_memory = model_size * 0.25 # 25% of FP32
|
|
safety_margin = 1.1 # Even less aggressive
|
|
else:
|
|
required_memory = model_size # Full FP32
|
|
safety_margin = 1.5 # Conservative for non-quantized
|
|
|
|
required_gb = (required_memory * safety_margin) / (1024**3)
|
|
free_gb = free_memory / (1024**3)
|
|
|
|
if free_gb < required_gb:
|
|
return False, f"Only {free_gb:.2f}GB free, need {required_gb:.2f}GB for {quantization_type}"
|
|
|
|
return True, f"Memory OK for {quantization_type}: {free_gb:.2f}GB free"
|
|
|
|
def detect_model_quantization(model):
|
|
"""
|
|
Detect if a model is quantized and return quantization type
|
|
"""
|
|
try:
|
|
# Check model dtype
|
|
model_dtype = model.model_dtype()
|
|
dtype_name = str(model_dtype).lower()
|
|
|
|
if 'int8' in dtype_name:
|
|
return "int8"
|
|
elif 'int4' in dtype_name:
|
|
return "int4"
|
|
elif 'float8' in dtype_name or 'fp8' in dtype_name:
|
|
return "fp8"
|
|
elif 'bfloat16' in dtype_name or 'bf16' in dtype_name:
|
|
return "bf16"
|
|
else:
|
|
return "fp32"
|
|
except Exception as e:
|
|
print(f"⚠️ Could not detect model quantization: {e}")
|
|
return "unknown"
|
|
|
|
|
|
|
|
# Removed force_memory_cleanup - using gentle_memory_cleanup instead
|
|
|
|
# Removed emergency_memory_reset - using emergency_memory_cleanup instead
|
|
|
|
|
|
|
|
|
|
|
|
def aggressive_memory_cleanup():
|
|
"""Gentle memory cleanup - renamed for compatibility"""
|
|
return gentle_memory_cleanup()
|
|
|
|
class ROCMOptimizedVAEDecode:
|
|
"""
|
|
ROCM-optimized VAE Decode node for all AMD GPUs with ROCm support.
|
|
Enhanced optimizations for Strix Halo (gfx1151) architecture.
|
|
|
|
Optimization Strategy:
|
|
1. GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs):
|
|
- Optimized memory management for ROCm
|
|
- Better batching strategy for AMD GPUs
|
|
- Reduced model conversion overhead
|
|
- Async memory operations (non-blocking)
|
|
|
|
2. ARCHITECTURE-SPECIFIC:
|
|
- Intelligent tiled vs direct decode decision
|
|
- Optimized tile sizes (enhanced for gfx1151)
|
|
- Video chunking with overlap handling
|
|
|
|
Key optimizations:
|
|
- Non-blocking memory operations
|
|
- Quantized model support (fp8/int8)
|
|
- Smart video chunking for large videos
|
|
- WAN VAE format handling
|
|
"""
|
|
|
|
# Class-level cache to avoid repeated model conversions
|
|
_vae_model_cache = {}
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"samples": ("LATENT", {"tooltip": "The latent to be decoded."}),
|
|
"vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."}),
|
|
"tile_size": ("INT", {
|
|
"default": 768,
|
|
"min": 256,
|
|
"max": 2048,
|
|
"step": 64,
|
|
"tooltip": "Tile size optimized for gfx1151. Larger values use more VRAM but are faster."
|
|
}),
|
|
"overlap": ("INT", {
|
|
"default": 96,
|
|
"min": 32,
|
|
"max": 512,
|
|
"step": 16,
|
|
"tooltip": "Overlap between tiles. Higher values reduce artifacts but use more VRAM."
|
|
}),
|
|
"use_rocm_optimizations": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable ROCm-specific optimizations for AMD GPUs"
|
|
}),
|
|
"precision_mode": (["auto", "fp32", "fp16", "bf16"], {
|
|
"default": "auto",
|
|
"tooltip": "Precision mode. 'auto' selects optimal for your GPU."
|
|
}),
|
|
"batch_optimization": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable batch processing optimizations"
|
|
}),
|
|
"video_chunk_size": ("INT", {
|
|
"default": 8,
|
|
"min": 1,
|
|
"max": 32,
|
|
"step": 1,
|
|
"tooltip": "Number of video frames to process at once (memory optimization)"
|
|
}),
|
|
"memory_optimization_enabled": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable memory optimization for video processing"
|
|
})
|
|
}
|
|
,
|
|
"optional": {
|
|
"compatibility_mode": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Enable stock ComfyUI compatibility mode (disables all ROCm optimizations)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("IMAGE",)
|
|
FUNCTION = "decode"
|
|
CATEGORY = "RocM Ninodes/VAE"
|
|
DESCRIPTION = "ROCM-optimized VAE Decode for all AMD GPUs with ROCm support. Enhanced for Strix Halo (gfx1151)."
|
|
|
|
def decode(self, vae, samples, tile_size=768, overlap=96, use_rocm_optimizations=True,
|
|
precision_mode="auto", batch_optimization=True, video_chunk_size=8,
|
|
memory_optimization_enabled=True, compatibility_mode=False):
|
|
"""
|
|
Optimized VAE decode for ROCm/AMD GPUs with video support and quantized model compatibility
|
|
"""
|
|
start_time = time.time()
|
|
log_debug(f"ROCMOptimizedVAEDecode.decode started with samples shape: {samples['samples'].shape}")
|
|
|
|
# CRITICAL FIX: Detect quantized models to avoid breaking them
|
|
is_quantized_model = False
|
|
vae_model_dtype = getattr(vae.first_stage_model, 'dtype', None)
|
|
if vae_model_dtype is not None:
|
|
# Check for quantized dtypes - be more specific to avoid false positives
|
|
quantized_dtypes = [torch.float8_e4m3fn, torch.float8_e5m2, torch.int8, torch.int4]
|
|
if vae_model_dtype in quantized_dtypes:
|
|
is_quantized_model = True
|
|
print(f"🔍 Detected quantized VAE model (dtype: {vae_model_dtype})")
|
|
elif hasattr(vae_model_dtype, '__name__') and 'int' in str(vae_model_dtype):
|
|
is_quantized_model = True
|
|
print(f"🔍 Detected quantized VAE model (dtype: {vae_model_dtype})")
|
|
|
|
# CRITICAL FIX: Detect WAN VAE models that use causal decoding with feature caching
|
|
# WAN VAE requires full video processing to maintain causal decoding chain
|
|
# Chunking breaks the feature cache and causes jitter on first frames of each chunk
|
|
is_wan_vae = False
|
|
try:
|
|
# Check if first_stage_model is a WAN VAE (from comfy.ldm.wan.vae or comfy.ldm.wan.vae2_2)
|
|
vae_model_class = vae.first_stage_model.__class__
|
|
vae_model_class_name = vae_model_class.__name__
|
|
vae_model_module = vae_model_class.__module__ if hasattr(vae_model_class, '__module__') else None
|
|
|
|
# Check if it's a WAN VAE by class name and module
|
|
if vae_model_class_name == 'WanVAE':
|
|
if vae_model_module and ('wan' in vae_model_module.lower()):
|
|
is_wan_vae = True
|
|
print(f"🔍 Detected WAN VAE model (causal decoding requires full video processing)")
|
|
except Exception as e:
|
|
# If detection fails, continue without WAN detection (safer)
|
|
if DEBUG_MODE:
|
|
print(f"⚠️ WAN VAE detection failed: {e}")
|
|
|
|
# Compatibility mode: disable all optimizations for quantized models
|
|
if compatibility_mode or is_quantized_model:
|
|
print("🛡️ Compatibility mode enabled - using stock ComfyUI behavior")
|
|
use_rocm_optimizations = False
|
|
batch_optimization = False
|
|
memory_optimization_enabled = False
|
|
|
|
# CRITICAL FIX: Disable chunking for WAN VAE models to preserve causal decoding chain
|
|
# WAN VAE uses causal decoding with feature caching - chunking breaks the cache
|
|
# Native ComfyUI processes WAN VAE videos in full, so we must match that behavior
|
|
if is_wan_vae:
|
|
print("🛡️ WAN VAE detected - disabling chunking to preserve causal decoding chain")
|
|
memory_optimization_enabled = False # Disable chunking for WAN models
|
|
|
|
# Capture input data for debugging
|
|
if DEBUG_MODE:
|
|
save_debug_data(samples, "vae_decode_input", "flux_1024x1024", {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'tile_size': tile_size,
|
|
'overlap': overlap,
|
|
'use_rocm_optimizations': use_rocm_optimizations,
|
|
'precision_mode': precision_mode,
|
|
'batch_optimization': batch_optimization,
|
|
'video_chunk_size': video_chunk_size,
|
|
'memory_optimization_enabled': memory_optimization_enabled
|
|
})
|
|
|
|
# Get device information
|
|
device = vae.device
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
|
|
# Check if this is video data (5D tensor: B, C, T, H, W)
|
|
is_video = len(samples["samples"].shape) == 5
|
|
if is_video:
|
|
B, C, T, H, W = samples["samples"].shape
|
|
|
|
# Progress indicator for Windows users experiencing hanging
|
|
print(f"🎬 Processing video: {T} frames, {H}x{W} resolution")
|
|
|
|
# CRITICAL: Match ComfyUI's native behavior - don't chunk unless absolutely necessary
|
|
# Native ComfyUI VAE decode processes the entire video at once
|
|
# WAN VAE models MUST use full video processing to preserve causal decoding chain
|
|
# Only chunk if we absolutely must for memory reasons AND it's not a WAN VAE
|
|
if is_wan_vae:
|
|
# WAN VAE requires full video processing - chunking breaks causal decoding
|
|
use_chunking = False
|
|
print("📹 WAN VAE detected - using full video processing (causal decoding requires full sequence)")
|
|
elif memory_optimization_enabled and T > video_chunk_size and T > 32: # Only chunk for very large videos > 32 frames
|
|
use_chunking = True
|
|
print(f"📹 Using chunked processing: {video_chunk_size} frames per chunk (large video)")
|
|
else:
|
|
use_chunking = False
|
|
print("📹 Using non-chunked processing (matching native ComfyUI behavior)")
|
|
|
|
if use_chunking:
|
|
# Process video in chunks to avoid memory exhaustion
|
|
# CRITICAL: NO OVERLAP - decode exact frames to avoid duplicates
|
|
# WAN VAE causal decoding means first frames might be slightly darker,
|
|
# but overlap causes frame duplication issues
|
|
chunk_results = []
|
|
num_chunks = (T + video_chunk_size - 1) // video_chunk_size
|
|
print(f"🔄 Processing {num_chunks} chunks (no overlap to prevent duplicates)...")
|
|
|
|
for chunk_idx in range(num_chunks):
|
|
# Calculate chunk boundaries - NO OVERLAP to prevent duplicates
|
|
i = chunk_idx * video_chunk_size
|
|
end_idx = min(i + video_chunk_size, T)
|
|
actual_frames_needed = end_idx - i # How many frames we need from this chunk
|
|
print(f" 📦 Chunk {chunk_idx + 1}/{num_chunks}: frames {i}-{end_idx-1} (need {actual_frames_needed} frames)")
|
|
|
|
# NO OVERLAP - decode exactly the frames we need
|
|
chunk_start = i
|
|
chunk_end = end_idx
|
|
|
|
# Extract chunk from latent (exact frame range, no overlap)
|
|
chunk = samples["samples"][:, :, chunk_start:chunk_end, :, :]
|
|
|
|
# Decode chunk
|
|
# CRITICAL FIX: vae.decode() ALWAYS returns (B, T, H, W, C) format for video
|
|
# regardless of whether it uses regular or tiled decoding internally
|
|
with torch.no_grad():
|
|
try:
|
|
chunk_decoded = vae.decode(chunk)
|
|
except Exception as e:
|
|
# If decode fails, try tiled decode explicitly
|
|
if "out of memory" in str(e).lower() or "OOM" in str(e).upper():
|
|
print(f"⚠️ Memory error during chunk decode, retrying with explicit tiled decode...")
|
|
# Use explicit tiled decode with NO temporal overlap to prevent duplicates
|
|
compression = vae.spacial_compression_decode()
|
|
temporal_compression = vae.temporal_compression_decode()
|
|
tile = 256 // compression if compression > 0 else 256
|
|
overlap_spatial = tile // 4
|
|
# NO temporal overlap - decode exactly what we need
|
|
chunk_decoded = vae.decode_tiled(chunk, tile_x=tile, tile_y=tile, overlap=overlap_spatial,
|
|
tile_t=video_chunk_size, overlap_t=None)
|
|
else:
|
|
raise e
|
|
|
|
# VAE decode returns a tuple, extract the tensor
|
|
if isinstance(chunk_decoded, tuple):
|
|
chunk_decoded = chunk_decoded[0]
|
|
|
|
# CRITICAL FIX: vae.decode() always returns (B, T, H, W, C) format for video
|
|
# Check format and handle accordingly
|
|
if len(chunk_decoded.shape) == 5:
|
|
if chunk_decoded.shape[-1] == 3 or chunk_decoded.shape[-1] == 4:
|
|
# (B, T, H, W, C) format - correct format from vae.decode()
|
|
format_channels_last = True
|
|
temporal_dim = 1
|
|
elif chunk_decoded.shape[1] == 3 or chunk_decoded.shape[1] == 4:
|
|
# (B, C, T, H, W) format - unexpected but handle it
|
|
format_channels_last = False
|
|
temporal_dim = 2
|
|
else:
|
|
# Fallback: assume channels-last based on ComfyUI's vae.decode() behavior
|
|
format_channels_last = True
|
|
temporal_dim = 1
|
|
else:
|
|
# Not 5D, shouldn't happen for video but handle gracefully
|
|
format_channels_last = True
|
|
temporal_dim = 1
|
|
|
|
# CRITICAL: NO OVERLAP - decoded frames should exactly match latent frames (WAN VAE is 1:1)
|
|
decoded_total_frames = chunk_decoded.shape[temporal_dim]
|
|
latent_frames_decoded = chunk_end - chunk_start # Frames we decoded in latent space
|
|
|
|
# Verify decoded frame count matches latent frame count (WAN VAE is 1:1)
|
|
if decoded_total_frames != latent_frames_decoded:
|
|
print(f" ⚠️ Chunk {chunk_idx + 1}: Frame count mismatch - decoded {decoded_total_frames}, expected {latent_frames_decoded} (latent input)")
|
|
# For WAN VAE, this should be 1:1, but adjust if needed
|
|
if decoded_total_frames > latent_frames_decoded:
|
|
# Trim extra frames from end
|
|
if format_channels_last:
|
|
chunk_decoded = chunk_decoded[:, :latent_frames_decoded, :, :, :]
|
|
else:
|
|
chunk_decoded = chunk_decoded[:, :, :latent_frames_decoded, :, :]
|
|
decoded_total_frames = latent_frames_decoded
|
|
elif decoded_total_frames < latent_frames_decoded:
|
|
print(f" ❌ Chunk {chunk_idx + 1}: Missing frames - decoded {decoded_total_frames}, expected {latent_frames_decoded}")
|
|
|
|
# NO CROPPING NEEDED - we decoded exactly what we need (no overlap)
|
|
final_frames = chunk_decoded.shape[temporal_dim]
|
|
if final_frames != actual_frames_needed:
|
|
print(f" ❌ Chunk {chunk_idx + 1}: Frame count mismatch - got {final_frames} frames, expected {actual_frames_needed} frames")
|
|
print(f" This will cause frame misalignment! Chunk should be frames {i}-{end_idx-1}")
|
|
elif chunk_idx < 3: # Only print for first few chunks to reduce noise
|
|
print(f" ✅ Chunk {chunk_idx + 1}: Decoded {decoded_total_frames} frames (frames {i}-{end_idx-1})")
|
|
|
|
chunk_results.append(chunk_decoded)
|
|
|
|
# Clear memory after each chunk (reduced frequency to prevent fragmentation)
|
|
if chunk_idx % 2 == 0: # Only clear every 2 chunks
|
|
torch.cuda.empty_cache()
|
|
|
|
# Concatenate results along temporal dimension
|
|
# CRITICAL FIX: vae.decode() always returns (B, T, H, W, C) format for video
|
|
# Verify all chunks have consistent shape before concatenation
|
|
first = chunk_results[0]
|
|
temporal_dim = 1 if (first.shape[-1] == 3 or first.shape[-1] == 4) else 2
|
|
|
|
# Verify frame counts match expectations before concatenation
|
|
total_concatenated_frames = sum(chunk.shape[temporal_dim] for chunk in chunk_results)
|
|
if total_concatenated_frames != T:
|
|
print(f"⚠️ WARNING: Total concatenated frames ({total_concatenated_frames}) != expected ({T})")
|
|
print(f" Chunk frame counts: {[chunk.shape[temporal_dim] for chunk in chunk_results]}")
|
|
|
|
# Concatenate along temporal dimension
|
|
if len(first.shape) == 5:
|
|
if first.shape[-1] == 3 or first.shape[-1] == 4:
|
|
# (B, T, H, W, C) format - correct format from vae.decode()
|
|
result = torch.cat(chunk_results, dim=1) # Concatenate along temporal dimension
|
|
elif first.shape[1] == 3 or first.shape[1] == 4:
|
|
# (B, C, T, H, W) format - unexpected but handle it
|
|
result = torch.cat(chunk_results, dim=2) # Concatenate along temporal dimension
|
|
else:
|
|
# Fallback: assume channels-last format based on ComfyUI behavior
|
|
result = torch.cat(chunk_results, dim=1)
|
|
else:
|
|
# Shouldn't happen for video, but handle gracefully
|
|
result = torch.cat(chunk_results, dim=1)
|
|
|
|
|
|
# Convert 5D video tensor to 4D image tensor for ComfyUI
|
|
# CRITICAL: Match ComfyUI's native behavior exactly
|
|
# Native does: images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
if len(result.shape) == 5:
|
|
# Match native ComfyUI reshape logic
|
|
result = result.reshape(-1, result.shape[-3], result.shape[-2], result.shape[-1])
|
|
|
|
return (result,)
|
|
else:
|
|
# Process entire video at once - EXACTLY match ComfyUI's native behavior
|
|
# Native ComfyUI: images = vae.decode(samples["samples"])
|
|
# if len(images.shape) == 5: images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
with torch.no_grad():
|
|
result = vae.decode(samples["samples"])
|
|
|
|
# VAE decode returns a tuple, extract the tensor (ComfyUI handles this internally)
|
|
if isinstance(result, tuple):
|
|
result = result[0]
|
|
|
|
# Convert 5D video tensor to 4D image tensor - EXACT native ComfyUI logic
|
|
if len(result.shape) == 5:
|
|
# Match native ComfyUI reshape logic exactly
|
|
result = result.reshape(-1, result.shape[-3], result.shape[-2], result.shape[-1])
|
|
|
|
# Capture output data and timing for debugging
|
|
if DEBUG_MODE:
|
|
end_time = time.time()
|
|
save_debug_data(result, "vae_decode_output", "flux_1024x1024", {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'execution_time': end_time - start_time,
|
|
'output_shape': result.shape,
|
|
'output_dtype': str(result.dtype),
|
|
'output_device': str(result.device)
|
|
})
|
|
capture_timing("vae_decode", start_time, end_time, {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'is_video': True,
|
|
'video_chunks': len(chunk_results) if 'chunk_results' in locals() else 1
|
|
})
|
|
capture_memory_usage("vae_decode", {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'is_video': True
|
|
})
|
|
|
|
return (result,)
|
|
|
|
# CRITICAL FIX: Preserve quantized model dtypes - do not convert
|
|
if is_quantized_model:
|
|
print(f"🔒 Preserving quantized model dtype: {vae_model_dtype}")
|
|
optimal_dtype = vae_model_dtype
|
|
# Skip all dtype conversions for quantized models
|
|
elif precision_mode == "auto":
|
|
if is_amd:
|
|
# Check VAE model's actual dtype first to avoid type mismatch
|
|
if vae_model_dtype is not None:
|
|
# Use the VAE model's existing dtype to avoid type mismatch errors
|
|
optimal_dtype = vae_model_dtype
|
|
logging.info(f"Using VAE model's existing dtype: {optimal_dtype}")
|
|
else:
|
|
# Fallback to VAE's configured dtype
|
|
optimal_dtype = vae.vae_dtype
|
|
logging.info(f"Using VAE's configured dtype: {optimal_dtype}")
|
|
else:
|
|
optimal_dtype = vae.vae_dtype
|
|
else:
|
|
dtype_map = {
|
|
"fp32": torch.float32,
|
|
"fp16": torch.float16,
|
|
"bf16": torch.bfloat16
|
|
}
|
|
optimal_dtype = dtype_map[precision_mode]
|
|
|
|
# CRITICAL FIX: Skip dtype conversion for quantized models
|
|
if not is_quantized_model:
|
|
# Handle BFloat16 model with Float32 input type mismatch
|
|
# If the VAE model has BFloat16 weights but we're using Float32, we need to match them
|
|
if vae_model_dtype == torch.bfloat16 and optimal_dtype == torch.float32:
|
|
logging.warning("VAE model has BFloat16 weights but input is Float32 - this will cause type mismatch")
|
|
logging.warning("Converting VAE model to Float32 to match input dtype")
|
|
vae.first_stage_model = vae.first_stage_model.to(torch.float32)
|
|
optimal_dtype = torch.float32
|
|
else:
|
|
print(f"🔒 Skipping dtype conversion for quantized model (dtype: {vae_model_dtype})")
|
|
|
|
# ROCm-specific optimizations (general for all AMD, enhanced for gfx1151)
|
|
if use_rocm_optimizations and is_amd:
|
|
# GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs)
|
|
torch.backends.cuda.matmul.allow_tf32 = False # TF32 not supported on AMD
|
|
torch.backends.cuda.matmul.allow_fp16_accumulation = True # Better performance on AMD
|
|
|
|
# Detect gfx1151 for architecture-specific optimizations
|
|
is_gfx1151 = False
|
|
try:
|
|
if torch.cuda.is_available():
|
|
arch = torch.cuda.get_device_properties(0).gcnArchName
|
|
if 'gfx1151' in arch:
|
|
is_gfx1151 = True
|
|
except:
|
|
pass
|
|
|
|
# Async memory management (non-blocking)
|
|
try:
|
|
# Light async cleanup - doesn't block GPU
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
# Don't call gentle_memory_cleanup() - it contains torch.cuda.synchronize()
|
|
except Exception as e:
|
|
if DEBUG_MODE:
|
|
print(f"⚠️ Memory optimization skipped: {e}")
|
|
|
|
# Architecture-specific tile size optimization (enhanced for gfx1151)
|
|
if is_gfx1151:
|
|
# Strix Halo (gfx1151) specific tile size cap
|
|
if tile_size > 1024:
|
|
tile_size = 1024 # Cap for gfx1151 memory
|
|
elif is_amd:
|
|
# General ROCm tile size cap (conservative for broader compatibility)
|
|
if tile_size > 2048:
|
|
tile_size = 2048 # Cap for general AMD GPUs
|
|
if overlap > tile_size // 4:
|
|
overlap = tile_size // 4
|
|
|
|
# Optimized memory management - avoid expensive memory_used_decode call
|
|
samples_shape = samples["samples"].shape
|
|
if len(samples_shape) == 4: # Standard image format
|
|
# Estimate memory usage without calling expensive memory_used_decode
|
|
estimated_memory = samples_shape[0] * samples_shape[1] * samples_shape[2] * samples_shape[3] * 4 # 4 bytes per float32
|
|
estimated_memory *= 8 # Decode expansion factor
|
|
else:
|
|
# For video or other formats, use a conservative estimate
|
|
estimated_memory = samples_shape[0] * samples_shape[1] * samples_shape[2] * samples_shape[3] * 4 * 8
|
|
|
|
# Check memory safety before loading models
|
|
estimated_memory_gb = estimated_memory / (1024**3)
|
|
is_safe, memory_msg = check_memory_safety(required_memory_gb=estimated_memory_gb + 2.0)
|
|
|
|
if not is_safe:
|
|
print(f"⚠️ VAE Decode Memory Warning: {memory_msg}")
|
|
print("🧹 Performing emergency cleanup before VAE decode...")
|
|
emergency_memory_cleanup()
|
|
|
|
# Load models with estimated memory (faster than exact calculation)
|
|
try:
|
|
model_management.load_models_gpu([vae.patcher], memory_required=estimated_memory)
|
|
except Exception as e:
|
|
if "out of memory" in str(e).lower():
|
|
print("💾 VAE model loading failed due to memory - performing cleanup")
|
|
emergency_memory_cleanup()
|
|
# Try again with reduced memory requirement
|
|
model_management.load_models_gpu([vae.patcher], memory_required=estimated_memory // 2)
|
|
else:
|
|
raise e
|
|
|
|
# Calculate optimal batch size for gfx1151 (less conservative)
|
|
free_memory = model_management.get_free_memory(device)
|
|
if batch_optimization and is_amd:
|
|
# Less conservative batching for better performance
|
|
batch_number = max(1, int(free_memory / (estimated_memory * 1.2))) # Reduced from 1.5x to 1.2x
|
|
else:
|
|
batch_number = max(1, int(free_memory / estimated_memory))
|
|
|
|
# Process in batches
|
|
pixel_samples = None
|
|
samples_tensor = samples["samples"]
|
|
|
|
# Optimized processing logic - avoid redundant conversions
|
|
samples_processed = samples_tensor.to(device).to(optimal_dtype)
|
|
|
|
# Use caching to avoid repeated model conversions
|
|
vae_id = id(vae.first_stage_model)
|
|
cache_key = f"{vae_id}_{optimal_dtype}"
|
|
|
|
if cache_key not in self._vae_model_cache:
|
|
# CRITICAL FIX: Only convert model dtype if it's different AND not quantized
|
|
if not is_quantized_model:
|
|
current_model_dtype = getattr(vae.first_stage_model, 'dtype', None)
|
|
if current_model_dtype is not None and current_model_dtype != optimal_dtype:
|
|
logging.info(f"Converting VAE model from {current_model_dtype} to {optimal_dtype}")
|
|
vae.first_stage_model = vae.first_stage_model.to(optimal_dtype)
|
|
else:
|
|
logging.info(f"VAE model already in correct dtype: {current_model_dtype}")
|
|
else:
|
|
logging.info(f"Preserving quantized model dtype: {vae_model_dtype}")
|
|
self._vae_model_cache[cache_key] = True
|
|
|
|
# Decide processing method based on image size and available memory
|
|
image_size = samples_tensor.shape[2] * samples_tensor.shape[3]
|
|
use_tiled = image_size > 512 * 512 or estimated_memory > free_memory * 0.8
|
|
|
|
if is_video:
|
|
print(f"✅ Video processing completed, switching to image processing")
|
|
else:
|
|
print(f"🖼️ Processing image: {samples_tensor.shape[2]}x{samples_tensor.shape[3]}")
|
|
if use_tiled:
|
|
print(f"🔧 Using tiled processing for large image")
|
|
else:
|
|
print(f"⚡ Using direct processing for optimal speed")
|
|
|
|
if not use_tiled:
|
|
# Direct decode for smaller images or when memory is sufficient
|
|
try:
|
|
if use_rocm_optimizations and is_amd:
|
|
# Direct decode without autocast for ROCm
|
|
out = vae.first_stage_model.decode(samples_processed)
|
|
else:
|
|
# Use autocast for non-AMD GPUs
|
|
with torch.cuda.amp.autocast(enabled=(optimal_dtype != torch.float32), dtype=optimal_dtype):
|
|
out = vae.first_stage_model.decode(samples_processed)
|
|
|
|
out = out.to(device).float() # CRITICAL FIX: Keep on GPU device, not output_device
|
|
pixel_samples = vae.process_output(out)
|
|
|
|
except Exception as e:
|
|
logging.warning(f"Direct decode failed, falling back to tiled: {e}")
|
|
use_tiled = True
|
|
|
|
if use_tiled:
|
|
# Use optimized tiled decoding
|
|
pixel_samples = self._decode_tiled_optimized(
|
|
vae, samples_tensor, tile_size, overlap, optimal_dtype, batch_number
|
|
)
|
|
|
|
# Reshape if needed (match standard VAE decode behavior)
|
|
if len(pixel_samples.shape) == 5:
|
|
pixel_samples = pixel_samples.reshape(-1, pixel_samples.shape[-3],
|
|
pixel_samples.shape[-2], pixel_samples.shape[-1])
|
|
|
|
# Move to GPU device (not output_device which might be CPU)
|
|
pixel_samples = pixel_samples.to(device)
|
|
|
|
# CRITICAL FIX: Preserve original tensor values - don't convert dtype unnecessarily
|
|
# ComfyUI's save_images function expects (B, H, W, C) format with preserved values
|
|
logging.info(f"Before ComfyUI format fix: shape={pixel_samples.shape}, dtype={pixel_samples.dtype}")
|
|
|
|
# Ensure the tensor is contiguous
|
|
pixel_samples = pixel_samples.contiguous()
|
|
|
|
# CRITICAL FIX: Only convert dtype if absolutely necessary for ComfyUI compatibility
|
|
# Preserve original VAE output values to prevent darker/colorless frames
|
|
if pixel_samples.dtype not in [torch.float32, torch.float16]:
|
|
logging.info(f"Converting dtype from {pixel_samples.dtype} to float32 for ComfyUI compatibility")
|
|
pixel_samples = pixel_samples.float()
|
|
else:
|
|
logging.info(f"Preserving original dtype: {pixel_samples.dtype}")
|
|
|
|
# Validate tensor value range before format conversion
|
|
min_val = pixel_samples.min().item()
|
|
max_val = pixel_samples.max().item()
|
|
logging.info(f"Before format conversion: min={min_val:.4f}, max={max_val:.4f}, mean={pixel_samples.mean().item():.4f}")
|
|
|
|
if min_val < -1.1 or max_val > 1.1:
|
|
logging.warning(f"Unexpected tensor value range [{min_val:.4f}, {max_val:.4f}] - this might cause visual artifacts")
|
|
|
|
# CRITICAL FIX: Convert from (B, C, H, W) to (B, H, W, C) for ComfyUI compatibility
|
|
# This is the key fix - ComfyUI's save_images expects (B, H, W, C) format
|
|
if len(pixel_samples.shape) == 4 and pixel_samples.shape[1] == 3:
|
|
logging.info(f"Converting tensor from (B, C, H, W) to (B, H, W, C) for ComfyUI compatibility")
|
|
pixel_samples = pixel_samples.permute(0, 2, 3, 1).contiguous()
|
|
logging.info(f"After permute: shape={pixel_samples.shape}, dtype={pixel_samples.dtype}")
|
|
|
|
# Final validation - ensure we have the right shape, dtype, and value range
|
|
if isinstance(pixel_samples, torch.Tensor):
|
|
final_min = pixel_samples.min().item()
|
|
final_max = pixel_samples.max().item()
|
|
final_mean = pixel_samples.mean().item()
|
|
logging.info(f"Final tensor validation: shape={pixel_samples.shape}, dtype={pixel_samples.dtype}, device={pixel_samples.device}")
|
|
logging.info(f"Final value range: min={final_min:.4f}, max={final_max:.4f}, mean={final_mean:.4f}")
|
|
|
|
# Validate final output quality
|
|
if final_min < -1.1 or final_max > 1.1:
|
|
logging.error(f"FINAL VALIDATION FAILED: Value range [{final_min:.4f}, {final_max:.4f}] is outside expected range!")
|
|
elif final_min < -0.1 or final_max > 1.1:
|
|
logging.warning(f"FINAL VALIDATION WARNING: Value range [{final_min:.4f}, {final_max:.4f}] might cause visual issues")
|
|
else:
|
|
logging.info(f"FINAL VALIDATION PASSED: Value range [{final_min:.4f}, {final_max:.4f}] is acceptable")
|
|
|
|
decode_time = time.time() - start_time
|
|
logging.info(f"ROCM VAE Decode completed in {decode_time:.2f}s")
|
|
|
|
# Completion message for user feedback
|
|
if is_video:
|
|
print(f"✅ Video decode completed in {decode_time:.2f}s")
|
|
else:
|
|
print(f"✅ Image decode completed in {decode_time:.2f}s")
|
|
|
|
return (pixel_samples,)
|
|
|
|
def _decode_tiled_optimized(self, vae, samples, tile_size, overlap, dtype, batch_number):
|
|
"""
|
|
Optimized tiled decoding for ROCm - avoid redundant model conversions
|
|
"""
|
|
compression = vae.spacial_compression_decode()
|
|
tile_x = tile_size // compression
|
|
tile_y = tile_size // compression
|
|
overlap_adj = overlap // compression
|
|
|
|
# Detect if this is a quantized model
|
|
is_quantized_model = False
|
|
vae_model_dtype = getattr(vae.first_stage_model, 'dtype', None)
|
|
if vae_model_dtype is not None:
|
|
quantized_dtypes = [torch.float8_e4m3fn, torch.float8_e5m2, torch.int8, torch.int4]
|
|
if vae_model_dtype in quantized_dtypes:
|
|
is_quantized_model = True
|
|
elif hasattr(vae_model_dtype, '__name__') and 'int' in str(vae_model_dtype):
|
|
is_quantized_model = True
|
|
|
|
# Use caching to avoid repeated model conversions in tiled decoding
|
|
vae_id = id(vae.first_stage_model)
|
|
cache_key = f"{vae_id}_{dtype}"
|
|
|
|
if cache_key not in self._vae_model_cache:
|
|
# CRITICAL FIX: Only convert model dtype if it's different AND not quantized
|
|
if not is_quantized_model:
|
|
current_model_dtype = getattr(vae.first_stage_model, 'dtype', None)
|
|
if current_model_dtype is not None and current_model_dtype != dtype:
|
|
logging.info(f"Tiled decode: Converting VAE model from {current_model_dtype} to {dtype}")
|
|
vae.first_stage_model = vae.first_stage_model.to(dtype)
|
|
else:
|
|
logging.info(f"Tiled decode: VAE model already in correct dtype: {current_model_dtype}")
|
|
else:
|
|
logging.info(f"Tiled decode: Preserving quantized model dtype: {vae_model_dtype}")
|
|
self._vae_model_cache[cache_key] = True
|
|
|
|
# Use ComfyUI's tiled scale with optimizations
|
|
def decode_fn(samples_tile):
|
|
# Ensure consistent data types for ROCm (minimal conversion)
|
|
samples_tile = samples_tile.to(vae.device).to(dtype)
|
|
return vae.first_stage_model.decode(samples_tile)
|
|
|
|
# Ensure a valid output device is defined for tiled scaling
|
|
device = vae.device if hasattr(vae, 'device') else (
|
|
samples.device if hasattr(samples, 'device') else (
|
|
torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu')
|
|
)
|
|
)
|
|
|
|
result = comfy.utils.tiled_scale(
|
|
samples,
|
|
decode_fn,
|
|
tile_x=tile_x,
|
|
tile_y=tile_y,
|
|
overlap=overlap_adj,
|
|
upscale_amount=vae.upscale_ratio,
|
|
out_channels=3, # RGB output channels, not latent channels
|
|
output_device=device # CRITICAL FIX: Use GPU device, not vae.output_device
|
|
)
|
|
|
|
# Handle WAN VAE output format - ensure correct shape and channels
|
|
if len(result.shape) == 5: # Video format (B, C, T, H, W)
|
|
# Reshape to (B*T, C, H, W) for video processing
|
|
result = result.permute(0, 2, 1, 3, 4).contiguous()
|
|
result = result.reshape(-1, result.shape[2], result.shape[3], result.shape[4])
|
|
elif len(result.shape) == 4 and result.shape[1] > 3:
|
|
# Handle case where we have too many channels - take first 3
|
|
result = result[:, :3, :, :]
|
|
|
|
# CRITICAL FIX: Keep standard VAE format (B, C, H, W) - don't convert to (B, H, W, C)
|
|
# The permute operation was causing wrong tensor dimensions
|
|
# Standard VAE decode should return (B, C, H, W) format
|
|
# The main decode method will handle the final conversion to (B, H, W, C)
|
|
|
|
# Capture output data and timing for debugging
|
|
if DEBUG_MODE:
|
|
end_time = time.time()
|
|
save_debug_data(result, "vae_decode_output", "flux_1024x1024", {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'execution_time': end_time - time.time(), # Use current time as fallback
|
|
'output_shape': result.shape,
|
|
'output_dtype': str(result.dtype),
|
|
'output_device': str(result.device)
|
|
})
|
|
capture_timing("vae_decode", time.time(), end_time, {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'is_video': False,
|
|
'tile_size': tile_size,
|
|
'overlap': overlap
|
|
})
|
|
capture_memory_usage("vae_decode", {
|
|
'node_type': 'ROCMOptimizedVAEDecode',
|
|
'is_video': False
|
|
})
|
|
|
|
return result
|
|
|
|
|
|
class ROCMOptimizedVAEDecodeTiled:
|
|
"""
|
|
Advanced tiled VAE decode with ROCm optimizations
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"samples": ("LATENT", {"tooltip": "The latent to be decoded."}),
|
|
"vae": ("VAE", {"tooltip": "The VAE model used for decoding the latent."}),
|
|
"tile_size": ("INT", {
|
|
"default": 768,
|
|
"min": 256,
|
|
"max": 2048,
|
|
"step": 64
|
|
}),
|
|
"overlap": ("INT", {
|
|
"default": 96,
|
|
"min": 32,
|
|
"max": 512,
|
|
"step": 16
|
|
}),
|
|
"temporal_size": ("INT", {
|
|
"default": 64,
|
|
"min": 8,
|
|
"max": 4096,
|
|
"step": 4,
|
|
"tooltip": "For video VAEs: frames to decode at once"
|
|
}),
|
|
"temporal_overlap": ("INT", {
|
|
"default": 8,
|
|
"min": 4,
|
|
"max": 4096,
|
|
"step": 4,
|
|
"tooltip": "For video VAEs: frame overlap"
|
|
}),
|
|
"rocm_optimizations": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable ROCm-specific optimizations"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
RETURN_NAMES = ("IMAGE",)
|
|
FUNCTION = "decode"
|
|
CATEGORY = "RocM Ninodes/VAE"
|
|
DESCRIPTION = "Advanced tiled VAE decode optimized for ROCm"
|
|
|
|
def decode(self, vae, samples, tile_size=768, overlap=96, temporal_size=64,
|
|
temporal_overlap=8, rocm_optimizations=True):
|
|
"""
|
|
Advanced tiled decode with ROCm optimizations
|
|
"""
|
|
start_time = time.time()
|
|
|
|
# Adjust tile size for ROCm
|
|
if rocm_optimizations:
|
|
if tile_size < overlap * 4:
|
|
overlap = tile_size // 4
|
|
if temporal_size < temporal_overlap * 2:
|
|
temporal_overlap = temporal_size // 2
|
|
|
|
# Handle temporal compression
|
|
temporal_compression = vae.temporal_compression_decode()
|
|
if temporal_compression is not None:
|
|
temporal_size = max(2, temporal_size // temporal_compression)
|
|
temporal_overlap = max(1, min(temporal_size // 2, temporal_overlap // temporal_compression))
|
|
else:
|
|
temporal_size = None
|
|
temporal_overlap = None
|
|
|
|
# Use VAE's tiled decode with optimizations
|
|
compression = vae.spacial_compression_decode()
|
|
images = vae.decode_tiled(
|
|
samples["samples"],
|
|
tile_x=tile_size // compression,
|
|
tile_y=tile_size // compression,
|
|
overlap=overlap // compression,
|
|
tile_t=temporal_size,
|
|
overlap_t=temporal_overlap
|
|
)
|
|
|
|
# Reshape if needed
|
|
if len(images.shape) == 5:
|
|
images = images.reshape(-1, images.shape[-3], images.shape[-2], images.shape[-1])
|
|
|
|
decode_time = time.time() - start_time
|
|
logging.info(f"ROCM Tiled VAE Decode completed in {decode_time:.2f}s")
|
|
|
|
return (images,)
|
|
|
|
|
|
class ROCMVAEPerformanceMonitor:
|
|
"""
|
|
Monitor VAE performance and provide optimization suggestions
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"vae": ("VAE", {"tooltip": "VAE to monitor"}),
|
|
"test_resolution": ("INT", {
|
|
"default": 1024,
|
|
"min": 256,
|
|
"max": 4096,
|
|
"step": 64,
|
|
"tooltip": "Test resolution for benchmarking"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("DEVICE_INFO", "PERFORMANCE_TIPS", "OPTIMAL_SETTINGS")
|
|
FUNCTION = "analyze"
|
|
CATEGORY = "RocM Ninodes/VAE"
|
|
DESCRIPTION = "Analyze VAE performance and provide optimization recommendations"
|
|
|
|
def analyze(self, vae, test_resolution=1024):
|
|
"""
|
|
Analyze VAE performance and provide recommendations
|
|
"""
|
|
device = vae.device
|
|
device_info = f"Device: {device}\n"
|
|
device_info += f"VAE dtype: {vae.vae_dtype}\n"
|
|
device_info += f"Output device: {vae.output_device}\n"
|
|
|
|
# Check if it's an AMD GPU
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
if is_amd:
|
|
try:
|
|
device_name = torch.cuda.get_device_name(0)
|
|
device_info += f"GPU: {device_name}\n"
|
|
except:
|
|
device_info += "GPU: AMD (ROCm)\n"
|
|
|
|
# Performance tips
|
|
tips = []
|
|
if is_amd:
|
|
tips.append("• Use fp32 precision for better ROCm performance")
|
|
tips.append("• Tile size 768-1024 works well for gfx1151")
|
|
tips.append("• Enable ROCm optimizations for best results")
|
|
tips.append("• Consider using tiled decode for images > 1024x1024")
|
|
else:
|
|
tips.append("• Use fp16 or bf16 for better performance")
|
|
tips.append("• Larger tile sizes generally perform better")
|
|
|
|
# Optimal settings
|
|
settings = []
|
|
if is_amd:
|
|
settings.append(f"Recommended tile_size: {min(1024, test_resolution)}")
|
|
settings.append(f"Recommended overlap: {min(128, test_resolution // 8)}")
|
|
settings.append("Recommended precision: fp32")
|
|
settings.append("Recommended batch_optimization: True")
|
|
else:
|
|
settings.append(f"Recommended tile_size: {test_resolution}")
|
|
settings.append(f"Recommended overlap: {test_resolution // 8}")
|
|
settings.append("Recommended precision: auto")
|
|
settings.append("Recommended batch_optimization: True")
|
|
|
|
return (
|
|
device_info,
|
|
"\n".join(tips),
|
|
"\n".join(settings)
|
|
)
|
|
|
|
|
|
class ROCMOptimizedKSampler:
|
|
"""
|
|
ROCM-optimized KSampler for all AMD GPUs with ROCm support.
|
|
|
|
Optimization Strategy:
|
|
1. GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs):
|
|
- TF32 disabled (not supported on AMD)
|
|
- FP16 accumulation enabled (better performance on AMD)
|
|
- Async memory management (non-blocking)
|
|
- WAN model full-load optimization
|
|
|
|
2. ARCHITECTURE-SPECIFIC OPTIMIZATIONS (if supported):
|
|
- PyTorch attention enabled for compatible architectures
|
|
(gfx90a, gfx942, gfx1100, gfx1101, gfx1151, gfx1201+)
|
|
- ROCm version detection for feature support
|
|
|
|
3. STRix HALO (gfx1151) SPECIFIC:
|
|
- Full PyTorch attention support
|
|
- Enhanced memory management
|
|
- Specialized optimizations for Strix Halo architecture
|
|
|
|
Key optimizations:
|
|
- Non-blocking memory operations (reduces idle time)
|
|
- Optimized precision handling for AMD GPUs
|
|
- Reduced memory fragmentation
|
|
- Better batch processing
|
|
- Architecture-aware feature detection
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL", {"tooltip": "The model to sample from"}),
|
|
"seed": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 0xffffffffffffffff,
|
|
"tooltip": "Random seed for generation"
|
|
}),
|
|
"steps": ("INT", {
|
|
"default": 20,
|
|
"min": 1,
|
|
"max": 10000,
|
|
"tooltip": "Number of sampling steps"
|
|
}),
|
|
"cfg": ("FLOAT", {
|
|
"default": 8.0,
|
|
"min": 0.0,
|
|
"max": 100.0,
|
|
"step": 0.1,
|
|
"round": 0.01,
|
|
"tooltip": "Classifier-free guidance scale"
|
|
}),
|
|
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {
|
|
"tooltip": "Sampling algorithm to use"
|
|
}),
|
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {
|
|
"tooltip": "Scheduler for noise timesteps"
|
|
}),
|
|
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning"}),
|
|
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning"}),
|
|
"latent_image": ("LATENT", {"tooltip": "Latent image to sample from"}),
|
|
"denoise": ("FLOAT", {
|
|
"default": 1.0,
|
|
"min": 0.0,
|
|
"max": 1.0,
|
|
"step": 0.01,
|
|
"tooltip": "Denoising strength"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("LATENT",)
|
|
FUNCTION = "sample"
|
|
CATEGORY = "RocM Ninodes/Sampling"
|
|
DESCRIPTION = "ROCM-optimized KSampler for all AMD GPUs with ROCm support. Enhanced for Strix Halo (gfx1151)."
|
|
|
|
def sample(self, model, seed, steps, cfg, sampler_name, scheduler, positive, negative,
|
|
latent_image, denoise=1.0):
|
|
"""
|
|
Optimized sampling for ROCm/AMD GPUs with minimal overhead and gfx1151-specific optimizations
|
|
"""
|
|
start_time = time.time()
|
|
|
|
# Detect WAN models for special handling (more reliable detection)
|
|
is_wan_model = False
|
|
try:
|
|
if hasattr(model, 'model') and model.model is not None:
|
|
model_name = model.model.__class__.__name__
|
|
# Check for WAN model variants (WAN21, WAN22, WAN22_S2V, etc.)
|
|
if 'WAN' in model_name or 'WanModel' in model_name or 'Wan22' in model_name:
|
|
is_wan_model = True
|
|
# Also check model_type attribute for additional detection
|
|
if hasattr(model, 'model_type'):
|
|
model_type_str = str(model.model_type).lower()
|
|
if 'wan' in model_type_str:
|
|
is_wan_model = True
|
|
except:
|
|
pass
|
|
|
|
# Detect architecture for optimizations
|
|
device = model_management.get_torch_device()
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
is_gfx1151 = False
|
|
arch = None
|
|
supports_pytorch_attention = False
|
|
|
|
# Apply general ROCm optimizations for ALL AMD GPUs
|
|
if is_amd and torch.cuda.is_available():
|
|
try:
|
|
arch = torch.cuda.get_device_properties(0).gcnArchName
|
|
rocm_version_str = str(torch.version.hip)
|
|
try:
|
|
rocm_version = tuple(map(int, rocm_version_str.split(".")[:2]))
|
|
except:
|
|
rocm_version = (6, 0)
|
|
|
|
# GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs)
|
|
torch.backends.cuda.matmul.allow_tf32 = False # TF32 not supported on AMD
|
|
torch.backends.cuda.matmul.allow_fp16_accumulation = True # Better performance on AMD
|
|
|
|
# Check if this is Strix Halo (gfx1151)
|
|
if 'gfx1151' in arch:
|
|
is_gfx1151 = True
|
|
supports_pytorch_attention = True # gfx1151 supports it
|
|
print(f"🚀 Detected Strix Halo (gfx1151) - applying architecture-specific optimizations")
|
|
|
|
# Check for PyTorch attention support (multiple architectures support it)
|
|
# Based on ComfyUI's model_management.py logic
|
|
pytorch_attention_arches = ["gfx90a", "gfx942", "gfx1100", "gfx1101", "gfx1151"]
|
|
if any((a in arch) for a in pytorch_attention_arches):
|
|
supports_pytorch_attention = True
|
|
|
|
# For ROCm 7.0+, additional architectures support PyTorch attention
|
|
if rocm_version >= (7, 0):
|
|
if any((a in arch) for a in ["gfx1201"]):
|
|
supports_pytorch_attention = True
|
|
|
|
# Enable PyTorch attention if supported (benefits all compatible AMD GPUs)
|
|
if supports_pytorch_attention:
|
|
torch.backends.cuda.enable_math_sdp(True)
|
|
torch.backends.cuda.enable_flash_sdp(True)
|
|
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
|
|
|
if is_gfx1151:
|
|
print(f"✅ Enabled PyTorch attention optimizations for gfx1151")
|
|
elif arch:
|
|
print(f"✅ Enabled PyTorch attention optimizations for {arch}")
|
|
|
|
# OPTIMIZATION: torch.compile for ROCm (experimental but can provide significant speedup)
|
|
# Check if torch.compile is available (PyTorch 2.0+)
|
|
try:
|
|
if hasattr(torch, 'compile') and torch.__version__ >= "2.0":
|
|
# For ROCm, use "inductor" backend (more compatible than "cudagraphs")
|
|
# This will be applied to the model during sampling via model options
|
|
# Note: torch.compile is applied lazily on first forward pass
|
|
if is_gfx1151:
|
|
# Enable torch.compile optimizations for gfx1151
|
|
# Will be applied via model.model_options if supported
|
|
torch._dynamo.config.suppress_errors = True # Don't fail on unsupported ops
|
|
print(f"✅ torch.compile optimizations ready for gfx1151 (will compile on first use)")
|
|
elif is_amd:
|
|
torch._dynamo.config.suppress_errors = True
|
|
if DEBUG_MODE:
|
|
print(f"✅ torch.compile optimizations ready for {arch}")
|
|
except Exception as e:
|
|
if DEBUG_MODE:
|
|
print(f"⚠️ torch.compile not available: {e}")
|
|
|
|
except Exception as e:
|
|
# Fallback: still apply general ROCm optimizations even if detection fails
|
|
if is_amd:
|
|
torch.backends.cuda.matmul.allow_tf32 = False
|
|
torch.backends.cuda.matmul.allow_fp16_accumulation = True
|
|
|
|
# OPTIMIZATION: Non-blocking memory cleanup (async - doesn't sync GPU)
|
|
if torch.cuda.is_available():
|
|
# Only cleanup if memory is critically low - use async method
|
|
total_mem, alloc_mem, reserved_mem, free_mem = get_gpu_memory_info()
|
|
if free_mem is not None and free_mem < 2 * 1024**3: # Less than 2GB free
|
|
torch.cuda.empty_cache() # Async - doesn't block
|
|
# Don't call gentle_memory_cleanup() - it contains torch.cuda.synchronize()
|
|
|
|
# Initialize progress early for UI feedback
|
|
is_video_workflow = latent_image["samples"].shape[0] > 1
|
|
if is_video_workflow:
|
|
print(f"🎬 Video workflow detected: {latent_image['samples'].shape[0]} frames")
|
|
else:
|
|
print(f"🖼️ Image workflow detected")
|
|
|
|
# Initialize progress bar early for UI feedback during initialization
|
|
init_pbar = comfy.utils.ProgressBar(5) # 5 phases: init, model_load, prep, noise, sample
|
|
init_pbar.update_absolute(1, 5, preview=None)
|
|
|
|
# Note: WAN model will be loaded during prepare_sampling() inside comfy.sample.sample()
|
|
# We skip pre-loading here to avoid duplicate loads and let prepare_sampling handle it
|
|
# with proper memory estimation
|
|
if is_wan_model:
|
|
init_pbar.update_absolute(2, 5, preview=None)
|
|
print(f"📦 WAN model will be loaded during sampling preparation...")
|
|
if is_gfx1151:
|
|
print(f"🚀 WAN model detected - will force full GPU load for Strix Halo (gfx1151) optimization")
|
|
elif is_amd:
|
|
print(f"🚀 WAN model detected - will force full GPU load for ROCm optimization")
|
|
|
|
print(f"⚙️ Preparing KSampler: {steps} steps, CFG {cfg}, {sampler_name}")
|
|
|
|
# Use vanilla ComfyUI sampling (standard path)
|
|
try:
|
|
init_pbar.update_absolute(3, 5, preview=None)
|
|
# Use ComfyUI's sample function directly
|
|
latent_image_tensor = latent_image["samples"]
|
|
latent_image_tensor = comfy.sample.fix_empty_latent_channels(model, latent_image_tensor)
|
|
init_pbar.update_absolute(4, 5, preview=None)
|
|
|
|
# OPTIMIZATION: Non-blocking latent validation (use tensor operations, not .item())
|
|
# Only check if needed for debugging, and use non-blocking tensor ops
|
|
if DEBUG_MODE and denoise < 1.0:
|
|
latent_shape = latent_image_tensor.shape
|
|
if len(latent_shape) >= 4:
|
|
# Use tensor comparisons - non-blocking
|
|
with torch.no_grad():
|
|
latent_mean_tensor = latent_image_tensor.mean()
|
|
latent_std_tensor = latent_image_tensor.std()
|
|
# Check without .item() - use tensor operations
|
|
is_empty = torch.abs(latent_mean_tensor) < 0.001 and latent_std_tensor < 0.001
|
|
is_noise = latent_std_tensor > 1.5
|
|
if is_empty or is_noise:
|
|
print(f"⚠️ WARNING: Latent may be invalid (img2img workflow)")
|
|
|
|
# Match original KSampler: Let ComfyUI handle device placement naturally
|
|
# Do not modify tensors before ComfyUI's sample() function - it handles device placement correctly
|
|
batch_inds = latent_image["batch_index"] if "batch_index" in latent_image else None
|
|
|
|
# Standard noise preparation (matches original KSampler exactly)
|
|
noise = comfy.sample.prepare_noise(latent_image_tensor, seed, batch_inds)
|
|
|
|
# Standard noise mask extraction (matches original KSampler exactly)
|
|
noise_mask = None
|
|
if "noise_mask" in latent_image:
|
|
noise_mask = latent_image["noise_mask"]
|
|
|
|
# Prepare callback - enable progress reporting for ALL workflows
|
|
is_video_workflow = latent_image_tensor.shape[0] > 1
|
|
|
|
# Complete initialization phase
|
|
init_pbar.update_absolute(5, 5, preview=None)
|
|
print(f"🚀 Starting sampling: {steps} steps, CFG {cfg}, {sampler_name}")
|
|
|
|
# OPTIMIZATION: Enhanced progress callback for all workflows
|
|
# Provides UI feedback with optional preview for image workflows
|
|
pbar = comfy.utils.ProgressBar(steps)
|
|
last_update_time = [time.time()] # Use list for mutable closure
|
|
callback_start_time = time.time() # Use current time for callback (after init)
|
|
|
|
def enhanced_callback(step, x0, x, total_steps):
|
|
"""Enhanced callback for all workflows - reports progress to UI and console"""
|
|
current_time = time.time()
|
|
# Update progress bar every 0.3 seconds or at completion for responsive UI
|
|
if current_time - last_update_time[0] >= 0.3 or step == total_steps - 1:
|
|
# Generate preview only for image workflows (not video to reduce overhead)
|
|
preview_bytes = None
|
|
if not is_video_workflow and step % 5 == 0: # Generate preview every 5 steps for images
|
|
try:
|
|
previewer = latent_preview.get_previewer(model.load_device, model.model.latent_format)
|
|
if previewer:
|
|
preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0)
|
|
except:
|
|
preview_bytes = None # Don't block on preview generation
|
|
|
|
# Update progress bar with preview (UI feedback)
|
|
pbar.update_absolute(step + 1, total_steps, preview=preview_bytes)
|
|
|
|
# Console feedback every 10% or at completion
|
|
if step % max(1, total_steps // 10) == 0 or step == total_steps - 1:
|
|
progress_pct = ((step + 1) / total_steps) * 100
|
|
elapsed = current_time - callback_start_time
|
|
if step > 0:
|
|
est_total = elapsed / ((step + 1) / total_steps)
|
|
est_remaining = est_total - elapsed
|
|
avg_time_per_step = elapsed / (step + 1)
|
|
workflow_type = "Video" if is_video_workflow else "Image"
|
|
print(f"📊 {workflow_type} KSampler: {step + 1}/{total_steps} ({progress_pct:.1f}%) | "
|
|
f"Elapsed: {elapsed:.1f}s | Remaining: ~{est_remaining:.1f}s | "
|
|
f"Avg: {avg_time_per_step:.2f}s/step")
|
|
else:
|
|
workflow_type = "Video" if is_video_workflow else "Image"
|
|
print(f"📊 {workflow_type} KSampler: {step + 1}/{total_steps} ({progress_pct:.1f}%) | Starting...")
|
|
|
|
last_update_time[0] = current_time
|
|
|
|
callback = enhanced_callback
|
|
disable_pbar = False # Always enable progress bar for UI feedback
|
|
|
|
# Note: comfy.sample.sample() will call prepare_sampling() internally
|
|
# which calls load_models_gpu() - this can take time for WAN models
|
|
# We show a message and periodically update progress during this phase
|
|
print(f"⏳ Preparing model for sampling (this may take a moment for WAN models)...")
|
|
prep_start = time.time()
|
|
|
|
# For WAN models, start a periodic progress update thread to show activity
|
|
# during the blocking prepare_sampling() -> load_models_gpu() call
|
|
import threading
|
|
progress_update_active = [True]
|
|
progress_pbar_ref = [pbar] # Reference to progress bar for thread
|
|
|
|
def periodic_progress_update():
|
|
"""Periodically update progress bar during model loading"""
|
|
update_interval = 2.0 # Update every 2 seconds
|
|
elapsed = 0
|
|
while progress_update_active[0]:
|
|
time.sleep(update_interval)
|
|
elapsed += update_interval
|
|
if elapsed > 5.0 and progress_pbar_ref[0] is not None: # Start showing updates after 5 seconds
|
|
try:
|
|
# Keep progress bar visible (at step 1 of total steps)
|
|
progress_pbar_ref[0].update_absolute(1, steps, preview=None)
|
|
except:
|
|
pass # Don't fail if progress bar isn't ready yet
|
|
if elapsed % 10 == 0: # Print every 10 seconds
|
|
print(f"⏳ Still preparing model for sampling... ({elapsed:.0f}s elapsed)")
|
|
|
|
if is_wan_model:
|
|
progress_thread = threading.Thread(target=periodic_progress_update, daemon=True)
|
|
progress_thread.start()
|
|
|
|
# OPTIMIZATION: Apply torch.compile to model if supported (for ROCm performance boost)
|
|
# torch.compile can provide 10-30% speedup on compatible operations
|
|
try:
|
|
if is_amd and hasattr(torch, 'compile') and torch.__version__ >= "2.0":
|
|
# Enable compile optimizations for ROCm
|
|
torch._dynamo.config.suppress_errors = True
|
|
if DEBUG_MODE and is_gfx1151:
|
|
print(f"🔧 torch.compile optimizations enabled (will compile on first forward pass)")
|
|
except:
|
|
pass # Don't fail if compile setup fails
|
|
|
|
# Match original KSampler: Let comfy.sample.sample() handle device placement
|
|
# Do not modify tensors - ComfyUI's sample() function handles device placement internally
|
|
# Sample (this internally calls prepare_sampling() -> load_models_gpu())
|
|
try:
|
|
samples = comfy.sample.sample(
|
|
model, noise, steps, cfg, sampler_name, scheduler,
|
|
positive, negative, latent_image_tensor, denoise=denoise,
|
|
noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=seed
|
|
)
|
|
finally:
|
|
# Stop progress updates once sampling starts (callback will handle it)
|
|
progress_update_active[0] = False
|
|
|
|
prep_time = time.time() - prep_start
|
|
if prep_time > 5.0: # Only log if it took significant time
|
|
print(f"✅ Model preparation completed in {prep_time:.1f}s - starting sampling now")
|
|
|
|
# Wrap in latent format
|
|
out = latent_image.copy()
|
|
out["samples"] = samples
|
|
result = (out,)
|
|
|
|
except Exception as e:
|
|
print(f"Sampling failed: {e}")
|
|
raise e
|
|
|
|
# OPTIMIZATION: Non-blocking post-sampling cleanup
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
# Don't call gentle_memory_cleanup() - it contains torch.cuda.synchronize()
|
|
|
|
sample_time = time.time() - start_time
|
|
print(f"✅ KSampler completed in {sample_time:.2f}s")
|
|
|
|
return result
|
|
|
|
|
|
|
|
class ROCMOptimizedKSamplerAdvanced:
|
|
"""
|
|
Advanced ROCM-optimized KSampler for all AMD GPUs with ROCm support.
|
|
|
|
Optimization Strategy:
|
|
1. GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs):
|
|
- TF32 disabled (not supported on AMD)
|
|
- FP16 accumulation enabled (better performance on AMD)
|
|
- Async memory management (non-blocking)
|
|
- WAN model full-load optimization
|
|
|
|
2. ARCHITECTURE-SPECIFIC OPTIMIZATIONS (if supported):
|
|
- PyTorch attention enabled for compatible architectures
|
|
(gfx90a, gfx942, gfx1100, gfx1101, gfx1151, gfx1201+)
|
|
- ROCm version detection for feature support
|
|
|
|
3. STRix HALO (gfx1151) SPECIFIC:
|
|
- Full PyTorch attention support
|
|
- Enhanced memory management
|
|
- Specialized optimizations for Strix Halo architecture
|
|
|
|
Provides extended control options including step control, precision mode,
|
|
and memory optimization settings while maintaining ROCm compatibility.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL", {"tooltip": "The model to sample from"}),
|
|
"add_noise": (["enable", "disable"], {
|
|
"default": "enable",
|
|
"tooltip": "Whether to add noise to the latent"
|
|
}),
|
|
"noise_seed": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 0xffffffffffffffff,
|
|
"tooltip": "Random seed for noise generation"
|
|
}),
|
|
"steps": ("INT", {
|
|
"default": 20,
|
|
"min": 1,
|
|
"max": 10000,
|
|
"tooltip": "Number of sampling steps"
|
|
}),
|
|
"cfg": ("FLOAT", {
|
|
"default": 8.0,
|
|
"min": 0.0,
|
|
"max": 100.0,
|
|
"step": 0.1,
|
|
"round": 0.01,
|
|
"tooltip": "Classifier-free guidance scale"
|
|
}),
|
|
"sampler_name": (comfy.samplers.KSampler.SAMPLERS, {
|
|
"tooltip": "Sampling algorithm to use"
|
|
}),
|
|
"scheduler": (comfy.samplers.KSampler.SCHEDULERS, {
|
|
"tooltip": "Scheduler for noise timesteps"
|
|
}),
|
|
"positive": ("CONDITIONING", {"tooltip": "Positive conditioning"}),
|
|
"negative": ("CONDITIONING", {"tooltip": "Negative conditioning"}),
|
|
"latent_image": ("LATENT", {"tooltip": "Latent image to sample from"}),
|
|
"start_at_step": ("INT", {
|
|
"default": 0,
|
|
"min": 0,
|
|
"max": 10000,
|
|
"tooltip": "Step to start sampling from"
|
|
}),
|
|
"end_at_step": ("INT", {
|
|
"default": 10000,
|
|
"min": 0,
|
|
"max": 10000,
|
|
"tooltip": "Step to end sampling at"
|
|
}),
|
|
"return_with_leftover_noise": (["disable", "enable"], {
|
|
"default": "disable",
|
|
"tooltip": "Whether to return with leftover noise"
|
|
}),
|
|
"use_rocm_optimizations": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable ROCm-specific optimizations"
|
|
}),
|
|
"precision_mode": (["auto", "fp32", "fp16", "bf16"], {
|
|
"default": "auto",
|
|
"tooltip": "Precision mode for sampling"
|
|
}),
|
|
"memory_optimization": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable memory optimization for AMD GPUs"
|
|
})
|
|
}
|
|
,
|
|
"optional": {
|
|
"compatibility_mode": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Enable compatibility mode for quantized models (disables optimizations)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("LATENT",)
|
|
RETURN_NAMES = ("LATENT",)
|
|
FUNCTION = "sample"
|
|
CATEGORY = "RocM Ninodes/Sampling"
|
|
DESCRIPTION = "Advanced ROCM-optimized KSampler for all AMD GPUs with ROCm support. Enhanced for Strix Halo (gfx1151)."
|
|
|
|
def sample(self, model, add_noise, noise_seed, steps, cfg, sampler_name, scheduler,
|
|
positive, negative, latent_image, start_at_step, end_at_step,
|
|
return_with_leftover_noise, use_rocm_optimizations=True,
|
|
precision_mode="auto", memory_optimization=True, denoise=1.0, compatibility_mode=False):
|
|
"""
|
|
Advanced optimized sampling for ROCm/AMD GPUs with video workflow optimizations and quantized model support
|
|
"""
|
|
start_time = time.time()
|
|
|
|
# CRITICAL FIX: Detect quantized models to avoid breaking them
|
|
is_quantized_model = False
|
|
try:
|
|
model_dtype = model.model_dtype()
|
|
if hasattr(model_dtype, '__name__'):
|
|
dtype_name = str(model_dtype)
|
|
# Check for quantized dtypes - be more specific
|
|
quantized_indicators = ['int8', 'int4', 'float8', 'quantized']
|
|
if any(indicator in dtype_name.lower() for indicator in quantized_indicators):
|
|
is_quantized_model = True
|
|
print(f"🔍 Detected quantized model (dtype: {model_dtype})")
|
|
except Exception as e:
|
|
print(f"⚠️ Could not detect model dtype: {e}")
|
|
|
|
# Compatibility mode: disable all optimizations for quantized models
|
|
if compatibility_mode or is_quantized_model:
|
|
print("🛡️ Compatibility mode enabled - using stock ComfyUI behavior")
|
|
use_rocm_optimizations = False
|
|
memory_optimization = False
|
|
|
|
# Detect WAN models for special handling (more reliable detection)
|
|
is_wan_model = False
|
|
try:
|
|
if hasattr(model, 'model') and model.model is not None:
|
|
model_name = model.model.__class__.__name__
|
|
# Check for WAN model variants (WAN21, WAN22, WAN22_S2V, etc.)
|
|
if 'WAN' in model_name or 'WanModel' in model_name or 'Wan22' in model_name:
|
|
is_wan_model = True
|
|
# Also check model_type attribute for additional detection
|
|
if hasattr(model, 'model_type'):
|
|
model_type_str = str(model.model_type).lower()
|
|
if 'wan' in model_type_str:
|
|
is_wan_model = True
|
|
except:
|
|
pass
|
|
|
|
# Detect video workflow by batch size
|
|
batch_size = latent_image["samples"].shape[0]
|
|
is_video_workflow = batch_size > 1
|
|
|
|
# Detect gfx1151 architecture for optimizations
|
|
is_gfx1151 = False
|
|
device = model_management.get_torch_device()
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
|
|
# OPTIMIZATION: Non-blocking memory cleanup (async - doesn't sync GPU)
|
|
if torch.cuda.is_available():
|
|
_total, _alloc, _reserved, _free = get_gpu_memory_info()
|
|
if _free is not None and _free < 2 * 1024**3: # < 2GB free
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
# Don't call gentle_memory_cleanup() - it contains torch.cuda.synchronize()
|
|
|
|
# OPTIMIZATION: For WAN models, force full load to prevent offloading between steps
|
|
if is_wan_model:
|
|
try:
|
|
model_management.load_models_gpu(
|
|
[model.patcher],
|
|
force_full_load=True, # Keep model in VRAM - prevents idle time
|
|
memory_required=0 # Already loaded, just ensure it stays
|
|
)
|
|
except Exception as e:
|
|
logging.warning(f"Could not force full load for WAN model: {e}")
|
|
|
|
# Minimal progress for video workflows to reduce CPU overhead
|
|
if is_video_workflow:
|
|
print(f"🎬 Video KSampler: {steps} steps, batch={batch_size}, {sampler_name}")
|
|
else:
|
|
print(f"🎬 Starting Advanced KSampler: {steps} steps, CFG {cfg}, {sampler_name}")
|
|
|
|
# ROCm optimizations: General for all AMD, enhanced for gfx1151
|
|
if use_rocm_optimizations:
|
|
arch = None
|
|
supports_pytorch_attention = False
|
|
|
|
if is_amd and torch.cuda.is_available():
|
|
try:
|
|
arch = torch.cuda.get_device_properties(0).gcnArchName
|
|
rocm_version_str = str(torch.version.hip)
|
|
try:
|
|
rocm_version = tuple(map(int, rocm_version_str.split(".")[:2]))
|
|
except:
|
|
rocm_version = (6, 0)
|
|
|
|
# Check if this is Strix Halo (gfx1151)
|
|
if 'gfx1151' in arch:
|
|
is_gfx1151 = True
|
|
supports_pytorch_attention = True
|
|
if not is_video_workflow:
|
|
print(f"🚀 Detected Strix Halo (gfx1151) - applying architecture-specific optimizations")
|
|
|
|
# Check for PyTorch attention support (multiple architectures)
|
|
pytorch_attention_arches = ["gfx90a", "gfx942", "gfx1100", "gfx1101", "gfx1151"]
|
|
if any((a in arch) for a in pytorch_attention_arches):
|
|
supports_pytorch_attention = True
|
|
|
|
# For ROCm 7.0+, additional architectures support PyTorch attention
|
|
if rocm_version >= (7, 0):
|
|
if any((a in arch) for a in ["gfx1201"]):
|
|
supports_pytorch_attention = True
|
|
except:
|
|
pass
|
|
|
|
# GENERAL ROCM OPTIMIZATIONS (apply to all AMD GPUs)
|
|
if is_amd:
|
|
torch.backends.cuda.matmul.allow_tf32 = False # TF32 not supported on AMD
|
|
torch.backends.cuda.matmul.allow_fp16_accumulation = True # Better performance on AMD
|
|
|
|
# Enable PyTorch attention if architecture supports it
|
|
if supports_pytorch_attention:
|
|
torch.backends.cuda.enable_math_sdp(True)
|
|
torch.backends.cuda.enable_flash_sdp(True)
|
|
torch.backends.cuda.enable_mem_efficient_sdp(True)
|
|
|
|
if is_gfx1151 and not is_video_workflow:
|
|
print(f"✅ Enabled PyTorch attention optimizations for Strix Halo")
|
|
elif arch and not is_video_workflow and DEBUG_MODE:
|
|
print(f"✅ Enabled PyTorch attention optimizations for {arch}")
|
|
|
|
if is_amd:
|
|
# Set optimal precision
|
|
if precision_mode == "auto":
|
|
optimal_dtype = torch.float32
|
|
else:
|
|
dtype_map = {
|
|
"fp32": torch.float32,
|
|
"fp16": torch.float16,
|
|
"bf16": torch.bfloat16
|
|
}
|
|
optimal_dtype = dtype_map[precision_mode]
|
|
|
|
# OPTIMIZED memory management for video workflows (non-blocking)
|
|
if memory_optimization:
|
|
# Check available memory and adjust strategy
|
|
total_memory, allocated_memory, reserved_memory, free_memory = get_gpu_memory_info()
|
|
|
|
if total_memory is not None:
|
|
# Only show detailed memory info for debugging
|
|
if not is_video_workflow and DEBUG_MODE:
|
|
print(f"🔍 Advanced KSampler Memory: {allocated_memory/1024**3:.2f}GB allocated, {reserved_memory/1024**3:.2f}GB reserved, {free_memory/1024**3:.2f}GB free")
|
|
|
|
# Video workflow optimization: more aggressive memory usage
|
|
if is_video_workflow:
|
|
# For video workflows, use more memory for better GPU utilization
|
|
memory_fraction = min(0.90, max(0.75, free_memory / total_memory))
|
|
|
|
# Only cleanup if memory is critically low (async)
|
|
if free_memory < 2 * 1024**3: # Less than 2GB free
|
|
print("⚠️ Critical memory - async cleanup")
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
memory_fraction = 0.80
|
|
# Don't call gentle_memory_cleanup() - it syncs
|
|
else:
|
|
# Regular workflow: conservative approach
|
|
memory_fraction = min(0.80, max(0.55, free_memory / total_memory))
|
|
|
|
if free_memory < 3 * 1024**3: # Less than 3GB free
|
|
print("⚠️ Low memory detected - async cleanup")
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
memory_fraction = 0.65
|
|
# Don't call gentle_memory_cleanup() - it syncs
|
|
|
|
# Apply memory fraction
|
|
if hasattr(torch.cuda, 'set_per_process_memory_fraction'):
|
|
try:
|
|
torch.cuda.set_per_process_memory_fraction(memory_fraction)
|
|
if not is_video_workflow and DEBUG_MODE:
|
|
print(f"🔧 Memory fraction set to {memory_fraction*100:.0f}%")
|
|
except Exception as e:
|
|
if DEBUG_MODE:
|
|
print(f"⚠️ Memory fraction setting failed: {e}")
|
|
|
|
# Configure sampling parameters
|
|
force_full_denoise = True
|
|
if return_with_leftover_noise == "enable":
|
|
force_full_denoise = False
|
|
|
|
disable_noise = False
|
|
if add_noise == "disable":
|
|
disable_noise = True
|
|
|
|
# Use advanced ksampler with optimizations
|
|
try:
|
|
# Minimal progress for video workflows
|
|
if not is_video_workflow:
|
|
print("⚡ Preparing sampling parameters...")
|
|
|
|
# Use ComfyUI's sample function directly with advanced options
|
|
latent_image_tensor = latent_image["samples"]
|
|
# Ensure latent on GPU to avoid CPU paging
|
|
if torch.cuda.is_available() and latent_image_tensor.device.type != 'cuda':
|
|
latent_image_tensor = latent_image_tensor.to(model_management.get_torch_device(), non_blocking=True)
|
|
latent_image_tensor = comfy.sample.fix_empty_latent_channels(model, latent_image_tensor)
|
|
|
|
# Guard 1: Only zero-out latent if it's truly a fresh generation (text2img), not img2img
|
|
# CRITICAL FIX: Don't zero out latents for img2img workflows (denoise < 1.0) or when latent contains image data
|
|
# OPTIMIZATION: Use non-blocking tensor operations instead of .item()
|
|
if add_noise == "enable" and start_at_step == 0:
|
|
# If denoise < 1.0, this is img2img - preserve the latent content
|
|
if denoise < 1.0:
|
|
if DEBUG_MODE:
|
|
logging.info(f"Preserving latent image content for img2img workflow (denoise={denoise})")
|
|
else:
|
|
# OPTIMIZATION: Use tensor operations instead of .item() to avoid blocking
|
|
# For full denoise, check if latent contains actual image data vs noise
|
|
with torch.no_grad():
|
|
latent_variance_tensor = latent_image_tensor.var()
|
|
latent_mean_abs_tensor = latent_image_tensor.abs().mean()
|
|
# Use tensor comparisons - non-blocking
|
|
is_likely_noise = (latent_variance_tensor > 1.2) or ((latent_mean_abs_tensor < 0.005) and (latent_variance_tensor < 0.05))
|
|
is_likely_noise = is_likely_noise.item() if is_likely_noise.numel() == 1 else bool(is_likely_noise.any().item())
|
|
|
|
# Only zero out if it's clearly noise, preserving img2img latents even in text2img mode
|
|
if is_likely_noise:
|
|
# Zero-out latent contents to avoid pass-through artifacts/scrambled frames when resolution changed
|
|
latent_image_tensor = torch.zeros_like(latent_image_tensor, device=latent_image_tensor.device)
|
|
# If a noise_mask exists but is mismatched, drop it to prevent shape errors
|
|
if "noise_mask" in latent_image and isinstance(latent_image["noise_mask"], torch.Tensor):
|
|
if tuple(latent_image["noise_mask"].shape) != tuple(latent_image_tensor.shape):
|
|
latent_image["noise_mask"] = None
|
|
elif DEBUG_MODE:
|
|
# Only log if debugging - avoid blocking operations
|
|
logging.info(f"Preserving latent image content")
|
|
|
|
# Prepare noise (minimal progress for video)
|
|
if not is_video_workflow:
|
|
print("🎲 Preparing noise for sampling...")
|
|
if disable_noise:
|
|
# CRITICAL FIX: Create noise on same device as latent_image_tensor, not CPU
|
|
noise = torch.zeros(latent_image_tensor.size(), dtype=latent_image_tensor.dtype, layout=latent_image_tensor.layout, device=latent_image_tensor.device)
|
|
else:
|
|
batch_inds = latent_image["batch_index"] if "batch_index" in latent_image else None
|
|
# Guard 2: Recompute noise with current latent shape to avoid stale shapes after resolution changes
|
|
noise = comfy.sample.prepare_noise(latent_image_tensor, noise_seed, batch_inds)
|
|
# Move noise to GPU if created on CPU by upstream helper
|
|
if torch.cuda.is_available() and noise.device.type != 'cuda':
|
|
noise = noise.to(latent_image_tensor.device, non_blocking=True)
|
|
|
|
# Prepare noise mask
|
|
noise_mask = None
|
|
if "noise_mask" in latent_image:
|
|
noise_mask = latent_image["noise_mask"]
|
|
|
|
# Prepare callback - enable progress reporting for ALL workflows
|
|
# OPTIMIZATION: Enhanced progress callback for all workflows
|
|
# Provides UI feedback with optional preview for image workflows
|
|
pbar = comfy.utils.ProgressBar(steps)
|
|
last_update_time = [time.time()] # Use list for mutable closure
|
|
adv_callback_start_time = start_time
|
|
|
|
def enhanced_callback(step, x0, x, total_steps):
|
|
"""Enhanced callback for all workflows - reports progress to UI and console"""
|
|
current_time = time.time()
|
|
# Update progress bar every 0.3 seconds or at completion for responsive UI
|
|
if current_time - last_update_time[0] >= 0.3 or step == total_steps - 1:
|
|
# Generate preview only for image workflows (not video to reduce overhead)
|
|
preview_bytes = None
|
|
if not is_video_workflow and step % 5 == 0: # Generate preview every 5 steps for images
|
|
try:
|
|
previewer = latent_preview.get_previewer(model.load_device, model.model.latent_format)
|
|
if previewer:
|
|
preview_bytes = previewer.decode_latent_to_preview_image("JPEG", x0)
|
|
except:
|
|
preview_bytes = None # Don't block on preview generation
|
|
|
|
# Update progress bar with preview (UI feedback)
|
|
pbar.update_absolute(step + 1, total_steps, preview=preview_bytes)
|
|
|
|
# Console feedback every 10% or at completion
|
|
if step % max(1, total_steps // 10) == 0 or step == total_steps - 1:
|
|
progress_pct = ((step + 1) / total_steps) * 100
|
|
elapsed = current_time - adv_callback_start_time
|
|
if step > 0:
|
|
est_total = elapsed / ((step + 1) / total_steps)
|
|
est_remaining = est_total - elapsed
|
|
avg_time_per_step = elapsed / (step + 1)
|
|
workflow_type = "Video" if is_video_workflow else "Image"
|
|
print(f"📊 Advanced {workflow_type} KSampler: {step + 1}/{total_steps} ({progress_pct:.1f}%) | "
|
|
f"Elapsed: {elapsed:.1f}s | Remaining: ~{est_remaining:.1f}s | "
|
|
f"Avg: {avg_time_per_step:.2f}s/step")
|
|
else:
|
|
workflow_type = "Video" if is_video_workflow else "Image"
|
|
print(f"📊 Advanced {workflow_type} KSampler: {step + 1}/{total_steps} ({progress_pct:.1f}%) | Starting...")
|
|
|
|
last_update_time[0] = current_time
|
|
|
|
callback = enhanced_callback
|
|
disable_pbar = False # Always enable progress bar for UI feedback
|
|
|
|
# Minimal progress for video workflows
|
|
if not is_video_workflow:
|
|
print(f"🚀 Starting sampling: {steps} steps, CFG {cfg}, {sampler_name}")
|
|
print("Using standard ComfyUI sampling path")
|
|
|
|
# Sample with advanced options
|
|
samples = comfy.sample.sample(
|
|
model, noise, steps, cfg, sampler_name, scheduler,
|
|
positive, negative, latent_image_tensor, denoise=denoise,
|
|
disable_noise=disable_noise, start_step=start_at_step,
|
|
last_step=end_at_step, force_full_denoise=force_full_denoise,
|
|
noise_mask=noise_mask, callback=callback, disable_pbar=disable_pbar, seed=noise_seed
|
|
)
|
|
|
|
# Wrap in latent format
|
|
out = latent_image.copy()
|
|
out["samples"] = samples
|
|
result = (out,)
|
|
|
|
except Exception as e:
|
|
print(f"⚠️ Advanced sampling failed, falling back to standard: {e}")
|
|
logging.warning(f"Advanced sampling failed, falling back to standard: {e}")
|
|
|
|
# Check if it's a memory error
|
|
is_memory_error = "out of memory" in str(e).lower() or "oom" in str(e).lower()
|
|
|
|
if is_memory_error:
|
|
print("💾 Memory error detected - performing async cleanup")
|
|
# Use async cleanup instead of blocking emergency_memory_cleanup()
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
|
|
# Light memory cleanup before fallback
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
|
|
# Fallback to direct sampling with memory safety
|
|
if not is_video_workflow:
|
|
print("🔄 Using fallback sampling method...")
|
|
|
|
try:
|
|
# Use a more conservative approach for fallback
|
|
latent_fallback = latent_image["samples"]
|
|
# Ensure latent on GPU
|
|
if torch.cuda.is_available() and latent_fallback.device.type != 'cuda':
|
|
latent_fallback = latent_fallback.to(model_management.get_torch_device(), non_blocking=True)
|
|
# Prepare noise on same device as latent to avoid None/CPU issues
|
|
batch_inds_fb = latent_image["batch_index"] if "batch_index" in latent_image else None
|
|
noise_fb = comfy.sample.prepare_noise(latent_fallback, noise_seed, batch_inds_fb)
|
|
if torch.cuda.is_available() and noise_fb.device.type != 'cuda':
|
|
noise_fb = noise_fb.to(latent_fallback.device, non_blocking=True)
|
|
samples = comfy.sample.sample(
|
|
model, noise_fb, steps, cfg, sampler_name, scheduler,
|
|
positive, negative, latent_fallback, denoise=denoise,
|
|
start_step=start_at_step, last_step=end_at_step,
|
|
force_full_denoise=force_full_denoise
|
|
)
|
|
# Wrap in latent format
|
|
out = latent_image.copy()
|
|
out["samples"] = samples
|
|
result = (out,)
|
|
except Exception as e2:
|
|
print(f"❌ Fallback sampling also failed: {e2}")
|
|
logging.error(f"Fallback sampling failed: {e2}")
|
|
|
|
# Last resort: try with minimal parameters
|
|
try:
|
|
print("🆘 Attempting minimal sampling...")
|
|
latent_min = latent_image["samples"]
|
|
if torch.cuda.is_available() and latent_min.device.type != 'cuda':
|
|
latent_min = latent_min.to(model_management.get_torch_device(), non_blocking=True)
|
|
batch_inds_min = latent_image["batch_index"] if "batch_index" in latent_image else None
|
|
noise_min = comfy.sample.prepare_noise(latent_min, noise_seed, batch_inds_min)
|
|
if torch.cuda.is_available() and noise_min.device.type != 'cuda':
|
|
noise_min = noise_min.to(latent_min.device, non_blocking=True)
|
|
samples = comfy.sample.sample(
|
|
model, noise_min, min(steps, 10), min(cfg, 7.0), sampler_name, scheduler,
|
|
positive, negative, latent_min, denoise=min(denoise, 0.8),
|
|
start_step=0, last_step=min(steps, 10),
|
|
force_full_denoise=False
|
|
)
|
|
out = latent_image.copy()
|
|
out["samples"] = samples
|
|
result = (out,)
|
|
print("✅ Minimal sampling succeeded")
|
|
except Exception as e3:
|
|
print(f"❌ All sampling attempts failed: {e3}")
|
|
raise e3
|
|
|
|
# OPTIMIZATION: Non-blocking post-sampling cleanup
|
|
if memory_optimization and torch.cuda.is_available():
|
|
torch.cuda.empty_cache() # Async - non-blocking
|
|
# Don't call gentle_memory_cleanup() - it contains torch.cuda.synchronize()
|
|
if not is_video_workflow and DEBUG_MODE:
|
|
print("🧹 Memory cache cleared (async)")
|
|
|
|
sample_time = time.time() - start_time
|
|
if is_video_workflow:
|
|
print(f"✅ Video KSampler completed in {sample_time:.2f}s")
|
|
else:
|
|
print(f"✅ ROCM Advanced KSampler completed in {sample_time:.2f}s")
|
|
logging.info(f"ROCM Advanced KSampler completed in {sample_time:.2f}s")
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
class ROCMSamplerPerformanceMonitor:
|
|
"""
|
|
Monitor sampler performance and provide optimization suggestions
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL", {"tooltip": "Model to analyze"}),
|
|
"test_steps": ("INT", {
|
|
"default": 20,
|
|
"min": 1,
|
|
"max": 100,
|
|
"tooltip": "Number of test steps for benchmarking"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("DEVICE_INFO", "PERFORMANCE_TIPS", "OPTIMAL_SETTINGS")
|
|
FUNCTION = "analyze"
|
|
CATEGORY = "RocM Ninodes/Sampling"
|
|
DESCRIPTION = "Analyze sampler performance and provide optimization recommendations"
|
|
|
|
def analyze(self, model, test_steps=20):
|
|
"""
|
|
Analyze sampler performance and provide recommendations
|
|
"""
|
|
device = model.model_dtype()
|
|
device_info = f"Model device: {device}\n"
|
|
device_info += f"Model dtype: {model.model_dtype()}\n"
|
|
|
|
# Check if it's an AMD GPU
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
if is_amd:
|
|
try:
|
|
device_name = torch.cuda.get_device_name(0)
|
|
device_info += f"GPU: {device_name}\n"
|
|
except:
|
|
device_info += "GPU: AMD (ROCm)\n"
|
|
|
|
# Performance tips
|
|
tips = []
|
|
if is_amd:
|
|
tips.append("• Use fp32 precision for better ROCm performance")
|
|
tips.append("• Enable memory optimization for better VRAM usage")
|
|
tips.append("• Use attention optimization for faster sampling")
|
|
tips.append("• Consider lower CFG values for faster generation")
|
|
tips.append("• Euler and Heun samplers work well with ROCm")
|
|
else:
|
|
tips.append("• Use fp16 or bf16 for better performance")
|
|
tips.append("• Enable all optimizations for best results")
|
|
tips.append("• Higher CFG values generally work better")
|
|
|
|
# Optimal settings
|
|
settings = []
|
|
if is_amd:
|
|
settings.append(f"Recommended precision: fp32")
|
|
settings.append(f"Recommended memory optimization: True")
|
|
settings.append(f"Recommended attention optimization: True")
|
|
settings.append(f"Recommended samplers: euler, heun, dpmpp_2m")
|
|
settings.append(f"Recommended schedulers: simple, normal")
|
|
settings.append(f"Recommended CFG: 7.0-8.0")
|
|
else:
|
|
settings.append(f"Recommended precision: auto")
|
|
settings.append(f"Recommended memory optimization: True")
|
|
settings.append(f"Recommended attention optimization: True")
|
|
settings.append(f"Recommended samplers: dpmpp_2m, dpmpp_sde")
|
|
settings.append(f"Recommended schedulers: normal, karras")
|
|
settings.append(f"Recommended CFG: 8.0-12.0")
|
|
|
|
return (
|
|
device_info,
|
|
"\n".join(tips),
|
|
"\n".join(settings)
|
|
)
|
|
|
|
|
|
class ROCMOptimizedCheckpointLoader:
|
|
"""
|
|
ROCM-optimized checkpoint loader for AMD GPUs (gfx1151)
|
|
|
|
Features:
|
|
- Optimized loading for Flux and WAN models
|
|
- Memory-efficient loading for AMD GPUs
|
|
- Automatic precision optimization
|
|
- Flux-specific optimizations (skip negative CLIP)
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"ckpt_name": (folder_paths.get_filename_list("checkpoints"), {
|
|
"tooltip": "Checkpoint file to load"
|
|
}),
|
|
"lazy_loading": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable lazy loading for better memory usage"
|
|
}),
|
|
"optimize_for_flux": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Optimize loading for Flux models"
|
|
}),
|
|
"precision_mode": (["auto", "fp32", "fp16", "bf16"], {
|
|
"default": "auto",
|
|
"tooltip": "Precision mode - auto detects quantized models and preserves their dtype"
|
|
}),
|
|
"compatibility_mode": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Enable compatibility mode for quantized models (disables optimizations)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
|
|
RETURN_NAMES = ("MODEL", "CLIP", "VAE")
|
|
FUNCTION = "load_checkpoint"
|
|
CATEGORY = "RocM Ninodes/Loaders"
|
|
DESCRIPTION = "ROCM-optimized checkpoint loader for AMD GPUs (gfx1151)"
|
|
|
|
def load_checkpoint(self, ckpt_name, lazy_loading=True, optimize_for_flux=True, precision_mode="auto", compatibility_mode=False):
|
|
"""
|
|
ROCM-optimized checkpoint loading with quantized model support
|
|
"""
|
|
import folder_paths
|
|
import comfy.sd
|
|
import torch
|
|
import os
|
|
|
|
# CRITICAL FIX: Detect quantized models from filename
|
|
is_quantized_model = False
|
|
quantized_indicators = ['fp8', 'int8', 'int4', 'quantized', 'bnb', 'gguf']
|
|
ckpt_name_lower = ckpt_name.lower()
|
|
for indicator in quantized_indicators:
|
|
if indicator in ckpt_name_lower:
|
|
is_quantized_model = True
|
|
print(f"🔍 Detected quantized checkpoint: {ckpt_name}")
|
|
break
|
|
|
|
# Compatibility mode: disable optimizations for quantized models
|
|
if compatibility_mode or is_quantized_model:
|
|
print("🛡️ Compatibility mode enabled - using stock ComfyUI behavior")
|
|
optimize_for_flux = False
|
|
lazy_loading = False
|
|
|
|
try:
|
|
# Force memory defragmentation before loading (skip for quantized models)
|
|
if torch.cuda.is_available() and not is_quantized_model:
|
|
# Check if defragmentation is needed and perform it
|
|
simple_memory_cleanup()
|
|
|
|
# Additional cleanup
|
|
torch.cuda.synchronize()
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
elif is_quantized_model:
|
|
print("🔒 Skipping memory cleanup for quantized model compatibility")
|
|
|
|
# Get checkpoint path
|
|
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
|
|
print(f"📁 Loading checkpoint: {ckpt_name}")
|
|
print(f"📂 Checkpoint path: {ckpt_path}")
|
|
print(f"✅ File exists: {os.path.exists(ckpt_path)}")
|
|
|
|
# Progress indicator for Windows users
|
|
print("🔄 Initializing model loading...")
|
|
|
|
# Check if ROCm is available
|
|
try:
|
|
if torch.cuda.is_available():
|
|
device_name = torch.cuda.get_device_name(0)
|
|
print(f"GPU: {device_name}")
|
|
if "AMD" in device_name or "Radeon" in device_name:
|
|
print("AMD GPU detected - using ROCm optimizations")
|
|
else:
|
|
print("Non-AMD GPU detected - using compatibility mode")
|
|
else:
|
|
print("CUDA not available - using CPU mode")
|
|
except Exception as e:
|
|
print(f"GPU detection failed: {e}")
|
|
print("ROCm not available - running in compatibility mode")
|
|
|
|
# Use ComfyUI's standard loading - this is the most reliable approach
|
|
print("📦 Loading model components...")
|
|
print("⏳ Large models may take 2-6 minutes to load (this is normal)")
|
|
print("🔄 Please be patient, especially on first run...")
|
|
|
|
out = comfy.sd.load_checkpoint_guess_config(
|
|
ckpt_path,
|
|
output_vae=True,
|
|
output_clip=True,
|
|
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
|
)
|
|
print("✅ Model loading completed")
|
|
|
|
# Validate the output
|
|
if len(out) < 3:
|
|
raise ValueError(f"Checkpoint loading returned {len(out)} items, expected 3")
|
|
|
|
model, clip, vae = out[:3]
|
|
print(f"Model loaded: {type(model)}")
|
|
print(f"CLIP loaded: {type(clip)}")
|
|
print(f"VAE loaded: {type(vae)}")
|
|
|
|
# CRITICAL FIX: Validate that CLIP is not None
|
|
if clip is None:
|
|
raise ValueError(f"CLIP model is None - checkpoint {ckpt_name} may not contain a valid CLIP model")
|
|
|
|
# Additional validation for Windows compatibility
|
|
if not hasattr(clip, 'encode'):
|
|
raise ValueError(f"CLIP model does not have encode method - invalid CLIP model")
|
|
|
|
print(f"CLIP validation passed: {type(clip)}")
|
|
|
|
return (model, clip, vae)
|
|
|
|
except Exception as e:
|
|
print(f"ROCM checkpoint loading failed: {e}")
|
|
print("Attempting fallback to standard ComfyUI loading...")
|
|
# Fallback to standard loading
|
|
try:
|
|
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
|
|
out = comfy.sd.load_checkpoint_guess_config(
|
|
ckpt_path,
|
|
output_vae=True,
|
|
output_clip=True,
|
|
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
|
)
|
|
|
|
# Validate fallback output
|
|
if len(out) < 3:
|
|
raise ValueError(f"Fallback loading returned {len(out)} items, expected 3")
|
|
|
|
model, clip, vae = out[:3]
|
|
|
|
# Validate CLIP in fallback
|
|
if clip is None:
|
|
raise ValueError(f"Fallback CLIP model is None - checkpoint {ckpt_name} may not contain a valid CLIP model")
|
|
|
|
if not hasattr(clip, 'encode'):
|
|
raise ValueError(f"Fallback CLIP model does not have encode method - invalid CLIP model")
|
|
|
|
print("Fallback loading successful")
|
|
return (model, clip, vae)
|
|
except Exception as e2:
|
|
print(f"Fallback loading failed: {e2}")
|
|
# Last resort: try without ROCm optimizations
|
|
try:
|
|
print("Attempting loading without any optimizations...")
|
|
ckpt_path = folder_paths.get_full_path_or_raise("checkpoints", ckpt_name)
|
|
out = comfy.sd.load_checkpoint_guess_config(
|
|
ckpt_path,
|
|
output_vae=True,
|
|
output_clip=True,
|
|
embedding_directory=folder_paths.get_folder_paths("embeddings")
|
|
)
|
|
print("Minimal loading successful")
|
|
return out[:3]
|
|
except Exception as e3:
|
|
print(f"All loading attempts failed: {e3}")
|
|
raise e3
|
|
|
|
|
|
class ROCMFluxBenchmark:
|
|
"""
|
|
Comprehensive Flux workflow benchmark for AMD GPUs
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL", {"tooltip": "The model to benchmark"}),
|
|
"vae": ("VAE", {"tooltip": "The VAE model"}),
|
|
"clip": ("CLIP", {"tooltip": "The CLIP model"}),
|
|
"test_resolutions": ("STRING", {
|
|
"default": "256x320,512x512,1024x1024",
|
|
"tooltip": "Comma-separated resolutions to test (WxH)"
|
|
}),
|
|
"test_steps": ("INT", {
|
|
"default": 20,
|
|
"min": 1,
|
|
"max": 100,
|
|
"tooltip": "Number of sampling steps for testing"
|
|
}),
|
|
"test_cfg_values": ("STRING", {
|
|
"default": "1.0,3.5,8.0",
|
|
"tooltip": "Comma-separated CFG values to test"
|
|
}),
|
|
"test_hipblas": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Test HIPBlas optimizations"
|
|
}),
|
|
"generate_report": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Generate detailed optimization report"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("BENCHMARK_RESULTS", "PERFORMANCE_CHART", "OPTIMIZATION_RECOMMENDATIONS", "MEMORY_ANALYSIS")
|
|
FUNCTION = "benchmark"
|
|
CATEGORY = "RocM Ninodes/Benchmark"
|
|
DESCRIPTION = "Comprehensive Flux workflow benchmark for AMD GPUs"
|
|
|
|
def benchmark(self, model, vae, clip, test_resolutions="256x320,512x512,1024x1024",
|
|
test_steps=20, test_cfg_values="1.0,3.5,8.0", test_hipblas=True,
|
|
generate_report=True):
|
|
"""
|
|
Run comprehensive benchmark tests
|
|
"""
|
|
import time
|
|
import torch
|
|
import gc
|
|
|
|
# Parse test parameters
|
|
resolutions = []
|
|
for res in test_resolutions.split(','):
|
|
w, h = map(int, res.strip().split('x'))
|
|
resolutions.append((w, h))
|
|
|
|
cfg_values = [float(cfg.strip()) for cfg in test_cfg_values.split(',')]
|
|
|
|
# Get device information
|
|
device = comfy.model_management.get_torch_device()
|
|
is_amd = hasattr(device, 'type') and device.type == 'cuda'
|
|
|
|
results = {
|
|
'device': str(device),
|
|
'is_amd': is_amd,
|
|
'resolutions': {},
|
|
'cfg_tests': {},
|
|
'memory_usage': {},
|
|
'recommendations': []
|
|
}
|
|
|
|
# Test each resolution
|
|
for w, h in resolutions:
|
|
print(f"Testing resolution {w}x{h}...")
|
|
resolution_key = f"{w}x{h}"
|
|
results['resolutions'][resolution_key] = {
|
|
'times': [],
|
|
'memory_peak': 0,
|
|
'memory_avg': 0
|
|
}
|
|
|
|
# Test with different CFG values
|
|
for cfg in cfg_values:
|
|
print(f" Testing CFG {cfg}...")
|
|
start_time = time.time()
|
|
|
|
# Clear memory
|
|
torch.cuda.empty_cache()
|
|
gc.collect()
|
|
|
|
# Create test latent
|
|
latent = torch.randn(1, 4, h//8, w//8, device=device)
|
|
|
|
# Test VAE decode
|
|
try:
|
|
with torch.no_grad():
|
|
decoded = vae.decode(latent)
|
|
decode_time = time.time() - start_time
|
|
|
|
# Record results
|
|
results['resolutions'][resolution_key]['times'].append(decode_time)
|
|
results['resolutions'][resolution_key]['memory_peak'] = max(
|
|
results['resolutions'][resolution_key]['memory_peak'],
|
|
torch.cuda.max_memory_allocated() / 1024**3
|
|
)
|
|
|
|
print(f" VAE decode time: {decode_time:.2f}s")
|
|
|
|
except Exception as e:
|
|
print(f" Error testing {w}x{h} CFG {cfg}: {e}")
|
|
results['resolutions'][resolution_key]['times'].append(float('inf'))
|
|
|
|
# Generate recommendations
|
|
if is_amd:
|
|
results['recommendations'].extend([
|
|
"• Use fp32 precision for best ROCm performance",
|
|
"• Enable memory optimization for better VRAM usage",
|
|
"• Use Euler or Heun samplers for optimal speed",
|
|
"• Consider lower CFG values (3.5-7.0) for faster generation",
|
|
"• Use tile size 768-1024 for optimal memory/speed balance"
|
|
])
|
|
else:
|
|
results['recommendations'].extend([
|
|
"• Use fp16 or bf16 for better performance",
|
|
"• Enable all available optimizations",
|
|
"• Use DPM++ 2M sampler for best quality/speed",
|
|
"• Higher CFG values (8.0-12.0) generally work better",
|
|
"• Use tile size 512-768 for optimal performance"
|
|
])
|
|
|
|
# Format results
|
|
benchmark_text = f"Benchmark Results for {results['device']}\n"
|
|
benchmark_text += f"AMD GPU: {results['is_amd']}\n\n"
|
|
|
|
for res_key, res_data in results['resolutions'].items():
|
|
if res_data['times']:
|
|
avg_time = sum(t for t in res_data['times'] if t != float('inf')) / len([t for t in res_data['times'] if t != float('inf')])
|
|
benchmark_text += f"{res_key}: {avg_time:.2f}s avg, {res_data['memory_peak']:.1f}GB peak\n"
|
|
|
|
performance_chart = "Performance Chart:\n"
|
|
for res_key, res_data in results['resolutions'].items():
|
|
if res_data['times']:
|
|
times_str = ", ".join(f"{t:.2f}s" for t in res_data['times'] if t != float('inf'))
|
|
performance_chart += f"{res_key}: [{times_str}]\n"
|
|
|
|
recommendations_text = "Optimization Recommendations:\n"
|
|
for rec in results['recommendations']:
|
|
recommendations_text += f"{rec}\n"
|
|
|
|
memory_analysis = f"Memory Analysis:\n"
|
|
memory_analysis += f"Device: {results['device']}\n"
|
|
memory_analysis += f"Total VRAM: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.1f}GB\n"
|
|
memory_analysis += f"Current VRAM usage: {torch.cuda.memory_allocated() / 1024**3:.1f}GB\n"
|
|
|
|
return (benchmark_text, performance_chart, recommendations_text, memory_analysis)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class ROCMMemoryOptimizer:
|
|
"""
|
|
Memory optimization helper for AMD GPUs
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"optimization_level": (["conservative", "balanced", "aggressive"], {
|
|
"default": "balanced",
|
|
"tooltip": "Memory optimization level"
|
|
}),
|
|
"enable_gc": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Enable garbage collection"
|
|
}),
|
|
"clear_cache": ("BOOLEAN", {
|
|
"default": True,
|
|
"tooltip": "Clear CUDA cache"
|
|
}),
|
|
"cleanup_frequency": ("INT", {
|
|
"default": 10,
|
|
"min": 1,
|
|
"max": 100,
|
|
"tooltip": "Cleanup frequency (operations between cleanups)"
|
|
})
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("MEMORY_STATUS", "OPTIMIZATION_LOG", "RECOMMENDATIONS")
|
|
FUNCTION = "optimize_memory"
|
|
CATEGORY = "RocM Ninodes/Memory"
|
|
DESCRIPTION = "Memory optimization helper for AMD GPUs"
|
|
|
|
def __init__(self):
|
|
self.operation_count = 0
|
|
|
|
def optimize_memory(self, optimization_level="balanced", enable_gc=True,
|
|
clear_cache=True, cleanup_frequency=10):
|
|
"""
|
|
Optimize memory usage for AMD GPUs
|
|
"""
|
|
import torch
|
|
import gc
|
|
|
|
self.operation_count += 1
|
|
|
|
# Get current memory status
|
|
if torch.cuda.is_available():
|
|
allocated = torch.cuda.memory_allocated() / 1024**3
|
|
reserved = torch.cuda.memory_reserved() / 1024**3
|
|
total = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
|
free = total - reserved
|
|
|
|
memory_status = f"Memory Status:\n"
|
|
memory_status += f"Allocated: {allocated:.2f}GB\n"
|
|
memory_status += f"Reserved: {reserved:.2f}GB\n"
|
|
memory_status += f"Free: {free:.2f}GB\n"
|
|
memory_status += f"Total: {total:.2f}GB\n"
|
|
else:
|
|
memory_status = "CUDA not available - no GPU memory to optimize"
|
|
|
|
# Perform optimizations
|
|
optimization_log = f"Optimization Log (Level: {optimization_level}):\n"
|
|
|
|
if enable_gc and self.operation_count % cleanup_frequency == 0:
|
|
gc.collect()
|
|
optimization_log += "✓ Garbage collection performed\n"
|
|
|
|
if clear_cache and self.operation_count % cleanup_frequency == 0:
|
|
if torch.cuda.is_available():
|
|
torch.cuda.empty_cache()
|
|
optimization_log += "✓ CUDA cache cleared\n"
|
|
|
|
# Level-specific optimizations
|
|
if optimization_level == "aggressive":
|
|
if torch.cuda.is_available():
|
|
torch.cuda.synchronize()
|
|
optimization_log += "✓ CUDA synchronization performed\n"
|
|
|
|
# Generate recommendations
|
|
recommendations = "Memory Optimization Recommendations:\n"
|
|
if torch.cuda.is_available():
|
|
allocated = torch.cuda.memory_allocated() / 1024**3
|
|
total = torch.cuda.get_device_properties(0).total_memory / 1024**3
|
|
|
|
if allocated / total > 0.8:
|
|
recommendations += "⚠ High memory usage detected\n"
|
|
recommendations += "• Consider reducing batch size\n"
|
|
recommendations += "• Use lower resolution\n"
|
|
recommendations += "• Enable aggressive optimization\n"
|
|
elif allocated / total > 0.6:
|
|
recommendations += "• Memory usage is moderate\n"
|
|
recommendations += "• Consider balanced optimization\n"
|
|
else:
|
|
recommendations += "✓ Memory usage is good\n"
|
|
recommendations += "• Current settings are optimal\n"
|
|
|
|
return (memory_status, optimization_log, recommendations)
|
|
|
|
|
|
# Node mappings
|
|
class ROCMLoRALoader:
|
|
"""
|
|
ROCM-optimized LoRA loader with aggressive memory management to prevent fragmentation.
|
|
Specifically designed to handle LoRA loading operations that cause OOM errors.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model": ("MODEL", {"tooltip": "The model to apply LoRA to"}),
|
|
"lora_name": (folder_paths.get_filename_list("loras"), {"tooltip": "LoRA file to load"}),
|
|
"strength_model": ("FLOAT", {
|
|
"default": 1.0,
|
|
"min": -10.0,
|
|
"max": 10.0,
|
|
"step": 0.01,
|
|
"tooltip": "Strength of LoRA effect on model"
|
|
}),
|
|
"strength_clip": ("FLOAT", {
|
|
"default": 1.0,
|
|
"min": -10.0,
|
|
"max": 10.0,
|
|
"step": 0.01,
|
|
"tooltip": "Strength of LoRA effect on CLIP"
|
|
}),
|
|
},
|
|
"optional": {
|
|
"clip": ("CLIP", {"tooltip": "CLIP model to apply LoRA to"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "CLIP")
|
|
RETURN_NAMES = ("MODEL", "CLIP")
|
|
FUNCTION = "load_lora"
|
|
CATEGORY = "RocM Ninodes/Loaders"
|
|
DESCRIPTION = "ROCM-optimized LoRA loader with aggressive memory management"
|
|
|
|
def load_lora(self, model, lora_name, strength_model, strength_clip, clip=None):
|
|
"""
|
|
Load LoRA with aggressive memory management to prevent fragmentation
|
|
"""
|
|
print(f"🔄 ROCM LoRA Loader: Loading {lora_name} with strengths {strength_model}/{strength_clip}")
|
|
|
|
# Pre-loading gentle memory cleanup
|
|
if torch.cuda.is_available():
|
|
print("🧹 Pre-loading memory cleanup...")
|
|
gentle_memory_cleanup()
|
|
|
|
# Log memory state before LoRA loading
|
|
allocated_memory = torch.cuda.memory_allocated(0)
|
|
reserved_memory = torch.cuda.memory_reserved(0)
|
|
fragmentation = reserved_memory - allocated_memory
|
|
|
|
print(f"Memory before LoRA loading: {allocated_memory/1024**3:.2f}GB allocated, {reserved_memory/1024**3:.2f}GB reserved")
|
|
print(f"Fragmentation: {fragmentation/1024**2:.1f}MB")
|
|
|
|
try:
|
|
# Import ComfyUI's LoRA loading functions
|
|
from comfy.lora import load_lora
|
|
from comfy.model_patcher import ModelPatcher
|
|
|
|
# Load LoRA with aggressive memory management
|
|
lora_path = folder_paths.get_full_path("loras", lora_name)
|
|
if lora_path is None:
|
|
raise FileNotFoundError(f"LoRA file not found: {lora_name}")
|
|
|
|
print(f"📁 Loading LoRA from: {lora_path}")
|
|
|
|
# Apply gentle memory cleanup before each major operation
|
|
if torch.cuda.is_available():
|
|
gentle_memory_cleanup()
|
|
|
|
# Load the LoRA
|
|
lora = load_lora(lora_path)
|
|
|
|
# Apply gentle memory cleanup after loading
|
|
if torch.cuda.is_available():
|
|
gentle_memory_cleanup()
|
|
|
|
# Apply LoRA to model with memory management
|
|
model_lora, clip_lora = lora.apply_to_model(model, clip, strength_model, strength_clip)
|
|
|
|
# Post-loading gentle memory cleanup
|
|
if torch.cuda.is_available():
|
|
print("🧹 Post-loading memory cleanup...")
|
|
gentle_memory_cleanup()
|
|
|
|
# Log memory state after LoRA loading
|
|
allocated_memory_after = torch.cuda.memory_allocated(0)
|
|
reserved_memory_after = torch.cuda.memory_reserved(0)
|
|
fragmentation_after = reserved_memory_after - allocated_memory_after
|
|
|
|
print(f"Memory after LoRA loading: {allocated_memory_after/1024**3:.2f}GB allocated, {reserved_memory_after/1024**3:.2f}GB reserved")
|
|
print(f"Fragmentation: {fragmentation_after/1024**2:.1f}MB")
|
|
|
|
print(f"✅ ROCM LoRA Loader: Successfully loaded {lora_name}")
|
|
return (model_lora, clip_lora)
|
|
|
|
except Exception as e:
|
|
print(f"❌ ROCM LoRA Loader: Failed to load {lora_name}: {e}")
|
|
|
|
# Emergency memory cleanup on failure
|
|
if torch.cuda.is_available():
|
|
print("🚨 Emergency memory cleanup...")
|
|
gentle_memory_cleanup()
|
|
|
|
raise e
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ROCMOptimizedCheckpointLoader": ROCMOptimizedCheckpointLoader,
|
|
"ROCMOptimizedVAEDecode": ROCMOptimizedVAEDecode,
|
|
"ROCMOptimizedVAEDecodeTiled": ROCMOptimizedVAEDecodeTiled,
|
|
"ROCMVAEPerformanceMonitor": ROCMVAEPerformanceMonitor,
|
|
"ROCMOptimizedKSampler": ROCMOptimizedKSampler,
|
|
"ROCMOptimizedKSamplerAdvanced": ROCMOptimizedKSamplerAdvanced,
|
|
"ROCMSamplerPerformanceMonitor": ROCMSamplerPerformanceMonitor,
|
|
"ROCMFluxBenchmark": ROCMFluxBenchmark,
|
|
"ROCMMemoryOptimizer": ROCMMemoryOptimizer,
|
|
"ROCMLoRALoader": ROCMLoRALoader,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ROCMOptimizedCheckpointLoader": "ROCM Checkpoint Loader",
|
|
"ROCMOptimizedVAEDecode": "ROCM VAE Decode",
|
|
"ROCMOptimizedVAEDecodeTiled": "ROCM VAE Decode Tiled",
|
|
"ROCMVAEPerformanceMonitor": "ROCM VAE Performance Monitor",
|
|
"ROCMOptimizedKSampler": "ROCM KSampler",
|
|
"ROCMOptimizedKSamplerAdvanced": "ROCM KSampler Advanced",
|
|
"ROCMSamplerPerformanceMonitor": "ROCM Sampler Performance Monitor",
|
|
"ROCMFluxBenchmark": "ROCM Flux Benchmark",
|
|
"ROCMMemoryOptimizer": "ROCM Memory Optimizer",
|
|
"ROCMLoRALoader": "ROCM LoRA Loader",
|
|
}
|