Upgrade transformers to 5.5.3 and refactor hf_transformers_utils into subpackage (#21569)
This commit is contained in:
@@ -35,6 +35,7 @@ dependencies = [
|
||||
"modelscope",
|
||||
"msgspec",
|
||||
"ninja",
|
||||
"easydict", # Required by remote model code (e.g. DeepSeek-OCR) loaded via trust_remote_code; validated by transformers 5.4+ check_imports
|
||||
"numpy",
|
||||
"nvidia-cutlass-dsl>=4.4.1",
|
||||
"nvidia-ml-py",
|
||||
@@ -70,8 +71,8 @@ dependencies = [
|
||||
"av ; sys_platform == 'linux' and (platform_machine == 'aarch64' or platform_machine == 'arm64' or platform_machine == 'armv7l')",
|
||||
"torchvision",
|
||||
"tqdm",
|
||||
"mistral_common>=1.9.0",
|
||||
"transformers==5.3.0",
|
||||
"mistral_common>=1.11.0",
|
||||
"transformers==5.5.4",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"watchfiles",
|
||||
|
||||
@@ -31,6 +31,7 @@ dependencies = [
|
||||
"llguidance>=0.7.11,<0.8.0",
|
||||
"modelscope",
|
||||
"msgspec",
|
||||
"easydict",
|
||||
"ninja",
|
||||
"numpy",
|
||||
"openai-harmony==0.0.4",
|
||||
@@ -60,8 +61,8 @@ dependencies = [
|
||||
"torchaudio==2.9.0",
|
||||
"torchvision==0.24.0",
|
||||
"tqdm",
|
||||
"mistral_common>=1.9.0",
|
||||
"transformers==5.3.0",
|
||||
"mistral_common>=1.11.0",
|
||||
"transformers==5.5.4",
|
||||
"triton==3.5.0",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
|
||||
@@ -25,6 +25,7 @@ dependencies = [
|
||||
"datasets",
|
||||
"einops",
|
||||
"fastapi",
|
||||
"easydict",
|
||||
"gguf",
|
||||
"hf_transfer",
|
||||
"huggingface_hub",
|
||||
@@ -57,8 +58,8 @@ dependencies = [
|
||||
"timm==1.0.16",
|
||||
"torchao==0.9.0",
|
||||
"tqdm",
|
||||
"mistral_common>=1.9.0",
|
||||
"transformers==5.3.0",
|
||||
"mistral_common>=1.11.0",
|
||||
"transformers==5.5.4",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"xgrammar==0.1.32",
|
||||
|
||||
@@ -25,6 +25,7 @@ runtime_common = [
|
||||
"build",
|
||||
"compressed-tensors",
|
||||
"datasets",
|
||||
"easydict",
|
||||
"einops",
|
||||
"fastapi",
|
||||
"gguf",
|
||||
@@ -57,8 +58,8 @@ runtime_common = [
|
||||
"timm==1.0.16",
|
||||
"torchao==0.9.0",
|
||||
"tqdm",
|
||||
"mistral_common>=1.9.0",
|
||||
"transformers==5.3.0",
|
||||
"mistral_common>=1.11.0",
|
||||
"transformers==5.5.4",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
"xgrammar==0.1.32",
|
||||
|
||||
@@ -27,7 +27,9 @@ dependencies = [
|
||||
"blobfile==3.0.0",
|
||||
"build",
|
||||
"compressed-tensors",
|
||||
"addict",
|
||||
"datasets",
|
||||
"easydict",
|
||||
"einops",
|
||||
"fastapi",
|
||||
"gguf",
|
||||
@@ -60,8 +62,8 @@ dependencies = [
|
||||
"timm==1.0.16",
|
||||
"torchao==0.9.0+xpu",
|
||||
"tqdm",
|
||||
"mistral_common>=1.9.0",
|
||||
"transformers==5.3.0",
|
||||
"mistral_common>=1.11.0",
|
||||
"transformers==5.5.4",
|
||||
"uvicorn",
|
||||
"uvloop",
|
||||
# "xgrammar==0.1.24", , xgrammar depends on CUDA PyTorch and Triton only
|
||||
|
||||
@@ -8,6 +8,9 @@ class Qwen3_5VisionConfig(Qwen3VLVisionConfig):
|
||||
model_type = "qwen3_5"
|
||||
base_config_key = "vision_config"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class Qwen3_5TextConfig(Qwen3NextConfig):
|
||||
model_type = "qwen3_5_text"
|
||||
@@ -109,14 +112,27 @@ class Qwen3_5Config(PretrainedConfig):
|
||||
class Qwen3_5MoeVisionConfig(Qwen3_5VisionConfig):
|
||||
model_type = "qwen3_5_moe"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
class Qwen3_5MoeTextConfig(Qwen3_5TextConfig):
|
||||
model_type = "qwen3_5_moe_text"
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
|
||||
# All Moe variant classes need explicit __init__ because the kw_only=True
|
||||
# dataclass decorator in transformers v5.5.3+ auto-generates __init__ for
|
||||
# subclasses, bypassing parent __init__ methods that set up attributes
|
||||
# (e.g. norm_topk_prob, rope_scaling) and convert sub-config dicts to objects.
|
||||
class Qwen3_5MoeConfig(Qwen3_5Config):
|
||||
model_type = "qwen3_5_moe"
|
||||
sub_configs = {
|
||||
"vision_config": Qwen3_5MoeVisionConfig,
|
||||
"text_config": Qwen3_5MoeTextConfig,
|
||||
}
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super().__init__(**kwargs)
|
||||
|
||||
@@ -94,4 +94,13 @@ class Step3p5Config(PretrainedConfig):
|
||||
self.moe_layers_enum = moe_layers_enum
|
||||
self.layer_types = layer_types
|
||||
self.sliding_window = sliding_window
|
||||
# The upstream Step-3.5-Flash config has layer_types with 48 entries
|
||||
# but num_hidden_layers=45. The extra 3 are for MTP/nextn predict
|
||||
# layers (indices 45-47) used by Step3p5DecoderLayer during EAGLE
|
||||
# speculative decoding. Temporarily align num_hidden_layers to pass
|
||||
# the transformers v5.5.3+ validator, then restore the real value.
|
||||
real_num_hidden_layers = self.num_hidden_layers
|
||||
if layer_types is not None and len(layer_types) != self.num_hidden_layers:
|
||||
self.num_hidden_layers = len(layer_types)
|
||||
super().__init__(**kwargs)
|
||||
self.num_hidden_layers = real_num_hidden_layers
|
||||
|
||||
@@ -1091,9 +1091,15 @@ class Qwen3VLForConditionalGeneration(nn.Module):
|
||||
if language_model_cls is Qwen3LLMModel:
|
||||
self.config: Qwen3VLConfig = config # for qwen3-vl
|
||||
else:
|
||||
self.config = config.text_config # for qwen3-omni
|
||||
self.config = config.text_config # for qwen3-omni / qwen3-vl-moe
|
||||
self.config.encoder_only = getattr(config, "encoder_only", False)
|
||||
self.config.language_only = getattr(config, "language_only", False)
|
||||
# Propagate tie_word_embeddings from parent config. In transformers
|
||||
# v5.5.3+, Qwen3VLMoeTextConfig sets tie_word_embeddings=True by
|
||||
# default but the actual model checkpoint has a separate lm_head.
|
||||
# The parent Qwen3VLMoeConfig correctly has tie_word_embeddings=False.
|
||||
if hasattr(config, "tie_word_embeddings"):
|
||||
self.config.tie_word_embeddings = config.tie_word_embeddings
|
||||
|
||||
if not hasattr(config, "encoder_only") or not config.encoder_only:
|
||||
self.model = language_model_cls(
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Hugging Face Transformers utilities.
|
||||
|
||||
This package provides HF Transformers helpers, split into submodules
|
||||
(common, compat, config, tokenizer, processor, mistral_utils).
|
||||
All public symbols are re-exported here for convenience. The old import
|
||||
path ``sglang.srt.utils.hf_transformers_utils`` is preserved by a
|
||||
separate shim module.
|
||||
"""
|
||||
|
||||
from .compat import apply_all as _apply_compat
|
||||
|
||||
_apply_compat()
|
||||
|
||||
from .common import ( # noqa: E402
|
||||
CONTEXT_LENGTH_KEYS,
|
||||
AutoConfig,
|
||||
attach_additional_stop_token_ids,
|
||||
check_gguf_file,
|
||||
download_from_hf,
|
||||
get_context_length,
|
||||
get_generation_config,
|
||||
get_hf_text_config,
|
||||
get_rope_config,
|
||||
get_sparse_attention_config,
|
||||
get_tokenizer_from_processor,
|
||||
)
|
||||
from .compat import normalize_rope_scaling_compat # noqa: E402
|
||||
from .config import get_config # noqa: E402
|
||||
from .processor import get_processor # noqa: E402
|
||||
from .tokenizer import ( # noqa: E402
|
||||
_fix_added_tokens_encoding,
|
||||
_fix_v5_add_bos_eos_token,
|
||||
get_tokenizer,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AutoConfig",
|
||||
"CONTEXT_LENGTH_KEYS",
|
||||
"_fix_added_tokens_encoding",
|
||||
"_fix_v5_add_bos_eos_token",
|
||||
"attach_additional_stop_token_ids",
|
||||
"check_gguf_file",
|
||||
"download_from_hf",
|
||||
"get_config",
|
||||
"get_context_length",
|
||||
"get_generation_config",
|
||||
"get_hf_text_config",
|
||||
"get_processor",
|
||||
"get_rope_config",
|
||||
"get_sparse_attention_config",
|
||||
"get_tokenizer",
|
||||
"get_tokenizer_from_processor",
|
||||
"normalize_rope_scaling_compat",
|
||||
]
|
||||
@@ -0,0 +1,438 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Shared helpers used by config, tokenizer, and processor modules."""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Type, Union
|
||||
|
||||
import torch
|
||||
from huggingface_hub import snapshot_download
|
||||
|
||||
from sglang.srt.configs import (
|
||||
AfmoeConfig,
|
||||
BailingHybridConfig,
|
||||
ChatGLMConfig,
|
||||
DbrxConfig,
|
||||
DeepseekVL2Config,
|
||||
DotsOCRConfig,
|
||||
DotsVLMConfig,
|
||||
ExaoneConfig,
|
||||
FalconH1Config,
|
||||
GraniteMoeHybridConfig,
|
||||
JetNemotronConfig,
|
||||
JetVLMConfig,
|
||||
KimiK25Config,
|
||||
KimiLinearConfig,
|
||||
KimiVLConfig,
|
||||
LongcatFlashConfig,
|
||||
MultiModalityConfig,
|
||||
NemotronH_Nano_VL_V2_Config,
|
||||
NemotronHConfig,
|
||||
Olmo3Config,
|
||||
Qwen3_5Config,
|
||||
Qwen3_5MoeConfig,
|
||||
Qwen3NextConfig,
|
||||
Step3p5Config,
|
||||
Step3VLConfig,
|
||||
)
|
||||
from sglang.srt.configs.deepseek_ocr import DeepseekVLV2Config
|
||||
from sglang.srt.configs.internvl import InternVLChatConfig
|
||||
from sglang.srt.utils import get_bool_env_var, logger, lru_cache_frozenset
|
||||
|
||||
from .compat import normalize_rope_scaling_compat
|
||||
|
||||
if get_bool_env_var("SGLANG_USE_MODELSCOPE"):
|
||||
from modelscope import AutoConfig, GenerationConfig
|
||||
else:
|
||||
from transformers import AutoConfig, GenerationConfig
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config registry
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_CONFIG_REGISTRY: Dict[str, Type[PretrainedConfig]] = {
|
||||
cls.model_type: cls
|
||||
for cls in [
|
||||
AfmoeConfig,
|
||||
BailingHybridConfig,
|
||||
ChatGLMConfig,
|
||||
DbrxConfig,
|
||||
ExaoneConfig,
|
||||
DeepseekVL2Config,
|
||||
MultiModalityConfig,
|
||||
KimiVLConfig,
|
||||
InternVLChatConfig,
|
||||
Step3VLConfig,
|
||||
LongcatFlashConfig,
|
||||
Olmo3Config,
|
||||
KimiLinearConfig,
|
||||
Qwen3NextConfig,
|
||||
FalconH1Config,
|
||||
GraniteMoeHybridConfig,
|
||||
DotsVLMConfig,
|
||||
DotsOCRConfig,
|
||||
NemotronH_Nano_VL_V2_Config,
|
||||
NemotronHConfig,
|
||||
DeepseekVLV2Config,
|
||||
Qwen3_5Config,
|
||||
Qwen3_5MoeConfig,
|
||||
JetNemotronConfig,
|
||||
JetVLMConfig,
|
||||
KimiK25Config,
|
||||
Step3p5Config,
|
||||
]
|
||||
}
|
||||
|
||||
for name, cls in _CONFIG_REGISTRY.items():
|
||||
try:
|
||||
AutoConfig.register(name, cls)
|
||||
except ValueError as e:
|
||||
err = str(e).lower()
|
||||
if "already registered" not in err and "already used" not in err:
|
||||
logger.warning("Failed to register config %s: %s", name, e)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Download / path helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def download_from_hf(
|
||||
model_path: str,
|
||||
allow_patterns: Optional[Union[str, list]] = None,
|
||||
):
|
||||
if os.path.exists(model_path):
|
||||
return model_path
|
||||
|
||||
if not allow_patterns:
|
||||
allow_patterns = ["*.json", "*.bin", "*.model"]
|
||||
|
||||
return snapshot_download(model_path, allow_patterns=allow_patterns)
|
||||
|
||||
|
||||
def _resolve_local_or_cached_file(model_name_or_path, filename, revision=None):
|
||||
"""Resolve a file from a local directory or HF hub cache (no network)."""
|
||||
local_path = Path(model_name_or_path) / filename
|
||||
if local_path.is_file():
|
||||
return str(local_path)
|
||||
from huggingface_hub import hf_hub_download
|
||||
|
||||
return hf_hub_download(
|
||||
model_name_or_path, filename, revision=revision, local_files_only=True
|
||||
)
|
||||
|
||||
|
||||
def check_gguf_file(model: Union[str, os.PathLike]) -> bool:
|
||||
model = Path(model)
|
||||
if not model.is_file():
|
||||
return False
|
||||
elif model.suffix == ".gguf":
|
||||
return True
|
||||
|
||||
with open(model, "rb") as f:
|
||||
header = f.read(4)
|
||||
return header == b"GGUF"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Rope / text config helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_rope_config(config):
|
||||
"""Get (rope_theta, rope_params) from config, supporting both v4 and v5.
|
||||
|
||||
Trust-remote-code configs or parent configs passed to sub-models may not
|
||||
have the v5 ``rope_parameters`` property, so we fall back to the v4-style
|
||||
``config.rope_theta`` / ``config.rope_scaling`` attributes.
|
||||
|
||||
Returns:
|
||||
(rope_theta, rope_params): In v5, rope_params is the full
|
||||
rope_parameters dict (which subsumes rope_scaling and includes
|
||||
rope_theta). In v4, rope_params is the rope_scaling dict or None.
|
||||
"""
|
||||
rope_params = getattr(config, "rope_parameters", None)
|
||||
if rope_params is not None:
|
||||
return rope_params["rope_theta"], rope_params
|
||||
return config.rope_theta, getattr(config, "rope_scaling", None)
|
||||
|
||||
|
||||
def _patch_text_config(parent_config: PretrainedConfig, text_config):
|
||||
"""Synchronize standard attributes between parent config and text sub-config.
|
||||
|
||||
In transformers v5, the "untangle config" refactor removed automatic
|
||||
inheritance of top-level PretrainedConfig attributes (pad_token_id,
|
||||
tie_word_embeddings, etc.) from sub-configs. Downstream code expects
|
||||
these attributes to be present on both configs (some models pass the
|
||||
parent directly to the language model, others pass the text sub-config),
|
||||
so we propagate in both directions when an attribute is missing.
|
||||
(See https://github.com/huggingface/transformers/pull/41541)
|
||||
"""
|
||||
_ATTRS_TO_PROPAGATE = [
|
||||
"pad_token_id",
|
||||
"bos_token_id",
|
||||
"eos_token_id",
|
||||
"tie_word_embeddings",
|
||||
]
|
||||
for attr in _ATTRS_TO_PROPAGATE:
|
||||
parent_has = hasattr(parent_config, attr)
|
||||
text_has = hasattr(text_config, attr)
|
||||
if parent_has and not text_has:
|
||||
setattr(text_config, attr, getattr(parent_config, attr))
|
||||
elif text_has and not parent_has:
|
||||
setattr(parent_config, attr, getattr(text_config, attr))
|
||||
return text_config
|
||||
|
||||
|
||||
def get_hf_text_config(config: PretrainedConfig):
|
||||
"""Get the "sub" config relevant to llm for multi modal models.
|
||||
No op for pure text models.
|
||||
"""
|
||||
if config.architectures is not None:
|
||||
class_name = config.architectures[0]
|
||||
if class_name.startswith("Llava") and class_name.endswith("ForCausalLM"):
|
||||
# We support non-hf version of llava models, so we do not want to
|
||||
# read the wrong values from the unused default text_config.
|
||||
# NOTE(HandH1998): We set `torch_dtype` of config to `torch.float16` for the weights, as
|
||||
# `torch.float16` is default used for image features in `python/sglang/srt/models/llava.py`.
|
||||
setattr(config, "dtype", torch.float16)
|
||||
return config
|
||||
|
||||
text_config = None
|
||||
|
||||
# Some models (e.g. DeepSeek-OCR) store sub-configs as plain dicts.
|
||||
# Convert to PretrainedConfig early so hasattr() checks and asserts work.
|
||||
parent_dtype = getattr(config, "torch_dtype", None)
|
||||
for _attr in ("text_config", "llm_config", "language_config", "thinker_config"):
|
||||
_sub = getattr(config, _attr, None)
|
||||
if isinstance(_sub, dict):
|
||||
_converted = PretrainedConfig(**_sub)
|
||||
if (
|
||||
getattr(_converted, "torch_dtype", None) is None
|
||||
and parent_dtype is not None
|
||||
):
|
||||
_converted.torch_dtype = parent_dtype
|
||||
setattr(config, _attr, _converted)
|
||||
|
||||
# Priority: thinker_config > llm_config > language_config > text_config
|
||||
if hasattr(config, "thinker_config"):
|
||||
# qwen2.5 omni
|
||||
thinker_config = config.thinker_config
|
||||
if hasattr(thinker_config, "text_config"):
|
||||
setattr(
|
||||
thinker_config.text_config,
|
||||
"torch_dtype",
|
||||
getattr(thinker_config, "torch_dtype", None),
|
||||
)
|
||||
text_config = thinker_config.text_config
|
||||
else:
|
||||
text_config = thinker_config
|
||||
elif hasattr(config, "llm_config"):
|
||||
# PointsV1.5 Chat Model
|
||||
assert hasattr(config.llm_config, "num_attention_heads")
|
||||
text_config = config.llm_config
|
||||
elif hasattr(config, "language_config"):
|
||||
text_config = config.language_config
|
||||
elif hasattr(config, "text_config"):
|
||||
# The code operates under the assumption that text_config should have
|
||||
# `num_attention_heads` (among others). Assert here to fail early
|
||||
# if transformers config doesn't align with this assumption.
|
||||
assert hasattr(config.text_config, "num_attention_heads")
|
||||
text_config = config.text_config
|
||||
|
||||
# Ensure rope_scaling dicts have "type" for remote-code compat (v5).
|
||||
normalize_rope_scaling_compat(config)
|
||||
|
||||
if text_config is not None:
|
||||
return _patch_text_config(config, text_config)
|
||||
return config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Model-specific helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ensure_sub_configs(config: PretrainedConfig, *attr_names: str) -> None:
|
||||
"""Convert dict-valued sub-configs to proper AutoConfig objects in-place."""
|
||||
for attr in attr_names:
|
||||
sub = getattr(config, attr, None)
|
||||
if sub is not None and isinstance(sub, dict):
|
||||
setattr(config, attr, AutoConfig.for_model(**sub))
|
||||
|
||||
|
||||
def _is_deepseek_ocr_model(config: PretrainedConfig) -> bool:
|
||||
# TODO: Remove this workaround once AutoConfig correctly identifies deepseek-ocr.
|
||||
# Hugging Face's AutoConfig currently misidentifies it as deepseekvl2.
|
||||
auto_map = getattr(config, "auto_map", None) or {}
|
||||
return auto_map.get("AutoModel") == "modeling_deepseekocr.DeepseekOCRForCausalLM"
|
||||
|
||||
|
||||
def _is_deepseek_ocr2_model(config: PretrainedConfig) -> bool:
|
||||
auto_map = getattr(config, "auto_map", None) or {}
|
||||
return auto_map.get("AutoModel") == "modeling_deepseekocr2.DeepseekOCR2ForCausalLM"
|
||||
|
||||
|
||||
def _override_v_head_dim_if_zero(config: PretrainedConfig, patch: int = 128) -> None:
|
||||
patched = False
|
||||
for attr in ("text_config", "language_config"):
|
||||
sub = getattr(config, attr, None)
|
||||
if sub is None:
|
||||
continue
|
||||
if isinstance(sub, dict):
|
||||
if sub.get("v_head_dim") == 0:
|
||||
sub["v_head_dim"] = patch
|
||||
patched = True
|
||||
elif getattr(sub, "v_head_dim", None) == 0:
|
||||
sub.v_head_dim = patch
|
||||
patched = True
|
||||
if patched:
|
||||
logger.warning(
|
||||
f"Overriding v_head_dim from 0 to {patch} to avoid potential issues."
|
||||
)
|
||||
|
||||
|
||||
def _load_deepseek_v32_model(
|
||||
model_path: str,
|
||||
trust_remote_code: bool = False,
|
||||
revision: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
import tempfile
|
||||
|
||||
local_path = download_from_hf(model_path)
|
||||
config_file = os.path.join(local_path, "config.json")
|
||||
if not os.path.exists(config_file):
|
||||
raise RuntimeError(f"Can't find config file in {local_path}.")
|
||||
|
||||
with open(config_file, "r") as f:
|
||||
config_json = json.load(f)
|
||||
|
||||
config_json["architectures"] = ["DeepseekV3ForCausalLM"]
|
||||
config_json["model_type"] = "deepseek_v3"
|
||||
|
||||
tmp_path = os.path.join(tempfile.gettempdir(), "_tmp_config_folder")
|
||||
os.makedirs(tmp_path, exist_ok=True)
|
||||
|
||||
unique_path = os.path.join(tmp_path, f"deepseek_v32_{os.getpid()}")
|
||||
with open(unique_path, "w") as f:
|
||||
json.dump(config_json, f)
|
||||
|
||||
return AutoConfig.from_pretrained(
|
||||
unique_path, trust_remote_code=trust_remote_code, revision=revision, **kwargs
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Context length / generation config / sparse attention
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Models don't use the same configuration key for determining the maximum
|
||||
# context length. Store them here so we can sanely check them.
|
||||
# NOTE: The ordering here is important. Some models have two of these and we
|
||||
# have a preference for which value gets used.
|
||||
CONTEXT_LENGTH_KEYS = [
|
||||
"max_sequence_length",
|
||||
"seq_length",
|
||||
"max_seq_len",
|
||||
"model_max_length",
|
||||
"max_position_embeddings",
|
||||
]
|
||||
|
||||
|
||||
def get_context_length(config):
|
||||
"""Get the context length of a model from a huggingface model configs."""
|
||||
text_config = config
|
||||
rope_scaling = getattr(text_config, "rope_scaling", None)
|
||||
if rope_scaling:
|
||||
rope_scaling_factor = rope_scaling.get("factor", 1)
|
||||
if "original_max_position_embeddings" in rope_scaling:
|
||||
rope_scaling_factor = 1
|
||||
if rope_scaling.get("rope_type", None) == "llama3":
|
||||
rope_scaling_factor = 1
|
||||
else:
|
||||
rope_scaling_factor = 1
|
||||
|
||||
for key in CONTEXT_LENGTH_KEYS:
|
||||
val = getattr(text_config, key, None)
|
||||
if val is not None:
|
||||
return int(rope_scaling_factor * val)
|
||||
return 2048
|
||||
|
||||
|
||||
@lru_cache_frozenset(maxsize=32)
|
||||
def get_generation_config(
|
||||
model: str,
|
||||
trust_remote_code: bool,
|
||||
revision: Optional[str] = None,
|
||||
**kwargs,
|
||||
):
|
||||
try:
|
||||
return GenerationConfig.from_pretrained(
|
||||
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
|
||||
)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except OSError as e:
|
||||
logger.warning(
|
||||
"Failed to load generation config for %s: %s. "
|
||||
"Proceeding without generation config.",
|
||||
model,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# Qwen-1M related
|
||||
def get_sparse_attention_config(
|
||||
model: str,
|
||||
sparse_attention_config_filename: str = "sparse_attention_config.json",
|
||||
) -> Dict[str, Any]:
|
||||
is_local = os.path.isdir(model)
|
||||
if not is_local:
|
||||
model = download_from_hf(model, allow_patterns=["*.json"])
|
||||
|
||||
config_file = os.path.join(model, sparse_attention_config_filename)
|
||||
if not os.path.exists(config_file):
|
||||
return {}
|
||||
|
||||
with open(config_file) as f:
|
||||
config = json.load(f)
|
||||
return config
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tokenizer / processor helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
# Some models don't have an available processor, e.g.: InternVL
|
||||
def get_tokenizer_from_processor(processor):
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
if isinstance(processor, PreTrainedTokenizerBase):
|
||||
return processor
|
||||
return processor.tokenizer
|
||||
|
||||
|
||||
def attach_additional_stop_token_ids(tokenizer):
|
||||
added = tokenizer.get_added_vocab()
|
||||
if "<|eom_id|>" in added:
|
||||
tokenizer.additional_stop_token_ids = {added["<|eom_id|>"]}
|
||||
else:
|
||||
tokenizer.additional_stop_token_ids = None
|
||||
@@ -0,0 +1,458 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Compatibility patches for transformers v5.x.
|
||||
|
||||
This module applies monkey-patches to work around breaking changes in
|
||||
transformers v5. Each patch is tagged with the upstream issue it works
|
||||
around so it can be removed once the upstream fix lands.
|
||||
|
||||
Import this module early (before any ``from_pretrained`` call) to activate
|
||||
all patches. It is safe to import multiple times -- patches are idempotent.
|
||||
|
||||
Patches fall into two categories:
|
||||
|
||||
1. **Transformers bugs / regressions** -- issues in transformers itself.
|
||||
2. **Remote-model-code compat** -- remote model code (trust_remote_code)
|
||||
that hasn't been updated for v5 yet. These should be removed once
|
||||
the model authors publish fixes.
|
||||
"""
|
||||
|
||||
import inspect
|
||||
|
||||
from sglang.srt.utils import logger
|
||||
|
||||
_applied = False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API: apply_all() -- import-time patches (idempotent)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def apply_all():
|
||||
"""Apply all transformers compatibility patches (idempotent).
|
||||
|
||||
Call this once at import time. It is safe to call multiple times.
|
||||
"""
|
||||
global _applied
|
||||
if _applied:
|
||||
return
|
||||
_applied = True
|
||||
|
||||
# v5.4 patches
|
||||
_patch_flash_attn_availability()
|
||||
_patch_rope_parameters_validation()
|
||||
_patch_removed_symbols()
|
||||
_patch_image_processor_kwargs()
|
||||
_patch_image_process_cuda_tensor()
|
||||
_patch_nemotron_h_pattern()
|
||||
|
||||
# v5 general patches
|
||||
_ensure_clean_up_tokenization_compat()
|
||||
_ensure_is_torch_fx_available_compat()
|
||||
|
||||
logger.debug("transformers compatibility patches applied")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public API: on-demand helpers (called explicitly by other modules)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def normalize_rope_scaling_compat(config) -> None:
|
||||
"""Ensure rope_scaling dicts have ``"type"`` alongside ``"rope_type"``.
|
||||
|
||||
Transformers v5 standardises rope_scaling to use ``"rope_type"`` and may
|
||||
omit the legacy ``"type"`` key. Remote-code models (e.g. Kimi-VL) still
|
||||
read ``rope_scaling["type"]``, causing a ``KeyError``. This helper adds
|
||||
``"type"`` from ``"rope_type"`` whenever it is missing, recursively across
|
||||
the config and all its sub-configs.
|
||||
"""
|
||||
|
||||
def _patch(cfg):
|
||||
rs = getattr(cfg, "rope_scaling", None)
|
||||
if isinstance(rs, dict) and "rope_type" in rs and "type" not in rs:
|
||||
rs["type"] = rs["rope_type"]
|
||||
# Recurse into sub-configs
|
||||
for attr in (
|
||||
"text_config",
|
||||
"llm_config",
|
||||
"language_config",
|
||||
"vision_config",
|
||||
"thinker_config",
|
||||
):
|
||||
sub = getattr(cfg, attr, None)
|
||||
if sub is not None:
|
||||
_patch(sub)
|
||||
|
||||
_patch(config)
|
||||
|
||||
|
||||
def _ensure_gguf_version():
|
||||
"""Workaround for transformers v5 bug where is_gguf_available() fails
|
||||
when the gguf package lacks __version__ and metadata lookup also fails,
|
||||
resulting in packaging.version.InvalidVersion: Invalid version: 'N/A'."""
|
||||
try:
|
||||
import gguf
|
||||
|
||||
if not hasattr(gguf, "__version__"):
|
||||
import importlib.metadata
|
||||
|
||||
try:
|
||||
gguf.__version__ = importlib.metadata.version("gguf")
|
||||
except importlib.metadata.PackageNotFoundError:
|
||||
gguf.__version__ = "0.0.0"
|
||||
except (ValueError, OSError, TypeError) as e:
|
||||
logger.warning(
|
||||
"Failed to determine gguf package version: %s. "
|
||||
"Falling back to '0.0.0'.",
|
||||
e,
|
||||
)
|
||||
gguf.__version__ = "0.0.0"
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v5.4 patches (merged from transformers_v54_compat.py)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _patch_rope_parameters_validation():
|
||||
"""Fix rope_parameters validation for unregistered model types.
|
||||
|
||||
For unregistered model types (e.g. ``deepseek_v32``), the generic
|
||||
``PretrainedConfig`` lacks a ``rope_parameters`` field so the conversion
|
||||
that injects ``rope_theta`` from the top-level config is skipped.
|
||||
Additionally, ``standardize_rope_params()`` accesses
|
||||
``self.max_position_embeddings`` during ``__post_init__`` before extra
|
||||
kwargs are set as attributes, causing ``AttributeError``.
|
||||
|
||||
Fix: (1) patch ``from_dict`` to inject ``rope_theta`` into
|
||||
``rope_scaling``, (2) guard ``standardize_rope_params`` against missing
|
||||
``max_position_embeddings``.
|
||||
|
||||
TODO(upstream): remove once unregistered model types handle rope
|
||||
standardization correctly in transformers.
|
||||
"""
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
original = PretrainedConfig.from_dict.__func__
|
||||
|
||||
@classmethod # type: ignore[misc]
|
||||
def patched(cls, config_dict, **kwargs):
|
||||
rope_scaling = config_dict.get("rope_scaling")
|
||||
rope_theta = config_dict.get("rope_theta")
|
||||
if (
|
||||
isinstance(rope_scaling, dict)
|
||||
and rope_theta is not None
|
||||
and "rope_theta" not in rope_scaling
|
||||
):
|
||||
config_dict = config_dict.copy()
|
||||
config_dict["rope_scaling"] = {**rope_scaling, "rope_theta": rope_theta}
|
||||
return original(cls, config_dict, **kwargs)
|
||||
|
||||
PretrainedConfig.from_dict = patched
|
||||
|
||||
# standardize_rope_params accesses self.max_position_embeddings before
|
||||
# __post_init__ sets extra kwargs — skip when the attribute is absent.
|
||||
if hasattr(PretrainedConfig, "standardize_rope_params"):
|
||||
_orig_standardize = PretrainedConfig.standardize_rope_params
|
||||
|
||||
def _safe_standardize(self):
|
||||
if not hasattr(self, "max_position_embeddings"):
|
||||
return
|
||||
return _orig_standardize(self)
|
||||
|
||||
PretrainedConfig.standardize_rope_params = _safe_standardize
|
||||
|
||||
|
||||
def _patch_flash_attn_availability():
|
||||
"""Prevent flash-attn-4 from masquerading as flash-attn-2.
|
||||
|
||||
flash-attn-4 registers a bare ``flash_attn`` namespace that makes
|
||||
``is_flash_attn_2_available()`` return True, but lacks the v2 API.
|
||||
Remote model code (e.g. Kimi-VL) guarded by that check will crash.
|
||||
|
||||
TODO(upstream): model authors should check for specific API symbols.
|
||||
"""
|
||||
try:
|
||||
import flash_attn as _fa
|
||||
|
||||
if not hasattr(_fa, "flash_attn_func"):
|
||||
import transformers.utils as _u
|
||||
import transformers.utils.import_utils as _ui
|
||||
|
||||
_ui.is_flash_attn_2_available = lambda: False
|
||||
_u.is_flash_attn_2_available = lambda: False
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
|
||||
def _patch_removed_symbols():
|
||||
"""Re-export symbols removed in transformers v5.4.0.
|
||||
|
||||
Remote model code (e.g. DeepSeek-OCR) still imports these.
|
||||
``check_imports`` in ``dynamic_module_utils.py`` validates imports at
|
||||
config-load time, so these must exist before any ``from_pretrained``.
|
||||
|
||||
Removed symbols:
|
||||
- ``LlamaFlashAttention2`` -- replaced by unified ``LlamaAttention``
|
||||
- ``is_flash_attn_greater_or_equal_2_10`` -- replaced by
|
||||
``is_flash_attn_greater_or_equal("2.10.0")``
|
||||
|
||||
TODO(upstream): DeepSeek-OCR / deepseek_vl_v2 remote code needs update.
|
||||
"""
|
||||
# LlamaFlashAttention2
|
||||
try:
|
||||
from transformers.models.llama import modeling_llama
|
||||
|
||||
if not hasattr(modeling_llama, "LlamaFlashAttention2"):
|
||||
if hasattr(modeling_llama, "LlamaAttention"):
|
||||
modeling_llama.LlamaFlashAttention2 = modeling_llama.LlamaAttention
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"Could not import transformers.models.llama.modeling_llama; "
|
||||
"LlamaFlashAttention2 compat patch not applied."
|
||||
)
|
||||
|
||||
# is_flash_attn_greater_or_equal_2_10
|
||||
try:
|
||||
import transformers.utils as _u
|
||||
|
||||
if not hasattr(_u, "is_flash_attn_greater_or_equal_2_10"):
|
||||
if hasattr(_u, "is_flash_attn_greater_or_equal"):
|
||||
_u.is_flash_attn_greater_or_equal_2_10 = (
|
||||
lambda: _u.is_flash_attn_greater_or_equal("2.10.0")
|
||||
)
|
||||
else:
|
||||
_u.is_flash_attn_greater_or_equal_2_10 = lambda: False
|
||||
except ImportError:
|
||||
logger.warning(
|
||||
"Could not import transformers.utils; "
|
||||
"is_flash_attn_greater_or_equal_2_10 compat patch not applied."
|
||||
)
|
||||
|
||||
|
||||
def _patch_image_processor_kwargs():
|
||||
"""Allow remote image processors that lack ``**kwargs`` in preprocess().
|
||||
|
||||
Transformers v5.4 passes new kwargs (e.g. ``device``) through
|
||||
``BaseImageProcessor.__call__`` -> ``preprocess()``. Remote model code
|
||||
(e.g. KimiVL) that defines ``preprocess()`` without ``**kwargs`` will
|
||||
crash with ``TypeError``.
|
||||
|
||||
Fix: wrap ``__call__`` to catch ``TypeError`` and retry with only the
|
||||
kwargs that ``preprocess()`` actually accepts.
|
||||
|
||||
TODO(upstream): KimiVL image_processing_kimi_vl.py needs ``**kwargs``.
|
||||
"""
|
||||
try:
|
||||
from transformers.image_processing_utils import BaseImageProcessor
|
||||
|
||||
original = BaseImageProcessor.__call__
|
||||
|
||||
def safe_call(self, images, *args, **kwargs):
|
||||
try:
|
||||
return original(self, images, *args, **kwargs)
|
||||
except TypeError as e:
|
||||
if "unexpected keyword argument" not in str(e):
|
||||
raise
|
||||
sig = inspect.signature(self.preprocess)
|
||||
params = sig.parameters
|
||||
if any(
|
||||
p.kind == inspect.Parameter.VAR_KEYWORD for p in params.values()
|
||||
):
|
||||
raise
|
||||
dropped = {k for k in kwargs if k not in params}
|
||||
if dropped:
|
||||
logger.warning(
|
||||
"Image processor %s.preprocess() does not accept %s; "
|
||||
"retrying without them. Update the model's image processor "
|
||||
"to accept **kwargs.",
|
||||
type(self).__name__,
|
||||
dropped,
|
||||
)
|
||||
valid = {k: v for k, v in kwargs.items() if k in params}
|
||||
return original(self, images, *args, **valid)
|
||||
|
||||
BaseImageProcessor.__call__ = safe_call
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"_patch_image_processor_kwargs: BaseImageProcessor not importable, patch skipped"
|
||||
)
|
||||
|
||||
|
||||
def _patch_image_process_cuda_tensor():
|
||||
"""Fix ``process_image()`` crashing on CUDA tensors.
|
||||
|
||||
Transformers v5.4's PIL image processing backend calls
|
||||
``image.numpy()`` on torch tensors, which fails for CUDA tensors.
|
||||
Patch to call ``.cpu().numpy()`` instead.
|
||||
|
||||
TODO(upstream): report to HF transformers.
|
||||
"""
|
||||
try:
|
||||
import torch
|
||||
import transformers.image_processing_backends as ipb
|
||||
|
||||
for cls_name in ("PilBackend", "PilImageProcessingMixin"):
|
||||
cls = getattr(ipb, cls_name, None)
|
||||
if cls is None or not hasattr(cls, "process_image"):
|
||||
continue
|
||||
original = cls.process_image
|
||||
|
||||
def patched_process_image(
|
||||
self, image, *args, _orig=original, _Tensor=torch.Tensor, **kwargs
|
||||
):
|
||||
if isinstance(image, _Tensor) and image.is_cuda:
|
||||
image = image.cpu()
|
||||
return _orig(self, image, *args, **kwargs)
|
||||
|
||||
cls.process_image = patched_process_image
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"_patch_image_process_cuda_tensor: required modules not importable, patch skipped"
|
||||
)
|
||||
|
||||
|
||||
def _patch_nemotron_h_pattern():
|
||||
"""Fix ``_pattern_to_list()`` crashing on ``-`` in hybrid_override_pattern.
|
||||
|
||||
Nemotron-H models (e.g. NVIDIA-Nemotron-Nano-9B-v2) use patterns like
|
||||
``M-M-M-MM-M-*-...`` where ``-`` denotes an MLP layer. The upstream
|
||||
``_pattern_to_list`` tries to map every character and crashes with
|
||||
``KeyError: '-'``. We skip ``-`` (and any other unmapped chars)
|
||||
since ``layers_block_type`` only tracks mamba/moe/attention layers.
|
||||
SGLang reads MLP positions from ``hybrid_override_pattern`` directly.
|
||||
|
||||
TODO(upstream): report to HF transformers.
|
||||
"""
|
||||
try:
|
||||
from transformers.models.nemotron_h.configuration_nemotron_h import (
|
||||
NemotronHConfig,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _pattern_to_list(pattern: str) -> list:
|
||||
pattern_mapping = {
|
||||
"M": "mamba",
|
||||
"E": "moe",
|
||||
"*": "attention",
|
||||
}
|
||||
return [
|
||||
pattern_mapping[char] for char in pattern if char in pattern_mapping
|
||||
]
|
||||
|
||||
NemotronHConfig._pattern_to_list = _pattern_to_list
|
||||
except ImportError:
|
||||
logger.debug(
|
||||
"_patch_nemotron_h_pattern: NemotronHConfig not importable, patch skipped"
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# v5 general patches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _ensure_clean_up_tokenization_compat() -> None:
|
||||
"""Re-add ``clean_up_tokenization`` removed in transformers v5.
|
||||
|
||||
Remote-code tokenizers (e.g. InternLM2Tokenizer) call
|
||||
``self.clean_up_tokenization()`` which was a static method on
|
||||
``PreTrainedTokenizerBase`` in v4 but removed in v5. Patch it back
|
||||
so existing HuggingFace Hub tokenizer code keeps working.
|
||||
"""
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
if hasattr(PreTrainedTokenizerBase, "clean_up_tokenization"):
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def clean_up_tokenization(out_string: str) -> str:
|
||||
out_string = (
|
||||
out_string.replace(" .", ".")
|
||||
.replace(" ?", "?")
|
||||
.replace(" !", "!")
|
||||
.replace(" ,", ",")
|
||||
.replace(" ' ", "'")
|
||||
.replace(" n't", "n't")
|
||||
.replace(" 'm", "'m")
|
||||
.replace(" 's", "'s")
|
||||
.replace(" 've", "'ve")
|
||||
.replace(" 're", "'re")
|
||||
)
|
||||
return out_string
|
||||
|
||||
PreTrainedTokenizerBase.clean_up_tokenization = clean_up_tokenization
|
||||
|
||||
|
||||
def _ensure_is_torch_fx_available_compat() -> None:
|
||||
"""Re-add ``is_torch_fx_available`` removed in transformers v5.
|
||||
|
||||
Remote-code models (e.g. MiniCPM-V) import ``is_torch_fx_available``
|
||||
from ``transformers.utils.import_utils``. The function was removed
|
||||
in v5. Patch it back so existing HuggingFace Hub model code keeps
|
||||
working. torch.fx is always available in PyTorch >= 2.0.
|
||||
"""
|
||||
import transformers.utils.import_utils as _import_utils
|
||||
|
||||
if hasattr(_import_utils, "is_torch_fx_available"):
|
||||
return
|
||||
|
||||
_import_utils.is_torch_fx_available = lambda: True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CI-only patches
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_is_base_mistral_patched = False
|
||||
|
||||
|
||||
def patch_is_base_mistral_in_ci():
|
||||
"""Patch transformers' _patch_mistral_regex to avoid HF API calls in CI.
|
||||
|
||||
transformers defines is_base_mistral as a local function inside
|
||||
_patch_mistral_regex, so it cannot be patched via module attribute.
|
||||
Instead we replace the entire _patch_mistral_regex classmethod with a
|
||||
version that simply returns the tokenizer unchanged.
|
||||
|
||||
In CI this prevents exhausting the 3000 req/5min HF API rate limit.
|
||||
|
||||
TODO(upstream): remove once transformers stops calling model_info()
|
||||
inside _patch_mistral_regex (or removes the method entirely).
|
||||
"""
|
||||
global _is_base_mistral_patched
|
||||
if _is_base_mistral_patched:
|
||||
return
|
||||
|
||||
from sglang.srt.environ import envs
|
||||
|
||||
if not envs.SGLANG_IS_IN_CI.get():
|
||||
return
|
||||
|
||||
from transformers import PreTrainedTokenizerFast
|
||||
|
||||
if hasattr(PreTrainedTokenizerFast, "_patch_mistral_regex"):
|
||||
|
||||
@classmethod
|
||||
def _noop_patch_mistral_regex(cls, tokenizer, *args, **kwargs):
|
||||
return tokenizer
|
||||
|
||||
PreTrainedTokenizerFast._patch_mistral_regex = _noop_patch_mistral_regex
|
||||
logger.info("CI: patched _patch_mistral_regex to skip HF API calls")
|
||||
|
||||
_is_base_mistral_patched = True
|
||||
@@ -0,0 +1,212 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Config loading utilities."""
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
from transformers.models.auto.modeling_auto import MODEL_FOR_CAUSAL_LM_MAPPING_NAMES
|
||||
|
||||
from sglang.srt.connector import create_remote_connector
|
||||
from sglang.srt.utils import is_remote_url, logger, lru_cache_frozenset
|
||||
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
|
||||
|
||||
from .common import (
|
||||
_CONFIG_REGISTRY,
|
||||
AutoConfig,
|
||||
DeepseekVLV2Config,
|
||||
_is_deepseek_ocr2_model,
|
||||
_is_deepseek_ocr_model,
|
||||
_load_deepseek_v32_model,
|
||||
_override_v_head_dim_if_zero,
|
||||
check_gguf_file,
|
||||
get_hf_text_config,
|
||||
)
|
||||
from .compat import _ensure_gguf_version
|
||||
from .mistral_utils import is_mistral_model, load_mistral_config
|
||||
|
||||
|
||||
def _set_architectures(config, arch_name):
|
||||
config.update({"architectures": [arch_name]})
|
||||
|
||||
|
||||
def _apply_deepseek_ocr_overrides(config, model):
|
||||
_override_v_head_dim_if_zero(config)
|
||||
_set_architectures(config, "DeepseekOCRForCausalLM")
|
||||
config._name_or_path = model
|
||||
|
||||
|
||||
@lru_cache_frozenset(maxsize=32)
|
||||
def get_config(
|
||||
model: str,
|
||||
trust_remote_code: bool,
|
||||
revision: Optional[str] = None,
|
||||
model_override_args: Optional[dict] = None,
|
||||
**kwargs,
|
||||
):
|
||||
is_gguf = check_gguf_file(model)
|
||||
if is_gguf:
|
||||
_ensure_gguf_version()
|
||||
kwargs["gguf_file"] = model
|
||||
model = Path(model).parent
|
||||
|
||||
if is_runai_obj_uri(model):
|
||||
model = ObjectStorageModel.get_path(model)
|
||||
|
||||
if is_remote_url(model):
|
||||
client = create_remote_connector(model)
|
||||
client.pull_files(ignore_pattern=["*.pt", "*.safetensors", "*.bin"])
|
||||
model = client.get_local_dir()
|
||||
|
||||
if is_mistral_model(model):
|
||||
config = load_mistral_config(
|
||||
model, trust_remote_code=trust_remote_code, revision=revision
|
||||
)
|
||||
else:
|
||||
try:
|
||||
config = AutoConfig.from_pretrained(
|
||||
model, trust_remote_code=trust_remote_code, revision=revision, **kwargs
|
||||
)
|
||||
except (ValueError, KeyError) as e:
|
||||
if "deepseek_v32" in str(e):
|
||||
config = _load_deepseek_v32_model(
|
||||
model,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
elif isinstance(e, ValueError):
|
||||
raise
|
||||
else:
|
||||
logger.warning(
|
||||
"AutoConfig.from_pretrained raised KeyError for %s: %s. "
|
||||
"Falling back to config registry lookup.",
|
||||
model,
|
||||
e,
|
||||
)
|
||||
config_dict, _ = PretrainedConfig.get_config_dict(
|
||||
model,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
model_type = config_dict.get("model_type")
|
||||
if model_type in _CONFIG_REGISTRY:
|
||||
config = _CONFIG_REGISTRY[model_type].from_dict(config_dict)
|
||||
config._name_or_path = model
|
||||
else:
|
||||
raise
|
||||
|
||||
if (
|
||||
config.architectures is not None
|
||||
and config.architectures[0] == "Phi4MMForCausalLM"
|
||||
):
|
||||
from transformers import SiglipVisionConfig
|
||||
|
||||
config.vision_config = SiglipVisionConfig(
|
||||
hidden_size=1152,
|
||||
image_size=448,
|
||||
intermediate_size=4304,
|
||||
model_type="siglip_vision_model",
|
||||
num_attention_heads=16,
|
||||
num_hidden_layers=26,
|
||||
patch_size=14,
|
||||
)
|
||||
|
||||
if config.architectures in [
|
||||
["LongcatCausalLM"],
|
||||
["LongcatFlashForCausalLM"],
|
||||
["LongcatFlashNgramForCausalLM"],
|
||||
]:
|
||||
config.model_type = "longcat_flash"
|
||||
|
||||
text_config = get_hf_text_config(config=config)
|
||||
|
||||
if isinstance(model, str) and text_config is not None:
|
||||
items = (
|
||||
text_config.items()
|
||||
if hasattr(text_config, "items")
|
||||
else vars(text_config).items()
|
||||
)
|
||||
for key, val in items:
|
||||
if not hasattr(config, key) and val is not None:
|
||||
setattr(config, key, val)
|
||||
|
||||
is_ocr = _is_deepseek_ocr_model(config)
|
||||
is_ocr2 = _is_deepseek_ocr2_model(config)
|
||||
|
||||
if is_ocr2:
|
||||
_override_v_head_dim_if_zero(config)
|
||||
config.model_type = "deepseek-ocr"
|
||||
_set_architectures(config, "DeepseekOCRForCausalLM")
|
||||
config = DeepseekVLV2Config.from_pretrained(model, revision=revision)
|
||||
_apply_deepseek_ocr_overrides(config, model)
|
||||
elif config.model_type in _CONFIG_REGISTRY:
|
||||
model_type = config.model_type
|
||||
if model_type == "deepseek_vl_v2" and is_ocr:
|
||||
model_type = "deepseek-ocr"
|
||||
config = _CONFIG_REGISTRY[model_type].from_pretrained(model, revision=revision)
|
||||
|
||||
# Re-check after reloading config from registry
|
||||
if _is_deepseek_ocr_model(config) or _is_deepseek_ocr2_model(config):
|
||||
_apply_deepseek_ocr_overrides(config, model)
|
||||
else:
|
||||
config._name_or_path = model
|
||||
|
||||
if isinstance(model, str) and config.model_type == "internvl_chat":
|
||||
for key, val in config.llm_config.__dict__.items():
|
||||
if not hasattr(config, key):
|
||||
setattr(config, key, val)
|
||||
|
||||
if config.model_type == "multi_modality":
|
||||
_set_architectures(config, "MultiModalityCausalLM")
|
||||
|
||||
if config.model_type == "gemma4":
|
||||
# Gemma4 configs use base attributes for SWA layers and `global_*`
|
||||
# variants for full-attention layers. SGLang expects the opposite:
|
||||
# base = full-attention, `swa_*` = sliding-window overrides.
|
||||
text_config = config.text_config
|
||||
global_head_dim = getattr(text_config, "global_head_dim", None)
|
||||
global_kv_heads = getattr(text_config, "num_global_key_value_heads", None)
|
||||
|
||||
swa_head_dim = text_config.head_dim
|
||||
swa_kv_heads = text_config.num_key_value_heads
|
||||
|
||||
text_config.swa_head_dim = swa_head_dim
|
||||
text_config.swa_v_head_dim = swa_head_dim
|
||||
text_config.swa_num_key_value_heads = swa_kv_heads
|
||||
|
||||
if global_head_dim is not None:
|
||||
text_config.head_dim = global_head_dim
|
||||
if global_kv_heads is not None:
|
||||
text_config.num_key_value_heads = global_kv_heads
|
||||
|
||||
if not hasattr(text_config, "v_head_dim"):
|
||||
text_config.v_head_dim = text_config.head_dim
|
||||
if not hasattr(text_config, "swa_v_head_dim"):
|
||||
text_config.swa_v_head_dim = text_config.swa_head_dim
|
||||
|
||||
if config.model_type == "longcat_flash":
|
||||
_set_architectures(config, "LongcatFlashForCausalLM")
|
||||
|
||||
if model_override_args:
|
||||
config.update(model_override_args)
|
||||
|
||||
if is_gguf:
|
||||
if config.model_type not in MODEL_FOR_CAUSAL_LM_MAPPING_NAMES:
|
||||
raise RuntimeError(f"Can't get gguf config for {config.model_type}.")
|
||||
_set_architectures(config, MODEL_FOR_CAUSAL_LM_MAPPING_NAMES[config.model_type])
|
||||
|
||||
return config
|
||||
+176
-22
@@ -1,13 +1,17 @@
|
||||
# Adapted from https://github.com/vllm-project/vllm/blob/main/vllm/transformers_utils/configs/mistral.py
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
import json
|
||||
import tempfile
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
from typing import Any, Optional
|
||||
|
||||
from transformers import PretrainedConfig, WhisperConfig
|
||||
from transformers import AutoConfig, PretrainedConfig, WhisperConfig
|
||||
|
||||
from sglang.srt.utils import logger
|
||||
|
||||
from .common import _ensure_sub_configs, download_from_hf
|
||||
|
||||
|
||||
def adapt_config_dict(
|
||||
config_dict: dict[str, Any], model: str, **kwargs
|
||||
@@ -73,18 +77,6 @@ def adapt_config_dict(
|
||||
if bool(config_dict.get("yarn")):
|
||||
config_dict = _remap_mistral_yarn_args(config_dict)
|
||||
|
||||
if bool(config_dict.get("llama_4_scaling")):
|
||||
llama_4_scaling_config_keys = ["original_max_position_embeddings", "beta"]
|
||||
assert all(
|
||||
[
|
||||
key in config_dict["llama_4_scaling"]
|
||||
for key in llama_4_scaling_config_keys
|
||||
]
|
||||
), (
|
||||
"llama_4_scaling config should define the keys: "
|
||||
f"{','.join(llama_4_scaling_config_keys)}"
|
||||
)
|
||||
|
||||
is_vision = bool(
|
||||
(config_dict.get("multimodal") or {}).get("vision_encoder_args")
|
||||
or config_dict.get("vision_encoder")
|
||||
@@ -267,14 +259,10 @@ class MistralConfigParser:
|
||||
):
|
||||
file_path = Path(model) / file_name
|
||||
if not file_path.is_file():
|
||||
# TODO: Add logic to download from HF in case file is not locally found
|
||||
raise FileNotFoundError(f"File not found {model}, {file_name}")
|
||||
|
||||
if file_path is not None and file_path.is_file():
|
||||
with open(file_path) as file:
|
||||
return json.load(file)
|
||||
|
||||
return None
|
||||
with open(file_path) as file:
|
||||
return json.load(file)
|
||||
|
||||
def _download_mistral_config_file(self, model, revision) -> dict:
|
||||
config_file_name = "params.json"
|
||||
@@ -294,8 +282,6 @@ class MistralConfigParser:
|
||||
revision: str | None = None,
|
||||
**kwargs,
|
||||
) -> tuple[dict, PretrainedConfig]:
|
||||
# This function loads a params.json config which
|
||||
# should be used when loading models in mistral format
|
||||
config_dict = self._download_mistral_config_file(model, revision)
|
||||
if config_dict.get("max_position_embeddings") is None:
|
||||
logger.warning(
|
||||
@@ -321,3 +307,171 @@ class MistralConfigParser:
|
||||
config.sliding_window = next(filter(None, sliding_window), None)
|
||||
|
||||
return config_dict, config
|
||||
|
||||
|
||||
def is_mistral_model(name) -> bool:
|
||||
"""Return True if *name* refers to a Mistral model needing the custom parser."""
|
||||
lower = str(name).lower()
|
||||
return (
|
||||
"mistral-large-3" in lower or "mistral-small-4" in lower or "leanstral" in lower
|
||||
)
|
||||
|
||||
|
||||
@lru_cache(maxsize=2)
|
||||
def load_mistral_config(
|
||||
model_path: str,
|
||||
trust_remote_code: bool = False,
|
||||
revision: Optional[str] = None,
|
||||
):
|
||||
"""Load and parse a Mistral model config via the custom params.json format.
|
||||
|
||||
Returns a ``PretrainedConfig`` with dict sub-configs (text_config,
|
||||
vision_config) converted to proper AutoConfig objects.
|
||||
"""
|
||||
local_path = download_from_hf(model_path)
|
||||
parser = MistralConfigParser()
|
||||
config_dict, _ = parser.parse(local_path)
|
||||
|
||||
with tempfile.NamedTemporaryFile(mode="w+", suffix=".json") as f:
|
||||
json.dump(config_dict, f)
|
||||
f.flush()
|
||||
loaded_config = AutoConfig.from_pretrained(
|
||||
f.name, trust_remote_code=trust_remote_code, revision=revision
|
||||
)
|
||||
_ensure_sub_configs(loaded_config, "text_config", "vision_config")
|
||||
|
||||
return loaded_config
|
||||
|
||||
|
||||
def wrap_as_pixtral(processor, config):
|
||||
"""Wrap a tokenizer as a PixtralProcessor for Mistral vision models."""
|
||||
from transformers.models.pixtral.image_processing_pixtral import (
|
||||
PixtralImageProcessor,
|
||||
)
|
||||
from transformers.models.pixtral.processing_pixtral import (
|
||||
PixtralProcessor as HFPixtralProcessor,
|
||||
)
|
||||
|
||||
vision_config = config.vision_config
|
||||
patch_size = vision_config.patch_size
|
||||
image_size = vision_config.image_size
|
||||
spatial_merge_size = getattr(vision_config, "spatial_merge_size", 1)
|
||||
|
||||
effective_patch = patch_size * spatial_merge_size
|
||||
image_processor = PixtralImageProcessor(
|
||||
do_resize=True,
|
||||
size={"longest_edge": image_size},
|
||||
patch_size={"height": effective_patch, "width": effective_patch},
|
||||
)
|
||||
return HFPixtralProcessor(
|
||||
image_processor=image_processor,
|
||||
tokenizer=processor,
|
||||
patch_size=patch_size,
|
||||
spatial_merge_size=spatial_merge_size,
|
||||
)
|
||||
|
||||
|
||||
# kwargs that MistralCommon tokenizers reject.
|
||||
_MISTRAL_COMMON_REJECTED_KWARGS = frozenset(
|
||||
{
|
||||
"trust_remote_code",
|
||||
"tokenizer_revision",
|
||||
"use_fast",
|
||||
"_from_auto",
|
||||
"clean_up_tokenization_spaces",
|
||||
}
|
||||
)
|
||||
|
||||
# Models whose tokenizer should be loaded from a different checkpoint.
|
||||
_MISTRAL_TOKENIZER_REDIRECTS = {
|
||||
# TODO(Xinyuan): Remove this once we have a proper tokenizer for Devstral
|
||||
"mistralai/Devstral-Small-2505": "mistralai/Mistral-Small-3.1-24B-Instruct-2503",
|
||||
}
|
||||
|
||||
|
||||
def retry_without_mistral_common_kwargs(tokenizer_name, *args, **common_kwargs):
|
||||
"""Retry ``AutoTokenizer.from_pretrained`` without kwargs that MistralCommon rejects.
|
||||
|
||||
Returns the loaded tokenizer, or *None* if the error is not a
|
||||
MistralCommon kwargs rejection.
|
||||
"""
|
||||
from transformers import AutoTokenizer
|
||||
|
||||
stripped = {
|
||||
k: v
|
||||
for k, v in common_kwargs.items()
|
||||
if k not in _MISTRAL_COMMON_REJECTED_KWARGS
|
||||
}
|
||||
return AutoTokenizer.from_pretrained(tokenizer_name, *args, **stripped)
|
||||
|
||||
|
||||
def patch_mistral_common_tokenizer(tokenizer):
|
||||
"""Patch MistralCommonTokenizer/Backend to be compatible with HF tokenizer API.
|
||||
|
||||
MistralCommon tokenizers (used by Voxtral, Pixtral, etc.) reject several
|
||||
standard kwargs and lack some attributes that sglang expects. We wrap the
|
||||
offending methods once at load time so that the rest of the codebase does
|
||||
not need any special-casing.
|
||||
"""
|
||||
cls_name = type(tokenizer).__name__
|
||||
if "MistralCommon" not in cls_name:
|
||||
return tokenizer
|
||||
if getattr(tokenizer, "_mistral_common_patched", False):
|
||||
return tokenizer
|
||||
tokenizer._mistral_common_patched = True
|
||||
|
||||
if not hasattr(tokenizer, "get_added_vocab"):
|
||||
tokenizer.get_added_vocab = lambda: {}
|
||||
|
||||
# Set a chat_template containing "audio" so that sglang's content format
|
||||
# detector returns "openai" (which preserves audio_url extraction).
|
||||
if not hasattr(tokenizer, "chat_template") or tokenizer.chat_template is None:
|
||||
tokenizer.chat_template = "<!-- audio/image multimodal -->"
|
||||
|
||||
_orig_convert = tokenizer.convert_tokens_to_ids
|
||||
|
||||
def _safe_convert(val):
|
||||
try:
|
||||
return _orig_convert(val)
|
||||
except AssertionError:
|
||||
logger.debug(
|
||||
"convert_tokens_to_ids failed for %r, returning unk_token_id", val
|
||||
)
|
||||
return getattr(tokenizer, "unk_token_id", None)
|
||||
|
||||
tokenizer.convert_tokens_to_ids = _safe_convert
|
||||
|
||||
def _drop_kwargs(fn, keys):
|
||||
def wrapper(*args, **kwargs):
|
||||
for k in keys:
|
||||
kwargs.pop(k, None)
|
||||
return fn(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
tokenizer.decode = _drop_kwargs(tokenizer.decode, ["spaces_between_special_tokens"])
|
||||
tokenizer.batch_decode = _drop_kwargs(
|
||||
tokenizer.batch_decode, ["spaces_between_special_tokens"]
|
||||
)
|
||||
|
||||
tokenizer._orig_apply_chat_template = tokenizer.apply_chat_template
|
||||
|
||||
def _safe_apply_chat_template(messages, **kwargs):
|
||||
kwargs.pop("add_generation_prompt", None)
|
||||
cleaned = []
|
||||
for msg in messages:
|
||||
if isinstance(msg, dict):
|
||||
content = msg.get("content", "")
|
||||
if isinstance(content, list):
|
||||
text_parts = [
|
||||
p.get("text", "")
|
||||
for p in content
|
||||
if isinstance(p, dict) and p.get("type") == "text"
|
||||
]
|
||||
msg = {**msg, "content": " ".join(text_parts) if text_parts else ""}
|
||||
cleaned.append(msg)
|
||||
else:
|
||||
cleaned.append(msg)
|
||||
return tokenizer._orig_apply_chat_template(cleaned, **kwargs)
|
||||
|
||||
tokenizer.apply_chat_template = _safe_apply_chat_template
|
||||
@@ -0,0 +1,288 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Processor loading utilities."""
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from transformers import (
|
||||
AutoProcessor,
|
||||
AutoTokenizer,
|
||||
PreTrainedTokenizerBase,
|
||||
)
|
||||
|
||||
from sglang.srt.multimodal.customized_mm_processor_utils import _CUSTOMIZED_MM_PROCESSOR
|
||||
from sglang.srt.utils import logger
|
||||
|
||||
from .common import (
|
||||
AutoConfig,
|
||||
_is_deepseek_ocr2_model,
|
||||
_is_deepseek_ocr_model,
|
||||
_override_v_head_dim_if_zero,
|
||||
_resolve_local_or_cached_file,
|
||||
attach_additional_stop_token_ids,
|
||||
download_from_hf,
|
||||
get_tokenizer_from_processor,
|
||||
)
|
||||
from .mistral_utils import (
|
||||
is_mistral_model,
|
||||
load_mistral_config,
|
||||
patch_mistral_common_tokenizer,
|
||||
wrap_as_pixtral,
|
||||
)
|
||||
from .tokenizer import (
|
||||
_TOKENIZERS_BACKEND,
|
||||
_fix_added_tokens_encoding,
|
||||
_fix_special_tokens_pattern,
|
||||
)
|
||||
|
||||
|
||||
def _build_processor_manually(
|
||||
model_path, config, trust_remote_code, revision, **kwargs
|
||||
):
|
||||
"""Build processor when AutoProcessor fails to resolve feature_extractor_type.
|
||||
|
||||
In transformers v5, AutoProcessor.from_pretrained calls
|
||||
AutoFeatureExtractor.from_pretrained which fails if
|
||||
preprocessor_config.json lacks 'feature_extractor_type'. This resolves
|
||||
the processor class via dynamic module resolution and constructs it with
|
||||
individually-loaded components.
|
||||
"""
|
||||
import transformers
|
||||
from transformers import AutoImageProcessor, AutoTokenizer
|
||||
from transformers.dynamic_module_utils import get_class_from_dynamic_module
|
||||
|
||||
# Resolve processor class from auto_map -- check both the model config
|
||||
# and the preprocessor_config.json (some models like MiniCPM-o only
|
||||
# declare AutoProcessor in the latter).
|
||||
auto_map = getattr(config, "auto_map", None) or {}
|
||||
proc_ref = auto_map.get("AutoProcessor")
|
||||
if not proc_ref:
|
||||
try:
|
||||
pp_file = _resolve_local_or_cached_file(
|
||||
model_path, "preprocessor_config.json", revision
|
||||
)
|
||||
with open(pp_file) as f:
|
||||
pp_auto_map = json.load(f).get("auto_map", {})
|
||||
proc_ref = pp_auto_map.get("AutoProcessor")
|
||||
except (OSError, json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning(
|
||||
"_build_processor_manually: could not read preprocessor_config.json "
|
||||
"for %s: %s",
|
||||
model_path,
|
||||
e,
|
||||
)
|
||||
if not proc_ref:
|
||||
raise ValueError(f"Cannot determine processor class for {model_path}")
|
||||
|
||||
proc_cls = get_class_from_dynamic_module(
|
||||
proc_ref, model_path, code_revision=revision
|
||||
)
|
||||
|
||||
# Load sub-components individually (these succeed)
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
model_path, trust_remote_code=trust_remote_code, revision=revision
|
||||
)
|
||||
init_kwargs = {"tokenizer": tokenizer}
|
||||
|
||||
if "image_processor" in getattr(proc_cls, "attributes", []):
|
||||
try:
|
||||
init_kwargs["image_processor"] = AutoImageProcessor.from_pretrained(
|
||||
model_path, trust_remote_code=trust_remote_code, revision=revision
|
||||
)
|
||||
except (ImportError, OSError, ValueError) as e:
|
||||
raise RuntimeError(
|
||||
f"Failed to load image_processor for {model_path}: {e}. "
|
||||
f"This model requires an image processor for multimodal features. "
|
||||
f"Check that the model files are complete and accessible."
|
||||
) from e
|
||||
|
||||
# Instantiate feature extractor from its declared class
|
||||
fe_class_name = getattr(proc_cls, "feature_extractor_class", None)
|
||||
if fe_class_name:
|
||||
fe_class = getattr(transformers, fe_class_name, None)
|
||||
if fe_class is not None:
|
||||
try:
|
||||
init_kwargs["feature_extractor"] = fe_class()
|
||||
except TypeError as e:
|
||||
logger.warning(
|
||||
"Cannot instantiate feature extractor %s with no arguments "
|
||||
"for %s: %s",
|
||||
fe_class_name,
|
||||
model_path,
|
||||
e,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Feature extractor class %s not found in transformers for %s",
|
||||
fe_class_name,
|
||||
model_path,
|
||||
)
|
||||
|
||||
return proc_cls(**init_kwargs)
|
||||
|
||||
|
||||
def get_processor(
|
||||
tokenizer_name: str,
|
||||
*args,
|
||||
tokenizer_mode: str = "auto",
|
||||
trust_remote_code: bool = False,
|
||||
tokenizer_revision: Optional[str] = None,
|
||||
use_fast: Optional[bool] = True,
|
||||
**kwargs,
|
||||
):
|
||||
revision = kwargs.pop("revision", tokenizer_revision)
|
||||
if is_mistral_model(tokenizer_name):
|
||||
config = load_mistral_config(
|
||||
tokenizer_name,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
)
|
||||
else:
|
||||
config = AutoConfig.from_pretrained(
|
||||
tokenizer_name,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
is_ocr2 = _is_deepseek_ocr2_model(config)
|
||||
if _is_deepseek_ocr_model(config) or is_ocr2:
|
||||
config.model_type = "deepseek-ocr"
|
||||
config.update({"architectures": ["DeepseekOCRForCausalLM"]})
|
||||
if is_ocr2:
|
||||
_override_v_head_dim_if_zero(config)
|
||||
|
||||
if config.model_type in {"qwen2_vl", "sarashina2_vision"}:
|
||||
if "size" not in kwargs:
|
||||
kwargs["size"] = {"shortest_edge": 3136, "longest_edge": 1003520}
|
||||
|
||||
if config.model_type not in {"llava", "clip"}:
|
||||
kwargs["use_fast"] = use_fast
|
||||
try:
|
||||
if "InternVL3_5" in tokenizer_name:
|
||||
processor = AutoTokenizer.from_pretrained(
|
||||
tokenizer_name,
|
||||
*args,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
if config.model_type in _CUSTOMIZED_MM_PROCESSOR:
|
||||
processor = _CUSTOMIZED_MM_PROCESSOR[config.model_type].from_pretrained(
|
||||
tokenizer_name,
|
||||
*args,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
processor = AutoProcessor.from_pretrained(
|
||||
tokenizer_name,
|
||||
*args,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
except ValueError as e:
|
||||
error_message = str(e)
|
||||
if "does not have a slow version" in error_message:
|
||||
logger.info(
|
||||
"Processor %s does not have a slow version. Automatically use fast version",
|
||||
tokenizer_name,
|
||||
)
|
||||
kwargs["use_fast"] = True
|
||||
processor = AutoProcessor.from_pretrained(
|
||||
tokenizer_name,
|
||||
*args,
|
||||
trust_remote_code=trust_remote_code,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
elif "Unrecognized feature extractor" in error_message:
|
||||
logger.info(
|
||||
"AutoProcessor failed on feature extractor for %s, "
|
||||
"constructing processor manually",
|
||||
tokenizer_name,
|
||||
)
|
||||
processor = _build_processor_manually(
|
||||
tokenizer_name,
|
||||
config,
|
||||
trust_remote_code,
|
||||
revision,
|
||||
**kwargs,
|
||||
)
|
||||
elif (
|
||||
"are not supported by" in error_message and "MistralCommon" in error_message
|
||||
):
|
||||
logger.info(
|
||||
"AutoProcessor for %s rejected standard kwargs, "
|
||||
"retrying without trust_remote_code/use_fast",
|
||||
tokenizer_name,
|
||||
)
|
||||
kwargs.pop("use_fast", None)
|
||||
kwargs.pop("_from_auto", None)
|
||||
processor = AutoProcessor.from_pretrained(
|
||||
tokenizer_name,
|
||||
*args,
|
||||
revision=revision,
|
||||
**kwargs,
|
||||
)
|
||||
else:
|
||||
raise
|
||||
if (
|
||||
isinstance(processor, PreTrainedTokenizerBase)
|
||||
and getattr(config, "model_type", None) == "pixtral"
|
||||
):
|
||||
processor = wrap_as_pixtral(processor, config)
|
||||
|
||||
tokenizer = get_tokenizer_from_processor(processor)
|
||||
|
||||
# AutoProcessor may internally create a TokenizersBackend tokenizer
|
||||
# (same issue as get_tokenizer). Replace it with a properly loaded one.
|
||||
if type(tokenizer).__name__ == _TOKENIZERS_BACKEND:
|
||||
from .tokenizer import get_tokenizer
|
||||
|
||||
logger.warning(
|
||||
"Processor tokenizer for %s is TokenizersBackend, "
|
||||
"reloading via get_tokenizer",
|
||||
tokenizer_name,
|
||||
)
|
||||
tokenizer = get_tokenizer(
|
||||
tokenizer_name,
|
||||
tokenizer_mode=tokenizer_mode,
|
||||
trust_remote_code=trust_remote_code,
|
||||
tokenizer_revision=revision,
|
||||
)
|
||||
if isinstance(processor, PreTrainedTokenizerBase):
|
||||
processor = tokenizer
|
||||
else:
|
||||
processor.tokenizer = tokenizer
|
||||
|
||||
if tokenizer.chat_template is None:
|
||||
local_path = download_from_hf(
|
||||
tokenizer_name, allow_patterns=["*.json", "*.jinja", "*.model"]
|
||||
)
|
||||
jinja_path = Path(local_path) / "chat_template.jinja"
|
||||
if jinja_path.is_file():
|
||||
tokenizer.chat_template = jinja_path.read_text()
|
||||
logger.info("Loaded chat_template from %s", jinja_path)
|
||||
|
||||
patch_mistral_common_tokenizer(tokenizer)
|
||||
_fix_special_tokens_pattern(tokenizer)
|
||||
_fix_added_tokens_encoding(tokenizer)
|
||||
attach_additional_stop_token_ids(tokenizer)
|
||||
return processor
|
||||
@@ -0,0 +1,551 @@
|
||||
# Copyright 2023-2024 SGLang Team
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
# ==============================================================================
|
||||
"""Tokenizer loading utilities."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import warnings
|
||||
from pathlib import Path
|
||||
from typing import Optional, Union
|
||||
|
||||
from transformers import (
|
||||
AutoTokenizer,
|
||||
PreTrainedTokenizer,
|
||||
PreTrainedTokenizerFast,
|
||||
)
|
||||
|
||||
from sglang.srt.connector import create_remote_connector
|
||||
from sglang.srt.utils import is_remote_url, logger
|
||||
from sglang.srt.utils.patch_tokenizer import patch_tokenizer
|
||||
from sglang.srt.utils.runai_utils import ObjectStorageModel, is_runai_obj_uri
|
||||
|
||||
from .common import (
|
||||
_resolve_local_or_cached_file,
|
||||
attach_additional_stop_token_ids,
|
||||
check_gguf_file,
|
||||
)
|
||||
from .compat import _ensure_gguf_version, patch_is_base_mistral_in_ci
|
||||
from .mistral_utils import (
|
||||
_MISTRAL_TOKENIZER_REDIRECTS,
|
||||
patch_mistral_common_tokenizer,
|
||||
retry_without_mistral_common_kwargs,
|
||||
)
|
||||
|
||||
# A fast LLaMA tokenizer with the pre-processed `tokenizer.json` file.
|
||||
_FAST_LLAMA_TOKENIZER = "hf-internal-testing/llama-tokenizer"
|
||||
|
||||
# Class name used by transformers v5 when no tokenizer mapping exists for a model_type.
|
||||
_TOKENIZERS_BACKEND = "TokenizersBackend"
|
||||
|
||||
|
||||
def _load_tokenizer_by_declared_class(tokenizer_name, *args, **kwargs):
|
||||
"""Load tokenizer by the class declared in tokenizer_config.json.
|
||||
|
||||
AutoTokenizer resolves to TokenizersBackend when the model's config
|
||||
model_type has no tokenizer class mapping (e.g. deepseek_vl_v2), even
|
||||
though tokenizer_config.json declares a standard class like
|
||||
LlamaTokenizerFast. Returns None if it cannot improve on AutoTokenizer.
|
||||
"""
|
||||
import transformers
|
||||
|
||||
try:
|
||||
revision = kwargs.get("revision") or kwargs.get("tokenizer_revision")
|
||||
config_file = _resolve_local_or_cached_file(
|
||||
tokenizer_name, "tokenizer_config.json", revision
|
||||
)
|
||||
with open(config_file) as f:
|
||||
tok_config = json.load(f)
|
||||
tok_class_name = tok_config.get("tokenizer_class")
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
except (OSError, json.JSONDecodeError) as e:
|
||||
logger.debug(
|
||||
"Failed to read tokenizer_config.json for %s: %s", tokenizer_name, e
|
||||
)
|
||||
return None
|
||||
|
||||
if not tok_class_name:
|
||||
return None
|
||||
|
||||
# Skip base classes that don't implement required methods (e.g. get_vocab)
|
||||
if tok_class_name in ("PreTrainedTokenizer", "PreTrainedTokenizerBase"):
|
||||
return None
|
||||
|
||||
tok_cls = getattr(transformers, tok_class_name, None)
|
||||
if tok_cls is None and kwargs.get("trust_remote_code"):
|
||||
# Class not in transformers — try loading via auto_map.
|
||||
try:
|
||||
auto_map = tok_config.get("auto_map", {})
|
||||
auto_tok_ref = auto_map.get("AutoTokenizer")
|
||||
if isinstance(auto_tok_ref, (list, tuple)):
|
||||
auto_tok_ref = auto_tok_ref[0]
|
||||
if auto_tok_ref:
|
||||
from transformers.dynamic_module_utils import (
|
||||
get_class_from_dynamic_module,
|
||||
)
|
||||
|
||||
tok_cls = get_class_from_dynamic_module(
|
||||
auto_tok_ref,
|
||||
tokenizer_name,
|
||||
code_revision=revision,
|
||||
)
|
||||
except (OSError, ImportError, ValueError, RuntimeError) as e:
|
||||
logger.debug("Dynamic module lookup for %s failed: %s", tok_class_name, e)
|
||||
if tok_cls is None:
|
||||
return None
|
||||
|
||||
logger.info(
|
||||
"Loading tokenizer for %s directly as %s (bypassing AutoTokenizer)",
|
||||
tokenizer_name,
|
||||
tok_class_name,
|
||||
)
|
||||
try:
|
||||
return tok_cls.from_pretrained(tokenizer_name, *args, **kwargs)
|
||||
except (OSError, ValueError, TypeError, ImportError) as e:
|
||||
logger.warning(
|
||||
"Direct load as %s failed for %s: %s. "
|
||||
"Falling back to AutoTokenizer result.",
|
||||
tok_class_name,
|
||||
tokenizer_name,
|
||||
e,
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# Filter warnings like: https://github.com/sgl-project/sglang/issues/8082
|
||||
class TokenizerWarningsFilter(logging.Filter):
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return "Calling super().encode with" not in record.getMessage()
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers for get_tokenizer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _resolve_tokenizer_name(tokenizer_name, kwargs):
|
||||
"""Resolve special name formats (GGUF, remote URLs, etc.) to a local path.
|
||||
|
||||
May mutate *kwargs* (e.g. to add ``gguf_file``).
|
||||
"""
|
||||
tokenizer_name = _MISTRAL_TOKENIZER_REDIRECTS.get(tokenizer_name, tokenizer_name)
|
||||
|
||||
if check_gguf_file(tokenizer_name):
|
||||
_ensure_gguf_version()
|
||||
kwargs["gguf_file"] = tokenizer_name
|
||||
tokenizer_name = Path(tokenizer_name).parent
|
||||
|
||||
if is_runai_obj_uri(tokenizer_name):
|
||||
tokenizer_name = ObjectStorageModel.get_path(tokenizer_name)
|
||||
|
||||
if is_remote_url(tokenizer_name):
|
||||
# BaseConnector implements __del__() to clean up the local dir.
|
||||
# Since config files need to exist all the time, so we DO NOT use
|
||||
# with statement to avoid closing the client.
|
||||
client = create_remote_connector(tokenizer_name)
|
||||
client.pull_files(ignore_pattern=["*.pt", "*.safetensors", "*.bin"])
|
||||
tokenizer_name = client.get_local_dir()
|
||||
|
||||
return tokenizer_name
|
||||
|
||||
|
||||
def _auto_tokenizer_from_pretrained(tokenizer_name, *args, **common_kwargs):
|
||||
"""Call ``AutoTokenizer.from_pretrained`` with error handling."""
|
||||
try:
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_name, *args, **common_kwargs
|
||||
)
|
||||
logging.getLogger(tokenizer.__class__.__module__).addFilter(
|
||||
TokenizerWarningsFilter()
|
||||
)
|
||||
return tokenizer
|
||||
except TypeError as e:
|
||||
err_msg = (
|
||||
"Failed to load the tokenizer. If you are using a LLaMA V1 model "
|
||||
f"consider using '{_FAST_LLAMA_TOKENIZER}' instead of the "
|
||||
"original tokenizer."
|
||||
)
|
||||
raise RuntimeError(err_msg) from e
|
||||
except ValueError as e:
|
||||
# MistralCommon tokenizers reject standard HF kwargs like
|
||||
# trust_remote_code, use_fast etc. Retry without them.
|
||||
if "are not supported by" in str(e) and "MistralCommon" in str(e):
|
||||
return retry_without_mistral_common_kwargs(
|
||||
tokenizer_name, *args, **common_kwargs
|
||||
)
|
||||
# If the error pertains to the tokenizer class not existing or not
|
||||
# currently being imported, suggest using the --trust-remote-code flag.
|
||||
if not common_kwargs.get("trust_remote_code") and (
|
||||
"does not exist or is not currently imported." in str(e)
|
||||
or "requires you to execute the tokenizer file" in str(e)
|
||||
):
|
||||
err_msg = (
|
||||
"Failed to load the tokenizer. If the tokenizer is a custom "
|
||||
"tokenizer not yet available in the HuggingFace transformers "
|
||||
"library, consider setting `trust_remote_code=True` in LLM "
|
||||
"or using the `--trust-remote-code` flag in the CLI."
|
||||
)
|
||||
raise RuntimeError(err_msg) from e
|
||||
raise
|
||||
|
||||
|
||||
def _resolve_tokenizers_backend(tokenizer_name, *args, **common_kwargs):
|
||||
"""Resolve generic ``TokenizersBackend`` to a proper tokenizer class.
|
||||
|
||||
In transformers v5, ``AutoTokenizer`` falls back to ``TokenizersBackend``
|
||||
when the model_type has no tokenizer mapping. This retries with
|
||||
``use_fast=False``, then attempts loading by the class declared in
|
||||
``tokenizer_config.json``. May still return a ``TokenizersBackend``
|
||||
if all retries fail (with a warning).
|
||||
"""
|
||||
logger.warning(
|
||||
"Tokenizer loaded as generic TokenizersBackend for %s, "
|
||||
"retrying with use_fast=False",
|
||||
tokenizer_name,
|
||||
)
|
||||
common_kwargs = {**common_kwargs, "use_fast": False}
|
||||
try:
|
||||
tokenizer = AutoTokenizer.from_pretrained(
|
||||
tokenizer_name, *args, **common_kwargs
|
||||
)
|
||||
except (ValueError, TypeError, OSError, ImportError, RuntimeError) as e:
|
||||
raise RuntimeError(
|
||||
f"Retry with use_fast=False for {tokenizer_name} also failed "
|
||||
f"(initial load returned TokenizersBackend): {e}"
|
||||
) from e
|
||||
|
||||
if type(tokenizer).__name__ == _TOKENIZERS_BACKEND:
|
||||
tokenizer = (
|
||||
_load_tokenizer_by_declared_class(tokenizer_name, *args, **common_kwargs)
|
||||
or tokenizer
|
||||
)
|
||||
|
||||
if type(tokenizer).__name__ == _TOKENIZERS_BACKEND:
|
||||
if common_kwargs.get("trust_remote_code"):
|
||||
logger.warning(
|
||||
"Tokenizer for %s is still TokenizersBackend after retries "
|
||||
"with --trust-remote-code. Model-specific tokenizer attributes "
|
||||
"may be missing.",
|
||||
tokenizer_name,
|
||||
)
|
||||
else:
|
||||
logger.warning(
|
||||
"Tokenizer for %s loaded as generic TokenizersBackend. "
|
||||
"Set --trust-remote-code to load the model-specific tokenizer.",
|
||||
tokenizer_name,
|
||||
)
|
||||
|
||||
return tokenizer
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Post-load fixups
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _fix_v5_tokenizer_components(tokenizer, model_name_or_path, revision=None):
|
||||
"""Fix pre_tokenizer/decoder when a v5 tokenizer class overwrites them.
|
||||
|
||||
In transformers v5, some tokenizer classes (e.g. LlamaTokenizer) have a
|
||||
custom __init__ that rebuilds the pre_tokenizer and decoder from scratch
|
||||
with class-specific components, discarding the originals from tokenizer.json.
|
||||
This breaks models that specify LlamaTokenizerFast but actually use a
|
||||
different tokenizer architecture (e.g. DeepSeek-V3.2 uses ByteLevel).
|
||||
|
||||
Detects the mismatch by comparing against the raw tokenizer.json and
|
||||
restores the original components when they differ.
|
||||
"""
|
||||
backend = getattr(tokenizer, "_tokenizer", None)
|
||||
if backend is None:
|
||||
return
|
||||
|
||||
try:
|
||||
from tokenizers import Tokenizer as RawTokenizer
|
||||
|
||||
tok_file = _resolve_local_or_cached_file(
|
||||
model_name_or_path, "tokenizer.json", revision
|
||||
)
|
||||
raw = RawTokenizer.from_file(tok_file)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except (OSError, ValueError, RuntimeError) as e:
|
||||
logger.warning(
|
||||
"_fix_v5_tokenizer_components: unexpected error loading tokenizer.json "
|
||||
"for %s, v5 component fix will not be applied: %s",
|
||||
model_name_or_path,
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
raw_pre = type(raw.pre_tokenizer).__name__ if raw.pre_tokenizer else None
|
||||
loaded_pre = type(backend.pre_tokenizer).__name__ if backend.pre_tokenizer else None
|
||||
|
||||
if raw_pre and loaded_pre and raw_pre != loaded_pre:
|
||||
logger.info(
|
||||
"Fixing v5 tokenizer component mismatch for %s: "
|
||||
"pre_tokenizer %s -> %s, decoder %s -> %s",
|
||||
model_name_or_path,
|
||||
loaded_pre,
|
||||
raw_pre,
|
||||
type(backend.decoder).__name__ if backend.decoder else None,
|
||||
type(raw.decoder).__name__ if raw.decoder else None,
|
||||
)
|
||||
backend.pre_tokenizer = raw.pre_tokenizer
|
||||
backend.decoder = raw.decoder
|
||||
|
||||
|
||||
def _fix_v5_add_bos_eos_token(tokenizer, model_name_or_path, revision=None):
|
||||
"""Restore add_bos_token/add_eos_token stripped by transformers v5.
|
||||
|
||||
In transformers v5, _from_pretrained() strips add_bos_token and
|
||||
add_eos_token from init kwargs when a tokenizer.json file is present,
|
||||
assuming the tokenizer.json post-processor handles BOS/EOS addition.
|
||||
However, many models (e.g. DeepSeek-V3) have a tokenizer.json whose
|
||||
post-processor does NOT add BOS/EOS, and rely on the add_bos_token flag
|
||||
from tokenizer_config.json instead. This causes silent accuracy regressions.
|
||||
|
||||
This function reads the tokenizer_config.json and restores the values,
|
||||
but only for tokenizer classes that actually supported these flags in v4.
|
||||
Classes like Qwen2Tokenizer did not support add_bos_token/add_eos_token
|
||||
in v4, so restoring them would change behavior.
|
||||
"""
|
||||
# In transformers v4, only certain tokenizer classes supported
|
||||
# add_bos_token / add_eos_token as init parameters. Restoring these
|
||||
# flags for classes that never supported them (e.g. Qwen2Tokenizer)
|
||||
# would incorrectly change tokenization behavior.
|
||||
_V4_CLASSES_WITH_BOS_EOS_FLAGS = frozenset(
|
||||
{
|
||||
"LlamaTokenizer",
|
||||
"LlamaTokenizerFast",
|
||||
"CodeLlamaTokenizer",
|
||||
"CodeLlamaTokenizerFast",
|
||||
"GemmaTokenizer",
|
||||
"GemmaTokenizerFast",
|
||||
"CohereTokenizerFast",
|
||||
}
|
||||
)
|
||||
|
||||
try:
|
||||
config_file = _resolve_local_or_cached_file(
|
||||
model_name_or_path, "tokenizer_config.json", revision
|
||||
)
|
||||
with open(config_file) as f:
|
||||
config = json.load(f)
|
||||
except FileNotFoundError:
|
||||
return
|
||||
except (OSError, json.JSONDecodeError, ValueError) as e:
|
||||
logger.warning(
|
||||
"_fix_v5_add_bos_eos_token: failed to read tokenizer_config.json "
|
||||
"for %s, BOS/EOS token restoration will not be applied: %s",
|
||||
model_name_or_path,
|
||||
e,
|
||||
)
|
||||
return
|
||||
|
||||
tokenizer_class = config.get("tokenizer_class", "")
|
||||
if tokenizer_class not in _V4_CLASSES_WITH_BOS_EOS_FLAGS:
|
||||
logger.debug(
|
||||
"_fix_v5_add_bos_eos_token: skipping %s (tokenizer_class=%s "
|
||||
"did not support add_bos/eos_token in v4)",
|
||||
model_name_or_path,
|
||||
tokenizer_class,
|
||||
)
|
||||
return
|
||||
|
||||
# In v4, Llama/Gemma tokenizers defaulted add_bos_token=True.
|
||||
# When the config omits the key or has null, use the v4 default so that
|
||||
# update_post_processor() doesn't drop BOS/EOS that was there before.
|
||||
_V4_DEFAULTS = {"add_bos_token": True, "add_eos_token": False}
|
||||
|
||||
changed = False
|
||||
for attr in ("add_bos_token", "add_eos_token"):
|
||||
config_val = config.get(attr)
|
||||
if config_val is None:
|
||||
# Key missing or null -> use v4 default for this tokenizer class
|
||||
config_val = _V4_DEFAULTS.get(attr, False)
|
||||
# Fast tokenizers in v4 used tokenizer.json post-processor for EOS —
|
||||
# the add_eos_token Python attribute was set but the post-processor
|
||||
# came from tokenizer.json, not from the attribute. In v5, the flag is
|
||||
# stripped and both sglang and HF reference end up with add_eos_token=False.
|
||||
# Restoring add_eos_token for fast tokenizers makes sglang diverge from
|
||||
# the HF reference, breaking embedding models like e5-mistral-7b-instruct.
|
||||
if attr == "add_eos_token" and isinstance(tokenizer, PreTrainedTokenizerFast):
|
||||
config_val = _V4_DEFAULTS["add_eos_token"] # False
|
||||
current_val = getattr(tokenizer, attr, None)
|
||||
if current_val != config_val:
|
||||
logger.info(
|
||||
"Restoring %s=%s for %s (was %s after v5 loading)",
|
||||
attr,
|
||||
config_val,
|
||||
model_name_or_path,
|
||||
current_val,
|
||||
)
|
||||
# Set the private backing attribute (not the property) because
|
||||
# transformers tokenizers expose add_bos/eos_token as properties
|
||||
# that read from the underscore-prefixed attribute.
|
||||
setattr(tokenizer, f"_{attr}", config_val)
|
||||
changed = True
|
||||
|
||||
# Rebuild the post-processor so it respects the restored flags
|
||||
if changed and hasattr(tokenizer, "update_post_processor"):
|
||||
tokenizer.update_post_processor()
|
||||
|
||||
|
||||
def _fix_special_tokens_pattern(tokenizer):
|
||||
"""Fix https://github.com/huggingface/transformers/pull/42563 which defaults
|
||||
special_tokens_pattern to "cls_sep", inserting None into token IDs when
|
||||
cls_token/sep_token are undefined (e.g. Kimi-VL's TikTokenTokenizer).
|
||||
"""
|
||||
pattern = getattr(tokenizer, "special_tokens_pattern", None)
|
||||
if pattern == "cls_sep" and (
|
||||
tokenizer.cls_token_id is None or tokenizer.sep_token_id is None
|
||||
):
|
||||
tokenizer.special_tokens_pattern = "none"
|
||||
|
||||
|
||||
def _apply_post_load_fixes(tokenizer, tokenizer_name, revision):
|
||||
"""Apply all post-load patches and return the final tokenizer."""
|
||||
_fix_v5_tokenizer_components(tokenizer, tokenizer_name, revision)
|
||||
_fix_v5_add_bos_eos_token(tokenizer, tokenizer_name, revision)
|
||||
|
||||
if not isinstance(tokenizer, PreTrainedTokenizerFast):
|
||||
warnings.warn(
|
||||
"Using a slow tokenizer. This might cause a significant "
|
||||
"slowdown. Consider using a fast tokenizer instead."
|
||||
)
|
||||
|
||||
patch_mistral_common_tokenizer(tokenizer)
|
||||
_fix_special_tokens_pattern(tokenizer)
|
||||
attach_additional_stop_token_ids(tokenizer)
|
||||
return patch_tokenizer(tokenizer)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Public entry point
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def get_tokenizer(
|
||||
tokenizer_name: str,
|
||||
*args,
|
||||
tokenizer_mode: str = "auto",
|
||||
trust_remote_code: bool = False,
|
||||
tokenizer_revision: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> Union[PreTrainedTokenizer, PreTrainedTokenizerFast]:
|
||||
"""Gets a tokenizer for the given model name via Huggingface."""
|
||||
if tokenizer_name.endswith(".json"):
|
||||
from sglang.srt.tokenizer.tiktoken_tokenizer import TiktokenTokenizer
|
||||
|
||||
return TiktokenTokenizer(tokenizer_name)
|
||||
|
||||
if tokenizer_mode == "slow":
|
||||
if kwargs.get("use_fast", False):
|
||||
raise ValueError("Cannot use the fast tokenizer in slow tokenizer mode.")
|
||||
kwargs["use_fast"] = False
|
||||
elif tokenizer_mode == "auto":
|
||||
# Transformers v5 AutoTokenizer ignores use_fast (always fast), but
|
||||
# some code paths pass kwargs to non-AutoTokenizer loaders where
|
||||
# use_fast still matters. Set explicitly for those fallback paths.
|
||||
if "use_fast" not in kwargs:
|
||||
kwargs["use_fast"] = True
|
||||
|
||||
tokenizer_name = _resolve_tokenizer_name(tokenizer_name, kwargs)
|
||||
patch_is_base_mistral_in_ci()
|
||||
|
||||
common_kwargs = dict(
|
||||
trust_remote_code=trust_remote_code,
|
||||
tokenizer_revision=tokenizer_revision,
|
||||
clean_up_tokenization_spaces=False,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
tokenizer = _auto_tokenizer_from_pretrained(tokenizer_name, *args, **common_kwargs)
|
||||
|
||||
if type(tokenizer).__name__ == _TOKENIZERS_BACKEND:
|
||||
tokenizer = _resolve_tokenizers_backend(tokenizer_name, *args, **common_kwargs)
|
||||
|
||||
return _apply_post_load_fixes(tokenizer, tokenizer_name, tokenizer_revision)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exported helpers (used by processor.py, etc.)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _fix_added_tokens_encoding(tokenizer):
|
||||
"""Ensure special tokens encode as single tokens in transformers v5.
|
||||
|
||||
Some model tokenizers (e.g. MiniCPM-V-4) define special tokens like <image>,
|
||||
<slice> as attributes on the tokenizer class with corresponding IDs in the
|
||||
vocabulary (via tokenizer.json's added_tokens). In transformers v5, these
|
||||
tokens may not appear in get_added_vocab() and encode() splits them into
|
||||
subwords, breaking multimodal pipelines that rely on finding them in input_ids.
|
||||
|
||||
This function discovers such tokens by scanning tokenizer attributes, checks
|
||||
if they encode correctly, and re-registers any that don't.
|
||||
"""
|
||||
|
||||
# Discover special token strings from tokenizer attributes.
|
||||
# Model tokenizers (e.g. MiniCPMVTokenizerFast) store them as attributes
|
||||
# like im_start="<image>", slice_start="<slice>", etc.
|
||||
def _is_special_token_attr(val):
|
||||
return (
|
||||
isinstance(val, str)
|
||||
and val.startswith("<")
|
||||
and val.endswith(">")
|
||||
and len(val) <= 20
|
||||
)
|
||||
|
||||
candidates = {}
|
||||
for attr in dir(tokenizer):
|
||||
if attr.startswith("_"):
|
||||
continue
|
||||
try:
|
||||
val = getattr(tokenizer, attr)
|
||||
except (AttributeError, TypeError, ValueError):
|
||||
continue
|
||||
if not _is_special_token_attr(val):
|
||||
continue
|
||||
token_id = tokenizer.convert_tokens_to_ids(val)
|
||||
if token_id is not None and token_id != tokenizer.unk_token_id:
|
||||
candidates[val] = token_id
|
||||
|
||||
if not candidates:
|
||||
return
|
||||
|
||||
def _encodes_correctly(token_str, expected_id):
|
||||
try:
|
||||
ids = tokenizer.encode(token_str, add_special_tokens=False)
|
||||
return len(ids) == 1 and ids[0] == expected_id
|
||||
except (ValueError, OverflowError, RuntimeError) as e:
|
||||
logger.debug("Token %s encode check failed: %s", token_str, e)
|
||||
return False
|
||||
|
||||
broken = [
|
||||
tok for tok, eid in candidates.items() if not _encodes_correctly(tok, eid)
|
||||
]
|
||||
|
||||
if not broken:
|
||||
return
|
||||
|
||||
from transformers import AddedToken
|
||||
|
||||
tokens_to_add = [AddedToken(tok, special=True, normalized=False) for tok in broken]
|
||||
tokenizer.add_tokens(tokens_to_add, special_tokens=True)
|
||||
logger.info(
|
||||
"Re-registered %d special tokens for correct v5 encoding: %s",
|
||||
len(broken),
|
||||
broken[:10],
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,586 @@
|
||||
"""Unit tests for the sglang.srt.utils.hf_transformers subpackage.
|
||||
|
||||
Tests cover the pure utility functions (compat patches, config helpers,
|
||||
context length, GGUF detection, etc.) that don't require actual model files.
|
||||
"""
|
||||
|
||||
import tempfile
|
||||
import unittest
|
||||
from types import SimpleNamespace
|
||||
|
||||
from transformers import PretrainedConfig
|
||||
|
||||
from sglang.srt.utils.hf_transformers.common import (
|
||||
_is_deepseek_ocr2_model,
|
||||
_is_deepseek_ocr_model,
|
||||
_override_v_head_dim_if_zero,
|
||||
_patch_text_config,
|
||||
check_gguf_file,
|
||||
get_context_length,
|
||||
get_hf_text_config,
|
||||
get_rope_config,
|
||||
)
|
||||
from sglang.srt.utils.hf_transformers.compat import normalize_rope_scaling_compat
|
||||
from sglang.srt.utils.hf_transformers.tokenizer import _fix_special_tokens_pattern
|
||||
from sglang.test.ci.ci_register import register_cpu_ci
|
||||
|
||||
register_cpu_ci(est_time=5, suite="stage-a-test-cpu")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# normalize_rope_scaling_compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestNormalizeRopeScalingCompat(unittest.TestCase):
|
||||
def test_adds_type_from_rope_type(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_scaling = {"rope_type": "llama3", "factor": 8.0}
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertEqual(cfg.rope_scaling["type"], "llama3")
|
||||
|
||||
def test_preserves_existing_type(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_scaling = {"rope_type": "llama3", "type": "custom", "factor": 8.0}
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertEqual(cfg.rope_scaling["type"], "custom")
|
||||
|
||||
def test_no_op_when_no_rope_scaling(self):
|
||||
cfg = PretrainedConfig()
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertIsNone(getattr(cfg, "rope_scaling", None))
|
||||
|
||||
def test_no_op_when_rope_scaling_is_none(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_scaling = None
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertIsNone(cfg.rope_scaling)
|
||||
|
||||
def test_recurses_into_text_config(self):
|
||||
text_cfg = PretrainedConfig()
|
||||
text_cfg.rope_scaling = {"rope_type": "yarn", "factor": 4.0}
|
||||
cfg = PretrainedConfig()
|
||||
cfg.text_config = text_cfg
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertEqual(text_cfg.rope_scaling["type"], "yarn")
|
||||
|
||||
def test_recurses_into_llm_config(self):
|
||||
llm_cfg = PretrainedConfig()
|
||||
llm_cfg.rope_scaling = {"rope_type": "dynamic", "factor": 2.0}
|
||||
cfg = PretrainedConfig()
|
||||
cfg.llm_config = llm_cfg
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertEqual(llm_cfg.rope_scaling["type"], "dynamic")
|
||||
|
||||
def test_no_crash_on_non_dict_rope_scaling(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_scaling = "not_a_dict"
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertEqual(cfg.rope_scaling, "not_a_dict")
|
||||
|
||||
def test_no_crash_on_dict_without_rope_type(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_scaling = {"factor": 4.0}
|
||||
normalize_rope_scaling_compat(cfg)
|
||||
self.assertNotIn("type", cfg.rope_scaling)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_rope_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetRopeConfig(unittest.TestCase):
|
||||
def test_v5_rope_parameters(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.rope_parameters = {"rope_theta": 10000.0, "rope_type": "default"}
|
||||
theta, params = get_rope_config(cfg)
|
||||
self.assertEqual(theta, 10000.0)
|
||||
self.assertIs(params, cfg.rope_parameters)
|
||||
|
||||
def test_v4_fallback_remote_code_config(self):
|
||||
# Remote-code configs (SimpleNamespace) lack the v5 rope_parameters property
|
||||
cfg = SimpleNamespace(
|
||||
rope_theta=500000.0,
|
||||
rope_scaling={"type": "llama3", "factor": 8.0},
|
||||
)
|
||||
theta, params = get_rope_config(cfg)
|
||||
self.assertEqual(theta, 500000.0)
|
||||
self.assertEqual(params, {"type": "llama3", "factor": 8.0})
|
||||
|
||||
def test_v4_no_scaling(self):
|
||||
cfg = SimpleNamespace(rope_theta=10000.0)
|
||||
theta, params = get_rope_config(cfg)
|
||||
self.assertEqual(theta, 10000.0)
|
||||
self.assertIsNone(params)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _patch_text_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPatchTextConfig(unittest.TestCase):
|
||||
def test_propagates_parent_to_text(self):
|
||||
parent = PretrainedConfig()
|
||||
parent.pad_token_id = 0
|
||||
parent.bos_token_id = 1
|
||||
parent.eos_token_id = 2
|
||||
parent.tie_word_embeddings = False
|
||||
|
||||
text = PretrainedConfig()
|
||||
text.num_attention_heads = 32
|
||||
|
||||
result = _patch_text_config(parent, text)
|
||||
self.assertEqual(result.pad_token_id, 0)
|
||||
self.assertEqual(result.bos_token_id, 1)
|
||||
self.assertEqual(result.eos_token_id, 2)
|
||||
self.assertIs(result, text)
|
||||
|
||||
def test_propagates_text_to_parent(self):
|
||||
parent = PretrainedConfig()
|
||||
text = PretrainedConfig()
|
||||
text.pad_token_id = 42
|
||||
|
||||
_patch_text_config(parent, text)
|
||||
self.assertEqual(parent.pad_token_id, 42)
|
||||
|
||||
def test_no_overwrite_when_both_have_attr(self):
|
||||
parent = PretrainedConfig()
|
||||
parent.pad_token_id = 0
|
||||
text = PretrainedConfig()
|
||||
text.pad_token_id = 99
|
||||
|
||||
_patch_text_config(parent, text)
|
||||
self.assertEqual(parent.pad_token_id, 0)
|
||||
self.assertEqual(text.pad_token_id, 99)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_context_length
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetContextLength(unittest.TestCase):
|
||||
def test_max_position_embeddings(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.max_position_embeddings = 4096
|
||||
self.assertEqual(get_context_length(cfg), 4096)
|
||||
|
||||
def test_max_sequence_length_takes_priority(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.max_sequence_length = 8192
|
||||
cfg.max_position_embeddings = 4096
|
||||
self.assertEqual(get_context_length(cfg), 8192)
|
||||
|
||||
def test_rope_scaling_factor(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.max_position_embeddings = 4096
|
||||
cfg.rope_scaling = {"factor": 4.0}
|
||||
self.assertEqual(get_context_length(cfg), 16384)
|
||||
|
||||
def test_rope_scaling_llama3_ignores_factor(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.max_position_embeddings = 131072
|
||||
cfg.rope_scaling = {"rope_type": "llama3", "factor": 8.0}
|
||||
self.assertEqual(get_context_length(cfg), 131072)
|
||||
|
||||
def test_original_max_position_embeddings_ignores_factor(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.max_position_embeddings = 131072
|
||||
cfg.rope_scaling = {
|
||||
"factor": 8.0,
|
||||
"original_max_position_embeddings": 8192,
|
||||
}
|
||||
self.assertEqual(get_context_length(cfg), 131072)
|
||||
|
||||
def test_default_when_no_keys(self):
|
||||
cfg = PretrainedConfig()
|
||||
self.assertEqual(get_context_length(cfg), 2048)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# check_gguf_file
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckGgufFile(unittest.TestCase):
|
||||
def test_gguf_suffix(self):
|
||||
with tempfile.NamedTemporaryFile(suffix=".gguf") as f:
|
||||
self.assertTrue(check_gguf_file(f.name))
|
||||
|
||||
def test_gguf_magic_header(self):
|
||||
with tempfile.NamedTemporaryFile(suffix=".bin") as f:
|
||||
f.write(b"GGUF" + b"\x00" * 100)
|
||||
f.flush()
|
||||
self.assertTrue(check_gguf_file(f.name))
|
||||
|
||||
def test_non_gguf_file(self):
|
||||
with tempfile.NamedTemporaryFile(suffix=".bin") as f:
|
||||
f.write(b"NOT_GGUF" + b"\x00" * 100)
|
||||
f.flush()
|
||||
self.assertFalse(check_gguf_file(f.name))
|
||||
|
||||
def test_nonexistent_file(self):
|
||||
self.assertFalse(check_gguf_file("/nonexistent/path/model.bin"))
|
||||
|
||||
def test_directory(self):
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
self.assertFalse(check_gguf_file(d))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _is_deepseek_ocr_model / _is_deepseek_ocr2_model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestDeepseekOcrDetection(unittest.TestCase):
|
||||
def test_ocr_model_detected(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.auto_map = {"AutoModel": "modeling_deepseekocr.DeepseekOCRForCausalLM"}
|
||||
self.assertTrue(_is_deepseek_ocr_model(cfg))
|
||||
|
||||
def test_ocr2_model_detected(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.auto_map = {"AutoModel": "modeling_deepseekocr2.DeepseekOCR2ForCausalLM"}
|
||||
self.assertTrue(_is_deepseek_ocr2_model(cfg))
|
||||
|
||||
def test_non_ocr_model(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.auto_map = {"AutoModel": "modeling_llama.LlamaForCausalLM"}
|
||||
self.assertFalse(_is_deepseek_ocr_model(cfg))
|
||||
self.assertFalse(_is_deepseek_ocr2_model(cfg))
|
||||
|
||||
def test_no_auto_map(self):
|
||||
cfg = PretrainedConfig()
|
||||
self.assertFalse(_is_deepseek_ocr_model(cfg))
|
||||
self.assertFalse(_is_deepseek_ocr2_model(cfg))
|
||||
|
||||
def test_empty_auto_map(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.auto_map = {}
|
||||
self.assertFalse(_is_deepseek_ocr_model(cfg))
|
||||
self.assertFalse(_is_deepseek_ocr2_model(cfg))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _override_v_head_dim_if_zero
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestOverrideVHeadDimIfZero(unittest.TestCase):
|
||||
def test_patches_zero_v_head_dim(self):
|
||||
text_cfg = SimpleNamespace(v_head_dim=0)
|
||||
cfg = PretrainedConfig()
|
||||
cfg.text_config = text_cfg
|
||||
_override_v_head_dim_if_zero(cfg)
|
||||
self.assertEqual(text_cfg.v_head_dim, 128)
|
||||
|
||||
def test_custom_patch_value(self):
|
||||
text_cfg = SimpleNamespace(v_head_dim=0)
|
||||
cfg = PretrainedConfig()
|
||||
cfg.text_config = text_cfg
|
||||
_override_v_head_dim_if_zero(cfg, patch=64)
|
||||
self.assertEqual(text_cfg.v_head_dim, 64)
|
||||
|
||||
def test_no_patch_when_nonzero(self):
|
||||
text_cfg = SimpleNamespace(v_head_dim=256)
|
||||
cfg = PretrainedConfig()
|
||||
cfg.text_config = text_cfg
|
||||
_override_v_head_dim_if_zero(cfg)
|
||||
self.assertEqual(text_cfg.v_head_dim, 256)
|
||||
|
||||
def test_dict_sub_config(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.text_config = {"v_head_dim": 0}
|
||||
_override_v_head_dim_if_zero(cfg)
|
||||
self.assertEqual(cfg.text_config["v_head_dim"], 128)
|
||||
|
||||
def test_no_sub_config(self):
|
||||
cfg = PretrainedConfig()
|
||||
_override_v_head_dim_if_zero(cfg) # should not raise
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# get_hf_text_config
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestGetHfTextConfig(unittest.TestCase):
|
||||
def test_returns_config_for_pure_text_model(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["LlamaForCausalLM"]
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, cfg)
|
||||
|
||||
def test_returns_text_config_for_multimodal(self):
|
||||
text_cfg = PretrainedConfig()
|
||||
text_cfg.num_attention_heads = 32
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["SomeVLMForCausalLM"]
|
||||
cfg.text_config = text_cfg
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, text_cfg)
|
||||
|
||||
def test_llm_config_priority_over_text_config(self):
|
||||
llm_cfg = PretrainedConfig()
|
||||
llm_cfg.num_attention_heads = 16
|
||||
text_cfg = PretrainedConfig()
|
||||
text_cfg.num_attention_heads = 32
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["SomeModel"]
|
||||
cfg.llm_config = llm_cfg
|
||||
cfg.text_config = text_cfg
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, llm_cfg)
|
||||
|
||||
def test_thinker_config_highest_priority(self):
|
||||
thinker_cfg = PretrainedConfig()
|
||||
thinker_cfg.num_attention_heads = 8
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["SomeModel"]
|
||||
cfg.thinker_config = thinker_cfg
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, thinker_cfg)
|
||||
|
||||
def test_thinker_config_with_text_sub_config(self):
|
||||
inner_text = PretrainedConfig()
|
||||
inner_text.num_attention_heads = 8
|
||||
thinker_cfg = PretrainedConfig()
|
||||
thinker_cfg.text_config = inner_text
|
||||
thinker_cfg.torch_dtype = "float16"
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["Qwen2OmniModel"]
|
||||
cfg.thinker_config = thinker_cfg
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, inner_text)
|
||||
self.assertEqual(inner_text.torch_dtype, "float16")
|
||||
|
||||
def test_converts_dict_sub_config(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["SomeModel"]
|
||||
cfg.text_config = {
|
||||
"num_attention_heads": 32,
|
||||
"hidden_size": 4096,
|
||||
}
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIsInstance(cfg.text_config, PretrainedConfig)
|
||||
self.assertEqual(result.num_attention_heads, 32)
|
||||
|
||||
def test_llava_returns_parent_config(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["LlavaForCausalLM"]
|
||||
text_cfg = PretrainedConfig()
|
||||
text_cfg.num_attention_heads = 32
|
||||
cfg.text_config = text_cfg
|
||||
result = get_hf_text_config(cfg)
|
||||
self.assertIs(result, cfg)
|
||||
|
||||
def test_calls_normalize_rope_scaling(self):
|
||||
cfg = PretrainedConfig()
|
||||
cfg.architectures = ["LlamaForCausalLM"]
|
||||
cfg.rope_scaling = {"rope_type": "llama3", "factor": 8.0}
|
||||
get_hf_text_config(cfg)
|
||||
self.assertIn("type", cfg.rope_scaling)
|
||||
self.assertEqual(cfg.rope_scaling["type"], "llama3")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# _fix_special_tokens_pattern
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestFixSpecialTokensPattern(unittest.TestCase):
|
||||
def test_fixes_cls_sep_with_missing_tokens(self):
|
||||
tok = SimpleNamespace(
|
||||
special_tokens_pattern="cls_sep",
|
||||
cls_token_id=None,
|
||||
sep_token_id=None,
|
||||
)
|
||||
_fix_special_tokens_pattern(tok)
|
||||
self.assertEqual(tok.special_tokens_pattern, "none")
|
||||
|
||||
def test_no_change_when_tokens_present(self):
|
||||
tok = SimpleNamespace(
|
||||
special_tokens_pattern="cls_sep",
|
||||
cls_token_id=101,
|
||||
sep_token_id=102,
|
||||
)
|
||||
_fix_special_tokens_pattern(tok)
|
||||
self.assertEqual(tok.special_tokens_pattern, "cls_sep")
|
||||
|
||||
def test_no_change_for_other_patterns(self):
|
||||
tok = SimpleNamespace(
|
||||
special_tokens_pattern="none",
|
||||
cls_token_id=None,
|
||||
sep_token_id=None,
|
||||
)
|
||||
_fix_special_tokens_pattern(tok)
|
||||
self.assertEqual(tok.special_tokens_pattern, "none")
|
||||
|
||||
def test_no_change_when_no_pattern(self):
|
||||
tok = SimpleNamespace(cls_token_id=None, sep_token_id=None)
|
||||
_fix_special_tokens_pattern(tok)
|
||||
self.assertFalse(hasattr(tok, "special_tokens_pattern"))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# __init__.py re-exports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestModuleReExports(unittest.TestCase):
|
||||
def test_all_public_symbols_importable(self):
|
||||
import sglang.srt.utils.hf_transformers as pkg
|
||||
|
||||
for name in pkg.__all__:
|
||||
self.assertTrue(
|
||||
hasattr(pkg, name),
|
||||
f"{name} listed in __all__ but not importable from package",
|
||||
)
|
||||
|
||||
def test_shim_module_exports_match(self):
|
||||
import sglang.srt.utils.hf_transformers as pkg
|
||||
import sglang.srt.utils.hf_transformers_utils as shim
|
||||
|
||||
for name in pkg.__all__:
|
||||
self.assertTrue(
|
||||
hasattr(shim, name),
|
||||
f"{name} not available through shim module hf_transformers_utils",
|
||||
)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compat: _patch_removed_symbols
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPatchRemovedSymbols(unittest.TestCase):
|
||||
def test_llama_flash_attention2_exists(self):
|
||||
from transformers.models.llama import modeling_llama
|
||||
|
||||
self.assertTrue(
|
||||
hasattr(modeling_llama, "LlamaFlashAttention2"),
|
||||
"LlamaFlashAttention2 should be patched onto modeling_llama",
|
||||
)
|
||||
|
||||
def test_is_flash_attn_greater_or_equal_2_10_callable(self):
|
||||
import transformers.utils as _u
|
||||
|
||||
self.assertTrue(
|
||||
hasattr(_u, "is_flash_attn_greater_or_equal_2_10"),
|
||||
"is_flash_attn_greater_or_equal_2_10 should be patched onto transformers.utils",
|
||||
)
|
||||
self.assertIsInstance(_u.is_flash_attn_greater_or_equal_2_10(), bool)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compat: _patch_rope_parameters_validation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPatchRopeParametersValidation(unittest.TestCase):
|
||||
def test_injects_rope_theta_into_rope_scaling(self):
|
||||
config_dict = {
|
||||
"model_type": "llama",
|
||||
"rope_theta": 500000.0,
|
||||
"max_position_embeddings": 131072,
|
||||
"rope_scaling": {
|
||||
"rope_type": "llama3",
|
||||
"factor": 8.0,
|
||||
"low_freq_factor": 1.0,
|
||||
"high_freq_factor": 4.0,
|
||||
"original_max_position_embeddings": 8192,
|
||||
},
|
||||
}
|
||||
config = PretrainedConfig.from_dict(config_dict)
|
||||
rope_params = getattr(config, "rope_parameters", None)
|
||||
if rope_params is not None:
|
||||
self.assertIn("rope_theta", rope_params)
|
||||
|
||||
def test_no_injection_when_rope_theta_already_in_scaling(self):
|
||||
config_dict = {
|
||||
"model_type": "llama",
|
||||
"rope_theta": 500000.0,
|
||||
"max_position_embeddings": 131072,
|
||||
"rope_scaling": {
|
||||
"rope_type": "llama3",
|
||||
"factor": 8.0,
|
||||
"rope_theta": 999.0,
|
||||
"low_freq_factor": 1.0,
|
||||
"high_freq_factor": 4.0,
|
||||
"original_max_position_embeddings": 8192,
|
||||
},
|
||||
}
|
||||
config = PretrainedConfig.from_dict(config_dict)
|
||||
rope_params = getattr(config, "rope_parameters", None)
|
||||
if rope_params is not None:
|
||||
self.assertEqual(rope_params["rope_theta"], 999.0)
|
||||
|
||||
def test_no_crash_without_rope_scaling(self):
|
||||
config_dict = {"model_type": "llama", "rope_theta": 10000.0}
|
||||
config = PretrainedConfig.from_dict(config_dict)
|
||||
self.assertIsNotNone(config)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compat: _ensure_clean_up_tokenization_compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestCleanUpTokenizationCompat(unittest.TestCase):
|
||||
def test_clean_up_tokenization_exists(self):
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
self.assertTrue(hasattr(PreTrainedTokenizerBase, "clean_up_tokenization"))
|
||||
|
||||
def test_clean_up_tokenization_callable(self):
|
||||
from transformers import PreTrainedTokenizerBase
|
||||
|
||||
self.assertTrue(callable(PreTrainedTokenizerBase.clean_up_tokenization))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compat: _ensure_is_torch_fx_available_compat
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestIsTorchFxAvailableCompat(unittest.TestCase):
|
||||
def test_is_torch_fx_available_exists(self):
|
||||
import transformers.utils.import_utils as _iu
|
||||
|
||||
self.assertTrue(hasattr(_iu, "is_torch_fx_available"))
|
||||
self.assertTrue(_iu.is_torch_fx_available())
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# compat: _patch_nemotron_h_pattern
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestPatchNemotronHPattern(unittest.TestCase):
|
||||
def test_pattern_to_list_skips_mlp_dash(self):
|
||||
try:
|
||||
from transformers.models.nemotron_h.configuration_nemotron_h import (
|
||||
NemotronHConfig,
|
||||
)
|
||||
|
||||
result = NemotronHConfig._pattern_to_list("M-*-")
|
||||
self.assertEqual(result, ["mamba", "attention"])
|
||||
except ImportError:
|
||||
self.skipTest("NemotronHConfig not available in this transformers version")
|
||||
|
||||
def test_pattern_to_list_standard_chars(self):
|
||||
try:
|
||||
from transformers.models.nemotron_h.configuration_nemotron_h import (
|
||||
NemotronHConfig,
|
||||
)
|
||||
|
||||
result = NemotronHConfig._pattern_to_list("ME*")
|
||||
self.assertEqual(result, ["mamba", "moe", "attention"])
|
||||
except ImportError:
|
||||
self.skipTest("NemotronHConfig not available in this transformers version")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -425,11 +425,13 @@ class TestInternVLUnderstandsImage(VLMInputTestBase, unittest.IsolatedAsyncioTes
|
||||
torch_dtype=torch.bfloat16,
|
||||
low_cpu_mem_usage=False,
|
||||
)
|
||||
except RuntimeError as e:
|
||||
if "meta" not in str(e):
|
||||
except (RuntimeError, AttributeError) as e:
|
||||
if isinstance(e, RuntimeError) and "meta" not in str(e):
|
||||
raise
|
||||
# Transformers v5 always uses meta tensors for init, which breaks
|
||||
# models calling .item() in __init__ (e.g. InternVL's drop_path_rate).
|
||||
# Transformers v5.5.3 may also raise AttributeError for remote-code
|
||||
# models missing new internal attributes (e.g. all_tied_weights_keys).
|
||||
# Fall back to from_config + manual weight loading.
|
||||
import gc
|
||||
import glob
|
||||
@@ -594,6 +596,13 @@ class TestMiniCPMVUnderstandsImage(VLMInputTestBase, unittest.IsolatedAsyncioTes
|
||||
cls.processor = AutoProcessor.from_pretrained(
|
||||
cls.model_path, trust_remote_code=True
|
||||
)
|
||||
# In transformers v5.5.3, AutoTokenizer may return TokenizersBackend
|
||||
# which lacks model-specific attributes (e.g. im_start_id for MiniCPM-V).
|
||||
# Replace with sglang's tokenizer which handles this via declared-class
|
||||
# fallback, then fix added tokens encoding.
|
||||
from sglang.srt.utils.hf_transformers import get_tokenizer
|
||||
|
||||
cls.processor.tokenizer = get_tokenizer(cls.model_path, trust_remote_code=True)
|
||||
_fix_added_tokens_encoding(cls.processor.tokenizer)
|
||||
cls._init_visual()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user