The load route's sidecar tier probe reached _remote_lora_base's raw HTTP request and Hub config reads, and the training guard's memory estimation can call hf model_info; neither honors offline mode, so both now run against the resolved snapshot path for local-only loads (a local path resolves from config.json on disk and skips the remote adapter probe). GGUF cached rows select a GGUF-bearing snapshot (top level or one folder deep) instead of reusing the safetensors-preferring model helper, so a mixed repo's safetensors revision cannot become the GGUF row's load target while the UI reports a quant.
252 lines
8.9 KiB
Python
252 lines
8.9 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-only
|
|
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
|
|
|
"""Local-only snapshot resolution for background auto-loads.
|
|
|
|
A cache populated outside Studio (no download manifest) passes the partial
|
|
check while missing shard files, and ``from_pretrained`` on a repo id would
|
|
download the gaps. Background loads therefore rewrite the load path to the
|
|
LOCALLY resolved snapshot: resolution never touches the network, an uncached
|
|
repo resolves to None (409 upstream), and an incomplete snapshot still
|
|
resolves so the weight load fails on the missing files instead of fetching
|
|
them. No GPU or network required.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
_BACKEND_DIR = str(Path(__file__).resolve().parent.parent)
|
|
if _BACKEND_DIR not in sys.path:
|
|
sys.path.insert(0, _BACKEND_DIR)
|
|
|
|
pytest.importorskip("huggingface_hub")
|
|
|
|
from hub.utils.local_snapshot import resolve_local_snapshot_path
|
|
|
|
|
|
_REV = "0123456789abcdef0123456789abcdef01234567"
|
|
|
|
|
|
def _build_cached_repo(
|
|
cache_dir: Path,
|
|
repo_id: str,
|
|
files: dict[str, str],
|
|
with_refs: bool = True,
|
|
rev: str = _REV,
|
|
) -> Path:
|
|
"""Lay out a minimal HF hub cache entry the way huggingface_hub expects:
|
|
``models--org--name/refs/main`` pointing at a snapshot directory.
|
|
``with_refs = False`` builds the revision-only layout (pruned or foreign
|
|
caches) the inventory scanner accepts."""
|
|
repo_dir = cache_dir / f"models--{repo_id.replace('/', '--')}"
|
|
snapshot = repo_dir / "snapshots" / rev
|
|
snapshot.mkdir(parents = True)
|
|
if with_refs:
|
|
(repo_dir / "refs").mkdir(exist_ok = True)
|
|
(repo_dir / "refs" / "main").write_text(rev)
|
|
for name, content in files.items():
|
|
(snapshot / name).write_text(content)
|
|
return snapshot
|
|
|
|
|
|
def test_cached_repo_resolves_to_its_snapshot_dir(tmp_path):
|
|
snapshot = _build_cached_repo(
|
|
tmp_path,
|
|
"org/tiny-model",
|
|
{"config.json": "{}", "model.safetensors": "weights"},
|
|
)
|
|
resolved = resolve_local_snapshot_path("org/tiny-model", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == snapshot.resolve()
|
|
|
|
|
|
def test_incomplete_snapshot_still_resolves_locally(tmp_path):
|
|
"""Missing shards must not block resolution: the local path is what makes
|
|
the subsequent weight load fail closed instead of downloading."""
|
|
snapshot = _build_cached_repo(
|
|
tmp_path,
|
|
"org/half-downloaded",
|
|
{
|
|
"config.json": "{}",
|
|
"model-00001-of-00002.safetensors": "first shard only",
|
|
},
|
|
)
|
|
resolved = resolve_local_snapshot_path("org/half-downloaded", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == snapshot.resolve()
|
|
|
|
|
|
def test_uncached_repo_resolves_to_none(tmp_path):
|
|
assert resolve_local_snapshot_path("org/never-downloaded", cache_dir = str(tmp_path)) is None
|
|
|
|
|
|
def test_newest_snapshot_preferred_over_refs_main(tmp_path):
|
|
"""A newer snapshot downloaded at an explicit revision outranks the older
|
|
refs/main target: the inventory surfaces the newest snapshot by mtime, so
|
|
the load must resolve the same one instead of an older (possibly
|
|
incomplete) main revision."""
|
|
import os
|
|
import time
|
|
|
|
old_main = _build_cached_repo(
|
|
tmp_path,
|
|
"org/newer-rev",
|
|
{"config.json": "{}"},
|
|
rev = "a" * 40,
|
|
)
|
|
stale = time.time() - 1000
|
|
os.utime(old_main, (stale, stale))
|
|
newer = _build_cached_repo(
|
|
tmp_path,
|
|
"org/newer-rev",
|
|
{"config.json": "{}", "model.safetensors": "weights"},
|
|
with_refs = False,
|
|
rev = "b" * 40,
|
|
)
|
|
resolved = resolve_local_snapshot_path("org/newer-rev", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == newer.resolve()
|
|
|
|
|
|
def test_revision_only_snapshot_resolves_without_refs(tmp_path):
|
|
"""snapshot_download(local_files_only = True) needs refs/main, but the
|
|
inventory scanner accepts revision-only layouts (pruned refs), so the
|
|
resolver must fall back to the snapshot directory itself."""
|
|
snapshot = _build_cached_repo(
|
|
tmp_path,
|
|
"org/no-refs",
|
|
{"config.json": "{}", "model.safetensors": "weights"},
|
|
with_refs = False,
|
|
)
|
|
resolved = resolve_local_snapshot_path("org/no-refs", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == snapshot.resolve()
|
|
|
|
|
|
def test_refless_fallback_picks_newest_snapshot_with_config(tmp_path):
|
|
"""With several revision dirs, the fallback must pick the newest one that
|
|
actually holds a config.json, skipping empty or partial revisions."""
|
|
import os
|
|
import time
|
|
|
|
old = _build_cached_repo(
|
|
tmp_path,
|
|
"org/multi-rev",
|
|
{"config.json": "{}"},
|
|
with_refs = False,
|
|
rev = "a" * 40,
|
|
)
|
|
stale = time.time() - 1000
|
|
os.utime(old, (stale, stale))
|
|
new = _build_cached_repo(
|
|
tmp_path,
|
|
"org/multi-rev",
|
|
{"config.json": "{}"},
|
|
with_refs = False,
|
|
rev = "b" * 40,
|
|
)
|
|
configless = _build_cached_repo(
|
|
tmp_path,
|
|
"org/multi-rev",
|
|
{"tokenizer.json": "{}"},
|
|
with_refs = False,
|
|
rev = "c" * 40,
|
|
)
|
|
assert configless.exists()
|
|
resolved = resolve_local_snapshot_path("org/multi-rev", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == new.resolve()
|
|
|
|
|
|
def test_gguf_rows_select_gguf_bearing_snapshot_in_mixed_repos(tmp_path):
|
|
"""A mixed repo caching a newer safetensors revision beside an older GGUF
|
|
revision: the GGUF row's snapshot selection must return the GGUF-bearing
|
|
revision, not the safetensors one the model row prefers."""
|
|
import os
|
|
import sys
|
|
import time
|
|
|
|
backend_dir = str(Path(__file__).resolve().parent.parent)
|
|
if backend_dir not in sys.path:
|
|
sys.path.insert(0, backend_dir)
|
|
from hub.services.models.cache_inventory import (
|
|
_cached_gguf_repo_snapshot_path,
|
|
_cached_model_snapshot_path,
|
|
)
|
|
|
|
repo_dir = tmp_path / "models--org--mixed"
|
|
gguf_rev = repo_dir / "snapshots" / ("a" * 40)
|
|
gguf_rev.mkdir(parents = True)
|
|
(gguf_rev / "mixed-Q4_K_M.gguf").write_text("gguf-bytes")
|
|
stale = time.time() - 1000
|
|
os.utime(gguf_rev, (stale, stale))
|
|
st_rev = repo_dir / "snapshots" / ("b" * 40)
|
|
st_rev.mkdir(parents = True)
|
|
(st_rev / "config.json").write_text("{}")
|
|
(st_rev / "model.safetensors").write_text("weights")
|
|
|
|
gguf_pick = _cached_gguf_repo_snapshot_path(repo_dir)
|
|
assert gguf_pick is not None
|
|
assert Path(gguf_pick).resolve() == gguf_rev.resolve()
|
|
model_pick = _cached_model_snapshot_path(repo_dir)
|
|
assert model_pick is not None
|
|
assert Path(model_pick).resolve() == st_rev.resolve()
|
|
|
|
|
|
def test_weightless_newest_snapshot_does_not_shadow_complete_older_one(tmp_path):
|
|
"""A newest metadata-only revision (config.json, no weights) must not win
|
|
over an older revision holding the inventoried safetensors weights: the
|
|
inventory made the row eligible from the weightful revision, so the load
|
|
must resolve that one instead of failing on the weightless dir."""
|
|
import os
|
|
import time
|
|
|
|
complete = _build_cached_repo(
|
|
tmp_path,
|
|
"org/meta-newest",
|
|
{"config.json": "{}", "model.safetensors": "weights"},
|
|
with_refs = False,
|
|
rev = "a" * 40,
|
|
)
|
|
stale = time.time() - 1000
|
|
os.utime(complete, (stale, stale))
|
|
_build_cached_repo(
|
|
tmp_path,
|
|
"org/meta-newest",
|
|
{"config.json": "{}"},
|
|
with_refs = False,
|
|
rev = "b" * 40,
|
|
)
|
|
resolved = resolve_local_snapshot_path("org/meta-newest", cache_dir = str(tmp_path))
|
|
assert resolved is not None
|
|
assert Path(resolved).resolve() == complete.resolve()
|
|
|
|
|
|
def test_refless_fallback_without_config_resolves_to_none(tmp_path):
|
|
"""A snapshots dir with no config.json anywhere is not a loadable text
|
|
model cache; resolution must stay None (409 upstream), not guess."""
|
|
_build_cached_repo(
|
|
tmp_path,
|
|
"org/no-config",
|
|
{"tokenizer.json": "{}"},
|
|
with_refs = False,
|
|
)
|
|
assert resolve_local_snapshot_path("org/no-config", cache_dir = str(tmp_path)) is None
|
|
|
|
|
|
def test_resolution_never_uses_the_network(tmp_path, monkeypatch):
|
|
"""local_files_only resolution must not open any connection even when the
|
|
repo is absent (the tempting fallback would be a Hub metadata call)."""
|
|
import socket
|
|
|
|
def _no_network(*_args, **_kwargs):
|
|
raise AssertionError("network access attempted during local resolution")
|
|
|
|
monkeypatch.setattr(socket.socket, "connect", _no_network)
|
|
_build_cached_repo(tmp_path, "org/offline-ok", {"config.json": "{}"})
|
|
assert resolve_local_snapshot_path("org/offline-ok", cache_dir = str(tmp_path))
|
|
assert resolve_local_snapshot_path("org/absent", cache_dir = str(tmp_path)) is None
|