189 lines
6.1 KiB
Python
189 lines
6.1 KiB
Python
"""Shared (cached) model loaders for the Silver node pack.
|
|
|
|
The core pain this solves: when jobs are submitted to ComfyUI over the HTTP
|
|
API, loader nodes run once per prompt. A plain CLIPLoader/UNetLoader creates
|
|
a brand new ModelPatcher every time, so its weights get garbage-collected once
|
|
the prompt finishes. On the next prompt the full file is re-read from disk --
|
|
the 8.2 GB flux2 Qwen text encoder takes ~3.5 minutes here.
|
|
|
|
By keeping a strong reference to the ModelPatcher in a module-level cache
|
|
keyed by a cache key, the object survives across prompts. The weights stay in
|
|
RAM, so a later prompt only re-transfers to VRAM (seconds) instead of reading
|
|
the file from disk (minutes).
|
|
"""
|
|
|
|
import logging
|
|
import uuid
|
|
|
|
from nodes import CLIPLoader, UNETLoader
|
|
import folder_paths
|
|
|
|
|
|
_CACHE = {}
|
|
_CACHE_COUNT = {}
|
|
|
|
|
|
def _cache_key(key_opt, *parts):
|
|
"""Compute the cache key, honouring an explicit key_opt."""
|
|
key_opt = key_opt.strip() if isinstance(key_opt, str) else ""
|
|
if key_opt:
|
|
return key_opt
|
|
return "_".join(parts)
|
|
|
|
|
|
def _cache_put(key, tag, value):
|
|
_CACHE[key] = (tag, value)
|
|
_CACHE_COUNT[key] = _CACHE_COUNT.get(key, 0) + 1
|
|
|
|
|
|
def _cache_get(key):
|
|
return _CACHE.get(key)
|
|
|
|
|
|
def _cache_weak_hash(key):
|
|
return (key, _CACHE_COUNT.get(key, 0))
|
|
|
|
|
|
def clear_all():
|
|
_CACHE.clear()
|
|
_CACHE_COUNT.clear()
|
|
|
|
|
|
class SilverDiffusionModelShared:
|
|
"""A shared diffusion (UNet/Flux) model loader that is cached across prompts.
|
|
|
|
key_opt defines the cache key so several jobs can reuse one loaded model
|
|
instead of re-reading the file each time.
|
|
"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"model_name": (folder_paths.get_filename_list("diffusion_models"),),
|
|
"weight_type": (["default", "fp8_e4m3fn", "fp8_e4m3fn_fast", "fp8_e5m2"],),
|
|
"key_opt": ("STRING", {"default": "", "multiline": False}),
|
|
"mode": (["Auto", "Override Cache", "Read Only"],),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("MODEL", "STRING")
|
|
RETURN_NAMES = ("model", "cache key")
|
|
FUNCTION = "doit"
|
|
CATEGORY = "silver/Backend"
|
|
|
|
def doit(self, model_name, weight_type="default", key_opt="", mode="Auto"):
|
|
key = _cache_key(key_opt, model_name, weight_type)
|
|
|
|
if mode == "Read Only":
|
|
entry = _cache_get(key)
|
|
if entry is None:
|
|
raise RuntimeError(f"[SilverDiffusionModelShared] cache key '{key}' is not loaded")
|
|
_, model = entry
|
|
return model, key
|
|
|
|
if key not in _CACHE or mode == "Override Cache":
|
|
loader = UNETLoader()
|
|
model = loader.load_unet(model_name, weight_type)[0]
|
|
_cache_put(key, "diffusion", model)
|
|
logging.info(f"[Silver] diffusion model '{model_name}' cached under key '{key}'")
|
|
else:
|
|
_, model = _CACHE[key]
|
|
logging.info(f"[Silver] diffusion model '{key}' reused (loading skip)")
|
|
|
|
return model, key
|
|
|
|
@staticmethod
|
|
def IS_CHANGED(model_name, weight_type="default", key_opt="", mode="Auto"):
|
|
key = _cache_key(key_opt, model_name, weight_type)
|
|
if mode == "Override Cache":
|
|
return model_name
|
|
return _cache_weak_hash(key)
|
|
|
|
|
|
class SilverTextEncoderShared:
|
|
"""A shared text-encoder (CLIP) loader that is cached across API prompts.
|
|
|
|
Drop-in caching wrapper around the stock CLIPLoader. Supports any type the
|
|
stock node does, including the flux2 Qwen text encoder used by the app.
|
|
"""
|
|
|
|
_TYPES = CLIPLoader.INPUT_TYPES()["required"]["type"][0]
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {
|
|
"required": {
|
|
"clip_name": (folder_paths.get_filename_list("text_encoders"),),
|
|
"type": (cls._TYPES,),
|
|
"key_opt": ("STRING", {"default": "", "multiline": False}),
|
|
"mode": (["Auto", "Override Cache", "Read Only"],),
|
|
},
|
|
"optional": {
|
|
"device": (["default", "cpu"], {"advanced": True}),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("CLIP", "STRING")
|
|
RETURN_NAMES = ("clip", "cache key")
|
|
FUNCTION = "doit"
|
|
CATEGORY = "silver/Backend"
|
|
|
|
def doit(self, clip_name, type="flux2", key_opt="", mode="Auto", device="default"):
|
|
key = _cache_key(key_opt, clip_name, type, device)
|
|
|
|
if mode == "Read Only":
|
|
entry = _cache_get(key)
|
|
if entry is None:
|
|
raise RuntimeError(f"[SilverTextEncoderShared] cache key '{key}' is not loaded")
|
|
_, clip = entry
|
|
return clip, key
|
|
|
|
if key not in _CACHE or mode == "Override Cache":
|
|
loader = CLIPLoader()
|
|
clip = loader.load_clip(clip_name, type=type, device=device)[0]
|
|
_cache_put(key, "text", clip)
|
|
logging.info(f"[Silver] text encoder '{clip_name}' cached under key '{key}'")
|
|
else:
|
|
logging.info(f"[Silver] text encoder '{clip_name}' reused from '{key}' (loading skip)")
|
|
clip = _CACHE[key][1]
|
|
|
|
return clip, key
|
|
|
|
@staticmethod
|
|
def IS_CHANGED(clip_name, type, key_opt="", mode="Auto", device="default"):
|
|
key = _cache_key(key_opt, clip_name, type, device)
|
|
if mode == "Override Cache":
|
|
return _cache_weak_hash(key) + (uuid.uuid4().hex,)
|
|
return _cache_weak_hash(key)
|
|
|
|
|
|
class SilverClearModelCache:
|
|
"""Drops all cached diffusion/text-encoder models to free RAM on the node."""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(cls):
|
|
return {"required": {}}
|
|
|
|
RETURN_TYPES = ()
|
|
FUNCTION = "doit"
|
|
CATEGORY = "silver/Backend"
|
|
OUTPUT_NODE = True
|
|
|
|
def doit(self):
|
|
cleared = len(_CACHE)
|
|
clear_all()
|
|
return {"ui": {"cleared": [cleared]}}
|
|
|
|
|
|
NODE_CLASS_MAPPINGS = {
|
|
"SilverDiffusionModelShared": SilverDiffusionModelShared,
|
|
"SilverTextEncoderShared": SilverTextEncoderShared,
|
|
"SilverClearModelCache": SilverClearModelCache,
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"SilverDiffusionModelShared": "Silver Shared Diffusion Model",
|
|
"SilverTextEncoderShared": "Silver Shared Text Encoder",
|
|
"SilverClearModelCache": "Silver Clear Model Cache",
|
|
} |