Repository navigation
Conversation
…kend convert_to_native_format passes vocab/merges/post_processor/padding/ truncation from tokenizer.json to classes with a custom __init__, but not the normalizer, pre-tokenizer or decoder. The class then rebuilds the backend with its own defaults, silently replacing whatever the checkpoint serialized - so a model_type qwen3_5 checkpoint whose tokenizer.json carries a custom normalizer (tencent/WeMM-Embedding-2B strips one trailing newline before NFC) loses it: every input keeps an extra newline token, which changes the last-position token an embedding model reads. Extract the three stages from the file like post_processor already is, and apply them in TokenizersBackend.__init__ AFTER the subclass has built the backend, so a stage serialized in tokenizer.json wins over the class default. A null stage in the file leaves the class default untouched, which keeps canonical checkpoints (whose files match the class defaults) byte-identical. Fixes huggingface#49578
|
Thank you for your contribution 🤗! CI Security Gate — waiting for maintainer approvalCI is approved automatically only for authors who already have a contribution merged in this repository. A maintainer will review this PR and approve CI to run on it. |
|
[For maintainers] Suggested jobs to run (before merge) run-slow: qwen3_5 |
|
Hello! Have you figured out which commit caused this change in behaviour? I'm quite curious if we can just fix at the root instead of patching the symptoms. It could've also been the upgrade to a newer cc @AdnanElAssadi (thanks for finding this regression, I wonder if it goes beyond just the WeMM model)
|
|
Hi Tom — I bisected it before writing the PR; the culprit is in our conversion layer, not Timeline (from the repo history):
Why I don't think there is a deeper root to fix at: the And to rule out the |
What does this PR do?
Fixes #49578.
Since 5.18, a checkpoint whose
config.jsonhas a Qwen3.5-familymodel_typeloads asQwen3_5Tokenizer, a class with a custom__init__that rebuilds the backend tokenizer from vocab/merges.convert_to_native_formatforwards vocab, merges, post_processor, padding and truncation fromtokenizer.jsonto that rebuild — but not the normalizer, pre-tokenizer or decoder. The class's own defaults then silently replace whatever the checkpoint serialized.For
tencent/WeMM-Embedding-2B(model_type: qwen3_5), the file's normalizer isSequence[Replace(Regex(\ \$\), ""), NFC]— it strips one trailing newline. The class default is plainNFC, so the Replace is lost and every input keeps an extra newline token:This is an embedding model that reads the last position, so the extra token changes every embedding. The same loss hits any other custom-
__init__class whose checkpoint customized one of the three stages.The fix mirrors the existing
post_processormechanism exactly:convert_to_native_formatextracts the three stages as Rust objects from the file, andTokenizersBackend.__init__applies them after the subclass has built the backend, so a stage serialized intokenizer.jsonwins over the class default. A null stage in the file leaves the class default untouched, keeping canonical checkpoints (whose files match the class defaults) byte-identical.Reproduction (verified locally)
Downloaded WeMM's
tokenizer.json+tokenizer_config.json, added a minimalconfig.jsonwithmodel_type: qwen3_5, thenAutoTokenizer.from_pretrained(local_dir):NFC()198(bug)Sequence[Replace, NFC]Middle newlines are unaffected (
tok("a b")keeps the newline token); loading the same files via the genericTokenizersBackendpath was and stays correct.Tests
test_tokenizer_json_normalizer_survives_the_class_rebuildintests/models/qwen3_5/test_tokenization_qwen3_5.py: builds a tiny synthetictokenizer.json(offline, byte-level BPE) whose normalizer strips one trailing newline, loads it throughQwen3_5Tokenizer.from_pretrained, and asserts both the structural preservation (file normalizer wins over the class default) and the behavior (trailing newline gone, middle newline kept). Red without the fix, green with it.tests/models/qwen3_5suite: 48 passed.