199 lines
14 KiB
Python
199 lines
14 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). 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.) 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,) |