Semantic Systems / Language / Glyphs
Tokenizer And Vocabulary Compression For Small Local .slm Models
Report summary
Tokenizer compression for small local models is not a simple “smaller is better” optimization. A smaller vocabulary can reduce embedding and output-head footprint, shrink tokenizer artifacts, and sometimes lower CPU-side tokenization cost. But the same change can also lengthen token sequences, which
Key topics
- Semantic Systems / Language / Glyphs
- Semantic Systems
- Language
- Glyphs
- AI
- Python
- Runtime
- Rust
- Research Archive
Research provenance
For citation, use the report title and canonical URL. Archival presence does not establish authorship or promote report statements into portfolio evidence.
Source availability: 28 citation markers in the source export have no recoverable source links. Those markers are omitted from this reader; any supplied bibliography and ordinary links remain. Check the original sources before relying on the cited claims.
This page renders the archived Markdown as safe, formatted HTML. It is background research and does not become a portfolio claim without evidence review.
Full report
On this page
The core tradeoff
Tokenizer compression for small local models is not a simple “smaller is better” optimization. A smaller vocabulary can reduce embedding and output-head footprint, shrink tokenizer artifacts, and sometimes lower CPU-side tokenization cost. But the same change can also lengthen token sequences, which directly reduces effective context, increases prompt-processing work, and grows KV-cache memory because caches scale with the number of past tokens rather than the number of original characters or bytes. Recent work is explicit that tokenizer size, pre-tokenization regex, and tokenizer training data can materially affect generation speed, effective context size, memory usage, and downstream performance, and that tokenizer quality is not explained by compression alone.
That tension is especially sharp for small local .slm deployments. Small models are more sensitive to every wasted token, every malformed role marker, and every broken delimiter because they have less representational slack than larger models. The evidence from controlled ablations is strong: tokenizer choice can significantly affect downstream quality and training cost, while common intrinsic metrics such as fertility and parity are useful but incomplete predictors. In multilingual settings, poor vocabulary allocation can be expensive enough to degrade performance and increase token-related cost substantially.
The practical implication is straightforward. For a small local model, vocabulary compression should be treated as a constrained optimization problem:
\[ \text{minimize memory and runtime} \quad \text{subject to preserving exact text interface behavior.} \]
The “text interface behavior” constraint includes code delimiters, JSON punctuation, casing, chat role markers, file extensions such as .slm, URLs, hashes, manifest keys, and multilingual coverage. If those are not explicitly protected, tokenizer compression often saves memory while silently breaking the model’s most important use cases.
What the major tokenizer families optimize and where they fail
BPE, WordPiece, Unigram, byte fallback, and byte-level tokenizers each solve a different version of the same problem: representing arbitrary text with a finite vocabulary while keeping sequences short enough for transformers to handle. BPE greedily merges the most frequent adjacent units; GPT-2’s byte-level BPE used bytes as the base alphabet so that any Unicode string remained representable, avoiding a huge Unicode-base vocabulary. The GPT-2 paper also explains why naïvely merging raw bytes is not enough: without additional constraints, BPE spends vocabulary on punctuation-attached variants, so GPT-2 prevented merges across character categories except spaces.
WordPiece is similar in spirit but differs at inference time: it uses greedy longest-match-first tokenization, historically known as MaxMatch. Efficient linear-time WordPiece implementations now exist, but the key operational property remains the same: once the vocabulary is fixed, inference tries to consume the longest legal piece at each step. That can be attractive for small runtimes because behavior is deterministic and mature, but fragmentation around punctuation-heavy or identifier-heavy text still depends heavily on the vocabulary and pre-tokenization rules.
Unigram tokenization, most commonly used through SentencePiece, starts from a large candidate inventory and prunes symbols whose removal least harms the corpus likelihood. SentencePiece’s design matters for local .slm models because it trains directly from raw sentences, supports language-independent processing, and provides a self-contained model file that includes normalization, vocabulary mapping, and segmentation logic. That makes deployment easier and reduces runtime drift across environments.
Byte fallback and byte-level tokenization solve different failure modes. Byte fallback means “use subwords when possible, but fall back to raw UTF-8 bytes for uncovered characters instead of emitting <unk>.” The Hugging Face Llama tokenizer docs explicitly describe Llama-style tokenization as byte-level BPE with ByteFallback and no normalization. By contrast, purely byte-level models like ByT5 eliminate tokenizer complexity almost entirely, but they pay for it with longer sequences; the ByT5 paper states the central cost clearly: byte or character sequences are longer than token sequences. For small local models, that means byte-level designs improve coverage and exactness, but they usually lose on context efficiency unless the model and inference stack are built around them.
A useful mental model is this. BPE and WordPiece are compression-first subword schemes. Unigram is likelihood-first vocabulary selection. Byte fallback is a coverage safety net. Byte-level tokenization is an interface-simplicity choice that shifts complexity from the tokenizer into the model and runtime. For .slm models that must preserve exact strings locally, the safest compression-oriented baseline is usually either SentencePiece BPE/Unigram with explicit protected spans, or Llama-style BPE with byte fallback.
Concrete pruning algorithms and tokenizer distillation
The safest way to shrink a tokenizer is not to “delete random low-frequency tokens,” because tokenizers are structural objects, not flat dictionaries. Pre-tokenization defines the upper bound on where tokens can form, the model defines how subwords are segmented, and post-processing defines how BOS/EOS and other control tokens are added. Pruning must therefore preserve structural invariants while optimizing for deployment goals.
A practical pruning recipe for small local .slm models is to train an overcomplete tokenizer first, then prune to a deployment vocabulary. I recommend computing the following per-token statistics on a calibration corpus that matches the intended use cases: token frequency, token-savings utility, domain coverage, and protected-span risk. For Unigram tokenizers, you can also estimate exact removal cost via increase in corpus negative log-likelihood; for BPE/WordPiece, a good proxy is the number of extra tokens introduced if the token is replaced by its decomposition. This synthesis follows directly from what the literature shows matters: vocabulary construction, pre-tokenization, domain alignment, and exact downstream behavior.
Input:
V0 = overcomplete tokenizer vocabulary
C = calibration corpus
P = protected tokens/spans/special tokens
D = domain shards with weights wd
Target:
V* = pruned vocabulary of size K
For each token t in V0:
F(t) = count of t on C
U(t) = average extra token count if t is removed
= E[len(reencode_without_t(x)) - len(reencode_with_t(x)) | x contains t]
G(t) = domain coverage score = Σd wd * min(1, count_d(t) > 0)
R(t) = protected risk score
= 1 if t in P
= high if t appears in JSON/code/URL/hash/file-name spans
If tokenizer is Unigram:
L(t) = ΔNLL on C when t is removed
Else:
L(t) = normalized U(t)
Keep all tokens with R(t) = protected.
For the rest, rank by:
Score(t) = a·log(1+F(t)) + b·U(t) + c·G(t) + d·L(t)
Then enforce quotas:
- minimum per language/script/domain
- minimum punctuation/control coverage
- maximum protected-span fragmentation
Rebuild tokenizer + remap embeddings, then rerun validation.
This recipe gives four concrete pruning modes. Frequency pruning is the simplest and should only remove tokens that are both rare and absent from protected spans. Merge-utility pruning is better for BPE because it prunes tokens whose removal introduces little sequence inflation. Entropy or likelihood pruning is the right fit for Unigram because Unigram is already a probability model over segmentations. Domain-coverage pruning is mandatory whenever the model must handle multiple languages or specialized domains, because research consistently shows that English-centric or misallocated vocabularies are a poor compression bargain outside their training distribution.
For tokenizer distillation, there are two distinct cases. If teacher and student share the same tokenizer, ordinary logits or hidden-state distillation is enough. If the student uses a smaller or different tokenizer, you need cross-tokenizer alignment. The recent ALM method is the cleanest reference point: it tokenizes the same raw text with teacher and student, aligns chunks that encode the same bytes, and matches chunk likelihoods approximately rather than pretending token IDs are directly comparable. That matters because sequence alignment and vocabulary alignment across mainstream tokenizers can be very poor; one recent analysis reports sequence alignment rates ranging roughly from 30% to 90% and vocabulary alignment rates from about 10% to 90% across common LLM tokenizers.
For a small local .slm student, the distillation loop should therefore look like this: choose the smaller tokenizer, initialize overlapping token embeddings by identical string match where possible, distill on a calibration corpus that overweights JSON, code, manifests, URLs, file names, and multilingual shards, and compute the loss over byte-aligned chunks rather than raw token positions. The student tokenizer should not be accepted unless it passes the structured-text validation suite described later in this report, because perplexity alone is too weak a guardrail.
Preserving JSON, code, file names, URLs, hashes, and chat formatting
Pre-tokenization is one of the most overlooked sources of corruption. Hugging Face’s tokenizer pipeline documentation states that pre-tokenization creates word-like splits that bound what tokens can exist later, and its own examples show punctuation being split out separately. That is exactly why aggressive punctuation splitting can damage .slm, package-lock.json, https://host/path?a=b, sha256:..., or code like foo.Bar<T>(): once the boundary is introduced early, later merges may not be able to reconstruct the exact span you care about.
The operational fix is to define protected span classes before training or pruning. At minimum, the calibration suite should extract and score these classes separately: JSON delimiters and quotes; code operators and paired punctuation; path and filename segments including extensions such as .slm; URLs and email addresses; hexadecimal and base64-like hashes; manifest keys and schema literals; chat control markers; and multilingual script examples. This is not speculative busywork. Classic production tokenizer work for mixed-script search already highlighted URLs, numbers, dates, names, email addresses, abbreviations, punctuation, and other special cases as areas that become harder when scripts mix.
For JSON and code, I recommend a two-layer defense. First, keep the primitive punctuation tokens stable and never prune them: {, }, [, ], :, ,, ", newline, backslash, slash, dot, underscore, hyphen, backtick, parentheses, brackets, braces, and common operators such as ::, ->, =>, ==, !=, <=, >=, &&, ||. Second, create a protected-span regex inventory so that filenames, URLs, hashes, and manifest literals are measured as first-class objects during pruning. In practice, this matters more than whether the base algorithm is BPE or Unigram. Current research increasingly emphasizes that tokenization effectiveness is not just “how many tokens,” but also what boundaries are learned and preserved.
Byte fallback is the right last-resort protection for exact string preservation. It guarantees that uncovered Unicode characters are still representable without <unk>, which is essential for manifests, checksums, foreign scripts, and rare codepoints. Llama-style tokenizers explicitly rely on ByteFallback; OpenAI’s tiktoken README makes the same broader point for BPE tokenizers: a good tokenizer should be reversible, lossless, and able to work on arbitrary text. If a compressed tokenizer cannot guarantee that property, it is a poor fit for .slm packaging or local structured generation.
Special tokens and chat templates must be treated as immutable API, not vocabulary candidates. Hugging Face’s docs are explicit that special tokens added by chat templates should not be duplicated, that apply_chat_template(tokenize=True) is usually safer, and that tokenizing a templated string later requires add_special_tokens=False to avoid BOS/EOS duplication. The same documentation also warns that missing generation prompts can make a model continue the user message rather than answer it, while continue_final_message=True is the correct mechanism for prefilling a partial assistant response such as a JSON prefix. Those details are directly relevant to prompt echo, malformed turns, and JSON starts that fail after tokenizer changes.
Measuring fragmentation, context inflation, KV cache growth, and speed
A compressed tokenizer should be evaluated with a metric set that distinguishes “shorter sequence” from “better interface.” The standard starting point is fertility, defined as tokens per whitespace-split word or document; higher fertility means weaker compression. Ali et al. provide a clean definition and also note that fertility and parity do not fully predict downstream quality. Rust et al. add another useful signal, proportion of continued words, meaning the share of words split into multiple subwords. More recently, STRR was proposed as single-token retention rate, the proportion of words preserved as single tokens, which complements fertility by exposing whole-word preservation rather than average compression alone.
For small local .slm models, I recommend the following fragmentation metrics as a minimum acceptance set. First, fertility. Second, continued-word rate. Third, STRR. Fourth, protected-span fragmentation, defined as average tokens per protected span class and exact-span atomicity rate. Fifth, byte-fallback rate, measured as the fraction of generated or encoded bytes handled through fallback rather than normal pieces. Sixth, multilingual parity on a held-out parallel corpus if the model is multilingual. These metrics jointly capture compression, fragmentation, and fairness better than any single number.
The runtime consequences are straightforward. Longer tokenized prompts reduce effective context length and increase KV-cache pressure because cache size grows with past token count; Hugging Face’s KV-cache documentation describes cache growth in terms of accumulated prior tokens and attention concatenation over past plus current key-values. Prompt-processing benchmarks in llama-bench are therefore the right way to quantify how much tokenizer compression helps or hurts prefill. Crucially, llama-bench does not include tokenization or sampling time, so encode/decode throughput must be benchmarked separately.
A practical derived quantity is context inflation:
\[ \text{ContextInflation} = \frac{\text{tokens under candidate tokenizer}}{\text{tokens under baseline tokenizer}} \]
If this ratio is 1.18 on your real workload, then a nominal 8k-token context behaves like about 6.8k baseline tokens, prompt prefill gets slower, and KV cache grows proportionally. Vocabulary shrinkage only wins if the saved embedding and output-head memory outweigh the token-count damage on your target corpus. That balance is workload-specific, and recent work from both Meta and academic benchmarks reinforces that the relevant axis is compression rate in bytes-per-token, not vocabulary size in isolation.
Hugging Face to Rust compatibility, corruption tests, prompt echo tests, and recommended .slm manifest metadata
Exact tokenizer compatibility between Python/Hugging Face tooling and a Rust runtime is possible, but only if the runtime reproduces the full pipeline: normalization, pre-tokenization, segmentation model, post-processing, decoder behavior, added-token behavior, and special-token IDs. Hugging Face’s tokenizers crate is itself implemented in Rust and models tokenization as exactly that pipeline. SentencePiece is similarly strong for portability because the .model file can contain normalization rules, vocabulary mapping, and segmentation behavior in one artifact. In practice, if you can embed a canonical tokenizer.json or SentencePiece .model and use a Rust runtime that consumes it directly, you minimize drift.
There are now Rust runtimes that explicitly target Hugging Face compatibility at higher performance. The IREE tokenizer bindings, for example, advertise full Hugging Face tokenizer.json and OpenAI tiktoken compatibility, support BPE, WordPiece, and Unigram, and expose streaming encode/decode for inference-oriented use. That is promising for local .slm runtimes, but it should still be treated as a compatibility claim that must be validated with a conformance harness, not assumed on faith.
The minimum corruption test suite should cover five families of failures. Round-trip exactness means decode(encode(x)) == x over a corpus containing Unicode, combining marks, code, JSON, URLs, hashes, filenames, and multilingual examples. Cross-runtime parity means Python/HF and Rust emit identical token IDs, offsets, special-token masks, and decoded text for the same golden corpus. Protected-span integrity means .slm, manifest.json, URLs, checksums, and JSON fragments do not exceed a configured fragmentation budget. Fallback coverage means unusual characters never collapse into <unk> when byte fallback is expected. Offset stability means offsets are monotonic and map back to the correct byte/character regions after normalization. These requirements follow directly from the tokenizer pipeline semantics and the deployment goal of exact local execution.
The minimum prompt-echo and chat-template suite should cover four cases. First, apply_chat_template(tokenize=True) versus tokenize=False followed by later tokenization, verifying that no duplicate BOS/EOS appears. Second, a standard user turn with add_generation_prompt=True, verifying that generation starts in assistant mode rather than continuing the user message. Third, a JSON-prefill case using continue_final_message=True, verifying that the model continues the partial JSON string rather than starting a new assistant turn. Fourth, a role-marker corruption test where special tokens are intentionally duplicated to ensure the harness catches the resulting failure. Hugging Face’s chat templating docs are unusually explicit about all four hazards.
For .slm manifests specifically, there is no universal standard, so the most defensible approach is to synthesize a manifest from two proven ideas: keep the tokenizer embedded or checksum-bound to the artifact, and store enough metadata to load the same tokenizer logic in every runtime. TinyRustLM’s .slm notes explicitly call for tokenizer inclusion or a verified sidecar, byte-level fallback support, bounded decode, and tokenizer drift checks. NVIDIA Megatron’s tokenizer metadata guidance shows a practical metadata surface: tokenizer library, tokenizer path, chat template, special-token configuration, and explicit reuse of a metadata JSON. Those are good anchors for a local .slm schema.
A recommended tokenizer section for a .slm manifest is below. The exact field names are a recommendation, but the content is the important part.
{
"tokenizer": {
"artifact_format": "tokenizer.json",
"artifact_sha256": "…",
"library": "huggingface-tokenizers",
"library_version": "0.23.1",
"algorithm": "bpe",
"byte_fallback": true,
"byte_level": false,
"normalizer": {
"name": "identity",
"spec_sha256": "…"
},
"pretokenizer": {
"name": "custom-regex-v3",
"pattern": "…",
"pattern_sha256": "…"
},
"model": {
"vocab_size": 16384,
"merges_sha256": "…",
"unk_token": "<unk>"
},
"decoder": {
"cleanup_spaces": false,
"skip_special_tokens_default": true
},
"special_tokens": {
"bos": { "id": 1, "text": "<s>" },
"eos": { "id": 2, "text": "</s>" },
"pad": { "id": 0, "text": "<pad>" },
"additional": [
{ "id": 32001, "text": "<|system|>" },
{ "id": 32002, "text": "<|user|>" },
{ "id": 32003, "text": "<|assistant|>" }
]
},
"added_tokens": [
{ "text": ".slm", "single_word": false, "normalized": false },
{ "text": "sha256:", "single_word": false, "normalized": false }
],
"chat_template": {
"template_sha256": "…",
"add_generation_prompt_default": true,
"continue_final_message_supported": true
},
"protected_span_classes": [
"json",
"code",
"filename",
"url",
"hash",
"manifest-key"
],
"training_fingerprint": {
"corpus_sha256": "…",
"languages": ["en", "…"],
"domains": ["chat", "code", "manifest", "json"]
},
"conformance": {
"golden_corpus_sha256": "…",
"python_hf_pass": true,
"rust_runtime_pass": true,
"roundtrip_pass": true,
"chat_template_pass": true
}
}
}
If I had to reduce this to a short deployment rule set for small local .slm models, it would be this: compress vocabulary only after measuring context inflation on real workload text; never prune special tokens or protected structured spans; prefer byte fallback over <unk> for local exactness; require Python/HF and Rust-runtime ID parity before shipping; and store tokenizer metadata inside the .slm package or behind a cryptographic checksum so that tokenizer drift is impossible to miss. That is the shortest path to getting real memory savings without sacrificing JSON correctness, code usability, filename preservation, or multilingual robustness.