tutorials / disk_cache_provider.py
bjooo's picture
Add files using upload-large-folder tool
89f7a50 verified
Raw History Blame
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}")