[Performance] Load HF tokenizers and processors from a local snapshot (#60825)

Signed-off-by: Harry Mellor <19981378+hmellor@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Kevin H. Luu <khluu000@gmail.com>
This commit is contained in:
Harry Mellor
2026-10-12 13:45:54 +00:00
committed by GitHub
co-authored by Claude Opus 5.5 Kevin H. Luu
parent 092c35dd15
commit 3facf822b7
5 changed files with 146 additions and 4 deletions
+16
View File
@@ -1,11 +1,13 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from typing import _get_protocol_attrs # type: ignore
from unittest.mock import patch
import pytest
from transformers import (
PreTrainedTokenizerBase,
TokenizersBackend,
tokenization_utils_base,
)
from vllm.tokenizers import TokenizerLike, get_tokenizer
@@ -56,6 +58,20 @@ def test_tokenizer_revision(tokenizer_name: str):
get_tokenizer(tokenizer_name, revision="never")
def test_hub_tokenizer_loads_from_local_snapshot():
"""Transformers makes a Hub request per file when given a repo ID."""
with patch.object(
tokenization_utils_base,
"list_repo_templates",
wraps=tokenization_utils_base.list_repo_templates,
) as list_repo_templates:
tokenizer = get_tokenizer("openai-community/gpt2")
assert isinstance(tokenizer, PreTrainedTokenizerBase)
assert tokenizer.name_or_path == "openai-community/gpt2"
list_repo_templates.assert_not_called()
@pytest.mark.parametrize("tokenizer_name", ["BAAI/bge-base-en"])
@pytest.mark.parametrize("n_tokens", [510])
def test_special_tokens(tokenizer_name: str, n_tokens: int):
@@ -2,14 +2,19 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import importlib
from unittest.mock import patch
from transformers import processing_utils
from transformers.processing_utils import ProcessingKwargs
from typing_extensions import Unpack
from vllm.tokenizers import get_tokenizer
from vllm.transformers_utils.processor import (
get_processor,
get_processor_kwargs_keys,
get_processor_kwargs_type,
)
from vllm.transformers_utils.repo_utils import _snapshot_non_weight_files
class _FakeProcessorKwargs(ProcessingKwargs, total=False): # type: ignore
@@ -65,3 +70,26 @@ def test_get_processor_kwargs_from_processor_module_scan_returns_full_union():
proc = _ProcWithoutUnpack()
keys = get_processor_kwargs_keys(get_processor_kwargs_type(proc))
_assert_has_all_expected(keys)
def test_hub_processor_loads_from_local_snapshot():
"""Transformers makes a Hub request per file when given a repo ID."""
with patch.object(
processing_utils,
"list_repo_templates",
wraps=processing_utils.list_repo_templates,
) as list_repo_templates:
processor = get_processor("Qwen/Qwen2-VL-2B-Instruct")
assert processor.tokenizer.name_or_path == "Qwen/Qwen2-VL-2B-Instruct"
assert processor.chat_template is not None
list_repo_templates.assert_not_called()
def test_processor_reuses_tokenizer_snapshot():
"""The chat template lookup must not snapshot the repo a second time."""
model = "Qwen/Qwen2-VL-2B-Instruct"
_snapshot_non_weight_files.cache_clear()
get_tokenizer(model)
get_processor(model)
assert _snapshot_non_weight_files.cache_info().misses == 1
+13 -2
View File
@@ -4,7 +4,7 @@ import contextlib
from dataclasses import dataclass, field
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING, assert_never
from typing import TYPE_CHECKING, assert_never, cast
import huggingface_hub
from typing_extensions import TypeVar
@@ -14,11 +14,12 @@ from vllm.logger import init_logger
from vllm.transformers_utils.config import _maybe_register_hf_config, get_config
from vllm.transformers_utils.repo_utils import (
any_pattern_in_repo_files,
get_non_weight_snapshot_path,
is_mistral_model_repo,
)
from vllm.utils.import_utils import resolve_obj_by_qualname
from .hf import CachedHfTokenizer
from .hf import CachedHfTokenizer, HfTokenizer
from .protocol import TokenizerLike
if TYPE_CHECKING:
@@ -164,6 +165,12 @@ def resolve_tokenizer_args(
"mistral_format=True is not supported with tokenizer_mode='hf'"
)
kwargs["mistral_format"] = False
tokenizer_name = get_non_weight_snapshot_path(
str(tokenizer_name),
revision=revision,
cache_dir=download_dir,
token=kwargs.get("token"),
)
return tokenizer_mode, tokenizer_name, args, kwargs
@@ -202,6 +209,7 @@ def get_tokenizer(
apply_fastokens_patch()
requested_name = str(tokenizer_name)
tokenizer_mode, tokenizer_name, args, kwargs = cached_resolve_tokenizer_args(
tokenizer_name,
*args,
@@ -254,6 +262,9 @@ def get_tokenizer(
kwargs.setdefault("config", config)
tokenizer = tokenizer_cls_.from_pretrained(tokenizer_name, *args, **kwargs)
# Report the Hub ID rather than the local snapshot it was loaded from
if tokenizer_mode == "hf" and not envs.VLLM_USE_MODELSCOPE:
cast(HfTokenizer, tokenizer).name_or_path = requested_name
if model_type in _MODEL_TYPES_WITH_INCORRECT_TOKENIZER_CLASS:
from vllm.tokenizers.hf import get_cached_tokenizer
+15 -2
View File
@@ -22,7 +22,10 @@ from typing_extensions import TypeVar
from vllm.logger import init_logger
from vllm.transformers_utils import processors
from vllm.transformers_utils.repo_utils import get_hf_file_to_dict
from vllm.transformers_utils.repo_utils import (
get_hf_file_to_dict,
get_non_weight_snapshot_path,
)
from vllm.transformers_utils.utils import convert_model_repo_to_path
from vllm.utils.func_utils import get_allowed_kwarg_only_overrides
@@ -214,7 +217,12 @@ def get_processor(
if revision is None:
revision = "main"
try:
processor_name = convert_model_repo_to_path(processor_name)
processor_name = requested_name = convert_model_repo_to_path(processor_name)
# A snapshot pinned to `revision` would shadow remote code at `code_revision`
if kwargs.get("code_revision") in (None, revision):
processor_name = get_non_weight_snapshot_path(
processor_name, revision=revision, token=kwargs.get("token")
)
registered_cls_name = get_processor_cls_name_from_config(
processor_name, revision=revision
)
@@ -269,6 +277,11 @@ def get_processor(
f"found type: {type(processor)}"
)
# Report the Hub ID rather than the local snapshot it was loaded from
tokenizer = getattr(processor, "tokenizer", processor)
if hasattr(tokenizer, "name_or_path"):
tokenizer.name_or_path = requested_name
return processor
+74
View File
@@ -304,6 +304,80 @@ def get_model_path(model: str | Path, revision: str | None = None):
)
_WEIGHT_FILE_PATTERNS = [
"*.safetensors",
"*.bin",
"*.pt",
"*.pth",
"*.ckpt",
"*.gguf",
"*.h5",
"*.msgpack",
"*.onnx",
"*.onnx_data",
# Large non-weight assets that tokenizers and processors never read
"*.png",
"*.jpg",
"*.jpeg",
"*.gif",
"*.webp",
"*.mp3",
"*.mp4",
"*.wav",
"*.flac",
"*.parquet",
"*.arrow",
"*.ipynb",
"*.zip",
"*.tar*",
]
@cache
def _snapshot_non_weight_files(
repo_id: str,
revision: str | None,
cache_dir: str | None,
token: str | bool | None,
) -> str:
# Avoid circular import
from vllm.model_executor.model_loader.weight_utils import get_lock
with get_lock(repo_id, cache_dir):
return hf_api().snapshot_download(
repo_id=repo_id,
revision=revision,
cache_dir=cache_dir,
token=token,
local_files_only=huggingface_hub.constants.HF_HUB_OFFLINE,
ignore_patterns=_WEIGHT_FILE_PATTERNS,
)
def get_non_weight_snapshot_path(
repo_id: str,
revision: str | None = None,
cache_dir: str | None = None,
token: str | bool | None = None,
) -> str:
"""Download all non-weight files of a Hub repo and return the local snapshot.
Loading tokenizers and processors from a local directory stops Transformers
from making a Hub request per file (including the `additional_chat_templates`
listing). Returns `repo_id` unchanged if it is local or cannot be fetched, so
Transformers handles it as before. Failures are not cached, so they are retried.
"""
if envs.VLLM_USE_MODELSCOPE or Path(repo_id).exists():
return repo_id
# Normalise so that `None` and "main" share a cache entry
revision = revision or huggingface_hub.constants.DEFAULT_REVISION
try:
return _snapshot_non_weight_files(repo_id, revision, cache_dir, token)
except (HfHubHTTPError, OSError, ValueError):
logger.debug("Failed to snapshot %s", repo_id, exc_info=True)
return repo_id
def _try_download_from_hf_hub(
model: str | Path, file_name: str, revision: str | None
) -> Path | None: