Skip to content

fix: Unigram tokenizer checkpoints crash with "BPETokenizer requires merges" - #158

Merged
solderzzc merged 1 commit into
mainfrom
fix/unigram-tokenizer-crash
Aug 26, 2026
Merged

fix: Unigram tokenizer checkpoints crash with "BPETokenizer requires merges"#158
solderzzc merged 1 commit into
mainfrom
fix/unigram-tokenizer-crash

Conversation

@solderzzc

Copy link
Copy Markdown
Member

Summary

Fixes #155. Reported and fully root-caused by @kei-Optim — this implements their local fix.

swift-transformers' TokenizerModel.from(...) picks a concrete tokenizer implementation purely from the tokenizer_class name string — it never inspects tokenizer.json's own model.type. After stripping a "Fast" suffix, "PreTrainedTokenizerFast" becomes "PreTrainedTokenizer", which maps explicitly to BPETokenizer. When the checkpoint's actual tokenizer.json is Unigram (SentencePiece-style, common for from-scratch vocabs — several Japanese LLM projects among them), there's no merges field and BPETokenizer hits a fatalError instead of throwing:

Tokenizers/BPETokenizer.swift:95: Fatal error: BPETokenizer requires merges

Confirmed affected: mlx-community/Tanuki-8B-dpo-v1.0-8bit, llm-jp/llm-jp-13b-v2.0, llm-jp/llm-jp-3-13b.

Fix

TransformersTokenizerLoader.load(from:) now inspects tokenizer.json's model.type before calling AutoTokenizer.from. If it's Unigram and tokenizer_class isn't already one of swift-transformers' Unigram-mapped names (XLMRobertaTokenizer, Xlm-RobertaTokenizer, T5Tokenizer), it builds a scratch copy of the checkpoint directory — original files symlinked, so the HF cache is untouched — with tokenizer_config.json's tokenizer_class rewritten to "XLMRobertaTokenizer", then loads from that instead. UnigramTokenizer's init doesn't depend on the class name, so the rewrite is otherwise inert.

The proper long-term fix belongs upstream in huggingface/swift-transformers (TokenizerModel.from should consult tokenizer.json's model.type when tokenizer_class is generic/unknown) — this is a local workaround in the meantime, same spirit as the reporter's own framing.

Test coverage

Adds a synthetic unigram-tokenizer fixture reproducing the exact shape — a real Unigram tokenizer.json built via the tokenizers library, saved with the generic tokenizer_class — so this has regression coverage without needing any of the affected multi-GB real checkpoints.

Red-green verified locally: reverting the Server.swift change reproduces the exact reported crash against the new fixture; restoring it, the fixture loads, prefills, and returns a well-formed completion.

Test plan

  • bash tests/test-fixtures.sh — 7/7 passed locally, including the new fixture.
  • Red-green: crash reproduces without the fix, resolved with it.
  • CI green.

…merges"

Fixes #155. Reported and fully root-caused by @kei-Optim, whose local
fix this implements.

swift-transformers' TokenizerModel.from(...) picks a concrete
tokenizer implementation purely from the tokenizer_class name string
(its knownTokenizers table) — it never inspects tokenizer.json's own
model.type. After stripping a "Fast" suffix, "PreTrainedTokenizerFast"
becomes "PreTrainedTokenizer", which the table maps explicitly to
BPETokenizer. When the checkpoint's actual tokenizer.json is Unigram
(SentencePiece-style — common for models trained from scratch with a
custom vocab, several Japanese LLM projects among them), there is no
merges field and BPETokenizer hits a fatalError instead of throwing.

Confirmed affected: mlx-community/Tanuki-8B-dpo-v1.0-8bit,
llm-jp/llm-jp-13b-v2.0, llm-jp/llm-jp-3-13b — all Unigram tokenizer.json
saved with the generic tokenizer_class "PreTrainedTokenizerFast".

Fix: TransformersTokenizerLoader.load(from:) now inspects
tokenizer.json's model.type before calling AutoTokenizer.from. If it's
Unigram and tokenizer_class isn't already one of the table's
Unigram-mapped names (XLMRobertaTokenizer, Xlm-RobertaTokenizer,
T5Tokenizer), it builds a scratch copy of the checkpoint directory
(original files referenced via symlink, so the HF cache is untouched)
with tokenizer_config.json's tokenizer_class rewritten to
"XLMRobertaTokenizer", then loads from that instead. UnigramTokenizer's
init(tokenizerConfig:tokenizerData:addedTokens:) doesn't depend on the
class name itself, so the rewrite is otherwise inert.

Adds a synthetic unigram-tokenizer fixture (scripts/make-test-fixtures.py,
tests/test-fixtures.sh) reproducing the exact shape — a real Unigram
tokenizer.json built via the tokenizers library, saved with the generic
tokenizer_class — so this has regression coverage without needing any
of the affected (multi-GB) real checkpoints.

Red-green verified: reverting the Server.swift change locally reproduces
the exact reported crash ("Tokenizers/BPETokenizer.swift:95: Fatal
error: BPETokenizer requires merges") against the new fixture; restoring
it, the fixture loads, prefills, and returns a well-formed completion.
@solderzzc
solderzzc merged commit 8705d3d into main Aug 26, 2026
14 checks passed
@solderzzc
solderzzc deleted the fix/unigram-tokenizer-crash branch August 26, 2026 06:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Unigram tokenizer checkpoints crash with "BPETokenizer requires merges" (tokenizer_class="PreTrainedTokenizerFast")

1 participant