199 lines
15 KiB
Python
199 lines
15 KiB
Python
import comfy.model_management as model_management
|
|
import gc
|
|
import torch
|
|
import psutil
|
|
|
|
class AnyType(str):
|
|
def __ne__(self, __value: object) -> bool: # Make this class instance be equal to everything, to bypass ComfyUI's type validation check.
|
|
return False
|
|
|
|
anyType = AnyType("*")
|
|
|
|
class MatryoshkaTuple(tuple):
|
|
def __getitem__(self, index): # Override the tuple "__getitem__" to always return the 1st element. This allows the node's return tuple to grow dynamically while ComfyUI believes the tuple length is still 1.
|
|
if index > 0:
|
|
index = 0
|
|
return super().__getitem__(index)
|
|
|
|
class ControlOrderFreeMemory:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"free_memory": ("BOOLEAN", {
|
|
"default": False,
|
|
"tooltip": "Unload all models and release as much VRAM & RAM as possible while routing & preserving all 'persist_any' passthrough data. Any models passed into 'persist_any' will stay loaded if they were not already unloaded by the sender nodes (e.g. CLIP / some GGUF loaders). If any routed model fails to persist, fallback to using it immediately after load and reload only when needed. Prints how much VRAM & RAM has been freed on the ComfyUI session terminal.",
|
|
}),
|
|
},
|
|
"optional": {
|
|
"persist_any_1": (anyType, {
|
|
"tooltip": "Persist any type of data through to the next node e.g. latents, conditioning, images, masks, models (except CLIP / some GGUF models already unloaded by the sender nodes), etc. This data survives the 'free_memory' operation. I/O slots expand adaptively",
|
|
}),
|
|
}
|
|
}
|
|
|
|
@classmethod
|
|
def VALIDATE_INPUTS(cls, **kwargs):
|
|
return True
|
|
|
|
OUTPUT_NODE = True # Let this be a hybrid node that can both be in the middle or at the end of a workflow (with no outputs connected).
|
|
RETURN_TYPES = MatryoshkaTuple((anyType, )) # Let this node's output slot cardinality grow dynamically from JS hooks, while ComfyUI type validation check believes this is just a tuple of 1 "AnyType" element.
|
|
RETURN_NAMES = MatryoshkaTuple(("persist_any_1", )) # Same trick
|
|
OUTPUT_TOOLTIPS = MatryoshkaTuple(("Persist any type of data through to the next node e.g. latents, conditioning, images, masks, models (except CLIP / some GGUF models already unloaded by the sender nodes), etc. This data survives the 'free_memory' operation. I/O slots expand adaptively", ))
|
|
FUNCTION = "passthrough"
|
|
CATEGORY = "Control Order & Free Memory"
|
|
DESCRIPTION = """Control the execution order of nodes by routing any data (as many as you need - I/O slots expand adaptively) through this node. Ensures all input-connected nodes finish executing first before the output-connected nodes start executing.
|
|
|
|
All input & output slots are AnyType (*). They can hook onto any node, including loader-type nodes like "Load Model", "Load VAE", "Load CLIP", etc. For connecting into loader-type nodes, you can use the "📁 Filename Selector" helper node (already installed with this custom node) to select & feed the filename into the input "persist_any_N" slot that corresponds to the output "persist_any_N" slot that is connecting into the loader-type node.
|
|
|
|
Optionally unload all models and release as much VRAM & RAM as possible while routing & preserving all 'persist_any' passthrough data. Any models passed into 'persist_any' will stay loaded if they were not already unloaded by the sender nodes (e.g. Load CLIP / some GGUF loaders, etc.). If any routed model fails to persist, fallback to using it immediately after load and reload only when needed. This node will also print how much VRAM & RAM has been freed on the ComfyUI session terminal.
|
|
"""
|
|
|
|
def passthrough(self, **kwargs):
|
|
# Collect every connected input (only connected slots appear in kwargs when the prompt is sent)
|
|
input_keys = [key for key in kwargs if key.startswith("persist_any_")]
|
|
|
|
# Secure the input values in the exact order they were connected - to persist them throughout the potential memory cleanup operation
|
|
if input_keys:
|
|
# Sort the connected inputs in the exact order they were added
|
|
input_keys.sort(key=lambda input_key: int(input_key[12:]))
|
|
output_values = [None] * int(input_keys[-1][12:]) # Only need to define output up to the highest seen persist_any_N, and let ComfyUI output None for any undefined outputs beyond N.
|
|
for input_key in input_keys:
|
|
output_values[int(input_key[12:]) - 1] = kwargs[input_key]
|
|
else:
|
|
output_values = [None]
|
|
|
|
# Run only if "free_memory" was toggled ON
|
|
if kwargs.get("free_memory"):
|
|
print("🔷 Control Order & Free Memory • Full VRAM & RAM cleanup 🔷")
|
|
|
|
try:
|
|
# Take snapshot of VRAM before cleanup
|
|
if torch.cuda.is_available(): # NVIDIA
|
|
initial_vram = torch.cuda.memory_allocated()
|
|
elif hasattr(torch, 'mps') and torch.mps.is_available(): # Apple Silicon
|
|
initial_vram = torch.mps.current_allocated_memory()
|
|
elif hasattr(torch, 'xpu') and torch.xpu.is_available(): # Intel XPU
|
|
initial_vram = torch.xpu.memory.memory_allocated()
|
|
elif hasattr(torch, 'npu') and torch.npu.is_available(): # Ascend NPU
|
|
initial_vram = torch.npu.memory_allocated()
|
|
elif hasattr(torch, 'mlu') and torch.mlu.is_available(): # Cambricon MLU
|
|
initial_vram = torch.mlu.memory_allocated()
|
|
else: # CPU / fallback
|
|
initial_vram = None
|
|
|
|
# Take snapshot of RAM before cleanup
|
|
initial_ram = psutil.virtual_memory().used # This is fully cross-platform & included in all ComfyUI installations.
|
|
|
|
# If any of the inputs were models (and were not already unloaded by the sender nodes, e.g. CLIP / some GGUF loaders), keep them loaded
|
|
loaded_models = model_management.loaded_models() # returns a list of the actual model objects currently tracked by ComfyUI in current_loaded_models
|
|
keep_loaded = []
|
|
for key in input_keys:
|
|
if kwargs[key] in loaded_models: # if any of the inputs was a model that's currently loaded, keep it loaded.
|
|
keep_loaded.append(kwargs[key])
|
|
|
|
# Unload everything else
|
|
if keep_loaded:
|
|
model_management.free_memory(1e30, model_management.get_torch_device(), keep_loaded)
|
|
# This is the core "free as much VRAM as possible" function, where "1e30" means "free this large amount of memory (i.e. everything)". It walks the ComfyUI internal list "current_loaded_models", skips unloading anything in keep_loaded, calls model_unload() on the rest (which does the model unload/detach, unpatch weights, set real_model=None, and for some models a controlled partially_unload() to the offload device - no forced CPU offload unless the model itself decides to partially unload), pops them from current_loaded_models, then calls cleanup_models_gc() (which conditionally runs gc.collect() + soft_empty_cache() if any momdel in current_loaded_models is_dead() i.e. memory leak is suspected) and soft_empty_cache() once (if it unloaded at least 1 model).
|
|
# This is the official ComfyUI maintained method that safely manages its internal model load states through current_loaded_models entries and other internal memory accounting.
|
|
print(" - All models unloaded (except models connected into persist_any)")
|
|
else:
|
|
model_management.unload_all_models()
|
|
# This just calls free_memory(1e30, get_torch_device()) with no keep_loaded models list, defaulting it to [] (i.e. it unloads all models).
|
|
# This is the official ComfyUI maintained method that safely manages its internal model load states through current_loaded_models entries and other internal memory accounting.
|
|
print(" - All models unloaded")
|
|
|
|
print(" - Synchronizing hardware accelerator")
|
|
model_management.soft_empty_cache(True)
|
|
# This is a device-agnostic wrapper that does:
|
|
# → CUDA: torch.cuda.synchronize() + torch.cuda.empty_cache() + torch.cuda.ipc_collect()
|
|
# → MPS / XPU / NPU / MLU: runs the equivalent empty_cache for that backend, and the force param is ignored in current ComfyUI (legacy).
|
|
except Exception as e:
|
|
print(f" - Non-fatal error during unload: {e}")
|
|
finally:
|
|
# First pass (Device-agnostic empty_cache)
|
|
# DESIGN: While model_management.soft_empty_cache(True) is a convenient one-liner for cross-device empty_cache(), I've extracted all the cross-device operations to extend & optimize the cleanup sequence per device, while skipping any extra calls to torch.cuda.synchronize() which is now redundant after model_management.soft_empty_cache(True)
|
|
print(" - Clearing VRAM")
|
|
if torch.cuda.is_available(): # NVIDIA
|
|
torch.cuda.empty_cache() # releases the cached VRAM and available memory held by the allocator but not currently in use
|
|
elif hasattr(torch, 'mps') and torch.mps.is_available(): # Apple Silicon
|
|
torch.mps.empty_cache()
|
|
elif hasattr(torch, 'xpu') and torch.xpu.is_available(): # Intel XPU
|
|
torch.xpu.empty_cache()
|
|
elif hasattr(torch, 'npu') and torch.npu.is_available(): # Ascend NPU
|
|
torch.npu.empty_cache()
|
|
elif hasattr(torch, 'mlu') and torch.mlu.is_available(): # Cambricon MLU
|
|
torch.mlu.empty_cache()
|
|
else: # CPU / fallback
|
|
print(" - No GPU accelerator detected.")
|
|
|
|
print(" - Clearing RAM")
|
|
gc.collect() # release objects from memory that no longer have active references. Critical for freeing CPU RAM + any Python object references after the tensors are gone.
|
|
|
|
# Second pass (catches anything GC just released + IPC on CUDA)
|
|
if torch.cuda.is_available(): # NVIDIA
|
|
torch.cuda.empty_cache() # double empty_cache to catch lingering tensors after GC
|
|
torch.cuda.ipc_collect() # frees any lingering CUDA IPC / shared-memory handles that empty_cache() sometimes misses. Useful when models were loaded with certain GGUF/quantized loaders or in multi-process scenarios. Harmless in a normal single-process ComfyUI server.
|
|
model_management.cleanup_models() # lightweight & harmless "remove dead/stale model wrappers" helper that scans current_loaded_models and removes entries where real_model() is None (i.e. dead wrappers that free_memory may have left behind in some edge cases), pops them from current_loaded_models, and deletes the wrapper. This is only meaningful after the model tensors have already been unloaded + garbage-collected such that every dead model wrapper whose real_model() just became None can be caught.
|
|
print(" - Clearing CUDA stats")
|
|
try:
|
|
torch.cuda.reset_peak_memory_stats() # Optional stats reset. try catch because this can raise in edge cases (no active CUDA context, older PyTorch, or after certain errors)
|
|
except:
|
|
pass
|
|
final_vram = torch.cuda.memory_allocated() # Take snapshot of VRAM after cleanup
|
|
elif hasattr(torch, 'mps') and torch.mps.is_available(): # Apple Silicon
|
|
torch.mps.empty_cache()
|
|
model_management.cleanup_models()
|
|
final_vram = torch.mps.current_allocated_memory() # Take snapshot of VRAM after cleanup
|
|
elif hasattr(torch, 'xpu') and torch.xpu.is_available(): # Intel XPU
|
|
torch.xpu.empty_cache()
|
|
model_management.cleanup_models()
|
|
final_vram = torch.xpu.memory.memory_allocated() # Take snapshot of VRAM after cleanup
|
|
elif hasattr(torch, 'npu') and torch.npu.is_available(): # Ascend NPU
|
|
torch.npu.empty_cache()
|
|
model_management.cleanup_models()
|
|
final_vram = torch.npu.memory_allocated() # Take snapshot of VRAM after cleanup
|
|
elif hasattr(torch, 'mlu') and torch.mlu.is_available(): # Cambricon MLU
|
|
torch.mlu.empty_cache()
|
|
model_management.cleanup_models()
|
|
final_vram = torch.mlu.memory_allocated() # Take snapshot of VRAM after cleanup
|
|
else: # CPU / fallback
|
|
final_vram = None
|
|
|
|
# Take snapshot of RAM after cleanup
|
|
final_ram = psutil.virtual_memory().used
|
|
|
|
# Print memory analytics
|
|
print(" - VRAM & RAM cleanup complete")
|
|
if initial_vram is None:
|
|
print(" - GPU VRAM: No GPU accelerator detected.")
|
|
else:
|
|
print(f" - GPU VRAM: Initial usage: {initial_vram/1073741824:.2f} GB, Final usage: {final_vram/1073741824:.2f} GB, Freed: {(initial_vram - final_vram)/1073741824:.2f} GB") # 1073741824 == (1024 ** 3)
|
|
print(f" - System RAM: Initial usage: {initial_ram/1073741824:.2f} GB, Final usage: {final_ram/1073741824:.2f} GB, Freed: {(initial_ram - final_ram)/1073741824:.2f} GB")
|
|
|
|
return tuple(output_values)
|
|
|
|
class FileNameSelector:
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"string": ("STRING", {
|
|
"default": "",
|
|
"multiline": False,
|
|
"tooltip": "Select a filename and further edit the string here."
|
|
}),
|
|
}
|
|
}
|
|
|
|
OUTPUT_NODE = True
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("STRING",)
|
|
OUTPUT_TOOLTIPS = ("Final string for downstream use", )
|
|
FUNCTION = "select_filename"
|
|
CATEGORY = "Control Order & Free Memory"
|
|
DESCRIPTION = "Select a filename from your native OS file picker and optionally edit the string for downstream use."
|
|
|
|
def select_filename(self, string):
|
|
return (string,) |