mirror of
https://github.com/jamiepine/voicebox.git
synced 2026-10-03 17:15:19 -07:00
fix(cache): don't treat orphaned .incomplete blobs as an in-progress download
is_model_cached() marked a model as not-cached whenever any .incomplete blob existed in its cache dir, even when a completed blob with the same hash already sat next to it. A retried/concurrent download can leave this orphan behind after the real transfer already finished, which made the model appear perpetually "downloading" and re-trigger a full re-download on every load. Only .incomplete files with no matching completed blob now count as a genuinely in-progress download.
This commit is contained in:
committed by
capy-ai-staging[bot]
parent
6ad47dda89
commit
f62aff0809
@@ -48,11 +48,18 @@ def is_model_cached(
|
|||||||
if not repo_cache.exists():
|
if not repo_cache.exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Incomplete blobs mean a download is still in progress
|
# An .incomplete blob means a download is still in progress -- unless
|
||||||
|
# a completed blob with the same hash already sits next to it, which
|
||||||
|
# happens when a retried/concurrent download leaves a stale .incomplete
|
||||||
|
# behind after the real transfer already finished. Only orphaned
|
||||||
|
# .incomplete files (no matching completed blob) count as "in progress".
|
||||||
blobs_dir = repo_cache / "blobs"
|
blobs_dir = repo_cache / "blobs"
|
||||||
if blobs_dir.exists() and any(blobs_dir.glob("*.incomplete")):
|
if blobs_dir.exists():
|
||||||
logger.debug(f"Found .incomplete files for {hf_repo}")
|
for incomplete in blobs_dir.glob("*.incomplete"):
|
||||||
return False
|
completed = incomplete.with_name(incomplete.name.removesuffix(".incomplete"))
|
||||||
|
if not completed.exists():
|
||||||
|
logger.debug(f"Found in-progress .incomplete file for {hf_repo}")
|
||||||
|
return False
|
||||||
|
|
||||||
snapshots_dir = repo_cache / "snapshots"
|
snapshots_dir = repo_cache / "snapshots"
|
||||||
if not snapshots_dir.exists():
|
if not snapshots_dir.exists():
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for ``is_model_cached``'s handling of stale ``.incomplete`` blobs.
|
||||||
|
|
||||||
|
A retried or concurrent download can leave an orphaned ``.incomplete`` file
|
||||||
|
next to its now-completed counterpart (same blob hash, no suffix). Only a
|
||||||
|
genuinely in-progress download -- an ``.incomplete`` with no completed blob
|
||||||
|
alongside it -- should mark the model as not cached.
|
||||||
|
|
||||||
|
``is_model_cached`` is extracted and exec'd standalone (instead of importing
|
||||||
|
``backend.backends.base``) so this test doesn't pull in the module's sibling
|
||||||
|
imports (audio/progress/hf_progress/tasks), which in turn require the full
|
||||||
|
ML stack (torch/transformers/librosa/fastapi/...) this pure filesystem check
|
||||||
|
never touches.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
_SOURCE = (Path(__file__).parent.parent / "backends" / "base.py").read_text()
|
||||||
|
_MODULE = ast.parse(_SOURCE)
|
||||||
|
_FUNC_SRC = next(
|
||||||
|
ast.get_source_segment(_SOURCE, node)
|
||||||
|
for node in _MODULE.body
|
||||||
|
if isinstance(node, ast.FunctionDef) and node.name == "is_model_cached"
|
||||||
|
)
|
||||||
|
|
||||||
|
_namespace = {
|
||||||
|
"Path": Path,
|
||||||
|
"Optional": Optional,
|
||||||
|
"logger": logging.getLogger("test_is_model_cached"),
|
||||||
|
}
|
||||||
|
exec(_FUNC_SRC, _namespace) # noqa: S102
|
||||||
|
is_model_cached = _namespace["is_model_cached"]
|
||||||
|
|
||||||
|
|
||||||
|
def _make_repo_cache(tmp_path, repo="org/model"):
|
||||||
|
repo_dir = tmp_path / ("models--" + repo.replace("/", "--"))
|
||||||
|
blobs_dir = repo_dir / "blobs"
|
||||||
|
snapshots_dir = repo_dir / "snapshots" / "abc123"
|
||||||
|
blobs_dir.mkdir(parents=True)
|
||||||
|
snapshots_dir.mkdir(parents=True)
|
||||||
|
return repo_dir, blobs_dir, snapshots_dir
|
||||||
|
|
||||||
|
|
||||||
|
def test_orphaned_incomplete_blob_does_not_block_cache_hit(tmp_path, monkeypatch):
|
||||||
|
import huggingface_hub.constants as hf_constants
|
||||||
|
|
||||||
|
monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path))
|
||||||
|
|
||||||
|
repo = "org/model"
|
||||||
|
_, blobs_dir, snapshots_dir = _make_repo_cache(tmp_path, repo)
|
||||||
|
|
||||||
|
completed_blob = blobs_dir / "deadbeef"
|
||||||
|
completed_blob.write_bytes(b"weights")
|
||||||
|
(blobs_dir / "deadbeef.incomplete").write_bytes(b"stale partial")
|
||||||
|
(snapshots_dir / "model.safetensors").symlink_to(completed_blob)
|
||||||
|
|
||||||
|
assert is_model_cached(repo) is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_genuinely_in_progress_download_is_not_cached(tmp_path, monkeypatch):
|
||||||
|
import huggingface_hub.constants as hf_constants
|
||||||
|
|
||||||
|
monkeypatch.setattr(hf_constants, "HF_HUB_CACHE", str(tmp_path))
|
||||||
|
|
||||||
|
repo = "org/model"
|
||||||
|
_, blobs_dir, snapshots_dir = _make_repo_cache(tmp_path, repo)
|
||||||
|
|
||||||
|
(blobs_dir / "feedface.incomplete").write_bytes(b"partial")
|
||||||
|
(snapshots_dir / "config.json").write_text("{}")
|
||||||
|
|
||||||
|
assert is_model_cached(repo) is False
|
||||||
Reference in New Issue
Block a user