- nvfp4: _quantization_metadata -> comfy_quant synthesis, SlotEntry extra params (block_scale), dual-scale LoRA fold, orig_dtype fixup - LoRA: plain-mode block LoRAs no longer silently dropped; folded on GPU after weights land - loader: parallel positioned reads, pipelined group load, no per-tensor clone, header-only dtype probes - block swap: read_block intra-block parallel + disk_workers node option; async D2H write-back on transfer stream; lazy evict_all skips wasted write-back - arch: split models/loader.py out of wan_model.py; single utils/keys.py; LoRA folding consolidated in utils/lora.py; global utils/log.py ([BerniniR][Tag] format, BERNINI_LOG_LEVEL)
117 lines
3.8 KiB
Python
117 lines
3.8 KiB
Python
"""
|
|
Shared text-embedding disk cache and CLIP encoding helpers.
|
|
|
|
Used by both ``PromptEmbedding`` and ``SegmentSchedule`` nodes to avoid
|
|
duplicated encoding of identical prompts across workflow runs.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
from pathlib import Path
|
|
from typing import Optional
|
|
|
|
import torch
|
|
import comfy.sd
|
|
import folder_paths
|
|
|
|
from .log import get_logger as _get_logger
|
|
|
|
logger = _get_logger("TextCache")
|
|
# ---------------------------------------------------------------------------
|
|
# Disk cache
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_CACHE_DIR = Path(__file__).parent.parent / "text_embed_cache"
|
|
_MAX_CACHE_FILES = 200 # evict oldest files when exceeded
|
|
|
|
|
|
def _ensure_cache_dir() -> Path:
|
|
_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
|
return _CACHE_DIR
|
|
|
|
|
|
def _evict_oldest_if_needed() -> None:
|
|
"""Remove oldest cache files if count exceeds _MAX_CACHE_FILES."""
|
|
try:
|
|
files = sorted(_CACHE_DIR.glob("*.pt"), key=lambda p: p.stat().st_mtime)
|
|
while len(files) > _MAX_CACHE_FILES:
|
|
oldest = files.pop(0)
|
|
oldest.unlink(missing_ok=True)
|
|
logger.debug(f"Evicted old cache file: {oldest.name}")
|
|
except Exception:
|
|
pass # never fail on cache maintenance
|
|
|
|
|
|
def _cache_key(prompt: str, tag: str = "") -> str:
|
|
payload = prompt if not tag else f"{tag}::{prompt}"
|
|
return hashlib.md5(payload.encode("utf-8")).hexdigest()
|
|
|
|
|
|
def _cache_path(prompt: str, tag: str = "") -> Path:
|
|
return _ensure_cache_dir() / f"{_cache_key(prompt, tag)}.pt"
|
|
|
|
|
|
def _load_cached_conditioning(prompt: str, tag: str = "") -> Optional[list]:
|
|
path = _cache_path(prompt, tag)
|
|
if not path.exists():
|
|
return None
|
|
try:
|
|
data = torch.load(path, map_location="cpu", weights_only=False)
|
|
cond = data.get("cond")
|
|
pooled = data.get("pooled", {})
|
|
if cond is None:
|
|
return None
|
|
return [[cond, pooled]]
|
|
except Exception as e:
|
|
logger.warning(f"Corrupt cache {path.name}: {e}")
|
|
try:
|
|
path.unlink(missing_ok=True)
|
|
except Exception:
|
|
pass
|
|
return None
|
|
|
|
|
|
def _save_cached_conditioning(prompt: str, conditioning: list, tag: str = "") -> None:
|
|
if not conditioning or len(conditioning[0]) < 2:
|
|
return
|
|
cond, pooled = conditioning[0]
|
|
safe_pooled = {}
|
|
if isinstance(pooled, dict):
|
|
for k, v in pooled.items():
|
|
if isinstance(v, (torch.Tensor, str, int, float, bool)) or v is None:
|
|
safe_pooled[k] = v
|
|
try:
|
|
path = _cache_path(prompt, tag)
|
|
torch.save({"cond": cond.cpu(), "pooled": safe_pooled}, path)
|
|
logger.info(f"Saved text embed cache: {path.name}")
|
|
_evict_oldest_if_needed()
|
|
except Exception as e:
|
|
logger.warning(f"Failed to save cache: {e}")
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Text encoding
|
|
# ---------------------------------------------------------------------------
|
|
|
|
def _encode_text(clip, text: str):
|
|
"""Encode text using ComfyUI's CLIPTextEncode logic."""
|
|
tokens = clip.tokenize(text)
|
|
return clip.encode_from_tokens_scheduled(tokens)
|
|
|
|
|
|
def _load_clip_internal(clip_name: str, clip_type: str = "wan", device: str = "cpu"):
|
|
"""Load a Wan-series text encoder on demand."""
|
|
clip_path = folder_paths.get_full_path_or_raise("text_encoders", clip_name)
|
|
clip_type_enum = getattr(comfy.sd.CLIPType, clip_type.upper(), comfy.sd.CLIPType.WAN)
|
|
model_options = {}
|
|
if device == "cpu":
|
|
model_options["load_device"] = torch.device("cpu")
|
|
model_options["offload_device"] = torch.device("cpu")
|
|
return comfy.sd.load_clip(
|
|
ckpt_paths=[clip_path],
|
|
embedding_directory=folder_paths.get_folder_paths("embeddings"),
|
|
clip_type=clip_type_enum,
|
|
model_options=model_options,
|
|
)
|