IndicPrayog-tokenizer-64k
A 64,000-vocabulary Unigram (SentencePiece-style) tokenizer for Hindi, Marathi, and English, selected out of a 6-way ablation and validated with a proxy-LM training run (bits-per-byte on held-out text) — not just intrinsic fertility metrics.
- Algorithm: Unigram (SentencePiece),
Metaspacepre-tokenization - Vocab size: 64,000
- Normalization: NFC (canonical Devanagari matra composition)
- Special tokens:
<pad>(0),<eos>(1),<bos>(2),<unk>(3) - Training mix: 40% Hindi / 35% English / 25% Marathi (
mix_A) - Selected by: proxy-LM bits-per-byte (BPB) on held-out hi/mr/en text — the winner disagreement rule between intrinsic ranking and BPB never fired (same variant won both).
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("prashantcp8/IndicPrayog-tokenizer-64k")
tok.encode("यह एक परीक्षण वाक्य है।")
# [84, 39, 6019, 7010, 26]
# -> ['▁यह', '▁एक', '▁परीक्षण', '▁वाक्य', '▁है।'] (5 tokens for a 5-word sentence, fertility 1.0)
vs. Sarvam-1 (the Indic gold standard we benchmark against, 68,096 vocab — 4K larger than ours) on the exact same sentence:
from transformers import AutoTokenizer
sarvam_tok = AutoTokenizer.from_pretrained("sarvamai/sarvam-1")
sarvam_tok.encode("यह एक परीक्षण वाक्य है।", add_special_tokens=False)
# [4999, 4551, 19566, 64104, 4432, 67546]
# -> ['▁यह', '▁एक', '▁परीक्षण', '▁वाक्य', '▁है', '।'] (6 tokens — splits "है।" into 2 pieces)
Same sentence: 5 tokens (ours) vs 6 tokens (Sarvam-1) — ours keeps the sentence-final
है। (word + punctuation) as one piece, Sarvam-1 splits it. This is a single illustrative
example, not the benchmark; see the full FLORES-200 comparison table below.
Note: intended final home is
AxisQuant/IndicPrayog-tokenizer-64k, but the cached HF token used for this session is scoped to thegemma-challengeorg and the personalprashantcp8namespace only — it lacksrepo.writeonAxisQuant. Published here (private) for now; transfer toAxisQuantonce org write access is granted.
Methodology
We trained 6 tokenizer variants (A–F) on an identical 1.5 GB byte-budgeted corpus (hi 600 MB / en 525 MB / mr 375 MB, streamed from Sangraha-verified / FineWeb-Edu), varying algorithm, language mix, and pre-tokenization — to isolate which design choices actually help, rather than shipping a single default run.
| ID | Algorithm | Language mix (hi/en/mr) | Pre-tokenization | Hypothesis tested |
|---|---|---|---|---|
| A | BPE | 40/35/25 | ByteLevel + digit-split | Balanced baseline |
| B | BPE | 33/33/34 | ByteLevel + digit-split | Does equal balance help Marathi enough to justify Hindi's loss? |
| C | BPE | 45/35/20 (=raw training mix) | ByteLevel + digit-split | Control: cost of naive (unbalanced) mixing |
| D | Unigram | 40/35/25 | SentencePiece Metaspace | Literature says Unigram beats BPE on morphologically rich languages — winner |
| E | BPE | 40/35/25 | ByteLevel + UnicodeScripts (script-boundary split) |
Does script-aware splitting reduce conjunct fragmentation? |
| F | BPE | 40/35/25 | ByteLevel, no digit-split | Control: cost/benefit of digit-splitting |
Evaluation
Primary: FLORES-200 devtest (1,012 parallel sentences, hin_Deva/mar_Deva/eng_Latn)
— parallel text makes cross-language comparison apples-to-apples (Petrov et al. 2023
methodology). Secondary: 2,000 held-out docs/language from Sangraha-verified / FineWeb-Edu.
Six metrics: fertility, compression (bytes/token), parity (Petrov-style fairness score), round-trip fidelity, Devanagari integrity (% of aksharas/conjuncts kept whole), vocab utilization.
Intrinsic results (6 ours + 8 public baselines)
| Tokenizer | Group | Vocab | FLORES fert hi/mr/en | Parity hi/en | Parity mr/en | Round-trip | Dev.Integrity | VocabUtil |
|---|---|---|---|---|---|---|---|---|
| ours_A | ours | 64,000 | 1.24/1.54/1.24 | 1.17 | 1.08 | 100.0% | 93.2% | 85.7% |
| ours_B | ours | 64,000 | 1.26/1.51/1.25 | 1.18 | 1.05 | 100.0% | 92.7% | 86.5% |
| ours_C | ours | 64,000 | 1.24/1.56/1.24 | 1.17 | 1.09 | 100.0% | 93.3% | 85.3% |
| ours_D (winner) | ours | 64,000 | 1.27/1.54/1.23 | 1.21 | 1.09 | 100.0% | 96.0% | 75.2% |
| ours_E | ours | 64,000 | 1.38/1.69/1.39 | 1.16 | 1.06 | 100.0% | 94.4% | 84.0% |
| ours_F | ours | 64,000 | 1.22/1.51/1.21 | 1.18 | 1.08 | 100.0% | 93.2% | 86.2% |
| baseline_sarvam-1 | baseline | 68,096 | 1.40/1.77/1.43 | 1.14 | 1.07 | 100.0% | 95.6% | 35.0% |
| baseline_sarvam-30b | baseline | 262,144 | 1.39/1.99/1.24 | 1.31 | 1.40 | 100.0% | 94.7% | 18.5% |
| baseline_gemma-2 | baseline | 256,000 | 1.96/3.20/1.23 | 1.86 | 2.26 | 100.0% | 81.0% | 15.2% |
| baseline_qwen2.5 | baseline | 151,643 | 4.76/6.69/1.26 | 4.42 | 4.62 | 100.0% | 40.5% | 20.8% |
| baseline_mistral-v0.3 | baseline | 32,768 | 5.33/7.34/1.37 | 4.55 | 4.66 | 100.0% | 39.9% | 57.0% |
| baseline_gpt2 | baseline | 50,257 | 7.82/11.16/1.23 | 7.41 | 7.86 | 100.0% | 39.9% | 60.5% |
| baseline_indicbert-v2 | baseline | 250,000 | 1.24/1.49/1.24 | 1.17 | 1.05 | 0.1% | 87.3% | 21.9% |
We beat every non-Indic baseline (Gemma-2, Qwen2.5, Mistral, GPT-2) on Hindi/Marathi
fertility and parity by a wide margin, and land within ~10% of Sarvam-1 (the Indic gold
standard near our vocab size) while using a 4K-smaller vocabulary. Llama-3.1 was dropped
(gated repo, license not token-bypassable). IndicBERTv2's 0.1% round-trip is expected
BERT-tokenizer (WordPiece, lossy by design) behavior, not a defect.
Shortlist (top-3 by mean FLORES fertility, gated on 100% round-trip AND Devanagari integrity ≥ variant A): A, C, D. (B and F were rejected on the integrity gate.)
Proxy-LM validation (the real test)
Intrinsic metrics are proxies. To answer "which tokenizer makes a model learn best," each shortlisted candidate trained an identical ~28M-param Llama-style dense LM from scratch (hidden=320, 6 layers, 5 heads, RoPE, SwiGLU, tied embeddings) on an identical 300M-token slice (same source documents, tokenized per-candidate), 3000 steps, same seed, then measured bits-per-byte (BPB) on held-out hi/en/mr text — BPB normalizes by raw bytes, not token count, making it the fair cross-tokenizer comparison metric.
| Variant | mean BPB | hi | en | mr |
|---|---|---|---|---|
| D (winner) | 1.0404 | 0.7229 | 1.6664 | 0.7320 |
| A | 1.0671 | 0.7324 | 1.7201 | 0.7489 |
| C | 1.0686 | 0.7316 | 1.7226 | 0.7516 |
D wins on every language individually, not just on average — confirming the Unigram algorithm's advantage on morphologically rich Devanagari script carries through to actual downstream LM quality, matching the intrinsic-metric ranking (no override needed).
Bugs found and fixed during this ablation
Six real bugs were caught and fixed via direct verification (not just accepting first-pass numbers) before this tokenizer was selected:
add_prefix_space=Truespurious "Ġ" markers. ByteLevel injected a leading-space marker at every upstream pretokenizer split boundary, not just at string start — corrupting round-trip fidelity around digits/script transitions. Fixed withadd_prefix_space=False.UnicodeScripts()silently dropping whitespace-only pretokens. When a lone space pretoken (left behind afterDigits()split off a digit) reachedUnicodeScripts()in isolation, it was dropped entirely, silently deleting a character. Fixed by runningUnicodeScripts()beforeDigits()in the pretokenizerSequence.- ByteLevel's word-boundary regex splits Devanagari at every matra (the big one).
ByteLevel's internal regex (
use_regex=True, default) groups only Unicode category Letter into a "word" — Devanagari matras are category Mark, not Letter — so every Devanagari syllable was pre-split at matra boundaries (भ|ा|रत instead of भारत), making it structurally impossible for BPE to ever merge across a matra. This alone caused ~3.3–5 tokens/word Hindi/Marathi fertility and 40.6% Devanagari integrity. Fixed with a customSplitregex that groups Letter+Mark runs, combined withByteLevel(use_regex=False). - Space-isolation regression (introduced by the fix above). The first fix regex
isolated every space as its own pretoken (unlike GPT-2's convention of attaching a
leading space to the following word), so BPE could never merge a space into a word —
nearly doubling English fertility (1.26→2.14). Fixed by attaching a leading space to the
following word/digit run:
" ?[^\s\d]+| ?\d+|\s+". - Eval script's non-NFC-aware round-trip check. All "ours_*" variants showed 96.9%
round-trip despite direct spot-checks showing 100%. Root cause: our tokenizer applies
NFC normalization by design, so
decode(encode(s))legitimately returnsNFC(s), which can differ as a raw string from non-canonically-composed FLORES source text while being visually/semantically identical. Fixed the eval script to NFC-normalize both sides before comparing — the correct definition of "lossless" for an NFC-normalizing tokenizer. - Proxy-LM tied-embedding weight-init bug.
nn.Embedding's PyTorch default init isN(0, 1); since the LM head is tied to the embedding, that same std≈1.0 matrix produced logits directly, giving step-0 loss ~307–309 nats instead of the correct ~ln(64000)≈11.07 for a freshly initialized 64K-vocab LM — wasting most of training recovering from a broken init rather than learning real signal, and producing an unreliable first BPB comparison. Fixed with GPT-2/Llama-styleN(0, 0.02)init applied to allLinear/Embeddingweights. Re-running with the fix confirmed step-0 loss ≈11.1 for all three candidates and changed nothing about the final ranking (D still wins), but the corrected numbers above are the ones that should be trusted.
Known limitation
tokenizers==0.22.2's UnigramTrainer(byte_fallback=True) silently ignores the
byte_fallback kwarg (Ignored unknown kwargs option byte_fallback) — so <unk> is not
structurally impossible for this Unigram tokenizer as it would be for the BPE variants
(A/B/C/E/F use ByteLevel byte-fallback, which IS structurally lossless). In practice,
round-trip fidelity measured 100.0% on the full FLORES-200 eval set, so OOV characters were
not encountered in testing.
Reproducibility
Full corpus-sampling, training, eval, shortlist, and proxy-LM scripts are included in
AxisQuant/IndicPrayog-tokenizer-ablations (all 6 variants + full results), together with
the original experiment plan. Fixed seed (1234) throughout; corpus streamed from
ai4bharat/sangraha and HuggingFaceFW/fineweb-edu; eval on haoranxu/FLORES-200.
The proxy-LM checkpoint that produced the winning BPB result above is published (private)
at prashantcp8/IndicPrayog-proxy-lm-D.