94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
614 lines
24 KiB
Python
614 lines
24 KiB
Python
# 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 ..hf_transformers_patches import _ensure_gguf_version
|
|
from .common import (
|
|
_resolve_local_or_cached_file,
|
|
attach_additional_stop_token_ids,
|
|
check_gguf_file,
|
|
resolve_runai_obj_uri,
|
|
)
|
|
from .mistral_utils import (
|
|
_MISTRAL_TOKENIZER_REDIRECTS,
|
|
is_bare_tekken_checkpoint,
|
|
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.debug(
|
|
"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
|
|
|
|
tokenizer_name = resolve_runai_obj_uri(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.debug(
|
|
"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.debug(
|
|
"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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
_fastokens_patched = False
|
|
|
|
|
|
def _ensure_fastokens_patched():
|
|
"""Monkey-patch transformers to use the fastokens backend (once)."""
|
|
global _fastokens_patched
|
|
if _fastokens_patched:
|
|
return
|
|
try:
|
|
import fastokens
|
|
except ImportError:
|
|
raise ImportError(
|
|
"The fastokens package is required when --tokenizer-backend=fastokens. "
|
|
"Install it with: pip install 'sglang[fastokens]'"
|
|
) from None
|
|
|
|
fastokens.patch_transformers()
|
|
_fastokens_patched = True
|
|
logger.info("fastokens backend enabled - transformers patched successfully")
|
|
|
|
|
|
def get_tokenizer(
|
|
tokenizer_name: str,
|
|
*args,
|
|
tokenizer_mode: str = "auto",
|
|
trust_remote_code: bool = False,
|
|
tokenizer_revision: Optional[str] = None,
|
|
tokenizer_backend: str = "huggingface",
|
|
**kwargs,
|
|
) -> Union[PreTrainedTokenizer, PreTrainedTokenizerFast]:
|
|
"""Gets a tokenizer for the given model name via Huggingface."""
|
|
# Tiktoken format has its own backend — no fastokens patching needed.
|
|
if tokenizer_name.endswith(".json"):
|
|
from sglang.srt.tokenizer.tiktoken_tokenizer import TiktokenTokenizer
|
|
|
|
return TiktokenTokenizer(tokenizer_name)
|
|
|
|
if tokenizer_backend == "fastokens":
|
|
_ensure_fastokens_patched()
|
|
|
|
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)
|
|
|
|
common_kwargs = dict(
|
|
trust_remote_code=trust_remote_code,
|
|
tokenizer_revision=tokenizer_revision,
|
|
clean_up_tokenization_spaces=False,
|
|
**kwargs,
|
|
)
|
|
|
|
try:
|
|
if is_bare_tekken_checkpoint(tokenizer_name, tokenizer_revision):
|
|
from transformers.tokenization_mistral_common import (
|
|
MistralCommonTokenizer,
|
|
)
|
|
|
|
logger.info(
|
|
"Detected bare-tekken checkpoint %s (tekken.json, no "
|
|
"tokenizer.json); loading via mistral-common MistralCommonTokenizer, "
|
|
"ignoring tokenizer_backend=%r.",
|
|
tokenizer_name,
|
|
tokenizer_backend,
|
|
)
|
|
|
|
tokenizer = MistralCommonTokenizer.from_pretrained(
|
|
tokenizer_name, revision=tokenizer_revision
|
|
)
|
|
else:
|
|
tokenizer = _auto_tokenizer_from_pretrained(
|
|
tokenizer_name, *args, **common_kwargs
|
|
)
|
|
|
|
# With fastokens, the patched TokenizersBackend.from_pretrained already
|
|
# returned a tokenizer whose backend is a fastokens shim. Re-resolving via
|
|
# the declared class (e.g. Qwen2Tokenizer) would discard that work.
|
|
if (
|
|
type(tokenizer).__name__ == _TOKENIZERS_BACKEND
|
|
and tokenizer_backend != "fastokens"
|
|
):
|
|
tokenizer = _resolve_tokenizers_backend(
|
|
tokenizer_name, *args, **common_kwargs
|
|
)
|
|
|
|
return _apply_post_load_fixes(tokenizer, tokenizer_name, tokenizer_revision)
|
|
except Exception as e:
|
|
if tokenizer_backend == "fastokens":
|
|
raise RuntimeError(
|
|
f"fastokens failed to load tokenizer for {tokenizer_name!r}. "
|
|
f"This model's tokenizer may not be supported by fastokens — "
|
|
f"see https://github.com/crusoecloud/fastokens. "
|
|
f"Re-run without --tokenizer-backend=fastokens to use the default backend."
|
|
) from e
|
|
raise
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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],
|
|
)
|