Files
FearL0rd 6c9af0ff02 make 'Vae Patched' the default mode for safer decoding
Updated the INPUT_TYPES definition to set "Vae Patched" as the default selection.

Why: The "Original" VAE mode often causes black images or NaN errors in mixed-precision workflows (like Flux, SD3, or FP16 contexts) on certain hardware.

Fix: By defaulting to "Vae Patched," the node now automatically handles precision casting (forcing Float32 when necessary) to ensure successful image decoding out of the box, while still allowing advanced users to switch back to "Original" if needed.
2026-01-30 07:32:17 -06:00

317 lines
14 KiB
Python

import torch
import torch.nn.functional as F
import types
import comfy.model_management
import gc
import sys
# --- GLOBAL ATTENTION CONTROLLER ---
try:
import xformers.ops
if not hasattr(xformers.ops, "original_memory_efficient_attention"):
xformers.ops.original_memory_efficient_attention = xformers.ops.memory_efficient_attention
def traffic_controlled_attention(query, key, value, attn_bias=None, p=0.0, scale=None):
device_type = query.device.type
original_dtype = query.dtype # Save for later restoration
# 1. HARDWARE CHECK (SM < 80)
use_fast_kernels = True
if device_type == "cuda":
try:
props = torch.cuda.get_device_properties(query.device)
sm_version = props.major * 10 + props.minor
if sm_version < 80:
use_fast_kernels = False
except:
pass
# 2. EXECUTION PATHS
if use_fast_kernels and device_type != "cpu":
return xformers.ops.original_memory_efficient_attention(query, key, value, attn_bias, p, scale)
# PATH B: Legacy / CPU Fallback
if query.dtype != torch.float32: query = query.to(torch.float32)
if key.dtype != torch.float32: key = key.to(torch.float32)
if value.dtype != torch.float32: value = value.to(torch.float32)
q = query.transpose(1, 2)
k = key.transpose(1, 2)
v = value.transpose(1, 2)
is_causal = False
attn_mask = None
if attn_bias is not None:
if isinstance(attn_bias, xformers.ops.LowerTriangularMask):
is_causal = True
attn_mask = None
elif isinstance(attn_bias, torch.Tensor):
attn_mask = attn_bias
if attn_mask.dtype != torch.float32 and attn_mask.dtype != torch.bool:
attn_mask = attn_mask.to(torch.float32)
out = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask, dropout_p=p, is_causal=is_causal, scale=scale)
# Restore Dtype (Fixes float != Half error)
if out.dtype != original_dtype:
out = out.to(original_dtype)
return out.transpose(1, 2)
xformers.ops.memory_efficient_attention = traffic_controlled_attention
except ImportError:
pass
# -----------------------------
def get_available_devices():
devices = []
if torch.cuda.is_available():
count = torch.cuda.device_count()
for i in range(count):
devices.append(f"cuda:{i}")
if hasattr(torch.backends, "mps") and torch.backends.mps.is_available():
devices.append("mps")
if hasattr(torch, "xpu") and torch.xpu.is_available():
devices.append("xpu")
devices.append("cpu")
return devices
class AnyDeviceOffload:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(s):
device_list = get_available_devices()
return {
"required": {
"target_device": (device_list, ),
# Swapped order: Vae Patched is now the first item (Default)
"vae_mode": (["Vae Patched", "Original"], ),
"keep_in_memory": ("BOOLEAN", {"default": True}),
},
"optional": {
"model": ("MODEL",),
"clip": ("CLIP",),
"vae": ("VAE",),
}
}
RETURN_TYPES = ("MODEL", "CLIP", "VAE")
RETURN_NAMES = ("MODEL", "CLIP", "VAE")
FUNCTION = "offload_process"
CATEGORY = "utils/hardware"
def offload_process(self, target_device, vae_mode, keep_in_memory, model=None, clip=None, vae=None):
device_str = target_device.split(" ")[0]
try:
device = torch.device(device_str)
except:
device = torch.device("cpu")
is_cpu = device.type == 'cpu'
offload_target = device if keep_in_memory else torch.device("cpu")
print(f"[Offload] Target: {device} | Keep: {keep_in_memory}")
# --- 1. Handle MODEL (UNet/Transformer) ---
if model is not None:
target_model = None
if hasattr(model, "model"):
target_model = model.model
if target_model:
# A. Weight Conversion
if is_cpu:
target_model.to(dtype=torch.float32)
# B. FORCE DEVICE SYNC (Initial Move)
if keep_in_memory:
try:
target_model.to(device)
except:
pass
if hasattr(model, "patcher"):
model.patcher.load_device = device
model.patcher.offload_device = offload_target
model.patcher.current_device = device
# C. [NUCLEAR FIX] INSTALL RUNTIME DEVICE GUARD
if not hasattr(target_model, "is_device_guarded"):
print(" -> [System] Installing Runtime Device Guard...")
if hasattr(target_model, "diffusion_model"):
victim_model = target_model.diffusion_model
victim_model.original_forward_guard = victim_model.forward
def guarded_forward(self, *args, **kwargs):
# 1. Identify Input Device (Check ARGS and KWARGS)
target_run_device = None
# Scan positional args
for arg in args:
if isinstance(arg, torch.Tensor):
target_run_device = arg.device
break
# Scan keyword args if not found yet
if target_run_device is None:
for v in kwargs.values():
if isinstance(v, torch.Tensor):
target_run_device = v.device
break
# 2. Self-Healing Check
if target_run_device is not None:
try:
current_param_device = next(self.parameters()).device
if current_param_device != target_run_device:
# print(f"[Guard] Mismatch! {current_param_device} vs {target_run_device}. Moving...")
self.to(target_run_device)
except Exception as e:
pass # Suppress generic guard errors
# 3. CPU Float32 Casting
# [FIX] Ensure target_run_device exists before checking .type
if target_run_device is not None and target_run_device.type == "cpu":
new_args = []
for arg in args:
if isinstance(arg, torch.Tensor) and arg.dtype != torch.float32:
new_args.append(arg.to(torch.float32))
else:
new_args.append(arg)
new_kwargs = {}
for k, v in kwargs.items():
if isinstance(v, torch.Tensor) and v.dtype != torch.float32:
new_kwargs[k] = v.to(torch.float32)
else:
new_kwargs[k] = v
return self.original_forward_guard(*new_args, **new_kwargs)
else:
return self.original_forward_guard(*args, **kwargs)
victim_model.forward = types.MethodType(guarded_forward, victim_model)
target_model.is_device_guarded = True
# D. Update Internal Flags
model.load_device = device
model.offload_device = offload_target
if keep_in_memory:
model.current_device = device
# --- 2. Handle CLIP ---
if clip is not None:
if hasattr(clip, "cond_stage_model"):
target_clip = clip.cond_stage_model
if is_cpu:
target_clip.to(dtype=torch.float32)
if keep_in_memory:
try:
target_clip.to(device)
except:
pass
if hasattr(clip, "patcher"):
clip.patcher.load_device = device
clip.patcher.offload_device = offload_target
if keep_in_memory:
clip.patcher.current_device = device
# --- 3. Handle VAE ---
if vae is not None:
target_vae = None
if hasattr(vae, "first_stage_model"):
target_vae = vae.first_stage_model
elif hasattr(vae, "model"):
target_vae = vae.model
if target_vae:
if not hasattr(target_vae, "offload_node_state"):
target_vae.offload_node_state = {}
target_vae.offload_node_state['keep'] = keep_in_memory
target_vae.offload_node_state['device'] = device
target_vae.offload_node_state['is_cpu'] = is_cpu
target_vae.offload_node_state['model_ref'] = model
target_vae.offload_node_state['clip_ref'] = clip
if is_cpu or vae_mode == "Vae Patched":
target_vae.to(dtype=torch.float32)
if keep_in_memory:
try:
target_vae.to(device)
target_vae.eval()
except:
pass
# VAE Wrapper (Preserved)
if not hasattr(target_vae, "is_offload_patched"):
target_vae.original_decode_patched = target_vae.decode
def dynamic_kill_switch_wrapper(self, z, *args, **kwargs):
state = getattr(self, "offload_node_state", {})
should_keep = state.get('keep', True)
running_on_cpu = state.get('is_cpu', False)
try:
# 1. Type Safety
if running_on_cpu:
if z.dtype != torch.float32: z = z.to(torch.float32)
else:
dtype_target = next(self.parameters()).dtype
if z.dtype != dtype_target: z = z.to(dtype_target)
# 2. Device Safety
current_weight_device = next(self.parameters()).device
if z.device != current_weight_device:
z = z.to(current_weight_device)
return self.original_decode_patched(z, *args, **kwargs)
finally:
# 3. KILL SWITCH
if not should_keep:
self.to("cpu")
linked_model = state.get('model_ref')
linked_clip = state.get('clip_ref')
if linked_model and hasattr(linked_model, "model"):
linked_model.model.to("cpu")
linked_model.current_device = torch.device("cpu")
if hasattr(linked_model, "patcher"):
linked_model.patcher.current_device = torch.device("cpu")
if linked_clip and hasattr(linked_clip, "cond_stage_model"):
linked_clip.cond_stage_model.to("cpu")
if hasattr(linked_clip, "patcher"):
linked_clip.patcher.current_device = torch.device("cpu")
gc.collect()
torch.cuda.empty_cache()
comfy.model_management.cleanup_models()
comfy.model_management.soft_empty_cache()
target_vae.decode = types.MethodType(dynamic_kill_switch_wrapper, target_vae)
target_vae.is_offload_patched = True
if hasattr(vae, "device") and keep_in_memory:
vae.device = device
if hasattr(vae, "patcher"):
vae.patcher.load_device = device
vae.patcher.offload_device = offload_target
if keep_in_memory:
vae.patcher.current_device = device
return (model, clip, vae)
NODE_CLASS_MAPPINGS = {
"AnyDeviceOffload": AnyDeviceOffload
}
NODE_DISPLAY_NAME_MAPPINGS = {
"AnyDeviceOffload": "Offload Anything (GPU/CPU)"
}