Make forced offline hub-specific and close the parent preflight gap

A forced guard now only no-ops when HF_HUB_OFFLINE itself is truthy:
huggingface_hub ignores TRANSFORMERS_OFFLINE, so with only that flag set the
GGUF cache-size check could still call get_paths_info. Snapshot selection
prefers revisions holding the inventoried safetensors weights, so a newest
metadata-only revision no longer shadows a complete older one and fails a
valid candidate. The orchestrator's parent-side preflight (transformers tier
probe and GPU sizing, which calls hf model_info) runs under the local-only
guard, closed before the worker spawn so the child does not inherit the
offline env for its whole lifetime; ordinary loads keep their current
unguarded behavior.
This commit is contained in:
Unsloth 2026-07-26 23:41:35 -07:00
commit 224b90eb02
6 changed files with 130 additions and 17 deletions

View file

@ -506,6 +506,13 @@ def _hf_env_offline() -> bool:
return os.environ.get("HF_HUB_OFFLINE", "").strip().lower() in {"1", "true", "yes", "on"}
def _hub_offline_env_truthy() -> bool:
"""HF_HUB_OFFLINE specifically is truthy. huggingface_hub does not honor
TRANSFORMERS_OFFLINE, so a forced local-only guard may only no-op when the
Hub flag itself is already set."""
return os.environ.get("HF_HUB_OFFLINE", "").strip().lower() in {"1", "true", "yes", "on"}
# Overlapping offline guards share one env override: the count tracks active
# guards and the saved values are restored only when the LAST guard exits, so
# a request finishing early cannot re-enable network for one still running.
@ -531,9 +538,11 @@ def _hf_offline_if_dns_dead(force: bool = False):
# exits, whichever request finishes first.
_OFFLINE_GUARD_STATE["count"] += 1
entered = True
elif _hf_env_offline():
elif _hf_env_offline() and (not force or _hub_offline_env_truthy()):
# User-set truthy env (count is 0): already offline, nothing to
# arrange or restore.
# arrange or restore. A forced guard only trusts HF_HUB_OFFLINE
# itself: TRANSFORMERS_OFFLINE=1 alone leaves hub API calls (e.g.
# get_paths_info) online, so force still installs both flags.
pass
elif not force and "HF_HUB_OFFLINE" in os.environ:
# A user-pinned falsy value stays authoritative for ordinary loads.

View file

@ -986,7 +986,24 @@ class InferenceOrchestrator:
self.loading_models.add(model_name)
try:
needed_major = "5" if needs_transformers_5(model_name) else "4"
# Parent-side metadata preflight stays offline for local-only
# loads: the tier probe reads model configs and GPU sizing calls
# hf model_info. The guard closes BEFORE the spawn below, so the
# worker does not inherit HF_HUB_OFFLINE for its whole lifetime
# (its own bootstrap guard scopes the load and restores for
# generation-time fetches).
import contextlib
from core.inference.llama_cpp import _hf_offline_if_dns_dead
def _preflight_offline():
# Ordinary loads keep their current (unguarded) behavior.
if local_files_only:
return _hf_offline_if_dns_dead(force = True)
return contextlib.nullcontext()
with _preflight_offline():
needed_major = "5" if needs_transformers_5(model_name) else "4"
# Build config dict for subprocess
sub_config = {
@ -1013,12 +1030,13 @@ class InferenceOrchestrator:
if mlx_distributed
else None,
}
resolved_gpu_ids, gpu_selection = prepare_gpu_selection(
gpu_ids,
model_name = model_name,
hf_token = hf_token,
load_in_4bit = load_in_4bit,
)
with _preflight_offline():
resolved_gpu_ids, gpu_selection = prepare_gpu_selection(
gpu_ids,
model_name = model_name,
hf_token = hf_token,
load_in_4bit = load_in_4bit,
)
sub_config["resolved_gpu_ids"] = resolved_gpu_ids
sub_config["gpu_selection"] = gpu_selection
# Parent-detected backend for the worker's apply_gpu_ids().

View file

@ -7,13 +7,29 @@ import os
from typing import Optional
def _snapshot_dir_fallback(repo_id: str, cache_dir: Optional[str]) -> Optional[str]:
"""Newest snapshots/* dir (by mtime) holding a config.json.
def _snapshot_has_weights(rev: str) -> bool:
"""Whether a snapshot dir holds at least one safetensors weight file.
This mirrors the inventory scanner's latest_snapshot_dir selection, so the
load targets the snapshot that made the row eligible. It also covers
revision-only layouts (refs/ missing or pruned) that
snapshot_download(local_files_only = True) cannot map through refs/main.
Background loads only target safetensors-format rows (adapters and
pickle checkpoints are excluded upstream), so this is the runnable-weight
signal that made the inventory row eligible.
"""
try:
return any(name.endswith(".safetensors") for name in os.listdir(rev))
except OSError:
return False
def _snapshot_dir_fallback(repo_id: str, cache_dir: Optional[str]) -> Optional[str]:
"""Newest snapshots/* dir (by mtime) holding a config.json, preferring
revisions that also hold weights.
This mirrors the inventory scanner's selection (newest revision that
actually carries the inventoried weights), so the load targets the
snapshot that made the row eligible: a newest metadata-only revision must
not shadow an older complete one. It also covers revision-only layouts
(refs/ missing or pruned) that snapshot_download(local_files_only = True)
cannot map through refs/main.
"""
if cache_dir is None:
try:
@ -33,7 +49,8 @@ def _snapshot_dir_fallback(repo_id: str, cache_dir: Optional[str]) -> Optional[s
candidates = [rev for rev in revisions if os.path.isfile(os.path.join(rev, "config.json"))]
if not candidates:
return None
return max(candidates, key = os.path.getmtime)
weightful = [rev for rev in candidates if _snapshot_has_weights(rev)]
return max(weightful or candidates, key = os.path.getmtime)
def resolve_local_snapshot_path(

View file

@ -162,6 +162,35 @@ def test_refless_fallback_picks_newest_snapshot_with_config(tmp_path):
assert Path(resolved).resolve() == new.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."""

View file

@ -34,7 +34,7 @@ class _NullLogger:
def _load_guard(dns_dead: bool = False):
src = _LLAMA_CPP.read_text()
start = src.index("# Overlapping offline guards")
start = src.index("def _hub_offline_env_truthy")
end = src.index("_SLOT_SAVE_MAX_BYTES")
end = src.rindex("try:", start, end)
block = src[start:end]
@ -100,6 +100,20 @@ def test_truthy_user_env_is_a_noop(clean_env):
assert os.environ["HF_HUB_OFFLINE"] == "1"
def test_transformers_only_env_does_not_satisfy_forced_guard(clean_env):
"""TRANSFORMERS_OFFLINE=1 alone is not hub-offline: huggingface_hub
ignores it, so a forced guard must still install HF_HUB_OFFLINE for the
block and restore the prior state after."""
guard = _load_guard()
os.environ["TRANSFORMERS_OFFLINE"] = "1"
g = guard(force = True)
assert g.__enter__() is True
assert os.environ.get("HF_HUB_OFFLINE") == "1"
g.__exit__(None, None, None)
assert "HF_HUB_OFFLINE" not in os.environ
assert os.environ.get("TRANSFORMERS_OFFLINE") == "1"
def test_nonforce_joins_active_override(clean_env):
"""A DNS-alive non-force guard entering while a forced guard is active must
JOIN the refcount (deferring the restore) rather than no-op."""

View file

@ -1115,7 +1115,7 @@ def test_background_candidate_filters_have_no_side_effects():
# TRUTHY env value short-circuits), and overlapping guards are refcounted
# so the env is restored only when the LAST one exits; behavior is
# exercised directly in test_offline_guard_refcount.py.
assert "elif _hf_env_offline():" in llama
assert "elif _hf_env_offline() and (not force or _hub_offline_env_truthy()):" in llama
assert "_OFFLINE_GUARD_LOCK" in llama
assert '_OFFLINE_GUARD_STATE["count"] += 1' in llama
@ -1218,6 +1218,32 @@ def test_background_picks_mirror_inventory_and_skip_installers():
assert "snapshot_size_bytes" in schema
def test_forced_offline_is_hub_specific_and_covers_parent_preflight():
"""Round-16 gates. A forced guard may only no-op when HF_HUB_OFFLINE
itself is truthy (huggingface_hub ignores TRANSFORMERS_OFFLINE), snapshot
selection prefers revisions that hold the inventoried safetensors weights
so a metadata-only newest revision cannot shadow a complete older one,
and the orchestrator's parent-side preflight (transformers tier probe,
GPU sizing via hf model_info) runs under the local-only guard, closed
before the worker spawn so the child does not inherit the env."""
llama = _read_backend("core/inference/llama_cpp.py")
assert "def _hub_offline_env_truthy(" in llama
assert "not force or _hub_offline_env_truthy()" in llama
helper = _read_backend("hub/utils/local_snapshot.py")
assert "def _snapshot_has_weights(" in helper
assert "max(weightful or candidates, key = os.path.getmtime)" in helper
orchestrator = _read_backend("core/inference/orchestrator.py")
preflight = orchestrator.split("def _preflight_offline():", 1)[1]
assert "_hf_offline_if_dns_dead(force = True)" in preflight
# Both metadata call sites are guarded, and the guard closes before spawn.
assert preflight.count("with _preflight_offline():") == 2
tier_probe = preflight.split("with _preflight_offline():", 1)[1]
assert "needs_transformers_5" in tier_probe.split("with _preflight_offline():", 1)[0]
assert "prepare_gpu_selection(" in tier_probe.split("_spawn_subprocess", 1)[0]
def test_gguf_background_loads_never_download_companions():
"""A cached GGUF load can still fetch from the Hub through its optional
companions (mmproj, MTP drafter) or a cache-miss main quant. Background