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
|
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):
|
def test_has_tokenizer_model_local_files_only_skips_model_info(tmp_path, monkeypatch):
|
||||||
from unsloth.save import _TOKENIZER_MODEL_CACHE, _has_tokenizer_model
|
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()
|
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):
|
def _tokenizer_wants_local_only(tokenizer):
|
||||||
"""True when Hub metadata probes should be skipped for this tokenizer."""
|
"""True when Hub metadata probes should be skipped for this tokenizer."""
|
||||||
if _env_says_offline():
|
if _env_says_offline():
|
||||||
return True
|
return True
|
||||||
|
if getattr(tokenizer, _LOCAL_FILES_ONLY_ATTR, False):
|
||||||
|
return True
|
||||||
init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
|
init_kwargs = getattr(tokenizer, "init_kwargs", None) or {}
|
||||||
return bool(init_kwargs.get("local_files_only"))
|
return bool(init_kwargs.get("local_files_only"))
|
||||||
|
|
||||||
|
|
@ -1075,7 +1105,9 @@ def _offline_aware_load(fn):
|
||||||
if _get_effective_local_files_only(kwargs):
|
if _get_effective_local_files_only(kwargs):
|
||||||
kwargs["local_files_only"] = True
|
kwargs["local_files_only"] = True
|
||||||
with _force_hf_offline():
|
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
|
_pb_were_disabled = _progress_bars_were_disabled() # restore before any retry
|
||||||
try:
|
try:
|
||||||
return fn(*args, **kwargs)
|
return fn(*args, **kwargs)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue