| |
| """ |
| λμ€ν¬ κΈ°λ° λ
Έλ μΆλ ₯ μΊμ (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)) |
| |
| |
| |
| 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): |
| |
| |
| |
| 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) |
| 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: |
| 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() |
| 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}") |
|
|