tutorials / custom_nodes /dolphin_nodes /disk_cache_provider.py
bjooo's picture
Upload folder using huggingface_hub
9c98083 verified
Raw
History Blame Contribute Delete
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}")