Studio: fix loading split GGUFs from the local HF cache (#7273)

* Studio: don't resolve HF cache GGUF symlinks to blob paths

* Studio: handle split GGUF symlink layouts
This commit is contained in:
oobabooga 2026-07-21 06:48:19 -03:00 committed by GitHub
commit 54f21b3a87
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 134 additions and 5 deletions

View file

@ -119,6 +119,13 @@ def _build_cache(
return snap
def _symlink_or_skip(link: Path, target: Path) -> None:
try:
link.symlink_to(target)
except OSError as exc:
pytest.skip(f"symlinks unavailable: {exc}")
@pytest.fixture
def hf_cache(tmp_path, monkeypatch):
"""Point ``huggingface_hub.constants.HF_HUB_CACHE`` at a temp dir."""
@ -1084,7 +1091,7 @@ class TestListLocalGgufVariantsSubdir:
target.write_bytes(b"\0" * 20)
out = _find_local_gguf_by_variant(str(tmp_path), "Q4_K_M")
assert out == str(target.resolve())
assert out == str(target.absolute())
def test_find_local_gguf_by_variant_skips_big_endian_only_match(self, tmp_path):
from utils.models.model_config import _find_local_gguf_by_variant
@ -1094,6 +1101,57 @@ class TestListLocalGgufVariantsSubdir:
assert _find_local_gguf_by_variant(str(tmp_path), "Q4_K_M") is None
def test_find_local_gguf_by_variant_keeps_split_symlink_name(self, tmp_path):
from utils.models.model_config import _find_local_gguf_by_variant
blobs = tmp_path / "blobs"
blobs.mkdir()
snap = tmp_path / "snapshots" / "rev" / "BF16"
snap.mkdir(parents = True)
(tmp_path / "snapshots" / "rev" / "config.json").write_text("{}")
for i, sha in enumerate(("aa" * 32, "bb" * 32), start = 1):
(blobs / sha).write_bytes(b"\0" * 10)
_symlink_or_skip(snap / f"model-BF16-0000{i}-of-00002.gguf", blobs / sha)
out = _find_local_gguf_by_variant(str(tmp_path / "snapshots" / "rev"), "BF16")
assert out is not None
assert Path(out).name == "model-BF16-00001-of-00002.gguf"
def test_detect_gguf_model_keeps_split_symlink_name(self, tmp_path):
from utils.models.model_config import detect_gguf_model
blobs = tmp_path / "blobs"
blobs.mkdir()
snap = tmp_path / "snapshots" / "rev"
snap.mkdir(parents = True)
for i, (sha, size) in enumerate((("cc" * 32, 10), ("dd" * 32, 20)), start = 1):
(blobs / sha).write_bytes(b"\0" * size)
_symlink_or_skip(snap / f"model-BF16-0000{i}-of-00002.gguf", blobs / sha)
out = detect_gguf_model(str(snap))
assert out is not None
assert Path(out).name == "model-BF16-00001-of-00002.gguf"
def test_lone_split_symlink_uses_colocated_target_shards(self, tmp_path):
from utils.models.model_config import _find_local_gguf_by_variant, detect_gguf_model
target_dir = tmp_path / "external" / "BF16"
target_dir.mkdir(parents = True)
target = target_dir / "model-BF16-00001-of-00002.gguf"
target.write_bytes(b"\0" * 10)
(target_dir / "model-BF16-00002-of-00002.gguf").write_bytes(b"\0" * 10)
local = tmp_path / "local"
local.mkdir()
(local / "config.json").write_text("{}")
link = local / target.name
_symlink_or_skip(link, target)
expected = str(target.absolute())
assert _find_local_gguf_by_variant(str(local), "BF16") == expected
assert detect_gguf_model(str(local)) == expected
assert detect_gguf_model(str(link)) == expected
def test_model_config_variant_ignores_big_endian_sibling(self, tmp_path):
from utils.models.model_config import ModelConfig

View file

@ -1249,6 +1249,77 @@ def _iter_gguf_files(directory: Path, recursive: bool = False):
yield f
_GGUF_SPLIT_FILE_RE = re.compile(
r"^(?P<prefix>.+)-(?P<index>\d{5})-of-(?P<total>\d{5})\.gguf$",
re.IGNORECASE,
)
def _colocated_first_split_shard(path: Path) -> tuple[Optional[Path], bool]:
"""Return shard 1 and whether every shard is beside *path*."""
match = _GGUF_SPLIT_FILE_RE.match(path.name)
if match is None:
return None, False
prefix = match.group("prefix").casefold()
total_text = match.group("total")
total = int(total_text)
if total < 1:
return None, False
first: Optional[Path] = None
indices: set[int] = set()
try:
siblings = path.parent.iterdir()
for sibling in siblings:
sibling_match = _GGUF_SPLIT_FILE_RE.match(sibling.name)
if (
sibling_match is None
or sibling_match.group("prefix").casefold() != prefix
or sibling_match.group("total") != total_text
):
continue
try:
if not sibling.is_file():
continue
except OSError:
continue
index = int(sibling_match.group("index"))
if not 1 <= index <= total:
continue
indices.add(index)
if index == 1:
first = sibling
except OSError:
return None, False
return first, first is not None and len(indices) == total
def _local_gguf_load_path(path: Path) -> Path:
"""Choose a loadable local path while preserving complete symlink sets."""
if _GGUF_SPLIT_FILE_RE.match(path.name) is None:
return path.absolute()
first, complete = _colocated_first_split_shard(path)
if complete and first is not None:
return first.absolute()
try:
is_symlink = path.is_symlink()
except OSError:
is_symlink = False
if is_symlink:
try:
target = path.resolve()
except OSError:
return (first or path).absolute()
target_first, _ = _colocated_first_split_shard(target)
return (target_first or target).absolute()
return (first or path).absolute()
def detect_mmproj_file(path: str, search_root: Optional[str] = None) -> Optional[str]:
"""Find the mmproj GGUF for a model.
@ -1434,7 +1505,7 @@ def detect_gguf_model(path: str) -> Optional[str]:
except OSError:
is_dir = False # stat() unavailable in the lock window
if not is_dir:
return str(p.absolute()) # absolute() keeps symlink names readable
return str(_local_gguf_load_path(p))
# Directory named "*.gguf": fall through to the dir scan below.
# Case 2: directory containing .gguf files (skip mmproj / MTP drafter)
@ -1452,7 +1523,7 @@ def detect_gguf_model(path: str) -> Optional[str]:
gguf_files.append(f)
gguf_files.sort(key = lambda f: f.stat().st_size, reverse = True)
if gguf_files:
return str(gguf_files[0].resolve())
return str(_local_gguf_load_path(gguf_files[0]))
return None
@ -1879,7 +1950,7 @@ def _find_local_gguf_by_variant(directory: str, variant: str) -> Optional[str]:
For sharded GGUFs (multiple files sharing a quant label), returns the
first shard (sorted by name), which is what ``llama-server -m`` expects.
Returns the resolved absolute path, or ``None`` if no match.
Returns the absolute path, or ``None`` if no match.
"""
p = _resolve_gguf_dir(Path(directory))
if p is None:
@ -1900,7 +1971,7 @@ def _find_local_gguf_by_variant(directory: str, variant: str) -> Optional[str]:
matches.append(f)
matches.sort()
if matches:
return str(matches[0].resolve())
return str(_local_gguf_load_path(matches[0]))
return None