unsloth/studio/backend/tests/test_local_snapshot_resolution.py
Unsloth 3026ff9e79 Preflight local-only loads against the resolved path and keep GGUF rows on GGUF snapshots
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.
2026-07-27 05:24:22 -07:00

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