Skip to content

Commit b0f5a9f

Browse files
rebel-sbparksubin9
authored andcommitted
Route byte-level llama tokenizers to TokenizersBackend
Model type "llama" spans both SentencePiece (Llama-1/2) and byte-level (Llama-3 / tiktoken) tokenizers under one Hub tokenizer_class (LlamaTokenizerFast). In v5, LlamaTokenizer.__init__ unconditionally installs a Metaspace pre-tokenizer/decoder, which silently drops spaces for byte-level repos (see #45488), e.g. deepseek-ai/DeepSeek-R1-Distill-Llama-*. The existing MODEL_IDS_TO_TOKENIZERS_BACKEND allowlist only covers specific checkpoints (e.g. the 8B) and misses others (the 70B is still broken). Instead, for the small set of dual-scheme model types, inspect the serialized tokenizer.json: if it declares a ByteLevel pre_tokenizer/decoder, route to TokenizersBackend (which respects tokenizer.json). SentencePiece Llama-1/2 stays on LlamaTokenizer unchanged.
1 parent 74920ed commit b0f5a9f

2 files changed

Lines changed: 88 additions & 0 deletions

File tree

src/transformers/models/auto/tokenization_auto.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,48 @@
429429
"salesforce/instructblip-flan-t5-*",
430430
]
431431

432+
# Model types whose single Hub `tokenizer_class` spans BOTH SentencePiece (older) and byte-level
433+
# (newer) tokenizers. For these, the declared class alone is ambiguous, so we must look at the
434+
# serialized tokenizer.json to decide. Kept intentionally small — most model types are consistent.
435+
_DUAL_SCHEME_MODEL_TYPES = {"llama"}
436+
437+
438+
def _tokenizer_json_is_byte_level(pretrained_model_name_or_path, **kwargs) -> bool:
439+
"""True if the repo's tokenizer.json declares a ByteLevel pre_tokenizer/decoder (GPT-2 / tiktoken
440+
style, e.g. Llama-3), as opposed to SentencePiece/Metaspace (Llama-1/2). Best-effort; returns
441+
False on any error so callers fall back to the normal resolution."""
442+
try:
443+
_pass = {
444+
k: kwargs[k]
445+
for k in ("cache_dir", "force_download", "proxies", "token", "revision", "local_files_only", "subfolder")
446+
if k in kwargs
447+
}
448+
resolved = cached_file(
449+
pretrained_model_name_or_path,
450+
"tokenizer.json",
451+
_raise_exceptions_for_gated_repo=False,
452+
_raise_exceptions_for_missing_entries=False,
453+
_raise_exceptions_for_connection_errors=False,
454+
**_pass,
455+
)
456+
if resolved is None:
457+
return False
458+
with open(resolved, encoding="utf-8") as f:
459+
tok_json = json.load(f)
460+
461+
def _has_byte_level(node):
462+
if isinstance(node, dict):
463+
if node.get("type") == "ByteLevel":
464+
return True
465+
return any(_has_byte_level(v) for v in node.values())
466+
if isinstance(node, list):
467+
return any(_has_byte_level(v) for v in node)
468+
return False
469+
470+
return _has_byte_level(tok_json.get("pre_tokenizer")) or _has_byte_level(tok_json.get("decoder"))
471+
except Exception:
472+
return False
473+
432474

433475
def load_vocab(vocab_file):
434476
"""Loads a vocabulary file into a dictionary."""
@@ -766,6 +808,19 @@ def from_pretrained(
766808
else:
767809
tokenizer_auto_map = tokenizer_config["auto_map"].get("AutoTokenizer", None)
768810

811+
# Dual-scheme model types (e.g. `llama`): the Hub tokenizer_class (LlamaTokenizerFast) is
812+
# ambiguous because Llama-1/2 are SentencePiece while Llama-3 is byte-level. Forcing the
813+
# SentencePiece LlamaTokenizer (Metaspace) onto a byte-level tokenizer.json silently drops
814+
# spaces (see #45488). If tokenizer.json is byte-level, respect it via TokenizersBackend;
815+
# SentencePiece Llama-1/2 (no ByteLevel in tokenizer.json) stays on LlamaTokenizer unchanged.
816+
if (
817+
tokenizer_auto_map is None
818+
and TokenizersBackend is not None
819+
and config_model_type in _DUAL_SCHEME_MODEL_TYPES
820+
and _tokenizer_json_is_byte_level(pretrained_model_name_or_path, **kwargs)
821+
):
822+
return TokenizersBackend.from_pretrained(pretrained_model_name_or_path, *inputs, **kwargs)
823+
769824
# Some specific checkpoints need TokenizersBackend because their config on the Hub really needs to be updated.
770825
_config_name_or_path = (
771826
name.lower() if isinstance((name := getattr(config, "_name_or_path", None)), str) else ""

tests/models/auto/test_tokenization_auto.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,39 @@ class AutoTokenizerTest(unittest.TestCase):
8181
def setUp(self):
8282
transformers.dynamic_module_utils.TIME_OUT_REMOTE_CODE = 0
8383

84+
def test_tokenizer_json_is_byte_level(self):
85+
from transformers.models.auto.tokenization_auto import _tokenizer_json_is_byte_level
86+
87+
cases = {
88+
# byte-level (GPT-2 / tiktoken style, e.g. Llama-3): detected -> True
89+
"byte_level_flat": ({"pre_tokenizer": {"type": "ByteLevel"}, "decoder": {"type": "ByteLevel"}}, True),
90+
# byte-level nested inside a Sequence pre_tokenizer (real Llama-3 layout)
91+
"byte_level_seq": (
92+
{
93+
"pre_tokenizer": {"type": "Sequence", "pretokenizers": [{"type": "Split"}, {"type": "ByteLevel"}]},
94+
"decoder": {"type": "ByteLevel"},
95+
},
96+
True,
97+
),
98+
# SentencePiece / Metaspace (Llama-1/2): not byte-level -> False
99+
"metaspace": (
100+
{
101+
"pre_tokenizer": {"type": "Metaspace", "replacement": "▁"},
102+
"decoder": {"type": "Metaspace", "replacement": "▁"},
103+
},
104+
False,
105+
),
106+
}
107+
for name, (tok_json, expected) in cases.items():
108+
with tempfile.TemporaryDirectory() as tmp_dir:
109+
with open(os.path.join(tmp_dir, "tokenizer.json"), "w", encoding="utf-8") as f:
110+
json.dump(tok_json, f)
111+
self.assertEqual(_tokenizer_json_is_byte_level(tmp_dir), expected, msg=name)
112+
113+
# No tokenizer.json available -> best-effort False (falls back to normal resolution).
114+
with tempfile.TemporaryDirectory() as tmp_dir:
115+
self.assertFalse(_tokenizer_json_is_byte_level(tmp_dir))
116+
84117
@slow
85118
def test_tokenizer_from_pretrained(self):
86119
for model_name in ("google-bert/bert-base-uncased", "google-bert/bert-base-cased"):

0 commit comments

Comments
 (0)