Pin diffusion and video loads to the live HF cache root
Both read huggingface_hub's import-time HF_HUB_CACHE, which changing the cache folder does not update: progress counted the old root while the download wrote to the new one, and from_pretrained could split one model across both.
This commit is contained in:
parent
5138c39d9b
commit
c095ccb96a
5 changed files with 91 additions and 15 deletions
|
|
@ -133,6 +133,16 @@ logger = get_logger(__name__)
|
|||
_MODEL_KINDS = frozenset({"gguf", "single_file", "pipeline"})
|
||||
|
||||
|
||||
def hub_cache_dir() -> str:
|
||||
"""The cache root every loader call must be pinned to.
|
||||
|
||||
diffusers resolves an unset cache_dir through huggingface_hub's import-time constant,
|
||||
which a mid-session cache-folder change does not update. The prefetch reads the live
|
||||
setting, so without this a single load could split across two roots."""
|
||||
from utils.hf_cache_settings import active_hf_hub_cache
|
||||
return active_hf_hub_cache()
|
||||
|
||||
|
||||
def resolve_model_kind(gguf_filename: Optional[str], model_kind: Optional[str] = None) -> str:
|
||||
"""Classify a load request into one of ``_MODEL_KINDS``.
|
||||
|
||||
|
|
@ -1041,9 +1051,12 @@ class DiffusionBackend:
|
|||
|
||||
@staticmethod
|
||||
def _hub_cache_repo_dir(repo_id: str) -> Path:
|
||||
"""Local HF hub cache dir for ``repo_id``."""
|
||||
from huggingface_hub import constants
|
||||
return Path(constants.HF_HUB_CACHE) / f"models--{repo_id.replace('/', '--')}"
|
||||
"""Local HF hub cache dir for ``repo_id``.
|
||||
|
||||
Reads the live setting, not huggingface_hub's import-time constant: changing the
|
||||
cache folder does not update the constant, so the old one would count bytes in a
|
||||
root the download no longer writes to (progress stuck at 0 for the whole pull)."""
|
||||
return Path(hub_cache_dir()) / f"models--{repo_id.replace('/', '--')}"
|
||||
|
||||
@staticmethod
|
||||
def _cache_bytes(repo_id: str) -> int:
|
||||
|
|
@ -1503,7 +1516,10 @@ class DiffusionBackend:
|
|||
# per-component too (see diffusion_ideogram4.py).
|
||||
pipe = load_ideogram4_pipeline(repo_id, dtype, hf_token = hf_token)
|
||||
else:
|
||||
pipe_kwargs: dict[str, Any] = {"torch_dtype": dtype}
|
||||
pipe_kwargs: dict[str, Any] = {
|
||||
"torch_dtype": dtype,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if hf_token:
|
||||
pipe_kwargs["token"] = hf_token
|
||||
if fam.name == HIDREAM_FAMILY_NAME:
|
||||
|
|
@ -1541,7 +1557,11 @@ class DiffusionBackend:
|
|||
# A single-file SDXL-style checkpoint is the WHOLE pipeline, so load it
|
||||
# through the pipeline class; ``config`` points at the base repo so diffusers
|
||||
# builds the correct structure around the single-file weights.
|
||||
sf_pipe_kwargs: dict[str, Any] = {"torch_dtype": dtype, "config": base}
|
||||
sf_pipe_kwargs: dict[str, Any] = {
|
||||
"torch_dtype": dtype,
|
||||
"config": base,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if hf_token:
|
||||
sf_pipe_kwargs["token"] = hf_token
|
||||
pipe = pipeline_cls.from_single_file(single_file_path, **sf_pipe_kwargs)
|
||||
|
|
@ -1553,6 +1573,7 @@ class DiffusionBackend:
|
|||
"subfolder": "transformer",
|
||||
# Config is fetched from the (possibly gated) base before auth.
|
||||
"token": hf_token,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if kind == "gguf":
|
||||
# Dequantise the GGUF transformer on-device at the compute dtype.
|
||||
|
|
@ -1585,7 +1606,11 @@ class DiffusionBackend:
|
|||
).get("text_encoder"),
|
||||
)
|
||||
else:
|
||||
pipe_kwargs = {"torch_dtype": dtype, "transformer": transformer}
|
||||
pipe_kwargs = {
|
||||
"torch_dtype": dtype,
|
||||
"transformer": transformer,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if hf_token:
|
||||
pipe_kwargs["token"] = hf_token
|
||||
if fam.name == HIDREAM_FAMILY_NAME:
|
||||
|
|
@ -2016,7 +2041,11 @@ class DiffusionBackend:
|
|||
"prequant checkpoint unavailable and the dense transformer does not fit resident"
|
||||
)
|
||||
transformer = transformer_cls.from_pretrained(
|
||||
base, subfolder = "transformer", torch_dtype = dtype, token = hf_token
|
||||
base,
|
||||
subfolder = "transformer",
|
||||
torch_dtype = dtype,
|
||||
token = hf_token,
|
||||
cache_dir = hub_cache_dir(),
|
||||
)
|
||||
pipe = self._assemble_pipe(
|
||||
pipeline_cls,
|
||||
|
|
@ -2104,7 +2133,11 @@ class DiffusionBackend:
|
|||
)
|
||||
pipe.to(device)
|
||||
return pipe
|
||||
pipe_kwargs: dict[str, Any] = {"torch_dtype": dtype, "transformer": transformer}
|
||||
pipe_kwargs: dict[str, Any] = {
|
||||
"torch_dtype": dtype,
|
||||
"transformer": transformer,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if hf_token:
|
||||
pipe_kwargs["token"] = hf_token
|
||||
if getattr(fam, "name", None) == HIDREAM_FAMILY_NAME:
|
||||
|
|
@ -2327,7 +2360,7 @@ class DiffusionBackend:
|
|||
cn_dtype = getattr(torch, str(state.dtype).replace("torch.", ""), None)
|
||||
# Force safetensors for an untrusted remote repo: if the Hub scan failed open above, an
|
||||
# embedded pickle would still deserialize on load. A local dir the user chose is exempt.
|
||||
cn_from_pretrained_kwargs: dict[str, Any] = {}
|
||||
cn_from_pretrained_kwargs: dict[str, Any] = {"cache_dir": hub_cache_dir()}
|
||||
if remote_cn:
|
||||
cn_from_pretrained_kwargs["use_safetensors"] = True
|
||||
cn_model = getattr(diffusers, model_cls_name).from_pretrained(
|
||||
|
|
|
|||
|
|
@ -92,6 +92,8 @@ from .video_families import (
|
|||
supported_video_family_names,
|
||||
)
|
||||
from utils.hardware import clear_gpu_cache
|
||||
# Shared with the image backend so both pin every loader call to the same live cache root.
|
||||
from core.inference.diffusion import hub_cache_dir
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
|
@ -784,9 +786,7 @@ class VideoBackend:
|
|||
try:
|
||||
import os
|
||||
|
||||
from huggingface_hub.constants import HF_HUB_CACHE
|
||||
|
||||
folder = Path(HF_HUB_CACHE) / ("models--" + repo_id.strip().replace("/", "--"))
|
||||
folder = Path(hub_cache_dir()) / ("models--" + repo_id.strip().replace("/", "--"))
|
||||
if not folder.is_dir():
|
||||
return 0
|
||||
total = 0
|
||||
|
|
@ -999,7 +999,9 @@ class VideoBackend:
|
|||
|
||||
# ── build the pipeline.
|
||||
pipeline_cls = getattr(diffusers, fam.pipeline_class)
|
||||
pipe_kwargs: dict[str, Any] = {"torch_dtype": dtype}
|
||||
# cache_dir pins every loader call to the live cache root, so a mid-session
|
||||
# change can't split one model across the old and new roots.
|
||||
pipe_kwargs: dict[str, Any] = {"torch_dtype": dtype, "cache_dir": hub_cache_dir()}
|
||||
if getattr(fam, "vae_force_fp32", False):
|
||||
# Wan's VAE must decode in float32. A scalar torch_dtype truncates its fp32 weights
|
||||
# to bf16 (no _keep_in_fp32_modules); a later .to(float32) only widens lossy values
|
||||
|
|
@ -1036,6 +1038,7 @@ class VideoBackend:
|
|||
"config": base,
|
||||
"subfolder": "transformer",
|
||||
"token": hf_token,
|
||||
"cache_dir": hub_cache_dir(),
|
||||
}
|
||||
if kind == "gguf":
|
||||
sf_kwargs["quantization_config"] = diffusers.GGUFQuantizationConfig(
|
||||
|
|
|
|||
|
|
@ -240,10 +240,14 @@ class _FakeCNModel:
|
|||
torch_dtype = None,
|
||||
token = None,
|
||||
use_safetensors = None,
|
||||
# cache_dir (and any future loader kwarg) rides through: the real call pins the
|
||||
# live cache root so a load cannot split across two of them.
|
||||
**kwargs,
|
||||
):
|
||||
m = cls()
|
||||
m.path = path
|
||||
m.use_safetensors = use_safetensors
|
||||
m.cache_dir = kwargs.get("cache_dir")
|
||||
return m
|
||||
|
||||
def to(self, device):
|
||||
|
|
|
|||
|
|
@ -288,3 +288,37 @@ def test_inactive_cache_model_loads_from_snapshot_path(tmp_path):
|
|||
assert row.model_id == "org/model"
|
||||
assert row.active_cache is False
|
||||
assert row.load_id == str(snapshot)
|
||||
|
||||
|
||||
def test_diffusion_cache_root_follows_a_live_switch(settings_store, tmp_path):
|
||||
# The image/video backends used huggingface_hub's import-time HF_HUB_CACHE constant,
|
||||
# which set_hf_cache_home does not update. The download then wrote to the new root
|
||||
# while progress counted the old one, and a load could split across both.
|
||||
import core.inference.diffusion as diffusion
|
||||
|
||||
moved = tmp_path / "external-c" / "huggingface"
|
||||
# Write the setting straight into the store: set_hf_cache_home's folder validation is
|
||||
# not what is under test, and it rejects the pytest tmp root on macOS.
|
||||
settings_store[hf_cache_settings.CACHE_HOME_SETTING_KEY] = str(moved)
|
||||
|
||||
assert diffusion.hub_cache_dir() == str(moved / "hub")
|
||||
assert diffusion.DiffusionBackend._hub_cache_repo_dir("org/model") == (
|
||||
moved / "hub" / "models--org--model"
|
||||
)
|
||||
|
||||
|
||||
def test_diffusion_loader_calls_pin_the_cache_dir():
|
||||
# Every from_pretrained / from_single_file must carry cache_dir, else diffusers
|
||||
# resolves it through the stale constant and half a load lands in the old root.
|
||||
for rel in ("core/inference/diffusion.py", "core/inference/video.py"):
|
||||
source = (Path(_BACKEND_DIR) / rel).read_text(encoding = "utf-8")
|
||||
for call in ("from_pretrained(", "from_single_file("):
|
||||
for index, line in enumerate(source.splitlines(), start = 1):
|
||||
if not line.strip().startswith(("pipe = ", "transformer = ", "cn_model = ")):
|
||||
continue
|
||||
if call not in line:
|
||||
continue
|
||||
window = "\n".join(source.splitlines()[index - 1 : index + 8])
|
||||
assert "cache_dir" in window or "kwargs" in window, (
|
||||
f"{rel}:{index} calls {call} without a pinned cache_dir"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1150,7 +1150,7 @@ def test_cache_bytes_counts_incomplete_blobs(fake_runtime, tmp_path, monkeypatch
|
|||
# scan_cache_dir skips in-flight *.incomplete blobs, so the old counter froze at the
|
||||
# last completed blob for the whole multi-GB shard pull. The walk must count both,
|
||||
# without double-counting snapshot symlinks.
|
||||
import huggingface_hub.constants as hub_constants
|
||||
import core.inference.video as video_mod
|
||||
|
||||
repo_dir = tmp_path / "models--Wan-AI--Wan2.2-TI2V-5B-Diffusers"
|
||||
blobs = repo_dir / "blobs"
|
||||
|
|
@ -1160,7 +1160,9 @@ def test_cache_bytes_counts_incomplete_blobs(fake_runtime, tmp_path, monkeypatch
|
|||
snap = repo_dir / "snapshots" / "deadbeef"
|
||||
snap.mkdir(parents = True)
|
||||
(snap / "model_index.json").symlink_to(blobs / "aa11") # must not double-count
|
||||
monkeypatch.setattr(hub_constants, "HF_HUB_CACHE", str(tmp_path))
|
||||
# The live cache root, not huggingface_hub's import-time constant: the counter follows
|
||||
# a mid-session cache-folder change, which the constant does not.
|
||||
monkeypatch.setattr(video_mod, "hub_cache_dir", lambda: str(tmp_path))
|
||||
|
||||
backend = VideoBackend()
|
||||
assert backend._cache_bytes("Wan-AI/Wan2.2-TI2V-5B-Diffusers") == 1500
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue