Download custom_nodes/dolphin_nodes/disk_cache_provider.py from bjooo/tutorials: direct link, hf CLI and curl.
- Browser
- Download file 7.93 kB
-
https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/disk_cache_provider.py
- Command line
-
hf download hf://bjooo/tutorials/custom_nodes/dolphin_nodes/disk_cache_provider.py
-
curl -L -o disk_cache_provider.py https://huggingface.co/bjooo/tutorials/resolve/main/custom_nodes/dolphin_nodes/disk_cache_provider.py
7.93 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| λμ€ν¬ κΈ°λ° λ Έλ μΆλ ₯ μΊμ (ComfyUI cache provider API). | |
| ComfyUIλ λ Έλ μΆλ ₯μ "μ λ ₯ μλͺ "(μμ ― κ° + λͺ¨λ μ‘°μ λ Έλμ μλͺ , μΈμ κ° | |
| κ²°μ μ SHA256)μΌλ‘ μΊμ±νλλ°, κ·Έ μΊμλ λ©λͺ¨λ¦¬μλ§ μμ΄μ (1) μ¬μμνλ©΄ | |
| μ¬λΌμ§κ³ (2) κΈ°λ³Έ RAM_PRESSURE λͺ¨λμμλ RAMμ΄ λΆμ‘±νλ©΄ μμλ‘ μ¦λ°νλ€. | |
| μ΄ νλ‘λ°μ΄λλ μΊμ μ μ₯ μ λμ€ν¬μλ μ°κ³ , λ©λͺ¨λ¦¬ μΊμ λ―Έμ€ μ λμ€ν¬μμ | |
| 볡μνλ€. ν¨κ³Ό: | |
| - μ¬μμν΄λ μλ/ν둬ννΈ/μ΄λ―Έμ§κ° κ°μ ν΄λ¦½μ JoyCaptionλΆν° μνλ¬κΉμ§ | |
| μ λΆ μ€ν΅ (μ€νλμ§ μκ³ λμ€ν¬μμ κ²°κ³Όλ§ λ³΅μ) | |
| - μλ νλλ§ λ°κΎΈλ©΄ μ νν κ·Έ ν΄λ¦½ 체μΈλ§ λ€μ μμ± (μλͺ μ λͺ¨λ μ λ ₯μ΄ | |
| ν¬ν¨λλ―λ‘ λ¬΄ν¨ν νμ μ ComfyUIκ° μμμ μ ννκ² ν΄μ€) | |
| - MODEL/CLIP/VAE/NOISE κ°μ κ°μ²΄ μΆλ ₯μ μ§λ ¬ν λΆκ°λ‘ μλ μ μΈλμ΄ μ μ₯ μ λ¨ | |
| μΊμ μμΉ/μ©λμ νκ²½λ³μλ‘ μ‘°μ : | |
| DOLPHIN_DISK_CACHE_DIR (κΈ°λ³Έ: <output>/_node_disk_cache) | |
| DOLPHIN_DISK_CACHE_MAX_GB (κΈ°λ³Έ: 40 β μ΄κ³Ό μ μ€λλ κ²λΆν° μμ ) | |
| DOLPHIN_DISK_CACHE_MAX_ENTRY_GB (κΈ°λ³Έ: 2 β μ΄λ³΄λ€ ν° λ¨μΌ μΆλ ₯μ μ μ₯ μ ν¨) | |
| """ | |
| import os | |
| import time | |
| import asyncio | |
| import logging | |
| import torch | |
| import folder_paths | |
| from comfy_execution.cache_provider import register_cache_provider | |
| from comfy_api.latest._caching import CacheProvider, CacheValue | |
| log = logging.getLogger("dolphin.diskcache") | |
| _DEFAULT_DIR = os.path.join(folder_paths.get_output_directory(), "_node_disk_cache") | |
| CACHE_DIR = os.environ.get("DOLPHIN_DISK_CACHE_DIR", _DEFAULT_DIR) | |
| MAX_TOTAL_BYTES = int(float(os.environ.get("DOLPHIN_DISK_CACHE_MAX_GB", "15")) * (1024 ** 3)) | |
| # VAEDecode/μ΄λ―Έμ§ λ°°μΉ κ²°κ³Ό(μλ°±MB~1GB, λμ½λλ νλ μ)λ μΌλΆλ¬ μν μλλ‘ λλ€. | |
| # λΉμΌ 건 λν¨μ μνλ§(latent, μμMB)μ΄κ³ VAE λμ½λλ latentλ§ μμΌλ©΄ λͺ μ΄λ©΄ | |
| # λ€μ λλ―λ‘, ν° λμ½λ κ²°κ³ΌκΉμ§ λμ€ν¬μ μμλ μ€μ΅μ΄ μλ€ - μ©λλ§ λ¨Ήλλ€. | |
| MAX_ENTRY_BYTES = int(float(os.environ.get("DOLPHIN_DISK_CACHE_MAX_ENTRY_GB", "0.2")) * (1024 ** 3)) | |
| _ALLOWED_SCALARS = (str, int, float, bool, bytes, type(None)) | |
| def _serializable(obj): | |
| """ν μ/μ€μΉΌλΌ/컨ν μ΄λλ§ νμ©. MODEL, NOISE, SAMPLER λ± κ°μ²΄κ° μμ΄λ©΄ False.""" | |
| if isinstance(obj, torch.Tensor): | |
| return True | |
| if isinstance(obj, _ALLOWED_SCALARS): | |
| return True | |
| if isinstance(obj, (list, tuple)): | |
| return all(_serializable(x) for x in obj) | |
| if isinstance(obj, dict): | |
| return all(isinstance(k, _ALLOWED_SCALARS) and _serializable(v) for k, v in obj.items()) | |
| return False | |
| def _to_cpu(obj): | |
| if isinstance(obj, torch.Tensor): | |
| return obj.detach().to("cpu") | |
| if isinstance(obj, list): | |
| return [_to_cpu(x) for x in obj] | |
| if isinstance(obj, tuple): | |
| return tuple(_to_cpu(x) for x in obj) | |
| if isinstance(obj, dict): | |
| return {k: _to_cpu(v) for k, v in obj.items()} | |
| return obj | |
| def _tensor_bytes(obj): | |
| if isinstance(obj, torch.Tensor): | |
| return obj.numel() * obj.element_size() | |
| if isinstance(obj, dict): | |
| return sum(_tensor_bytes(v) for v in obj.values()) | |
| if isinstance(obj, (list, tuple)): | |
| return sum(_tensor_bytes(v) for v in obj) | |
| return 0 | |
| class DolphinDiskCache(CacheProvider): | |
| def __init__(self, directory=CACHE_DIR): | |
| self.dir = directory | |
| os.makedirs(self.dir, exist_ok=True) | |
| self.log_path = os.path.join(self.dir, "_diskcache.log") | |
| def _flog(self, msg): | |
| # μ½μμ΄ μ 보μ΄λ νκ²½μμλ μ μ₯/μ€ν΅ κ²½λ‘λ₯Ό μΆμ ν μ μκ² νμΌλ‘ λ¨κΈ΄λ€. | |
| # μ£Όμ: should_cacheκ° μμ νΈμΆ μ λ λ Έλλ ComfyUI μͺ½ NaN ν€ κ²μ΄νΈ | |
| # (_contains_self_unequal)μμ κ±Έλ¬μ§ κ² - λ‘κ·Έμ μ μ°νλ κ² μμ²΄κ° λ¨μ. | |
| try: | |
| with open(self.log_path, "a", encoding="utf-8") as f: | |
| f.write(f"{time.strftime('%H:%M:%S')} {msg}\n") | |
| except OSError: | |
| pass | |
| def _path(self, context): | |
| return os.path.join(self.dir, f"{context.cache_key_hash}.pt") | |
| async def on_lookup(self, context): | |
| path = self._path(context) | |
| if not os.path.isfile(path): | |
| return None | |
| def load(): | |
| return torch.load(path, map_location="cpu", weights_only=False) | |
| try: | |
| data = await asyncio.to_thread(load) | |
| except Exception as e: | |
| log.warning(f"[DiskCache] μμλ μΊμ μμ ({context.class_type}): {e}") | |
| try: | |
| os.remove(path) | |
| except OSError: | |
| pass | |
| return None | |
| try: | |
| os.utime(path, None) # LRU: μ κ·Ό μκ° κ°±μ (prune μ μ€λλ κ²λΆν° μμ ) | |
| except OSError: | |
| pass | |
| print(f"πΎ [DiskCache] {context.class_type} λμ€ν¬μμ 볡μ (node {context.node_id})") | |
| self._flog(f"RESTORE {context.class_type} (node {context.node_id})") | |
| return CacheValue(outputs=data["outputs"], ui=data.get("ui")) | |
| def should_cache(self, context, value=None): | |
| if value is None: # lookup μμ - νμΌ μ‘΄μ¬ μ¬λΆλ‘ νλ¨νλ―λ‘ νμ μλ | |
| return True | |
| if not _serializable(value.outputs): | |
| self._flog(f"SKIP not-serializable {context.class_type} (node {context.node_id})") | |
| return False | |
| size = _tensor_bytes(value.outputs) | |
| if size > MAX_ENTRY_BYTES: | |
| self._flog(f"SKIP too-big {context.class_type} {size/1024/1024:.0f}MB (node {context.node_id})") | |
| return False | |
| self._flog(f"STORE-OK {context.class_type} {size/1024/1024:.1f}MB (node {context.node_id})") | |
| return True | |
| async def on_store(self, context, value): | |
| path = self._path(context) | |
| if os.path.exists(path): | |
| return | |
| try: | |
| payload = { | |
| "outputs": _to_cpu(value.outputs), | |
| "ui": value.ui, | |
| "class_type": context.class_type, | |
| "saved_at": time.time(), | |
| } | |
| def save(): | |
| tmp = path + ".tmp" | |
| torch.save(payload, tmp) | |
| os.replace(tmp, path) | |
| await asyncio.to_thread(save) | |
| self._flog(f"SAVED {context.class_type} (node {context.node_id})") | |
| except Exception as e: | |
| log.warning(f"[DiskCache] μ μ₯ μ€ν¨ ({context.class_type}): {e}") | |
| self._flog(f"SAVE-FAIL {context.class_type} (node {context.node_id}): {type(e).__name__}: {e}") | |
| try: | |
| os.remove(path + ".tmp") | |
| except OSError: | |
| pass | |
| def on_prompt_end(self, prompt_id): | |
| try: | |
| self._prune() | |
| except Exception as e: | |
| log.warning(f"[DiskCache] prune μ€ν¨: {e}") | |
| def _prune(self): | |
| entries = [] | |
| total = 0 | |
| with os.scandir(self.dir) as it: | |
| for e in it: | |
| if e.name.endswith(".pt") and e.is_file(): | |
| st = e.stat() | |
| entries.append((st.st_mtime, st.st_size, e.path)) | |
| total += st.st_size | |
| if total <= MAX_TOTAL_BYTES: | |
| return | |
| entries.sort() # mtime μ€λλ μ | |
| for _, size, path in entries: | |
| try: | |
| os.remove(path) | |
| total -= size | |
| except OSError: | |
| pass | |
| if total <= MAX_TOTAL_BYTES: | |
| break | |
| _provider = None | |
| def register(): | |
| global _provider | |
| if _provider is None: | |
| _provider = DolphinDiskCache() | |
| register_cache_provider(_provider) | |
| print(f"πΎ [Dolphin] λμ€ν¬ λ Έλ μΊμ νμ±ν: {_provider.dir}") | |