Encode-only multilingual tokenizer efficiency on FLORES-200 devtest across 12 languages and 8 tokenizers.
Each tokenizer encodes the full FLORES-200 devtest split for a locked set of 12 languages (one per continent, plus English as reference). No decoding or model inference — only tokenization and metric aggregation.
| Metric | Description |
|---|---|
| CTC (Corpus Token Count) | Total number of tokens produced when encoding the whole language corpus (BOS/EOS excluded). The base count the other metrics derive from. |
| Fertility | Tokens per whitespace-delimited word (CTC / word_count) after NFKC |
| Chars/token | Non-whitespace Unicode code points per token (chars / CTC) |
| Token premium | CTC_lang / CTC_eng — relative token cost vs English |
| STRR (Single Token Retention Rate) | Share of whitespace-delimited words that encode to exactly one token |
| STFR (Single Token Fragmentation Rate) | Share of emitted tokens whose decoded surface form is a single character |
STRR and STFR use per-token surface decode (each token decoded in isolation) to detect fragmentation without full-sequence decoding.
data/flores200_dataset/ (local; not in git)
│
▼
src/load_flores.py load dev + devtest sentences for locked languages
│
▼
src/metrics.py CTC, fertility, chars/token, STRR/STFR, token premium
│
▼
src/run_eval.py CLI — writes results/ tables + metrics.json
│
▼
scripts/export_web_data.py embeds results into web/data.js
│
▼
web/ static viewer (index.html, app.js, styles.css)
Supporting modules: src/tokenizers_registry.py (tokenizer loading), src/grapheme_wrap.py (optional o200k grapheme wrapper).
python -m pip install -r requirements.txt
The dataset is not checked into git. Download the official tarball and extract into data/flores200_dataset/:
mkdir -p data
curl -L -o data/flores200_dataset.tar.gz \
https://dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz
tar -xzf data/flores200_dataset.tar.gz -C data/
Alternatively, point to an existing extraction with the FLORES200_DIR environment variable.
The Llama tokenizer requires Hugging Face authentication and model access:
huggingface-cli login
python -m src.run_eval --tokenizers o200k glm llama qwen multi --out-dir results
python scripts/export_web_data.py
cd web
python -m http.server 8765
Open http://localhost:8765 for the metrics viewer (production heatmap + grapheme / parity experiment tabs).
From-scratch o200k-pretok BPE on an English-skewed FLORES mix, classical byte vs Parity-aware fair-max merge selection (Foroutan/Meister et al.):
python scripts/train_bpe_sweep.py --skew english_aggressive --units byte parity --out-dir artifacts/bpe_parity
python scripts/eval_bpe_sweep.py --artifact-dir artifacts/bpe_parity --baseline-unit byte --compare-unit parity --out-dir artifacts/bpe_parity
python scripts/export_web_data.py
This is a training-time sibling tokenizer (new merge list), not a frozen-o200k wrap. Eval adds Gini of tokens-per-line alongside fertility / premium / STFR / STRR.
This benchmark is infrastructure validation for Plan A, not a scientific
tokenizer result. The smoke corpus may use FLORES dev to exercise the
pipeline, but final tokenizers must train only on the decontaminated pretraining
mixture and must never consume FLORES or AmericasNLP evaluation data.
On Windows, create an isolated Python 3.11 environment and build the two pinned official repositories:
./scripts/setup_tokenizer_benchmark.ps1
Build a deterministic, byte-bounded corpus from explicitly selected language files:
python scripts/build_tokenizer_benchmark_corpus.py `
--input data/flores200_dataset/dev/eng_Latn.dev <other-language-files> `
--output artifacts/tokenizer_benchmark/corpus/train.txt `
--manifest artifacts/tokenizer_benchmark/corpus/manifest.json `
--target-bytes 10000000
Then run the pinned official pipeline. The BPE arm must complete before the SuperBPE continuation:
.venv-benchmark/Scripts/python.exe scripts/run_official_tokenizer_benchmark.py `
--arm bpe --superbpe-repo .cache/superbpe `
--corpus-dir artifacts/tokenizer_benchmark/corpus `
--output-dir artifacts/tokenizer_benchmark/bpe_4k `
--result artifacts/tokenizer_benchmark/results/bpe_4k.json `
--log artifacts/tokenizer_benchmark/logs/bpe_4k.log `
--num-bytes 2495955 --vocab-size 4096 `
--patched-tokenizers-commit 757f2a55c0820ed47064e1fe473deea39b7b611b `
--max-rss-gb 8 --force
.venv-benchmark/Scripts/python.exe scripts/run_official_tokenizer_benchmark.py `
--arm superbpe --superbpe-repo .cache/superbpe `
--corpus-dir artifacts/tokenizer_benchmark/corpus `
--baseline-dir artifacts/tokenizer_benchmark/bpe_4k `
--output-dir artifacts/tokenizer_benchmark/superbpe_4k_t3072 `
--result artifacts/tokenizer_benchmark/results/superbpe_4k_t3072.json `
--log artifacts/tokenizer_benchmark/logs/superbpe_4k_t3072.log `
--num-bytes 2495955 --vocab-size 4096 --transition-vocab-size 3072 `
--patched-tokenizers-commit 757f2a55c0820ed47064e1fe473deea39b7b611b `
--max-rss-gb 8 --force
Verify that both vocabularies have the requested size and the exact BPE prefix was supplied to stage two:
python scripts/verify_official_tokenizer_pair.py `
--baseline-dir artifacts/tokenizer_benchmark/bpe_4k `
--superbpe-dir artifacts/tokenizer_benchmark/superbpe_4k_t3072 `
--transition-vocab-size 3072 --expected-vocab-size 4096 `
--result artifacts/tokenizer_benchmark/results/pair_verification.json
configs/benchmarks/tokenizer_local.json defines smoke, pilot, and scale gates.
Each result records process-tree peak RSS, runtime, CPU time, input hash,
trainer commits, logs, and a clearly marked linear 10 GB runtime projection.
Memory is not extrapolated; measure every tier and stop if a guard fires.
The same runner is the AWS Batch entrypoint in
docker/tokenizer-benchmark/Dockerfile. AWS only changes corpus/artifact
staging (S3 to local job storage and back); command arguments and result schemas
remain identical.
Plan A trains two arms on one balanced corpus: official BPE, and SuperBPE continued
off the exact BPE merge prefix. Scope and byte budgets are in plans/02-tokenizer-training.md;
where the corpus text comes from and how it reaches S3 is in
plans/01-data-sourcing.md; tier parameters are in
configs/benchmarks/tokenizer_local.json.
Build the equal-content CR-dev used for premium calibration:
.venv-benchmark/Scripts/python.exe scripts/build_plan_a_cr_dev.py `
--output-dir artifacts/plan_a/research_cpu/cr_dev
Orchestrate both arms, verification, premium calibration, and READY.json:
.venv-benchmark/Scripts/python.exe scripts/run_plan_a_tokenizer_pair.py `
--work-dir artifacts/plan_a `
--dev-lang-dir artifacts/plan_a/research_cpu/cr_dev `
--corpus-dir artifacts/plan_a/research_cpu/corpus `
--corpus-manifest artifacts/plan_a/research_cpu/corpus/manifest.json `
--superbpe-repo .cache/superbpe `
--num-bytes 12000000 --vocab-size 4096 --transition-vocab-size 3072 `
--stage smoke --force
--num-bytes must equal the corpus manifest's exact total. corpus_dir holds one
.txt per language and the official trainer selects whole files, so a smaller
--num-bytes would silently train on a subset of the languages rather than a
proportional sample of all of them.
Plan B CPU materialization (after handoff/READY.json):
.venv-benchmark/Scripts/python.exe scripts/run_plan_b_materialize_shards.py `
--ready artifacts/plan_a/handoff/READY.json `
--documents artifacts/tokenizer_benchmark/corpus/train.txt `
--output-dir artifacts/plan_b/materialize `
--max-documents 200
.venv-benchmark/Scripts/python.exe scripts/build_plan_b_mixture.py `
--result artifacts/plan_b/mixture.json
.venv-benchmark/Scripts/python.exe scripts/run_plan_b_preflight.py `
--ready artifacts/plan_a/handoff/READY.json `
--materialization artifacts/plan_b/materialize/materialization.json `
--mixture artifacts/plan_b/mixture.json `
--result artifacts/plan_b/preflight_schedule.json
.venv-benchmark/Scripts/python.exe scripts/emit_plan_b_olmo_jobs.py `
--preflight artifacts/plan_b/preflight_schedule.json `
--materialization artifacts/plan_b/materialize/materialization.json `
--result artifacts/plan_b/olmo_job_bundle.json
Plan B unique-text pool pull (EC2 in $AWS_REGION, ~300 GB disk — not a laptop):
pip install zstandard datasets
.venv-benchmark/Scripts/python.exe scripts/pull_plan_b_pools.py `
--output-dir artifacts/plan_b/pools
# Publish into edullm-data (in-region):
# python scripts/stage_fineweb2_unimax_pools.py --pools-dir <pools>
# python scripts/publish_fineweb2_unimax_pools.py
A separate two-stage experiment asking a different question from the efficiency metrics above. Those say how expensive a language is; this asks whether the token distribution a tokenizer produces is structurally sound for that language.
Word frequencies are Zipfian in every language. A tokenizer that serves a language well should preserve that; one that shreds it into byte fragments cannot. Each language is evaluated against its own active subset of the vocabulary, not the full 200k.
18 languages — the locked 12 plus spa_Latn, hin_Deva, kor_Hang, tha_Thai,
tir_Ethi, sat_Olck — spanning 9 scripts and a 48× range in active-vocabulary
share. Four tokenizers: o200k (primary), llama, qwen, and multi (NLLB-200,
the multilingual-by-design control). Corpus is FLORES dev+devtest (2,009
parallel sentences per language); parallel content is the cross-language control.
python scripts/run_vocab_profile.py --out-dir results/zipf
Classifies all 199,998 mergeable ranks by Unicode script, attributes the 1,562 partial-UTF-8 byte fragments to Unicode blocks via their leading byte, then profiles every one of the 204 FLORES languages empirically.
| Output | Contents |
|---|---|
vocab_allocation_by_script.csv | share of vocab per script |
vocab_allocation_fragments.csv | byte fragments by best-guess block, with a certainty flag |
vocab_allocation_mixed.csv | mixed-script token combinations |
lang_vocab_profile.csv | per language: active types, share_of_vocab, exclusivity vs English (type- and mass-weighted), share_fragment_mass, type-level whole-word coverage |
Two metrics deserve attention. share_fragment_mass is the fraction of a
language's tokens that are raw partial-UTF-8 fragments — the most legible single
number in the study. Mass-weighted exclusivity exists because the type-level
version misleads: Amharic shares 538 of its 676 types with English, so type-level
exclusivity reads 0.20, but those shared types are punctuation and Latin names
appearing a few times each while the 76 exclusive Ethiopic fragments carry 88.8%
of all Amharic tokens.
# smoke first
python scripts/run_zipf_eval.py --tokenizers o200k --max-sentences 50 --bootstrap 5 \
--out-dir results/zipf/_smoke
# full run
python scripts/run_zipf_eval.py --out-dir results/zipf
python scripts/plot_zipf.py --results-dir results/zipf
Fits Zipf–Mandelbrot p(r) ∝ (r+b)^−α by MLE over each language's observed
support. OLS on the log-log curve is reported for interpretability only — it is
biased (Clauset et al. 2009); measured here,
MLE error stays under 0.01 while OLS error runs 0.03–0.07.
Three units, because the baselines are what attribute a deviation to the
tokenizer rather than the language: token, word (whitespace, unavailable for
zho_Hans and tha_Thai), and grapheme (UAX #29, defined for every script and
therefore the universal reference).
Two views, both reported since they answer different questions —
matched_token (every language subsampled to an identical unit budget: matched
power, unmatched content) and matched_sentence (the whole parallel corpus:
matched content, unmatched size). Budgets are 95% of the smallest corpus for that
unit type, so every language carries sampling variability.
Two distinct goodness-of-fit quantities are kept separate:
| Metric | Question |
|---|---|
ks | distance from the best-fit Zipf–Mandelbrot — is this power-law shaped at all? |
ks_zipf | distance from Zipf's law itself (α=1, b=0) — how far from the law? |
A uniform distribution shows why: it is Zipf–Mandelbrot with α=0, so its ks is
near zero while its ks_zipf is large.
Because α and the KS statistics degrade on a few-hundred-type support — exactly the
regime the byte-level languages occupy — the entropy family is reported alongside
every fit: Shannon entropy, normalized entropy, Rényi efficiency at order 2.5
(Zouhar et al.), and effective vocabulary
size exp(H), the number of equally-likely units the distribution behaves like.
Support size n_types appears beside every fit; truncation is the dominant effect
and must not be normalized out of view.
| Output | Contents |
|---|---|
zipf_fits.csv / .json | every (language, tokenizer, unit, view) fit with bootstrap intervals, converged_share and at_bound_share |
zipf_deltas.csv | token − word and token − grapheme deltas |
hypotheses.json | H1–H4 scored against the numbers, with Spearman statistics |
zipf_word_coverage.png | whole-word coverage and byte-fragment mass per language |
zipf_allocation_vs_deviation.png | the headline scatter |
zipf_rank_frequency.png | log-log rank-frequency small multiples with fitted ZM and pure-Zipf curves |
zipf_script_allocation.png | vocabulary split across Unicode scripts |
zipf_token_vs_baseline.png | how far the tokenizer moves the distribution off the text's own |
zipf_deviation_heatmap.png | log effective vocabulary, language × tokenizer |
Measured on o200k_base, matched-token view (49,788 tokens), 200 bootstrap draws.
Vocabulary allocation is wildly uneven. o200k_base contains no token holding
a complete Ethiopic character and only 38 for Oriya. Whole-word coverage — the
share of a language's distinct word types that survive as a single token — runs
English 52.9%, Spanish 29.9%, Hindi 14.2%, Hungarian 5.3%, down to Amharic 0.70%,
Odia 0.63%, Tigrinya 0.31%, Santali 0.30%. Byte-fragment mass runs the other way:
Santali 93.3%, Amharic 88.8%, Tigrinya 87.8%, Odia 27.0%, English 0.0%.
The tokenizer can be worse than doing nothing. Comparing each language's token distribution against the raw Unicode grapheme clusters of the same text, tokenizing multiplies English's effective vocabulary by 56× (23 → 1,317) and Spanish's by 51×. For Odia, Tigrinya, Amharic and Santali the ratio falls below 1 (0.23–0.36): the tokens carry fewer effective units than reading the text character by character.
Attribution holds. Token-minus-word Δα — same text, same sample size, so small-corpus bias cancels — averages 2.809 for the byte-level languages against 0.051 for English and Spanish, a 55× gap.
Pre-registered hypotheses, as scored in hypotheses.json:
| Result | Evidence | |
|---|---|---|
| H1 allocation vs exclusivity | supported | ρ = −0.686, p = 0.0017. High raw share goes with low exclusivity — Quechua has 3.04% of the vocab but 48% exclusive mass; Amharic 0.34% but 89%. |
| H2 allocation vs deviation | supported | ρ = −0.711 (share vs ks_zipf), +0.653 (share vs log eff. vocab), +0.706 (fragment mass vs ks_zipf), all p < 0.004 |
| H3 delta vs allocation | partly supported | Effective vocabulary: ρ = +0.712 (word), +0.802 (grapheme). ks_zipf: ρ = −0.812 (grapheme) but −0.109, p = 0.69 (word) — reported rather than dropped |
| H4 NLLB spread | refuted | Qwen has the smallest cross-language spread (σ = 0.0495), not NLLB (0.0549) |
ks_zipf is measured over each language's own support, so it partly
normalizes away the truncation that is the main effect. It answers "conditional
on its support, is this Zipf-shaped" — not "how well is this language served."
Use effective_vocab and the token-minus-baseline deltas for the latter.converged=False from L-BFGS-B on a locally flat likelihood surface.
converged_share is reported per cell so this is visible rather than silent.at_bound flag, aggregated per cell as
at_bound_share. A fit on a bound is a truncation of the optimum, not an
estimate of it, and its α and b must not be read at face value. α = 0 is
deliberately not flagged: that is the uniform limit and a natural edge of the
parameter space, since a negative exponent cannot fit a descending count vector.bpe/, bpe_constrained/, bpe_skew/, bpe_parity/)metrics.json, wide CSV tables, decision JSON, premium heatmap PNG (gitignored; produced by run_eval)run_vocab_profile + run_zipf_eval)data.js is generated; re-run export_web_data.py after each eval)quy_Latn) substitutes for Nahuatl, which is not in FLORES-200/FLORES+.data/ and results/ are gitignored; obtain data locally and regenerate results.web/data.js is generated by scripts/export_web_data.py, not hand-edited.o200k_grapheme is supplemental and not part of the default viewer export.o200k_base IDs.src/zipf_langs.py deliberately does not extend src/load_flores.py:LANGUAGES. That set is the locked 12-language efficiency scope consumed by run_eval.py and export_web_data.py; editing it in place would shift already-published results.src/metrics.py:compute_strr is occurrence-weighted and is left untouched. The Zipf study's whole-word coverage is the type-level sibling in src/vocab_profile.py, so frequent function words cannot mask how a language's long tail fares.o200k_base holds no token containing a complete Ethiopic character and only 38 for Oriya, so Amharic and Tigrinya are encoded almost entirely from partial-UTF-8 byte fragments. This is measured, not assumed — see the regression anchors in tests/test_vocab_profile.py.17 commits
Python
49.4%
JavaScript
44.4%
TeX
3.9%
Encode-only multilingual tokenizer efficiency on FLORES-200 devtest across 12 languages and 8 tokenizers.
Each tokenizer encodes the full FLORES-200 devtest split for a locked set of 12 languages (one per continent, plus English as reference). No decoding or model inference — only tokenization and metric aggregation.
| Metric | Description |
|---|---|
| CTC (Corpus Token Count) | Total number of tokens produced when encoding the whole language corpus (BOS/EOS excluded). The base count the other metrics derive from. |
| Fertility | Tokens per whitespace-delimited word (CTC / word_count) after NFKC |
| Chars/token | Non-whitespace Unicode code points per token (chars / CTC) |
| Token premium | CTC_lang / CTC_eng — relative token cost vs English |
| STRR (Single Token Retention Rate) | Share of whitespace-delimited words that encode to exactly one token |
| STFR (Single Token Fragmentation Rate) | Share of emitted tokens whose decoded surface form is a single character |
STRR and STFR use per-token surface decode (each token decoded in isolation) to detect fragmentation without full-sequence decoding.
data/flores200_dataset/ (local; not in git)
│
▼
src/load_flores.py load dev + devtest sentences for locked languages
│
▼
src/metrics.py CTC, fertility, chars/token, STRR/STFR, token premium
│
▼
src/run_eval.py CLI — writes results/ tables + metrics.json
│
▼
scripts/export_web_data.py embeds results into web/data.js
│
▼
web/ static viewer (index.html, app.js, styles.css)
Supporting modules: src/tokenizers_registry.py (tokenizer loading), src/grapheme_wrap.py (optional o200k grapheme wrapper).
python -m pip install -r requirements.txt
The dataset is not checked into git. Download the official tarball and extract into data/flores200_dataset/:
mkdir -p data
curl -L -o data/flores200_dataset.tar.gz \
https://dl.fbaipublicfiles.com/nllb/flores200_dataset.tar.gz
tar -xzf data/flores200_dataset.tar.gz -C data/
Alternatively, point to an existing extraction with the FLORES200_DIR environment variable.
The Llama tokenizer requires Hugging Face authentication and model access:
huggingface-cli login
python -m src.run_eval --tokenizers o200k glm llama qwen multi --out-dir results
python scripts/export_web_data.py
cd web
python -m http.server 8765
Open http://localhost:8765 for the metrics viewer (production heatmap + grapheme / parity experiment tabs).
From-scratch o200k-pretok BPE on an English-skewed FLORES mix, classical byte vs Parity-aware fair-max merge selection (Foroutan/Meister et al.):
python scripts/train_bpe_sweep.py --skew english_aggressive --units byte parity --out-dir artifacts/bpe_parity
python scripts/eval_bpe_sweep.py --artifact-dir artifacts/bpe_parity --baseline-unit byte --compare-unit parity --out-dir artifacts/bpe_parity
python scripts/export_web_data.py
This is a training-time sibling tokenizer (new merge list), not a frozen-o200k wrap. Eval adds Gini of tokens-per-line alongside fertility / premium / STFR / STRR.
This benchmark is infrastructure validation for Plan A, not a scientific
tokenizer result. The smoke corpus may use FLORES dev to exercise the
pipeline, but final tokenizers must train only on the decontaminated pretraining
mixture and must never consume FLORES or AmericasNLP evaluation data.
On Windows, create an isolated Python 3.11 environment and build the two pinned official repositories:
./scripts/setup_tokenizer_benchmark.ps1
Build a deterministic, byte-bounded corpus from explicitly selected language files:
python scripts/build_tokenizer_benchmark_corpus.py `
--input data/flores200_dataset/dev/eng_Latn.dev <other-language-files> `
--output artifacts/tokenizer_benchmark/corpus/train.txt `
--manifest artifacts/tokenizer_benchmark/corpus/manifest.json `
--target-bytes 10000000
Then run the pinned official pipeline. The BPE arm must complete before the SuperBPE continuation:
.venv-benchmark/Scripts/python.exe scripts/run_official_tokenizer_benchmark.py `
--arm bpe --superbpe-repo .cache/superbpe `
--corpus-dir artifacts/tokenizer_benchmark/corpus `
--output-dir artifacts/tokenizer_benchmark/bpe_4k `
--result artifacts/tokenizer_benchmark/results/bpe_4k.json `
--log artifacts/tokenizer_benchmark/logs/bpe_4k.log `
--num-bytes 2495955 --vocab-size 4096 `
--patched-tokenizers-commit 757f2a55c0820ed47064e1fe473deea39b7b611b `
--max-rss-gb 8 --force
.venv-benchmark/Scripts/python.exe scripts/run_official_tokenizer_benchmark.py `
--arm superbpe --superbpe-repo .cache/superbpe `
--corpus-dir artifacts/tokenizer_benchmark/corpus `
--baseline-dir artifacts/tokenizer_benchmark/bpe_4k `
--output-dir artifacts/tokenizer_benchmark/superbpe_4k_t3072 `
--result artifacts/tokenizer_benchmark/results/superbpe_4k_t3072.json `
--log artifacts/tokenizer_benchmark/logs/superbpe_4k_t3072.log `
--num-bytes 2495955 --vocab-size 4096 --transition-vocab-size 3072 `
--patched-tokenizers-commit 757f2a55c0820ed47064e1fe473deea39b7b611b `
--max-rss-gb 8 --force
Verify that both vocabularies have the requested size and the exact BPE prefix was supplied to stage two:
python scripts/verify_official_tokenizer_pair.py `
--baseline-dir artifacts/tokenizer_benchmark/bpe_4k `
--superbpe-dir artifacts/tokenizer_benchmark/superbpe_4k_t3072 `
--transition-vocab-size 3072 --expected-vocab-size 4096 `
--result artifacts/tokenizer_benchmark/results/pair_verification.json
configs/benchmarks/tokenizer_local.json defines smoke, pilot, and scale gates.
Each result records process-tree peak RSS, runtime, CPU time, input hash,
trainer commits, logs, and a clearly marked linear 10 GB runtime projection.
Memory is not extrapolated; measure every tier and stop if a guard fires.
The same runner is the AWS Batch entrypoint in
docker/tokenizer-benchmark/Dockerfile. AWS only changes corpus/artifact
staging (S3 to local job storage and back); command arguments and result schemas
remain identical.
Plan A trains two arms on one balanced corpus: official BPE, and SuperBPE continued
off the exact BPE merge prefix. Scope and byte budgets are in plans/02-tokenizer-training.md;
where the corpus text comes from and how it reaches S3 is in
plans/01-data-sourcing.md; tier parameters are in
configs/benchmarks/tokenizer_local.json.
Build the equal-content CR-dev used for premium calibration:
.venv-benchmark/Scripts/python.exe scripts/build_plan_a_cr_dev.py `
--output-dir artifacts/plan_a/research_cpu/cr_dev
Orchestrate both arms, verification, premium calibration, and READY.json:
.venv-benchmark/Scripts/python.exe scripts/run_plan_a_tokenizer_pair.py `
--work-dir artifacts/plan_a `
--dev-lang-dir artifacts/plan_a/research_cpu/cr_dev `
--corpus-dir artifacts/plan_a/research_cpu/corpus `
--corpus-manifest artifacts/plan_a/research_cpu/corpus/manifest.json `
--superbpe-repo .cache/superbpe `
--num-bytes 12000000 --vocab-size 4096 --transition-vocab-size 3072 `
--stage smoke --force
--num-bytes must equal the corpus manifest's exact total. corpus_dir holds one
.txt per language and the official trainer selects whole files, so a smaller
--num-bytes would silently train on a subset of the languages rather than a
proportional sample of all of them.
Plan B CPU materialization (after handoff/READY.json):
.venv-benchmark/Scripts/python.exe scripts/run_plan_b_materialize_shards.py `
--ready artifacts/plan_a/handoff/READY.json `
--documents artifacts/tokenizer_benchmark/corpus/train.txt `
--output-dir artifacts/plan_b/materialize `
--max-documents 200
.venv-benchmark/Scripts/python.exe scripts/build_plan_b_mixture.py `
--result artifacts/plan_b/mixture.json
.venv-benchmark/Scripts/python.exe scripts/run_plan_b_preflight.py `
--ready artifacts/plan_a/handoff/READY.json `
--materialization artifacts/plan_b/materialize/materialization.json `
--mixture artifacts/plan_b/mixture.json `
--result artifacts/plan_b/preflight_schedule.json
.venv-benchmark/Scripts/python.exe scripts/emit_plan_b_olmo_jobs.py `
--preflight artifacts/plan_b/preflight_schedule.json `
--materialization artifacts/plan_b/materialize/materialization.json `
--result artifacts/plan_b/olmo_job_bundle.json
Plan B unique-text pool pull (EC2 in $AWS_REGION, ~300 GB disk — not a laptop):
pip install zstandard datasets
.venv-benchmark/Scripts/python.exe scripts/pull_plan_b_pools.py `
--output-dir artifacts/plan_b/pools
# Publish into edullm-data (in-region):
# python scripts/stage_fineweb2_unimax_pools.py --pools-dir <pools>
# python scripts/publish_fineweb2_unimax_pools.py
A separate two-stage experiment asking a different question from the efficiency metrics above. Those say how expensive a language is; this asks whether the token distribution a tokenizer produces is structurally sound for that language.
Word frequencies are Zipfian in every language. A tokenizer that serves a language well should preserve that; one that shreds it into byte fragments cannot. Each language is evaluated against its own active subset of the vocabulary, not the full 200k.
18 languages — the locked 12 plus spa_Latn, hin_Deva, kor_Hang, tha_Thai,
tir_Ethi, sat_Olck — spanning 9 scripts and a 48× range in active-vocabulary
share. Four tokenizers: o200k (primary), llama, qwen, and multi (NLLB-200,
the multilingual-by-design control). Corpus is FLORES dev+devtest (2,009
parallel sentences per language); parallel content is the cross-language control.
python scripts/run_vocab_profile.py --out-dir results/zipf
Classifies all 199,998 mergeable ranks by Unicode script, attributes the 1,562 partial-UTF-8 byte fragments to Unicode blocks via their leading byte, then profiles every one of the 204 FLORES languages empirically.
| Output | Contents |
|---|---|
vocab_allocation_by_script.csv | share of vocab per script |
vocab_allocation_fragments.csv | byte fragments by best-guess block, with a certainty flag |
vocab_allocation_mixed.csv | mixed-script token combinations |
lang_vocab_profile.csv | per language: active types, share_of_vocab, exclusivity vs English (type- and mass-weighted), share_fragment_mass, type-level whole-word coverage |
Two metrics deserve attention. share_fragment_mass is the fraction of a
language's tokens that are raw partial-UTF-8 fragments — the most legible single
number in the study. Mass-weighted exclusivity exists because the type-level
version misleads: Amharic shares 538 of its 676 types with English, so type-level
exclusivity reads 0.20, but those shared types are punctuation and Latin names
appearing a few times each while the 76 exclusive Ethiopic fragments carry 88.8%
of all Amharic tokens.
# smoke first
python scripts/run_zipf_eval.py --tokenizers o200k --max-sentences 50 --bootstrap 5 \
--out-dir results/zipf/_smoke
# full run
python scripts/run_zipf_eval.py --out-dir results/zipf
python scripts/plot_zipf.py --results-dir results/zipf
Fits Zipf–Mandelbrot p(r) ∝ (r+b)^−α by MLE over each language's observed
support. OLS on the log-log curve is reported for interpretability only — it is
biased (Clauset et al. 2009); measured here,
MLE error stays under 0.01 while OLS error runs 0.03–0.07.
Three units, because the baselines are what attribute a deviation to the
tokenizer rather than the language: token, word (whitespace, unavailable for
zho_Hans and tha_Thai), and grapheme (UAX #29, defined for every script and
therefore the universal reference).
Two views, both reported since they answer different questions —
matched_token (every language subsampled to an identical unit budget: matched
power, unmatched content) and matched_sentence (the whole parallel corpus:
matched content, unmatched size). Budgets are 95% of the smallest corpus for that
unit type, so every language carries sampling variability.
Two distinct goodness-of-fit quantities are kept separate:
| Metric | Question |
|---|---|
ks | distance from the best-fit Zipf–Mandelbrot — is this power-law shaped at all? |
ks_zipf | distance from Zipf's law itself (α=1, b=0) — how far from the law? |
A uniform distribution shows why: it is Zipf–Mandelbrot with α=0, so its ks is
near zero while its ks_zipf is large.
Because α and the KS statistics degrade on a few-hundred-type support — exactly the
regime the byte-level languages occupy — the entropy family is reported alongside
every fit: Shannon entropy, normalized entropy, Rényi efficiency at order 2.5
(Zouhar et al.), and effective vocabulary
size exp(H), the number of equally-likely units the distribution behaves like.
Support size n_types appears beside every fit; truncation is the dominant effect
and must not be normalized out of view.
| Output | Contents |
|---|---|
zipf_fits.csv / .json | every (language, tokenizer, unit, view) fit with bootstrap intervals, converged_share and at_bound_share |
zipf_deltas.csv | token − word and token − grapheme deltas |
hypotheses.json | H1–H4 scored against the numbers, with Spearman statistics |
zipf_word_coverage.png | whole-word coverage and byte-fragment mass per language |
zipf_allocation_vs_deviation.png | the headline scatter |
zipf_rank_frequency.png | log-log rank-frequency small multiples with fitted ZM and pure-Zipf curves |
zipf_script_allocation.png | vocabulary split across Unicode scripts |
zipf_token_vs_baseline.png | how far the tokenizer moves the distribution off the text's own |
zipf_deviation_heatmap.png | log effective vocabulary, language × tokenizer |
Measured on o200k_base, matched-token view (49,788 tokens), 200 bootstrap draws.
Vocabulary allocation is wildly uneven. o200k_base contains no token holding
a complete Ethiopic character and only 38 for Oriya. Whole-word coverage — the
share of a language's distinct word types that survive as a single token — runs
English 52.9%, Spanish 29.9%, Hindi 14.2%, Hungarian 5.3%, down to Amharic 0.70%,
Odia 0.63%, Tigrinya 0.31%, Santali 0.30%. Byte-fragment mass runs the other way:
Santali 93.3%, Amharic 88.8%, Tigrinya 87.8%, Odia 27.0%, English 0.0%.
The tokenizer can be worse than doing nothing. Comparing each language's token distribution against the raw Unicode grapheme clusters of the same text, tokenizing multiplies English's effective vocabulary by 56× (23 → 1,317) and Spanish's by 51×. For Odia, Tigrinya, Amharic and Santali the ratio falls below 1 (0.23–0.36): the tokens carry fewer effective units than reading the text character by character.
Attribution holds. Token-minus-word Δα — same text, same sample size, so small-corpus bias cancels — averages 2.809 for the byte-level languages against 0.051 for English and Spanish, a 55× gap.
Pre-registered hypotheses, as scored in hypotheses.json:
| Result | Evidence | |
|---|---|---|
| H1 allocation vs exclusivity | supported | ρ = −0.686, p = 0.0017. High raw share goes with low exclusivity — Quechua has 3.04% of the vocab but 48% exclusive mass; Amharic 0.34% but 89%. |
| H2 allocation vs deviation | supported | ρ = −0.711 (share vs ks_zipf), +0.653 (share vs log eff. vocab), +0.706 (fragment mass vs ks_zipf), all p < 0.004 |
| H3 delta vs allocation | partly supported | Effective vocabulary: ρ = +0.712 (word), +0.802 (grapheme). ks_zipf: ρ = −0.812 (grapheme) but −0.109, p = 0.69 (word) — reported rather than dropped |
| H4 NLLB spread | refuted | Qwen has the smallest cross-language spread (σ = 0.0495), not NLLB (0.0549) |
ks_zipf is measured over each language's own support, so it partly
normalizes away the truncation that is the main effect. It answers "conditional
on its support, is this Zipf-shaped" — not "how well is this language served."
Use effective_vocab and the token-minus-baseline deltas for the latter.converged=False from L-BFGS-B on a locally flat likelihood surface.
converged_share is reported per cell so this is visible rather than silent.at_bound flag, aggregated per cell as
at_bound_share. A fit on a bound is a truncation of the optimum, not an
estimate of it, and its α and b must not be read at face value. α = 0 is
deliberately not flagged: that is the uniform limit and a natural edge of the
parameter space, since a negative exponent cannot fit a descending count vector.bpe/, bpe_constrained/, bpe_skew/, bpe_parity/)metrics.json, wide CSV tables, decision JSON, premium heatmap PNG (gitignored; produced by run_eval)run_vocab_profile + run_zipf_eval)data.js is generated; re-run export_web_data.py after each eval)quy_Latn) substitutes for Nahuatl, which is not in FLORES-200/FLORES+.data/ and results/ are gitignored; obtain data locally and regenerate results.web/data.js is generated by scripts/export_web_data.py, not hand-edited.o200k_grapheme is supplemental and not part of the default viewer export.o200k_base IDs.src/zipf_langs.py deliberately does not extend src/load_flores.py:LANGUAGES. That set is the locked 12-language efficiency scope consumed by run_eval.py and export_web_data.py; editing it in place would shift already-published results.src/metrics.py:compute_strr is occurrence-weighted and is left untouched. The Zipf study's whole-word coverage is the type-level sibling in src/vocab_profile.py, so frequent function words cannot mask how a language's long tail fares.o200k_base holds no token containing a complete Ethiopic character and only 38 for Oriya, so Amharic and Tigrinya are encoded almost entirely from partial-UTF-8 byte fragments. This is measured, not assumed — see the regression anchors in tests/test_vocab_profile.py.17 commits
Python
49.4%
JavaScript
44.4%
TeX
3.9%