Files
obisin-ComfyUI-DGLS/dynamic_swapping_loader.py
T
2025-08-19 10:25:49 +01:00

1959 lines
82 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
PROFILE = False
import comfy.sd
import folder_paths
import datetime
from functools import wraps
from torch import nn
import time
import torch
import gc
import threading
import warnings
import psutil
import types
import torch._dynamo
from functools import partial
import torch.utils.checkpoint
from collections import defaultdict
import bitsandbytes as bnb
if PROFILE == True:
try:
import torch.profiler
import torch.cuda.nvtx as nvtx
except ImportError:
PROFILE = False
nvtx = None
"""
Dynamic GPU Layer Swapping for ComfyUI by obisin
======================================
Enhanced inference with automatic memory optimization for diffusion models.
Supports Cosmos, Flux, Wan2.1, Wan2.2, HunyuanVideo, and generic transformer blocks.
PERFORMANCE DESIGN NOTE:
This file uses module-level functions with local variable binding for optimal performance.
Hot path functions bind frequently-accessed attributes to locals at function start (LOAD_FAST)
to avoid Python's attribute lookup overhead (LOAD_ATTR). Call chains are minimized to
reduce function call overhead.
Architecture:
- Globals: Intentionally used for performance (documented below)
- Hot functions: Bind globals to locals once per call
- Minimal call depth: Direct function calls without abstraction layers
- Factory pattern: Clean call sites with fast execution
CRITICAL PATH: Functions marked "HOT PATH" - profile before changing!
Author: obisin
"""
class Args:
def __init__(self):
# Set defaults - these will be overridden by ComfyUI node
self.dynamic_swapping = True
self.initial_gpu_layers = 1 # Number of initial layers to keep permanently on GPU
self.final_gpu_layers = 1 # Number of final layers to keep permanently on GPU
self.gpu_layers = None # Comma-separated list of layer indices to keep permanently on GPU
self.prefetch = 0 # Number of layers to prefetch ahead (0=off, 1=single layer, 2+=multiple layers)
self.threading = False # Enable background threading for automatic layer management
self.cuda_streams = False # Enable CUDA streams for copy-compute overlap (needs more VRAM)
self.batch_move = False # Use batch layer moving (experimental, may cause device issues)
self.cast_target = None # Cast FROM dtype TO dtype at start-up (e.g., f32 bf16)
self.selective_packing = 0 # Size threshold in MB for using packed transfers (default: 64MB)
self.event_sync = False # Use CUDA events instead of torch.cuda.synchronize() for better performance
self.verbose = False # Enable verbose output with detailed timing and transfer information
self.device_sync_mode = 'off' # Not needed for inference
self.sync_only_on_resume = False
self.compile = False
self.autocast = None
self.mixed_precision = 'auto'
self.compute_casting = "disabled"
args = Args()
TOOLTIPS = {
"dynamic_swapping": "Smart dynamic layer swapping between GPU and CPU for optimal performance",
"initial_gpu_layers": "Number of initial layers to keep permanently on GPU. If not specified, uses reasonable defaults based on estimated VRAM",
"final_gpu_layers": "Number of final layers to keep permanently on GPU",
"gpu_layers": "Comma-separated list of layer indices to keep permanently on GPU (e.g., '0,1,2,14,18,19,20,21,22'). Overrides initial_gpu_layers and final_gpu_layers",
"prefetch": "Number of layers to prefetch ahead (0=off, 1=single layer, 2+=multiple layers), might not work with mixed layer type models",
"threading": "Enable background threading for automatic layer management",
"cuda_streams": "Enable CUDA streams for copy-compute overlap (needs more VRAM)",
"batch_move": "Use batch layer moving (experimental, may cause device issues)",
"cast_target": "Cast FROM dtype TO dtype at start-up (e.g., f32 bf16) choices=[f32, bf16, f16, f8_e4m3, f8_e5m2, nf4, fp4]", #DISABLED DUE TO COMFY AUTOCAST
"selective_packing": "Size threshold in MB for using packed transfers (default: 64MB)",
"event_sync": "Use CUDA events instead of torch.cuda.synchronize() for better performance",
"compile": "Enable torch.compile optimization for permanent GPU layers (experimental)",
"verbose": "Enable verbose output with detailed timing and transfer information"
}
# =============================================================================
# GLOBAL VARIABLES
# =============================================================================
swap_stats = {'to_gpu': 0, 'to_cpu': 0}
transfer_events = {}
transfer_stats = {
'to_gpu_times': [], 'to_cpu_times': [], 'to_gpu_speeds': [], 'to_cpu_speeds': [],
'current_step_gpu_times': [], 'current_step_cpu_times': [],
'current_step_gpu_speeds': [], 'current_step_cpu_speeds': []
}
device_cache = None
packed_layers = {}
layer_sizes_mb = {}
# VERBOSE = True
GPU_DEVICE = 'cuda'
CPU_DEVICE = 'cpu'
layers = None
cpu_swappable_layers = set()
gpu_resident_layers = set()
GPU_DEVICE = 'cuda' if torch.cuda.is_available() else 'cpu'
CPU_DEVICE = 'cpu'
print(f"Device configuration: GPU={GPU_DEVICE}, CPU={CPU_DEVICE}")
casting_handler = None
# =============================================================================
# CORE FUNCTIONS by obisin
# =============================================================================
def fix_inference_tensor_parameters(layer):
"""Fix inference tensor parameters in a layer if needed"""
# Quick check if ANY parameters need fixing
needs_fixing = False
with torch.no_grad():
for name, param in layer.named_parameters():
if param is not None:
try:
_ = param._version
except (RuntimeError, AttributeError):
needs_fixing = True
break
# Only fix if needed
if needs_fixing:
with torch.no_grad():
for name, param in layer.named_parameters():
if param is not None:
try:
_ = param._version
except (RuntimeError, AttributeError):
param.data = param.data.detach().clone()
return needs_fixing
def get_cached_layer_size_mb(idx):
return layer_sizes_mb.get(idx, 0)
def event_based_sync(operation_name, idx=None):
"""Replace torch.cuda.synchronize() with specific event tracking"""
if args.event_sync:
event = torch.cuda.Event()
event.record()
key = f"{operation_name}_{idx}" if idx is not None else operation_name
transfer_events[key] = event
return event
else:
torch.cuda.synchronize()
return None
class LayerDeviceCache:
def __init__(self, model, layers): # Add layers parameter
self.cache = {}
self.dirty = set()
# Initialize cache using the layers list
for i, layer in enumerate(layers):
self.cache[i] = self.get_layer_device(layer)
def get_layer_device(self, layer):
"""Your existing function"""
try:
return next(layer.parameters()).device
except StopIteration:
return torch.device('cpu')
def get_device(self, layer_idx): # Simplified - no need for model parameter
"""Fast cached lookup"""
if layer_idx in self.dirty:
if layers is not None and layer_idx < len(layers):
self.cache[layer_idx] = self.get_layer_device(layers[layer_idx])
self.dirty.remove(layer_idx)
else:
return torch.device('cpu') # Safe fallback
return self.cache.get(layer_idx, torch.device('cpu'))
def mark_moved(self, layer_idx, new_device):
"""Update cache when we move a layer"""
self.cache[layer_idx] = new_device
def safe_move_to_cpu(layer, idx):
"""Move layer back to CPU and clean up GPU memory"""
if PROFILE:
nvtx.range_push(f"CPU_Transfer_L{idx}")
try:
try:
current_device = next(layer.parameters()).device
if current_device.type == 'cpu':
return # Already on CPU
except StopIteration:
# Layer has no parameters
pass
transfer_time = 0
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
layer.to(CPU_DEVICE)
add_smart_swapping_to_layer.cleanup_stream = torch.cuda.Stream()
swap_stats['to_cpu'] += 1
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_cpu_times'].append(transfer_time)
transfer_stats['current_step_cpu_speeds'].append(speed_mbps)
device_cache.mark_moved(idx, torch.device('cpu'))
# if idx % 10 == 0:
# print(f" Layer {idx} → CPU, memory: {torch.cuda.memory_allocated(0) / 1e9:.1f}GB")
return True
finally:
if PROFILE:
nvtx.range_pop()
def safe_move_to_gpu(layer, idx):
"""Move layer to GPU with dtype casting"""
if PROFILE:
nvtx.range_push(f"GPU_Transfer_L{idx}")
try:
try:
current_device = device_cache.get_device(idx)
if current_device.type == 'cuda':
return True
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
layer.to(GPU_DEVICE, non_blocking=True)
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_gpu_times'].append(transfer_time)
transfer_stats['current_step_gpu_speeds'].append(speed_mbps)
event_based_sync("gpu_transfer", idx)
device_cache.mark_moved(idx, torch.device('cuda'))
swap_stats['to_gpu'] += 1
return True
except RuntimeError as e:
if "out of memory" in str(e):
return False
raise e
finally:
if PROFILE:
nvtx.range_pop()
# THREADING: Thread-safe GPU operations
def safe_move_to_gpu_threaded(layer, idx):
"""Thread-safe move to GPU"""
with add_smart_swapping_to_layer.gpu_lock:
if device_cache.get_device(idx).type == 'cpu':
return safe_move_to_gpu(layer, idx)
return True
def safe_move_to_cpu_threaded(layer, idx):
"""Thread-safe move to CPU"""
with add_smart_swapping_to_layer.gpu_lock:
if layers is not None and device_cache is not None and device_cache.get_device(idx).type == 'cuda':
return safe_move_to_cpu(layer, idx)
return True
def stop_background_threading():
"""Stop the background threading system"""
if hasattr(add_smart_swapping_to_layer, 'training_active'):
add_smart_swapping_to_layer.training_active = False
if add_smart_swapping_to_layer.background_thread:
add_smart_swapping_to_layer.background_thread.join(timeout=1.0)
print(" Background threading stopped")
def calculate_needed_layers(layer_idx, prefetch):
needed = set()
needed.add(layer_idx)
if layers is None:
return needed
for i in range(1, prefetch + 1):
prefetch_idx = layer_idx + i
# CIRCULAR: If we go past the end, wrap to the beginning of swappable layers
if prefetch_idx >= len(layers):
prefetch_idx = min(cpu_swappable_layers) + (prefetch_idx - len(layers))
if prefetch_idx in cpu_swappable_layers:
needed.add(prefetch_idx)
return needed
def cleanup_excess_layers(keep_layers):
"""Remove layers from GPU that are not in keep_layers set"""
# current_step = getattr(add_smart_swapping_to_layer, 'current_sampling_step', 0)
if PROFILE:
nvtx.range_push(f"Cleanup_Excess_{len(cpu_swappable_layers - keep_layers)}layers")
try:
if args.batch_move:
# Batch approach (your current code)
layers_to_remove = []
for idx in cpu_swappable_layers:
if (idx < len(layers) and
idx not in keep_layers and
device_cache.get_device(idx).type == 'cuda'):
layers_to_remove.append(idx)
cleaned_count = batch_safe_move_to_cpu(layers_to_remove)
else:
# Individual approach (fallback)
cleaned_count = 0
layers_to_remove = []
for idx in cpu_swappable_layers:
if (idx < len(layers) and
idx not in keep_layers and
device_cache.get_device(idx).type == 'cuda'):
layers_to_remove.append(idx)
safe_move_to_cpu(layers[idx], idx)
cleaned_count += 1
return cleaned_count
finally:
if PROFILE:
nvtx.range_pop()
def fetch_missing_layers(needed_layers):
"""Ensure all needed layers are on GPU"""
current_step = getattr(add_smart_swapping_to_layer, 'current_sampling_step', 0)
layers_to_fetch = [idx for idx in needed_layers
if (idx < len(layers) and
idx in cpu_swappable_layers and
device_cache.get_device(idx).type == 'cpu')]
if PROFILE:
nvtx.range_push(f"Fetch_Missing_{len(needed_layers)}layers")
try:
# Your existing fetch logic...
fetched_count = 0
if args.batch_move:
fetched_count = batch_safe_move_to_gpu(layers_to_fetch)
else:
for idx in layers_to_fetch:
success = safe_move_to_gpu(layers[idx], idx)
if success:
fetched_count += 1
return fetched_count
finally:
if PROFILE:
nvtx.range_pop()
def batch_safe_move_to_gpu_packed(layer_indices, threshold_mb=64):
"""Batch move to GPU with selective packing"""
if not layer_indices:
return 0
moved_count = 0
large_layers = []
small_layers = []
# Separate layers by size
for idx in layer_indices:
if (idx < len(layers) and
device_cache.get_device(idx).type == 'cpu'):
layer_size_mb = get_cached_layer_size_mb(idx)
if layer_size_mb > threshold_mb and idx in packed_layers:
large_layers.append(idx)
else:
small_layers.append(idx)
# Handle large layers with packed transfer
if large_layers:
for idx in large_layers:
success = safe_move_to_gpu_packed(layers[idx], idx)
if success:
moved_count += 1
# Handle small layers with direct transfer
if small_layers:
for idx in small_layers:
success = safe_move_to_gpu(layers[idx], idx)
if success:
moved_count += 1
return moved_count
def batch_safe_move_to_cpu_packed(layer_indices, threshold_mb=64):
"""Batch move to CPU with selective packing"""
if not layer_indices:
return 0
moved_count = 0
large_layers = []
small_layers = []
# Separate layers by size
for idx in layer_indices:
if (idx < len(layers) and
device_cache.get_device(idx).type == 'cuda'):
layer_size_mb = get_cached_layer_size_mb(idx)
if layer_size_mb > threshold_mb:
large_layers.append(idx)
else:
small_layers.append(idx)
# Handle large layers with packed transfer
if large_layers:
for idx in large_layers:
success = safe_move_to_cpu_packed(layers[idx], idx)
if success:
moved_count += 1
# Handle small layers with direct transfer
if small_layers:
for idx in small_layers:
success = safe_move_to_cpu(layers[idx], idx)
if success:
moved_count += 1
return moved_count
def cleanup_excess_layers_packed(keep_layers, threshold_mb=64):
"""Remove layers from GPU with selective packing awareness"""
if args.batch_move:
# Collect all layers to remove, then use batch function
layers_to_remove = []
for idx in cpu_swappable_layers:
if (idx < len(layers) and
idx not in keep_layers and
device_cache.get_device(idx).type == 'cuda'):
layers_to_remove.append(idx)
return batch_safe_move_to_cpu_packed(layers_to_remove, threshold_mb)
else:
# Individual processing
moved_count = 0
for idx in cpu_swappable_layers:
if (idx < len(layers) and
idx not in keep_layers and
device_cache.get_device(idx).type == 'cuda'):
layer_size_mb = get_cached_layer_size_mb(idx)
if layer_size_mb > threshold_mb:
success = safe_move_to_cpu_packed(layers[idx], idx)
else:
success = safe_move_to_cpu(layers[idx], idx)
if success:
moved_count += 1
return moved_count
def fetch_missing_layers_packed(needed_layers, threshold_mb=64):
"""Use packed transfers only for large layers, direct for small ones"""
if args.batch_move:
# Use batch function
return batch_safe_move_to_gpu_packed(needed_layers, threshold_mb)
else:
# Individual processing
fetched_count = 0
for idx in needed_layers:
if (idx < len(layers) and
idx in cpu_swappable_layers and
device_cache.get_device(idx).type == 'cpu'):
layer_size_mb = get_cached_layer_size_mb(idx)
if layer_size_mb > threshold_mb and idx in packed_layers:
success = safe_move_to_gpu_packed(layers[idx], idx)
else:
success = safe_move_to_gpu(layers[idx], idx)
if success:
fetched_count += 1
return fetched_count
def background_layer_manager():
"""Background thread that maintains sliding window of layers"""
last_seen_step = -1
while add_smart_swapping_to_layer.training_active:
try:
# Detect new sampling step - reset and be extra aggressive
current_step = getattr(add_smart_swapping_to_layer, 'current_sampling_step', 0)
current_idx = add_smart_swapping_to_layer.current_layer_idx
if current_step != last_seen_step:
# print(f"🔄 Background thread detected step {current_step}, preloading early layers")
last_seen_step = current_step
# Preload for new step using actual prefetch setting
first_swappable = min(cpu_swappable_layers)
next_step_needed = calculate_needed_layers(first_swappable, args.prefetch)
for layer_idx in next_step_needed:
if device_cache.get_device(layer_idx).type == 'cpu':
safe_move_to_gpu_threaded(layers[layer_idx], layer_idx)
#OVERLY AGGRESSIVE DSEBUG VERSION
# first_swappable = min(cpu_swappable_layers)
# if device_cache.get_device(first_swappable).type == 'cpu':
# # print(f"🎯 Pre-warming first swappable layer {first_swappable}")
# safe_move_to_gpu_threaded(layers[first_swappable], first_swappable)
#
# # Aggressively preload first few layers for new step
# for early_idx in range(min(5, len(layers))): # First 5 layers
# if (early_idx in cpu_swappable_layers and
# device_cache.get_device(early_idx).type == 'cpu'):
# safe_move_to_gpu_threaded(layers[early_idx], early_idx)
if device_cache is None or layers is None:
time.sleep(0.1) # Wait for initialization
continue
if PROFILE:
nvtx.range_push("Background_Manager_Cycle")
current_idx = add_smart_swapping_to_layer.current_layer_idx
needed_layers = set()
for future_idx in range(current_idx, min(current_idx + args.prefetch + 2, len(layers))):
if future_idx in cpu_swappable_layers:
needed_layers.add(future_idx)
# Also include the standard prefetch calculation
standard_needed = calculate_needed_layers(current_idx, args.prefetch)
needed_layers.update(standard_needed)
# needed_layers = calculate_needed_layers(current_idx, args.prefetch)
# Collect layers to move instead of moving one by one
layers_to_cpu = []
layers_to_gpu = []
if args.batch_move:
# One in, one out: remove old layers, add new ones
for idx in cpu_swappable_layers:
if idx not in needed_layers and device_cache.get_device(idx).type == 'cuda':
layers_to_cpu.append(idx)
for idx in needed_layers:
if device_cache.get_device(idx).type == 'cpu':
layers_to_gpu.append(idx)
# Batch move with thread safety
if layers_to_cpu or layers_to_gpu:
with add_smart_swapping_to_layer.gpu_lock:
if layers_to_cpu:
batch_safe_move_to_cpu(layers_to_cpu)
if layers_to_gpu:
batch_safe_move_to_gpu(layers_to_gpu)
else:
# One in, one out: remove old layers, add new ones
for idx in cpu_swappable_layers:
if idx not in needed_layers and device_cache.get_device(idx).type == 'cuda':
safe_move_to_cpu_threaded(layers[idx], idx)
for idx in needed_layers:
if device_cache.get_device(idx).type == 'cpu':
safe_move_to_gpu_threaded(layers[idx], idx)
time.sleep(0.000001) #do not touch. very needed for sync
except Exception as e:
print(f" Background thread error: {e}")
time.sleep(0.1)
finally:
if PROFILE:
nvtx.range_pop()
def batch_safe_move_to_cpu(layer_indices):
"""Move multiple layers to CPU in batch"""
if not layer_indices:
return 0
if PROFILE:
nvtx.range_push(f"Batch_CPU_Transfer_{len(layer_indices)}layers")
try:
moved_count = 0
for idx in layer_indices:
if (idx < len(layers) and
device_cache.get_device(idx).type == 'cuda'):
# Measure transfer timing
transfer_time = 0
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
layers[idx].to(CPU_DEVICE)
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_cpu_times'].append(transfer_time)
transfer_stats['current_step_cpu_speeds'].append(speed_mbps)
device_cache.mark_moved(idx, torch.device('cpu'))
swap_stats['to_cpu'] += 1
moved_count += 1
return moved_count
finally:
if PROFILE:
nvtx.range_pop()
def batch_safe_move_to_gpu(layer_indices):
"""Batch move to GPU with dtype casting"""
if not layer_indices:
return 0
if PROFILE:
nvtx.range_push(f"Batch_GPU_Transfer_{len(layer_indices)}layers")
try:
moved_count = 0
for idx in layer_indices:
if (idx < len(layers) and
device_cache.get_device(idx).type == 'cpu'):
layer = layers[idx]
transfer_time = 0
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
layer.to(GPU_DEVICE, non_blocking=True)
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
event_based_sync("gpu_transfer", idx)
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_gpu_times'].append(transfer_time)
transfer_stats['current_step_gpu_speeds'].append(speed_mbps)
device_cache.mark_moved(idx, torch.device('cuda'))
swap_stats['to_gpu'] += 1
moved_count += 1
return moved_count
finally:
if PROFILE:
nvtx.range_pop()
# =============================================================================
# PACKED CODE by obisin
# =============================================================================
class PackedCPUBlock:
def __init__(self, layer):
self.packed_buffers = {} # One buffer per dtype
self.tensor_specs = []
self.total_elements = 0
self.gpu_blocks = {} # Keep GPU buffers alive when resident
self.gpu_events = {} # CUDA events for synchronization
self.pack_layer(layer)
def pack_layer(self, layer):
"""Pack all layer parameters/buffers into contiguous CPU blocks by dtype"""
dtype_groups = {}
# Build param/buffer maps once (avoid rebuilding in loops)
param_map = layer._parameters
buffer_map = layer._buffers
# Collect parameters by dtype
for name, param in layer.named_parameters(recurse=False):
if param is None:
continue
# Move to CPU first if needed (read-only copy during collection)
src_data = param.detach().to('cpu', copy=True) if param.device.type != 'cpu' else param.data
dtype = param.dtype
if dtype not in dtype_groups:
dtype_groups[dtype] = []
dtype_groups[dtype].append({
'name': name,
'data': src_data.flatten(),
'shape': param.shape,
'is_param': True,
'param_ref': param
})
# Collect buffers by dtype
for name, buffer in layer.named_buffers(recurse=True):
if buffer is None:
continue
# Move to CPU first if needed (read-only copy during collection)
src_data = buffer.detach().to('cpu', copy=True) if buffer.device.type != 'cpu' else buffer.data
dtype = buffer.dtype
if dtype not in dtype_groups:
dtype_groups[dtype] = []
dtype_groups[dtype].append({
'name': name,
'data': src_data.flatten(),
'shape': buffer.shape,
'is_param': False,
'buffer_ref': buffer
})
# Pack each dtype group into contiguous buffers
for dtype, tensors in dtype_groups.items():
if not tensors:
continue
total_size = sum(t['data'].numel() for t in tensors)
packed_buffer = torch.empty(total_size, dtype=dtype)
self.packed_buffers[dtype] = packed_buffer
# Pack data and create specs
offset = 0
for tensor_info in tensors:
data = tensor_info['data']
size = data.numel()
# Copy into packed buffer
packed_buffer[offset:offset + size] = data
# Store spec for later rebinding
spec = {
'name': tensor_info['name'],
'dtype': dtype,
'offset': offset,
'size': size,
'shape': tensor_info['shape'],
'is_param': tensor_info['is_param'],
'param_ref': tensor_info.get('param_ref'),
'buffer_ref': tensor_info.get('buffer_ref')
}
self.tensor_specs.append(spec)
offset += size
self.total_elements += total_size
# ATOMIC COMMIT: Rebind CPU params/buffers to views into packed_buffers
# This eliminates duplicate host RAM
for spec in self.tensor_specs:
dtype = spec['dtype']
start_idx = spec['offset']
end_idx = start_idx + spec['size']
cpu_view = self.packed_buffers[dtype][start_idx:end_idx].view(spec['shape'])
if spec['is_param']:
spec['param_ref'].data = cpu_view
else:
spec['buffer_ref'].data = cpu_view
def unpack_to_gpu(self, layer):
"""Move packed buffers to GPU and rebind layer parameters"""
# Group specs by dtype for efficient transfer
dtype_specs = {}
for spec in self.tensor_specs:
dtype = spec['dtype']
if dtype not in dtype_specs:
dtype_specs[dtype] = []
dtype_specs[dtype].append(spec)
# Transfer each dtype group
for dtype, specs in dtype_specs.items():
if dtype not in self.packed_buffers:
continue
packed_cpu = self.packed_buffers[dtype]
gpu_buffer = packed_cpu.to(GPU_DEVICE, non_blocking=True)
# Record event and keep buffer alive
event = torch.cuda.Event()
event.record()
self.gpu_blocks[dtype] = gpu_buffer
self.gpu_events[dtype] = event
# ATOMIC COMMIT: Rebind all params/buffers for this dtype
for spec in specs:
start_idx = spec['offset']
end_idx = start_idx + spec['size']
gpu_view = gpu_buffer[start_idx:end_idx].view(spec['shape'])
if spec['is_param']:
spec['param_ref'].data = gpu_view
else:
spec['buffer_ref'].data = gpu_view
def unpack_to_cpu(self, layer):
"""Move GPU data back to packed CPU buffers and rebind layer parameters"""
# Group specs by dtype
dtype_specs = {}
for spec in self.tensor_specs:
dtype = spec['dtype']
if dtype not in dtype_specs:
dtype_specs[dtype] = []
dtype_specs[dtype].append(spec)
# Copy GPU → CPU for each dtype
for dtype, specs in dtype_specs.items():
if dtype not in self.gpu_blocks:
continue
gpu_buffer = self.gpu_blocks[dtype]
packed_cpu = self.packed_buffers[dtype]
# Synchronous copy back to packed CPU buffer
packed_cpu.copy_(gpu_buffer.to('cpu'))
# packed_cpu.copy_(gpu_buffer)
# ATOMIC COMMIT: Rebind params/buffers back to CPU views
for spec in specs:
start_idx = spec['offset']
end_idx = start_idx + spec['size']
cpu_view = packed_cpu[start_idx:end_idx].view(spec['shape'])
if spec['is_param']:
spec['param_ref'].data = cpu_view
else:
spec['buffer_ref'].data = cpu_view
# Release GPU memory
self.gpu_blocks.clear()
self.gpu_events.clear()
def wait_for_gpu_transfer(self):
"""Wait for all async GPU transfers to complete"""
for event in self.gpu_events.values():
event.wait()
def get_memory_usage(self):
"""Return memory usage info"""
cpu_bytes = sum(buf.numel() * buf.element_size() for buf in self.packed_buffers.values())
gpu_bytes = sum(buf.numel() * buf.element_size() for buf in self.gpu_blocks.values())
return {
'cpu_bytes': cpu_bytes,
'gpu_bytes': gpu_bytes,
'total_elements': self.total_elements
}
def safe_move_to_gpu_packed(layer, idx):
"""Move layer to GPU using pre-packed buffer"""
if PROFILE:
nvtx.range_push(f"GPU_Transfer_Packed_L{idx}")
try:
current_device = device_cache.get_device(idx)
if current_device.type == 'cuda':
return True
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
if idx in packed_layers:
# Use packed transfer (automatically uses pinned if enabled)
packed_layers[idx].unpack_to_gpu(layer, GPU_DEVICE)
else:
# Fallback to normal transfer
layer.to(GPU_DEVICE, non_blocking=True)
transfer_time = 0
event_based_sync("gpu_transfer", idx)
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_gpu_times'].append(transfer_time)
transfer_stats['current_step_gpu_speeds'].append(speed_mbps)
device_cache.mark_moved(idx, torch.device('cuda'))
swap_stats['to_gpu'] += 1
return True
except RuntimeError as e:
if "out of memory" in str(e):
print(f" OOM moving packed layer {idx}")
return False
raise e
finally:
if PROFILE:
nvtx.range_pop()
def safe_move_to_cpu_packed(layer, idx):
"""Move layer to CPU - packed version already stored"""
if PROFILE:
nvtx.range_push(f"CPU_Transfer_Packed_L{idx}")
try:
try:
current_device = next(layer.parameters()).device
if current_device.type == 'cpu':
return True
except StopIteration:
pass
transfer_time = 0
if VERBOSE:
start_time = time.time()
layer_size_mb = get_cached_layer_size_mb(idx)
layer.to(CPU_DEVICE)
if VERBOSE:
end_time = time.time()
transfer_time = end_time - start_time
if transfer_time > 0 and VERBOSE:
speed_mbps = layer_size_mb / transfer_time
# Track current step stats
transfer_stats['current_step_cpu_times'].append(transfer_time)
transfer_stats['current_step_cpu_speeds'].append(speed_mbps)
device_cache.mark_moved(idx, torch.device('cpu'))
swap_stats['to_cpu'] += 1
return True
finally:
if PROFILE:
nvtx.range_pop()
# =============================================================================
# DYNAMIC LAYER SWAPPING INFERENCE for Comfy-UI - by obisin
# =============================================================================
def add_smart_swapping_to_layer(layer, layer_idx, layers_list, gpu_resident_layers, cpu_swappable_layers):
"""Add swapping capability with ComfyUI coordination"""
global layers, device_cache
layers = layers_list
original_forward = layer.forward
if not hasattr(add_smart_swapping_to_layer, 'prefetch_hits'):
add_smart_swapping_to_layer.prefetch_hits = 0
add_smart_swapping_to_layer.prefetch_misses = 0
if not hasattr(add_smart_swapping_to_layer, 'layer_compute_times'):
add_smart_swapping_to_layer.layer_compute_times = []
# Initialize coordination state (once) - this should match _patch_model_patcher
if not hasattr(add_smart_swapping_to_layer, 'current_sampling_step'):
add_smart_swapping_to_layer.current_sampling_step = 0
add_smart_swapping_to_layer.last_processed_step = -1
add_smart_swapping_to_layer.apply_model_call_count = 0
add_smart_swapping_to_layer.current_forward_logged = False
# Initialize coordination state (once)
if not hasattr(add_smart_swapping_to_layer, 'current_sampling_step'):
add_smart_swapping_to_layer.current_sampling_step = 0
add_smart_swapping_to_layer.last_processed_step = -1
# THREADING: Initialize threading components (once)
if not hasattr(add_smart_swapping_to_layer, 'threading_initialized'):
add_smart_swapping_to_layer.threading_initialized = True
add_smart_swapping_to_layer.gpu_lock = threading.Lock()
add_smart_swapping_to_layer.current_layer_idx = 0
add_smart_swapping_to_layer.training_active = True
add_smart_swapping_to_layer.background_thread = None
# CUDA STREAMS: Initialize streams (once) - only if enabled
if args.cuda_streams and not hasattr(add_smart_swapping_to_layer, 'streams_initialized'):
add_smart_swapping_to_layer.streams_initialized = True
add_smart_swapping_to_layer.copy_stream = torch.cuda.Stream()
add_smart_swapping_to_layer.compute_stream = torch.cuda.Stream()
print(" CUDA Streams enabled for copy-compute overlap")
# THREADING: Start background thread (once)
if args.threading and add_smart_swapping_to_layer.background_thread is None:
add_smart_swapping_to_layer.background_thread = threading.Thread(
target=background_layer_manager,
daemon=True
)
add_smart_swapping_to_layer.background_thread.start()
@wraps(original_forward)
def swapped_forward(*fwd_args, **kwargs):
# Detect Flux calling patterns
layer_type = type(layer).__name__
is_flux_call = 'DoubleStream' in layer_type or 'SingleStream' in layer_type
if is_flux_call:
# Flux patterns - preserve exact argument structure
if fwd_args:
# SingleStreamBlock: block(img, vec=vec, pe=pe)
x = fwd_args[0] # img
new_args = fwd_args[1:] # should be empty
new_kwargs = kwargs # vec=vec, pe=pe
else:
# DoubleStreamBlock: block(img=img, txt=txt, vec=vec, pe=pe)
x = kwargs.get('img')
new_args = ()
new_kwargs = kwargs
else:
# WAN/SDXL pattern
x = fwd_args[0] if fwd_args else None
new_args = fwd_args[1:]
new_kwargs = kwargs
if VERBOSE and not hasattr(add_smart_swapping_to_layer, 'step_timing_initialized'):
add_smart_swapping_to_layer.step_timing_initialized = True
add_smart_swapping_to_layer.step_start_time = time.time()
add_smart_swapping_to_layer.current_step = 0
if not hasattr(add_smart_swapping_to_layer, 'total_forward_calls'):
add_smart_swapping_to_layer.total_forward_calls = 0
add_smart_swapping_to_layer.current_step = 0
add_smart_swapping_to_layer.calls_this_step = 0
add_smart_swapping_to_layer.total_forward_calls += 1
add_smart_swapping_to_layer.calls_this_step += 1
# Detect new step when we hit layer 0 again (reset to early layers)
if layer_idx <= 2: # Early layer indicates new step
if add_smart_swapping_to_layer.calls_this_step > len(layers): # Processed full model
add_smart_swapping_to_layer.current_step += 1
add_smart_swapping_to_layer.calls_this_step = 0
if VERBOSE:
print(f"🔄 New sampling step {add_smart_swapping_to_layer.current_step} detected")
# At the start of swapped_forward
if VERBOSE and layer_idx % 10 == 0: # Every 10th layer
current_step = getattr(add_smart_swapping_to_layer, 'current_step', 0)
print(f"Layer {layer_idx} step {current_step}: device={device_cache.get_device(layer_idx)}")
# Step progress logging
if VERBOSE and layer_idx % 20 == 0:
current_step = getattr(add_smart_swapping_to_layer, 'current_step', 0)
if (add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) > 0:
hit_rate = add_smart_swapping_to_layer.prefetch_hits / (
add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) * 100
print(
f" Prefetch hit rate: {hit_rate:.1f}% ({add_smart_swapping_to_layer.prefetch_hits}/{add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses})")
# THREADING: Update current layer for background thread
if args.threading:
add_smart_swapping_to_layer.current_layer_idx = layer_idx
if PROFILE:
nvtx.range_push(f"Layer_{layer_idx}_{'Forward'}")
try:
# Initialize global flags if they don't exist
# if not hasattr(add_smart_swapping_to_layer, 'current_forward_logged'):
# add_smart_swapping_to_layer.current_forward_logged = False
# # Summary logging at the start - only once per pass type
if not add_smart_swapping_to_layer.current_forward_logged:
current_step = getattr(add_smart_swapping_to_layer, 'current_step', 0)
if VERBOSE:
print(
f" Forward pass step {current_step}: layers {min(cpu_swappable_layers)}-{max(cpu_swappable_layers)}")
add_smart_swapping_to_layer.current_forward_logged = True
if not hasattr(add_smart_swapping_to_layer, 'stats_initialized'):
add_smart_swapping_to_layer.stats_initialized = True
add_smart_swapping_to_layer.prefetch_hits = 0
add_smart_swapping_to_layer.prefetch_misses = 0
# 2. SMART LAYER MANAGEMENT
if layer_idx in cpu_swappable_layers:
if PROFILE:
nvtx.range_push(f"Smart_Management_L{layer_idx}")
try:
current_device = device_cache.get_device(layer_idx)
layer_already_on_gpu = (current_device.type == 'cuda')
if not layer_already_on_gpu:
# THREADING: If threading enabled, wait briefly for background thread
if args.threading:
print(f" Layer {layer_idx} not ready, waiting for background thread...")
for _ in range(50):
time.sleep(0.000001) #do not touch. very needed for sync
if device_cache.get_device(layer_idx).type == 'cuda':
layer_already_on_gpu = True
print(f" Background thread caught up!")
break
# If still not ready, run fallback
if not layer_already_on_gpu:
# THREADING: Use lock to prevent race condition
if args.threading:
with add_smart_swapping_to_layer.gpu_lock:
needed_layers = calculate_needed_layers(layer_idx, args.prefetch)
if args.selective_packing:
cleaned = cleanup_excess_layers_packed(needed_layers, args.selective_packing)
fetched = fetch_missing_layers_packed(needed_layers, args.selective_packing)
else:
cleaned = cleanup_excess_layers(needed_layers)
fetched = fetch_missing_layers(needed_layers)
else:
# No threading, run normally
needed_layers = calculate_needed_layers(layer_idx, args.prefetch)
if args.selective_packing:
cleaned = cleanup_excess_layers_packed(needed_layers, args.selective_packing)
fetched = fetch_missing_layers_packed(needed_layers, args.selective_packing)
else:
cleaned = cleanup_excess_layers(needed_layers)
fetched = fetch_missing_layers(needed_layers)
if layer_already_on_gpu:
add_smart_swapping_to_layer.prefetch_hits += 1
# if VERBOSE:
# print(f" ✅ Layer {layer_idx} prefetch hit")
else:
add_smart_swapping_to_layer.prefetch_misses += 1
# if VERBOSE:
# print(f" ⚠️ Layer {layer_idx} prefetch miss, fetching...")
device = device_cache.get_device(layer_idx)
gpu_success = device.type == 'cuda'
# if VERBOSE and layer_idx % 20 == 0:
# current_step = getattr(add_smart_swapping_to_layer, 'current_step', 0)
# if (
# add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) > 0:
# hit_rate = add_smart_swapping_to_layer.prefetch_hits / (
# add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) * 100
# print(
# f" Prefetch hit rate: {hit_rate:.1f}% ({add_smart_swapping_to_layer.prefetch_hits}/{add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses})")
finally:
if PROFILE:
nvtx.range_pop()
if layer_already_on_gpu:
add_smart_swapping_to_layer.prefetch_hits += 1
else:
add_smart_swapping_to_layer.prefetch_misses += 1
device = device_cache.get_device(layer_idx)
gpu_success = device.type == 'cuda'
else:
# Layer not in cpu_swappable_layers (permanent resident)
device = device_cache.get_device(layer_idx)
gpu_success = device.type == 'cuda'
# Handle GPU failure case
if not gpu_success:
print(f" Layer {layer_idx} failed to be on GPU, forcing aggressive cleanup...")
# Nuclear cleanup - evict ALL layers except this one
for cleanup_idx in cpu_swappable_layers:
if cleanup_idx != layer_idx and cleanup_idx < len(layers):
try:
layers[cleanup_idx].to('cpu')
except:
pass
gc.collect()
torch.cuda.empty_cache()
# Try again after cleanup
try:
layer.to(GPU_DEVICE)
device = torch.device(GPU_DEVICE)
gpu_success = True
print(f" Layer {layer_idx} moved to GPU after aggressive cleanup")
except RuntimeError:
print(f" CRITICAL: Layer {layer_idx} cannot fit on GPU, skipping computation!")
return x # Pass input through unchanged
# # Debug output every 10th layer
# if layer_idx % 10 == 0: # Every 10th layer
# current_step = getattr(add_smart_swapping_to_layer, 'current_step', 0)
# print(f"Layer {layer_idx} step {current_step}: device={device_cache.get_device(layer_idx)}")
def move_to_device(tensor, target_device):
# Only move device, don't touch dtype at all
if hasattr(tensor, 'device') and tensor.device != target_device:
tensor = tensor.to(target_device)
# Fix inference tensors (without dtype changes)
if isinstance(tensor, torch.Tensor):
try:
_ = tensor._version
return tensor
except (RuntimeError, AttributeError):
return tensor.detach().clone()
return tensor
move_to_device.current_layer = layer_idx
# DISABLE FLUX DETECTION FOR TESTING
# is_flux_call = False # Force to False
# Move tensors to device (respect the calling pattern you already set up)
if is_flux_call:
if fwd_args:
# SingleStreamBlock - move fwd_args and kwargs
new_args = tuple(move_to_device(arg, device) for arg in fwd_args)
new_kwargs = {k: move_to_device(v, device) for k, v in kwargs.items()}
else:
# DoubleStreamBlock - move only kwargs
new_args = ()
new_kwargs = {k: move_to_device(v, device) for k, v in kwargs.items()}
else:
x = move_to_device(x, device)
new_kwargs = {k: move_to_device(v, device) for k, v in kwargs.items()}
new_args = tuple(move_to_device(arg, device) for arg in new_args)
# Wait for any pending GPU transfers before executing the layer
if hasattr(layer, '_packed_block'):
for event in layer._packed_block.gpu_events.values():
torch.cuda.current_stream().wait_event(event)
# 4. COMPUTATION
if PROFILE:
nvtx.range_push(f"Compute_L{layer_idx}")
try:
if args.cuda_streams and hasattr(add_smart_swapping_to_layer, 'compute_stream'):
with torch.cuda.stream(add_smart_swapping_to_layer.compute_stream):
if VERBOSE:
layer_compute_start = time.time()
if args.compute_casting != "disabled":
# Compute casting: use specified dtype
dtype_map = {'fp32': torch.float32, 'bf16': torch.bfloat16, 'fp16': torch.float16}
cast_dtype = dtype_map[args.compute_casting]
with torch.autocast(device_type='cuda', dtype=cast_dtype, enabled=True):
fix_inference_tensor_parameters(layer)
# result = original_forward(x, *tuple(new_args), **new_kwargs)
# Call with preserved argument structure
if is_flux_call:
if fwd_args:
result = original_forward(*new_args, **new_kwargs)
return result
else:
return original_forward(**new_kwargs)
else:
# WAN/SDXL: original logic
return original_forward(x, *new_args, **new_kwargs)
elif args.autocast and args.autocast != 'fp32':
if args.autocast in ['f8_e4m3', 'f8_e5m2']:
# Use Transformer Engine for FP8
import transformer_engine.pytorch as te
with te.fp8_autocast():
fix_inference_tensor_parameters(layer)
result = original_forward(x, *tuple(new_args), **new_kwargs)
else:
# Regular PyTorch autocast for FP16/BF16
dtype_map = {'fp16': torch.float16, 'bf16': torch.bfloat16}
autocast_dtype = dtype_map[args.autocast]
with torch.autocast(device_type='cuda', dtype=autocast_dtype):
fix_inference_tensor_parameters(layer)
result = original_forward(x, *tuple(new_args), **new_kwargs)
else:
fix_inference_tensor_parameters(layer)
# result = original_forward(x, *tuple(new_args), **new_kwargs)
# Call with preserved argument structure
if is_flux_call:
if fwd_args:
result = original_forward(*new_args, **new_kwargs)
return result
else:
return original_forward(**new_kwargs)
else:
return original_forward(x, *new_args, **new_kwargs)
if VERBOSE:
layer_compute_end = time.time()
layer_compute_time = layer_compute_end - layer_compute_start
add_smart_swapping_to_layer.layer_compute_times.append(layer_compute_time)
return result
else:
if VERBOSE:
layer_compute_start = time.time()
if args.compute_casting != "disabled":
# Compute casting: use specified dtype
dtype_map = {'fp32': torch.float32, 'bf16': torch.bfloat16, 'fp16': torch.float16}
cast_dtype = dtype_map[args.compute_casting]
with torch.autocast(device_type='cuda', dtype=cast_dtype, enabled=True):
fix_inference_tensor_parameters(layer)
# result = original_forward(x, *tuple(new_args), **new_kwargs)
# Call with preserved argument structure
if is_flux_call:
if fwd_args:
result = original_forward(*new_args, **new_kwargs)
return result
else:
return original_forward(**new_kwargs)
else:
# WAN/SDXL: original logic
return original_forward(x, *new_args, **new_kwargs)
elif args.autocast and args.autocast != 'fp32':
if args.autocast in ['f8_e4m3', 'f8_e5m2']:
# Use Transformer Engine for FP8
import transformer_engine.pytorch as te
with te.fp8_autocast():
fix_inference_tensor_parameters(layer)
result = original_forward(x, *tuple(new_args), **new_kwargs)
else:
# Regular PyTorch autocast for FP16/BF16
dtype_map = {'fp16': torch.float16, 'bf16': torch.bfloat16}
autocast_dtype = dtype_map[args.autocast]
with torch.autocast(device_type='cuda', dtype=autocast_dtype):
fix_inference_tensor_parameters(layer)
result = original_forward(x, *tuple(new_args), **new_kwargs)
else:
fix_inference_tensor_parameters(layer)
# result = original_forward(x, *tuple(new_args), **new_kwargs)
# Call with preserved argument structure
if is_flux_call:
if fwd_args:
result = original_forward(*new_args, **new_kwargs)
return result
else:
return original_forward(**new_kwargs)
else:
# WAN/SDXL: original logic
return original_forward(x, *new_args, **new_kwargs)
if VERBOSE:
layer_compute_end = time.time()
layer_compute_time = layer_compute_end - layer_compute_start
add_smart_swapping_to_layer.layer_compute_times.append(layer_compute_time)
return result
finally:
if PROFILE:
nvtx.range_pop()
finally:
if PROFILE:
nvtx.range_pop()
# Replace forward method
layer.forward = swapped_forward
# =============================================================================
# COMFYUI NODE CLASS - by obisin
# =============================================================================
class DynamicSwappingLoader:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"layers": ("LAYERS",),
"initial_gpu_layers": ("INT",
{"default": 2, "min": 0, "max": 50, "tooltip": TOOLTIPS["initial_gpu_layers"]}),
"final_gpu_layers": ("INT",
{"default": 2, "min": 0, "max": 50, "tooltip": TOOLTIPS["final_gpu_layers"]}),
"prefetch": ("INT", {"default": 1, "min": 0, "max": 100, "tooltip": TOOLTIPS["prefetch"]}),
"threading": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["threading"]}),
"event_sync": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["event_sync"]}),
"cuda_streams": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["cuda_streams"]}),
"batch_move": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["batch_move"]}),
"selective_packing": ("INT",
{"default": 0, "min": 0, "max": 128, "tooltip": TOOLTIPS["selective_packing"]}),
"verbose": ("BOOLEAN", {"default": True, "tooltip": TOOLTIPS["verbose"]}),
"compile": ("BOOLEAN", {"default": False, "tooltip": TOOLTIPS["compile"]}),
},
"optional": {
"gpu_layer_indices": ("STRING", {"default": "", "tooltip": TOOLTIPS["gpu_layers"]}),
#Precision tools have been disabled due to confliction with comfy auto precision and casting
# "compute_casting": (["disabled", "fp32", "bf16", "fp16"],
# {"default": "disabled",
# "tooltip": "Cast to higher precision for computation only. Model stays in original dtype for storage/transfer, temporarily upcasts during forward pass for better accuracy"}),
# "cast_target": ("STRING", {"default": "", "tooltip": TOOLTIPS["cast_target"]}),
# "autocast": (["", 'auto', "fp16", "bf16", "f8_e4m3", "f8_e5m2"],
# {"default": "",}),
# "mixed_precision": (["auto", "f32", "bf16", "f16"],
# {"default": "auto", "tooltip": TOOLTIPS["mixed_precision"]}),
}
}
RETURN_TYPES = ("MODEL",)
RETURN_NAMES = ("model",)
FUNCTION = "load_model_with_swapping"
CATEGORY = "loaders"
TITLE = "Dynamic Swapping Loader"
def load_model_with_swapping(self, model, layers, initial_gpu_layers, final_gpu_layers,
prefetch, threading, event_sync, cuda_streams, batch_move,
selective_packing, verbose, compile, compute_casting=False, gpu_layer_indices="",
cast_target="", autocast="", mixed_precision="auto"):
# Force cleanup before starting
torch.cuda.empty_cache()
gc.collect()
global swap_stats, transfer_stats
swap_stats = {'to_gpu': 0, 'to_cpu': 0}
transfer_stats = {
'to_gpu_times': [], 'to_cpu_times': [], 'to_gpu_speeds': [], 'to_cpu_speeds': [],
'current_step_gpu_times': [], 'current_step_cpu_times': [],
'current_step_gpu_speeds': [], 'current_step_cpu_speeds': []
}
# Reset layer-specific stats
if hasattr(add_smart_swapping_to_layer, 'prefetch_hits'):
add_smart_swapping_to_layer.prefetch_hits = 0
add_smart_swapping_to_layer.prefetch_misses = 0
# Update ALL args with node parameters
args.dynamic_swapping = True
args.initial_gpu_layers = initial_gpu_layers
args.final_gpu_layers = final_gpu_layers
args.prefetch = prefetch
args.threading = threading
args.cuda_streams = cuda_streams
args.batch_move = batch_move
args.selective_packing = selective_packing
args.event_sync = event_sync
args.verbose = verbose
global VERBOSE
VERBOSE = args.verbose
args.compile = compile
args.device_sync_mode = 'off'
args.sync_only_on_resume = False
args.mixed_precision = mixed_precision
# Handle string parameters
args.gpu_layers = gpu_layer_indices.strip() if gpu_layer_indices.strip() else None
args.cast_target = False #cast_target.strip() if cast_target.strip() else None
args.autocast = False #autocast if autocast else None
args.compute_casting = "disabled"#compute_casting
if compute_casting:
args.autocast = False
if VERBOSE:
print(f"\n=== LAYER DTYPE INSPECTION ===")
print(f"Total layers: {len(layers)}")
for i, layer in enumerate(layers):
layer_type = type(layer).__name__
# Get parameter dtypes for this layer
param_dtypes = set()
param_count = 0
for name, param in layer.named_parameters():
param_dtypes.add(param.dtype)
param_count += 1
# Get buffer dtypes
buffer_dtypes = set()
buffer_count = 0
for name, buffer in layer.named_buffers():
buffer_dtypes.add(buffer.dtype)
buffer_count += 1
# Format dtype info
dtype_info = []
if param_dtypes:
dtype_info.append(f"params: {list(param_dtypes)}")
if buffer_dtypes:
dtype_info.append(f"buffers: {list(buffer_dtypes)}")
dtype_str = ", ".join(dtype_info) if dtype_info else "no tensors"
print(f" {i:2d}: {layer_type:<20} | {dtype_str}")
print("===============================\n")
if gpu_layer_indices and gpu_layer_indices.strip():
specified_gpu_layers = set(map(int, gpu_layer_indices.split(',')))
specified_gpu_layers.add(0) # First layer
specified_gpu_layers.add(len(layers) - 1) # Last layer
if VERBOSE:
print(f" Automatically added layers 0 and {len(layers) - 1} for stability")
else:
# Use initial/final logic, but ensure at least 1 initial and 1 final
initial_gpu_layers = max(1, args.initial_gpu_layers) # At least 1
final_gpu_layers = max(1, args.final_gpu_layers) # At least 1
if args.dynamic_swapping:
if VERBOSE:
print("Applying ENHANCED smart GPU allocation + dynamic swapping...")
print(f"Setting up enhanced swapping for {len(layers)} layers...")
if args.gpu_layers:
try:
specified_gpu_layers = set(map(int, args.gpu_layers.split(',')))
if VERBOSE:
print(f" Using specified GPU layers: {sorted(specified_gpu_layers)}")
# Validate layer indices
invalid_layers = [idx for idx in specified_gpu_layers if idx >= len(layers) or idx < 0]
if invalid_layers:
raise ValueError(f"Invalid layer indices: {invalid_layers}. Must be 0-{len(layers) - 1}")
except ValueError as e:
print(f" Error parsing --gpu_layers: {e}")
print("Example usage: --gpu_layers 0,1,2,14,18,19,20,21,22")
raise e
else:
specified_gpu_layers = None
if VERBOSE:
print(f"Using initial/final layer allocation: {initial_gpu_layers}/{final_gpu_layers}")
# PHASE 1: Determine which layers go where based on specified layers or initial/final counts
for i, layer in enumerate(layers):
if hasattr(layer, 'to'):
if specified_gpu_layers is not None:
# Use specified layer indices for GPU placement
if i in specified_gpu_layers:
try:
layer.to(GPU_DEVICE)
gpu_resident_layers.add(i)
if VERBOSE:
print(f"Layer {i} (specified) -> GPU permanent")
except RuntimeError as e:
print(f" CRITICAL: Cannot fit specified layer {i} on GPU!")
print(f"GPU memory may be insufficient. Consider removing layer {i} from --gpu_layers")
raise e
else:
# Not in specified list, make swappable
layer.to(CPU_DEVICE)
cpu_swappable_layers.add(i)
if VERBOSE:
print(f"Layer {i} (not specified) -> CPU swappable")
else:
# Use original initial/final logic as fallback
if i < initial_gpu_layers:
# Initial layers on GPU permanently
try:
layer.to(GPU_DEVICE)
gpu_resident_layers.add(i)
if VERBOSE:
print(f"Layer {i} (initial) -> GPU permanent")
except RuntimeError as e:
print(f"GPU exhausted at layer {i}, moving to CPU with swapping")
layer.to(CPU_DEVICE)
cpu_swappable_layers.add(i)
elif i >= (len(layers) - final_gpu_layers):
# Final layers on GPU permanently
try:
layer.to(GPU_DEVICE)
gpu_resident_layers.add(i)
if VERBOSE:
print(f"Layer {i} (final) -> GPU permanent")
except RuntimeError as e:
print(f"CRITICAL: Cannot fit final layer {i} on GPU!")
raise e
else:
# Middle layers on CPU with swapping capability
layer.to(CPU_DEVICE)
cpu_swappable_layers.add(i)
print(f"Layer {i} (middle) -> CPU swappable")
print(f"✓ {len(gpu_resident_layers)} layers permanently on GPU: {sorted(gpu_resident_layers)}")
print(f"✓ {len(cpu_swappable_layers)} layers on CPU with smart swapping: {sorted(cpu_swappable_layers)}")
if cast_target:
casting_handler = CastingHandler()
casting_handler.precast_all_layers(cpu_swappable_layers, layers)
global device_cache
device_cache = LayerDeviceCache(model, layers)
if args.selective_packing:
print(" Packing CPU-swappable layers for optimized transfers...")
for i in cpu_swappable_layers:
try:
# Pass pinned flag to PackedCPUBlock
packed_layers[i] = PackedCPUBlock(layers[i])
# print(f"✓ Layer {i} packed: {packed_layers[i].total_elements} elements")
except Exception as e:
print(f" Failed to pack layer {i}: {e}")
else:
print(" Selective packing disabled - using direct transfers only")
print(" Pre-calculating layer sizes...")
layer_sizes_mb = {}
for i, layer in enumerate(layers):
layer_sizes_mb[i] = sum(p.numel() * p.element_size() for p in layer.parameters()) / (1024 * 1024)
if VERBOSE:
print(f" Layer {i}: {layer_sizes_mb[i]:.1f}MB")
if args.cast_target:
print(f"\n=== LAYER CASTING DTYPES ===")
print(f"Total layers: {len(layers)}")
for i, layer in enumerate(layers):
layer_type = type(layer).__name__
# Get parameter dtypes for this layer
param_dtypes = set()
param_count = 0
for name, param in layer.named_parameters():
param_dtypes.add(param.dtype)
param_count += 1
# Get buffer dtypes
buffer_dtypes = set()
buffer_count = 0
for name, buffer in layer.named_buffers():
buffer_dtypes.add(buffer.dtype)
buffer_count += 1
# Format dtype info
dtype_info = []
if param_dtypes:
dtype_info.append(f"params: {list(param_dtypes)}")
if buffer_dtypes:
dtype_info.append(f"buffers: {list(buffer_dtypes)}")
dtype_str = ", ".join(dtype_info) if dtype_info else "no tensors"
print(f" {i:2d}: {layer_type:<20} | {dtype_str}")
print("===============================\n")
for layer_idx in cpu_swappable_layers:
add_smart_swapping_to_layer(
layers[layer_idx],
layer_idx,
layers,
gpu_resident_layers,
cpu_swappable_layers)
if VERBOSE:
print("Adding compute timing to permanent GPU layers...")
for layer_idx in gpu_resident_layers:
layer = layers[layer_idx]
original_forward = layer.forward
if args.compile:
# Try torch.compile version
@torch.compile
def compiled_forward( *layer_args, **layer_kwargs):
return original_forward( *layer_args, **layer_kwargs)
def create_resident_forward(layer_idx, original_forward, compiled_fn):
current_args = args
@wraps(original_forward)
def resident_forward( *args_tuple, **kwargs):
if VERBOSE:
layer_compute_start = time.time()
try:
if current_args.compute_casting != "disabled":
dtype_map = {'fp32': torch.float32, 'bf16': torch.bfloat16, 'fp16': torch.float16}
cast_dtype = dtype_map[args.compute_casting]
with torch.autocast(device_type='cuda', dtype=cast_dtype, enabled=True):
result = original_forward( *args_tuple, **kwargs)
else:
result = compiled_fn( *args_tuple, **kwargs)
except Exception as e:
print(f" Compile failed for layer {layer_idx}: {e}")
result = original_forward( *args_tuple, **kwargs)
if VERBOSE:
layer_compute_end = time.time()
layer_compute_time = layer_compute_end - layer_compute_start
add_smart_swapping_to_layer.layer_compute_times.append(layer_compute_time)
return result
return resident_forward
layer.forward = create_resident_forward(layer_idx, original_forward, compiled_forward)
if VERBOSE:
print(f" Added compiled timing to permanent GPU layer {layer_idx}")
else:
# Original working version (no compile)
def create_resident_forward(layer_idx, original_forward):
current_args = args
@wraps(original_forward)
def resident_forward( *args_tuple, **kwargs):
if VERBOSE:
layer_compute_start = time.time()
if current_args.compute_casting != "disabled":
dtype_map = {'fp32': torch.float32, 'bf16': torch.bfloat16, 'fp16': torch.float16}
cast_dtype = dtype_map[args.compute_casting]
with torch.autocast(device_type='cuda', dtype=cast_dtype, enabled=True):
result = original_forward( *args_tuple, **kwargs)
else:
result = original_forward( *args_tuple, **kwargs)
# result = original_forward( *args_tuple, **kwargs)
if VERBOSE:
layer_compute_end = time.time()
layer_compute_time = layer_compute_end - layer_compute_start
add_smart_swapping_to_layer.layer_compute_times.append(layer_compute_time)
return result
return resident_forward
layer.forward = create_resident_forward(layer_idx, original_forward)
if VERBOSE:
print(f"✓ Added timing to permanent GPU layer {layer_idx}")
# if step % 5 == 0:
# print(f"\n=== STEP {step} STATS ===")
# print(f"Expected total steps: {model_engine.total_steps}")
# if (add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) > 0:
# hit_rate = add_smart_swapping_to_layer.prefetch_hits / (
# add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses) * 100
# print(f"Prefetch hit rate: {hit_rate:.1f}% ({add_smart_swapping_to_layer.prefetch_hits}/{add_smart_swapping_to_layer.prefetch_hits + add_smart_swapping_to_layer.prefetch_misses})")
# print("========================")
if VERBOSE:
print("✅ Dynamic swapping successfully integrated with ComfyUI inference")
return (model,)
# =============================================================================
# MIXED PRECISION FRAMEWORK CODE by obisin
# =============================================================================
"""
Universal Mixed Precision Handler by obisin
=================================
Drop-in utility to handle gradient operations with mixed precision (float8, bfloat16, float32)
across any model architecture. Patches PyTorch's gradient functions to handle unsupported
dtype operations gracefully.
Usage:
from utils.mixed_precision_handler import patch_mixed_precision
patch_mixed_precision()
# or with user control:
patch_mixed_precision(mixed_precision_target='f32')
"""
class CastingHandler:
def __init__(self):
self.original_clip_grad_norm = None
self.dtype_conversion_map = {
torch.float8_e4m3fn: torch.bfloat16,
torch.float8_e5m2: torch.bfloat16,
}
self.safe_dtypes = {torch.float32, torch.float16, torch.bfloat16}
self.is_patched = False
def detect_model_highest_precision(self, layers):
"""Find the single highest precision dtype in the model"""
all_dtypes = set()
for layer in layers:
for param in layer.parameters():
all_dtypes.add(param.dtype)
# Precision hierarchy (highest to lowest)
precision_order = [torch.float64, torch.float32, torch.bfloat16, torch.float16,
getattr(torch, 'float8_e4m3fn', None)]
# Find the highest precision actually used
for dtype in precision_order:
if dtype and dtype in all_dtypes:
return dtype
return torch.float32 # fallback
def cast_to_dtype(self, param, target_device, cast_target=None):
"""Cast specific source dtype to target dtype"""
source_dtype = param.dtype
if cast_target:
cast_from, cast_to = cast_target
# Map string to torch dtypes
dtype_map = {
'f32': torch.float32,
'bf16': torch.bfloat16,
'f16': torch.float16,
'f8_e4m3': torch.float8_e4m3fn,
'f8_e5m2': torch.float8_e5m2,
'nf4': 'nf4', # Special handling needed NOT TESTED
'fp4': 'fp4' # Special handling needed
}
# Only cast if source matches the FROM dtype
if source_dtype == dtype_map[cast_from]:
# Handle 4-bit quantization
if cast_to in ['nf4', 'fp4']:
try:
import bitsandbytes as bnb
quantized, quant_state = bnb.functional.quantize_4bit(
param.data, blocksize=64, quant_type=cast_to
)
# Return dequantized version on target device
return bnb.functional.dequantize_4bit(
quantized.to(target_device), quant_state.to(target_device)
)
except ImportError:
raise RuntimeError(
" bitsandbytes required for 4-bit quantization. Install with: pip install bitsandbytes")
except Exception as e:
raise RuntimeError(f" 4-bit quantization failed: {e}")
else:
# Regular dtype casting
return param.to(target_device, dtype=dtype_map[cast_to], non_blocking=True)
# No casting needed
return param.to(target_device, non_blocking=True)
def replace_with_4bit_layer(self, layer):
"""Replace torch.nn.Linear with bitsandbytes Linear4bit, skip LoRA layers"""
try:
# import bitsandbytes as bnb
for name, module in layer.named_modules():
if isinstance(module, torch.nn.Linear):
# Skip LoRA layers - they cause dropout issues with 4-bit
if any(lora_term in name.lower() for lora_term in ['lora', 'adapter']):
# print(f" Skipping LoRA layer: {name}")
continue
parent_names = name.split('.')
if any(any(lora_term in part.lower() for lora_term in ['lora', 'adapter'])
for part in parent_names):
continue
print(f" Converting layer: {name}")
linear_4bit = bnb.nn.Linear4bit(
module.in_features,
module.out_features,
bias=module.bias is not None,
compute_dtype=torch.float16,
quant_type='nf4'
)
linear_4bit.load_state_dict(module.state_dict())
linear_4bit = linear_4bit.to('cuda')
# Replace in parent module
parent_name = '.'.join(name.split('.')[:-1])
module_name = name.split('.')[-1]
parent = layer
for part in parent_name.split('.'):
if part:
parent = getattr(parent, part)
setattr(parent, module_name, linear_4bit)
return layer
except Exception as e:
print(f"️ 4-bit replacement failed: {e}")
return layer
def precast_all_layers(self, cpu_swappable_layers, layers):
"""Cast all layer dtypes once at startup using existing cast_to_dtype"""
print(" Pre-casting layer dtypes (one-time setup)...")
casted_layers = 0
for i, layer in enumerate(layers):
if i in cpu_swappable_layers: # Only cast swappable layers
layer_casted = False
for name, param in layer.named_parameters():
# Use your existing function but stay on current device
new_param = self.cast_to_dtype(param, param.device) # Keep on same device
if new_param.dtype != param.dtype:
param.data = new_param.data
layer_casted = True
if layer_casted:
casted_layers += 1
print(f"✓ Pre-casted {casted_layers} layers using existing cast_to_dtype function")