From b41bbbef7dbcaa4a352794bf1120958bde3d8d36 Mon Sep 17 00:00:00 2001 From: Souravrajvi0 Date: Mon, 27 Jul 2026 03:45:02 +0000 Subject: [PATCH] fix: probe HF cache before model_info for local-only GGUF saves (#7481) Always resolve tokenizer.model from the local Hub cache before calling model_info, and skip Hub metadata when the tokenizer was loaded with local_files_only or offline env vars. Fixes Codex review on PR #7482. --- ...est_offline_gguf_real_cache_integration.py | 4 +- .../test_offline_gguf_vlm_tokenizer_7481.py | 36 ++++++++++ unsloth/models/loader_utils.py | 8 +++ unsloth/save.py | 71 ++++++++++++------- 4 files changed, 91 insertions(+), 28 deletions(-) diff --git a/tests/saving/test_offline_gguf_real_cache_integration.py b/tests/saving/test_offline_gguf_real_cache_integration.py index 2796aba4a3..a0ed091887 100644 --- a/tests/saving/test_offline_gguf_real_cache_integration.py +++ b/tests/saving/test_offline_gguf_real_cache_integration.py @@ -95,8 +95,6 @@ def test_real_cached_unsloth_helpers_offline(monkeypatch): _offline_env(monkeypatch) _block_network(monkeypatch) - from types import SimpleNamespace - from unsloth.models.loader_utils import _load_pretrained_tokenizer_fast from unsloth.save import _has_tokenizer_model @@ -106,4 +104,4 @@ def test_real_cached_unsloth_helpers_offline(monkeypatch): cache_dir = str(CACHE_ROOT / "hub"), ) assert tok.vocab_size > 0 - assert _has_tokenizer_model(SimpleNamespace(name_or_path = REPO, tokenizer = None)) is True + assert _has_tokenizer_model(tok) is True diff --git a/tests/saving/test_offline_gguf_vlm_tokenizer_7481.py b/tests/saving/test_offline_gguf_vlm_tokenizer_7481.py index 6558b24c95..850dc03f2d 100644 --- a/tests/saving/test_offline_gguf_vlm_tokenizer_7481.py +++ b/tests/saving/test_offline_gguf_vlm_tokenizer_7481.py @@ -180,3 +180,39 @@ def test_has_tokenizer_model_offline_skips_model_info(tmp_path, monkeypatch): with patch("huggingface_hub.HfApi.model_info") as model_info: model_info.side_effect = AssertionError("model_info must not run offline") assert _has_tokenizer_model(tok, token = None) is False + + +def test_has_tokenizer_model_probes_cache_before_model_info(tmp_path, monkeypatch): + from unsloth.save import _TOKENIZER_MODEL_CACHE, _has_tokenizer_model + + snap = _write_gemma4_cache(tmp_path) + (snap / "tokenizer.model").write_bytes(b"sp-model") + 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() + + tok = SimpleNamespace(name_or_path = _REPO) + + with patch("huggingface_hub.HfApi.model_info") as model_info: + model_info.side_effect = AssertionError("model_info must not run when cache hit") + assert _has_tokenizer_model(tok, token = None) is True + + +def test_has_tokenizer_model_local_files_only_skips_model_info(tmp_path, monkeypatch): + from unsloth.save import _TOKENIZER_MODEL_CACHE, _has_tokenizer_model + + _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() + + tok = SimpleNamespace( + name_or_path = _REPO, + init_kwargs = {"local_files_only": True}, + ) + + with patch("huggingface_hub.HfApi.model_info") as model_info: + model_info.side_effect = AssertionError("model_info must not run with local_files_only") + assert _has_tokenizer_model(tok, token = None) is False diff --git a/unsloth/models/loader_utils.py b/unsloth/models/loader_utils.py index 97fc96e265..b621393dec 100644 --- a/unsloth/models/loader_utils.py +++ b/unsloth/models/loader_utils.py @@ -843,6 +843,14 @@ def _get_effective_local_files_only(kwargs): return _env_says_offline() +def _tokenizer_wants_local_only(tokenizer): + """True when Hub metadata probes should be skipped for this tokenizer.""" + if _env_says_offline(): + return True + init_kwargs = getattr(tokenizer, "init_kwargs", None) or {} + return bool(init_kwargs.get("local_files_only")) + + def _is_offline_related_error(exc): """True if exc (or its cause/context chain) is a lost-connection error, not a missing file. Plain FileNotFoundError propagates; LocalEntryNotFoundError is offline.""" diff --git a/unsloth/save.py b/unsloth/save.py index 0785188a14..855a7ea8f1 100644 --- a/unsloth/save.py +++ b/unsloth/save.py @@ -52,7 +52,12 @@ import traceback import psutil import re from transformers.models.llama.modeling_llama import logger -from .models.loader_utils import get_model_name, _env_says_offline, _resolve_hub_repo_cached_file +from .models.loader_utils import ( + get_model_name, + _env_says_offline, + _resolve_hub_repo_cached_file, + _tokenizer_wants_local_only, +) from .models._utils import _convert_torchao_model from .ollama_template_mappers import OLLAMA_TEMPLATES, MODEL_TO_OLLAMA_TEMPLATE_MAPPER from transformers import ProcessorMixin, PreTrainedTokenizerBase @@ -446,18 +451,25 @@ def _has_tokenizer_model(tokenizer, token = None): if source in _TOKENIZER_MODEL_CACHE: return _TOKENIZER_MODEL_CACHE[source] - # Offline: probe the local cache instead of model_info (issue #7481). - if _env_says_offline(): - cached_path = _resolve_hub_repo_cached_file( - source, - "tokenizer.model", - token = token, - local_files_only = True, - cache_dir = os.environ.get("HF_HUB_CACHE"), - ) - if cached_path is not None: - _TOKENIZER_MODEL_CACHE[source] = True - return True + # Hub repo id: probe local cache before model_info (issue #7481). + cache_dir = os.environ.get("HF_HUB_CACHE") + if not cache_dir: + hf_home = os.environ.get("HF_HOME") + if hf_home: + cache_dir = os.path.join(hf_home, "hub") + + cached_path = _resolve_hub_repo_cached_file( + source, + "tokenizer.model", + token = token, + local_files_only = True, + cache_dir = cache_dir, + ) + if cached_path is not None: + _TOKENIZER_MODEL_CACHE[source] = True + return True + + if _tokenizer_wants_local_only(tokenizer): return False try: @@ -519,15 +531,19 @@ def _preserve_sentencepiece_tokenizer_assets( if os.path.isfile(local_path): downloaded_path = local_path else: - cached_path = None - if _env_says_offline(): - cached_path = _resolve_hub_repo_cached_file( - source, - "tokenizer.model", - token = token, - local_files_only = True, - cache_dir = os.environ.get("HF_HUB_CACHE"), - ) + cache_dir = os.environ.get("HF_HUB_CACHE") + if not cache_dir: + hf_home = os.environ.get("HF_HOME") + if hf_home: + cache_dir = os.path.join(hf_home, "hub") + + cached_path = _resolve_hub_repo_cached_file( + source, + "tokenizer.model", + token = token, + local_files_only = True, + cache_dir = cache_dir, + ) if cached_path is not None: downloaded_path = cached_path else: @@ -537,8 +553,8 @@ def _preserve_sentencepiece_tokenizer_assets( repo_id = source, filename = "tokenizer.model", token = token, - local_files_only = _env_says_offline(), - cache_dir = os.environ.get("HF_HUB_CACHE"), + local_files_only = _tokenizer_wants_local_only(tokenizer), + cache_dir = cache_dir, ) except Exception: downloaded_path = None @@ -3821,7 +3837,12 @@ def unsloth_convert_lora_to_ggml_and_save_locally( return _unsloth_save_lora_gguf(self, tokenizer, save_directory, outtype = outtype) -from .models.loader_utils import get_model_name, _env_says_offline, _resolve_hub_repo_cached_file +from .models.loader_utils import ( + get_model_name, + _env_says_offline, + _resolve_hub_repo_cached_file, + _tokenizer_wants_local_only, +) from unsloth_zoo.saving_utils import ( merge_and_overwrite_lora, prepare_saving,