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:
parent
efa72431cc
commit
224b90eb02
6 changed files with 130 additions and 17 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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().
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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."""
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue