Keep an explicit local_files_only load local-only at save time
transformers takes local_files_only as an explicit from_pretrained parameter, so it never lands in tokenizer.init_kwargs, and _offline_aware_load restores HF_HUB_OFFLINE / TRANSFORMERS_OFFLINE as soon as the load window closes. A VLM loaded with local_files_only = True but no offline env var therefore came back with the Hub repo id in name_or_path and nothing recording the request, so _tokenizer_wants_local_only returned False on the later save and _has_tokenizer_model fell through to HfApi.model_info - and then _preserve_sentencepiece_tokenizer_assets fetched tokenizer.model from the Hub with local_files_only = False. On a disconnected host that is a network wait before the export gives up. Stamp the load's local-only mode onto the returned processor and its tokenizer inside the forced-offline window, and honour that stamp in _tokenizer_wants_local_only, so the save path inherits the load's contract. Verified against a real hub-cache layout whose snapshot has tokenizer metadata but no tokenizer.model: before, one model_info call plus an hf_hub_download with local_files_only = False; after, zero model_info calls and cache probes only. Two tests added to tests/saving/test_offline_gguf_vlm_tokenizer_7481.py; both fail with the loader_utils hunk reverted and pass with it in place.
This commit is contained in:
parent
6b11123e55
commit
e7b7400dee
2 changed files with 118 additions and 1 deletions
|
|
@ -204,6 +204,91 @@ def test_has_tokenizer_model_probes_cache_before_model_info(tmp_path, monkeypatc
|
|||
assert _has_tokenizer_model(tok, token = None) is True
|
||||
|
||||
|
||||
def test_offline_aware_load_persists_local_only_for_saving(tmp_path, monkeypatch):
|
||||
"""An explicit ``local_files_only = True`` load must still be local-only at save time.
|
||||
|
||||
``transformers`` takes ``local_files_only`` as an explicit ``from_pretrained``
|
||||
parameter, so it never reaches ``tokenizer.init_kwargs``, and
|
||||
``_offline_aware_load`` restores the offline env vars once the load returns.
|
||||
Without the stamp the request is invisible by the time we save.
|
||||
"""
|
||||
from unsloth.save import _TOKENIZER_MODEL_CACHE, _has_tokenizer_model
|
||||
|
||||
# Snapshot has tokenizer metadata but deliberately no tokenizer.model, so the
|
||||
# cache probe misses and only the local-only stamp can stop the Hub request.
|
||||
_write_gemma4_cache(tmp_path)
|
||||
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
|
||||
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
|
||||
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
|
||||
_TOKENIZER_MODEL_CACHE.clear()
|
||||
|
||||
@L._offline_aware_load
|
||||
def _load(model_name, **kwargs):
|
||||
assert os.environ.get("HF_HUB_OFFLINE") == "1"
|
||||
# A processor keeps the Hub repo id and carries no local_files_only.
|
||||
return object(), SimpleNamespace(
|
||||
tokenizer = SimpleNamespace(name_or_path = model_name, init_kwargs = {}),
|
||||
)
|
||||
|
||||
_model, processor = _load(_REPO, local_files_only = True)
|
||||
|
||||
assert os.environ.get("HF_HUB_OFFLINE") is None
|
||||
assert processor.tokenizer.init_kwargs.get("local_files_only") is None
|
||||
assert L._tokenizer_wants_local_only(processor.tokenizer) is True
|
||||
|
||||
with patch("huggingface_hub.HfApi.model_info") as model_info:
|
||||
model_info.return_value = SimpleNamespace(
|
||||
siblings = [SimpleNamespace(rfilename = "tokenizer.model")],
|
||||
)
|
||||
assert _has_tokenizer_model(processor, token = None) is False
|
||||
assert model_info.call_count == 0
|
||||
|
||||
|
||||
def test_preserve_sentencepiece_after_local_only_load_never_downloads(tmp_path, monkeypatch):
|
||||
"""The save path inherits the load's local-only mode: no metadata probe, no download."""
|
||||
import huggingface_hub
|
||||
|
||||
from unsloth.save import _TOKENIZER_MODEL_CACHE, _preserve_sentencepiece_tokenizer_assets
|
||||
|
||||
_write_gemma4_cache(tmp_path)
|
||||
monkeypatch.delenv("HF_HUB_OFFLINE", raising = False)
|
||||
monkeypatch.delenv("TRANSFORMERS_OFFLINE", raising = False)
|
||||
monkeypatch.setenv("HF_HUB_CACHE", str(tmp_path))
|
||||
_TOKENIZER_MODEL_CACHE.clear()
|
||||
|
||||
@L._offline_aware_load
|
||||
def _load(model_name, **kwargs):
|
||||
return object(), SimpleNamespace(
|
||||
tokenizer = SimpleNamespace(name_or_path = model_name, init_kwargs = {}),
|
||||
)
|
||||
|
||||
_model, processor = _load(_REPO, local_files_only = True)
|
||||
|
||||
save_dir = tmp_path / "export"
|
||||
save_dir.mkdir()
|
||||
(save_dir / "tokenizer_config.json").write_text("{}", encoding = "utf-8")
|
||||
|
||||
real_download = huggingface_hub.hf_hub_download
|
||||
seen_local_files_only = []
|
||||
|
||||
def _recording_download(*args, **kwargs):
|
||||
seen_local_files_only.append(kwargs.get("local_files_only"))
|
||||
return real_download(*args, **kwargs)
|
||||
|
||||
monkeypatch.setattr("huggingface_hub.hf_hub_download", _recording_download)
|
||||
|
||||
with patch("huggingface_hub.HfApi.model_info") as model_info:
|
||||
model_info.return_value = SimpleNamespace(
|
||||
siblings = [SimpleNamespace(rfilename = "tokenizer.model")],
|
||||
)
|
||||
_preserve_sentencepiece_tokenizer_assets(processor, str(save_dir), token = None)
|
||||
|
||||
assert model_info.call_count == 0
|
||||
# Every hf_hub_download here must be a cache probe, never a Hub fetch.
|
||||
assert seen_local_files_only and all(seen_local_files_only)
|
||||
assert not (save_dir / "tokenizer.model").exists()
|
||||
|
||||
|
||||
def test_has_tokenizer_model_local_files_only_skips_model_info(tmp_path, monkeypatch):
|
||||
from unsloth.save import _TOKENIZER_MODEL_CACHE, _has_tokenizer_model
|
||||
|
||||
|
|
|
|||
|
|
@ -843,10 +843,40 @@ def _get_effective_local_files_only(kwargs):
|
|||
return _env_says_offline()
|
||||
|
||||
|
||||
# Attribute stamped on a tokenizer/processor that was loaded local-only, so a later
|
||||
# save still knows. transformers takes local_files_only as an explicit from_pretrained
|
||||
# parameter and never copies it into tokenizer.init_kwargs, and _offline_aware_load
|
||||
# restores the offline env vars when the load window closes, so without this stamp an
|
||||
# explicit local_files_only = True load is invisible by the time we save (issue #7481).
|
||||
_LOCAL_FILES_ONLY_ATTR = "_unsloth_local_files_only"
|
||||
|
||||
|
||||
def _mark_loaded_local_files_only(result):
|
||||
"""Stamp a load's local-only mode onto the returned tokenizer/processor objects."""
|
||||
for obj in result if isinstance(result, (tuple, list)) else (result,):
|
||||
try:
|
||||
# A processor keeps the tokenizer that _has_tokenizer_model unwraps to,
|
||||
# so stamp both (a wrapped model can raise from its own __getattr__).
|
||||
targets = (obj, getattr(obj, "tokenizer", None))
|
||||
except Exception:
|
||||
targets = (obj,)
|
||||
for target in targets:
|
||||
if target is None:
|
||||
continue
|
||||
# Objects that reject new attributes (__slots__) are skipped.
|
||||
try:
|
||||
setattr(target, _LOCAL_FILES_ONLY_ATTR, True)
|
||||
except Exception:
|
||||
pass
|
||||
return result
|
||||
|
||||
|
||||
def _tokenizer_wants_local_only(tokenizer):
|
||||
"""True when Hub metadata probes should be skipped for this tokenizer."""
|
||||
if _env_says_offline():
|
||||
return True
|
||||
if getattr(tokenizer, _LOCAL_FILES_ONLY_ATTR, False):
|
||||
return True
|
||||
init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
|
||||
return bool(init_kwargs.get("local_files_only"))
|
||||
|
||||
|
|
@ -1075,7 +1105,9 @@ def _offline_aware_load(fn):
|
|||
if _get_effective_local_files_only(kwargs):
|
||||
kwargs["local_files_only"] = True
|
||||
with _force_hf_offline():
|
||||
return fn(*args, **kwargs)
|
||||
# Stamp inside the window: the env vars are restored on exit, so the
|
||||
# request has to travel on the objects themselves to reach saving.
|
||||
return _mark_loaded_local_files_only(fn(*args, **kwargs))
|
||||
_pb_were_disabled = _progress_bars_were_disabled() # restore before any retry
|
||||
try:
|
||||
return fn(*args, **kwargs)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue