Summary
Loading a checkpoint whose tokenizer.json uses a Unigram model (SentencePiece-style) but whose tokenizer_config.json sets tokenizer_class to the generic "PreTrainedTokenizerFast" crashes at load time with:
Tokenizers/BPETokenizer.swift:95: Fatal error: BPETokenizer requires merges
Root cause
swift-transformers' TokenizerModel.from(...) (Tokenizer.swift) 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 actual tokenizer.json is Unigram, there is no merges field, and BPETokenizer hits a fatalError instead of throwing.
This isn't specific to one checkpoint — it hits any model whose tokenizer was trained from scratch (custom vocab/algorithm) and saved via AutoTokenizer/PreTrainedTokenizerFast without a dedicated subclass, which is common for Japanese LLM projects that build their own SentencePiece vocab.
Confirmed affected checkpoints
| Repo |
tokenizer_class |
tokenizer.json model.type |
mlx-community/Tanuki-8B-dpo-v1.0-8bit |
PreTrainedTokenizerFast |
Unigram |
llm-jp/llm-jp-13b-v2.0 |
PreTrainedTokenizerFast |
Unigram |
llm-jp/llm-jp-3-13b |
PreTrainedTokenizerFast |
Unigram |
Models that inherit an existing architecture (Llama/GPT-NeoX/Qwen-based Japanese LLMs like rinna, ELYZA, Swallow, Sarashina, PLaMo) were not affected — they save a specific tokenizer_class (e.g. LlamaTokenizer) that swift-transformers already maps correctly.
Repro
./SwiftLM --model mlx-community/Tanuki-8B-dpo-v1.0-8bit --port 5413
Fails during tokenizer load, after the model weights finish downloading.
Proposed fix (implemented locally, not yet committed)
In Sources/SwiftLM/Server.swift, TransformersTokenizerLoader.load(from:) now inspects tokenizer.json's model.type before calling AutoTokenizer.from(modelFolder:). If model.type == "Unigram" and the checkpoint's tokenizer_class isn't one of swift-transformers' known 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 directory instead. This causes swift-transformers to select UnigramTokenizer, whose init(tokenizerConfig:tokenizerData:addedTokens:) doesn't depend on the class name itself.
Verified working end-to-end with mlx-community/Tanuki-8B-dpo-v1.0-8bit (Japanese generation succeeds, ~18.5 tok/s on M4 Pro 48GB).
Happy to open a PR with this change if useful. The proper long-term fix likely belongs upstream in huggingface/swift-transformers (have TokenizerModel.from consult tokenizer.json's model.type when tokenizer_class is a generic/unknown name), but this local workaround unblocks affected checkpoints in the meantime.
Summary
Loading a checkpoint whose
tokenizer.jsonuses a Unigram model (SentencePiece-style) but whosetokenizer_config.jsonsetstokenizer_classto the generic"PreTrainedTokenizerFast"crashes at load time with:Root cause
swift-transformers'TokenizerModel.from(...)(Tokenizer.swift) picks a concrete tokenizer implementation purely from thetokenizer_classname string (itsknownTokenizerstable) — it never inspectstokenizer.json's ownmodel.type. After stripping a"Fast"suffix,"PreTrainedTokenizerFast"becomes"PreTrainedTokenizer", which the table maps explicitly toBPETokenizer. When the actualtokenizer.jsonis Unigram, there is nomergesfield, andBPETokenizerhits afatalErrorinstead of throwing.This isn't specific to one checkpoint — it hits any model whose tokenizer was trained from scratch (custom vocab/algorithm) and saved via
AutoTokenizer/PreTrainedTokenizerFastwithout a dedicated subclass, which is common for Japanese LLM projects that build their own SentencePiece vocab.Confirmed affected checkpoints
mlx-community/Tanuki-8B-dpo-v1.0-8bitPreTrainedTokenizerFastUnigramllm-jp/llm-jp-13b-v2.0PreTrainedTokenizerFastUnigramllm-jp/llm-jp-3-13bPreTrainedTokenizerFastUnigramModels that inherit an existing architecture (Llama/GPT-NeoX/Qwen-based Japanese LLMs like rinna, ELYZA, Swallow, Sarashina, PLaMo) were not affected — they save a specific
tokenizer_class(e.g.LlamaTokenizer) that swift-transformers already maps correctly.Repro
Fails during tokenizer load, after the model weights finish downloading.
Proposed fix (implemented locally, not yet committed)
In
Sources/SwiftLM/Server.swift,TransformersTokenizerLoader.load(from:)now inspectstokenizer.json'smodel.typebefore callingAutoTokenizer.from(modelFolder:). Ifmodel.type == "Unigram"and the checkpoint'stokenizer_classisn't one of swift-transformers' known 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) withtokenizer_config.json'stokenizer_classrewritten to"XLMRobertaTokenizer", then loads from that directory instead. This causes swift-transformers to selectUnigramTokenizer, whoseinit(tokenizerConfig:tokenizerData:addedTokens:)doesn't depend on the class name itself.Verified working end-to-end with
mlx-community/Tanuki-8B-dpo-v1.0-8bit(Japanese generation succeeds, ~18.5 tok/s on M4 Pro 48GB).Happy to open a PR with this change if useful. The proper long-term fix likely belongs upstream in
huggingface/swift-transformers(haveTokenizerModel.fromconsulttokenizer.json'smodel.typewhentokenizer_classis a generic/unknown name), but this local workaround unblocks affected checkpoints in the meantime.