CoolFace
Modelpublic

openeurollm/tokenizer-256k-v2

sourceHugging Faceapache-2.0updated 28d agoView on Hugging Face
0likes
Model Card

OpenEuroLLM Tokenizer v2 (256k)

SentencePiece BPE tokenizer for the OpenEuroLLM flagship models. v2 is a full retrain of the v1 256k tokenizer on a larger, cleaner corpus with the language list driven from the canonical `training-data-catalogue/languages` file (no hardcoded list this time — see v1's Georgian-gap incident).

Highlights vs SOTA — multi-domain eval

Evaluation is a held-out 5-domain suite (8,600 samples total), designed to test more than Wikipedia prose. Each column is mean tokens-per-whitespace-word (lower = better).

TokenizerVocab**Overall**FLORES-200<br>(36 langs parallel)Code<br>(Python)Math<br>(LaTeX+GSM8K)Chat<br>(ChatML)PDFs<br>(5 langs)
OpenEuroLLM v2 256k (this model)262,1441.90 🥇1.79 🥇3.241.911.44 🥇2.31
GPT-OSS 20B200,0002.072.072.621.641.542.01
OpenEuroLLM v2 128k131,0722.092.003.321.921.522.43
Gemma 3 4B256,0002.192.133.201.921.592.27
Mistral Nemo131,0722.232.202.841.921.622.26
EuroLLM 9B128,0002.302.213.792.021.572.48
OpenEuroLLM v1 256k (predecessor)262,1442.452.433.331.951.682.29
DeepSeek V3128,0002.472.512.831.651.652.13
OpenEuroLLM v1 128k131,0722.622.623.421.981.762.40
Llama 3.1 8B128,2562.682.782.60 🥇1.65 🥇1.652.18
Qwen 3 8B151,9362.702.782.641.901.512.35

Eval composition: 7,200 FLORES (36 langs × 200 parallel sentences, held-out) · 500 Python (codeparrot) · 200 MATH+GSM8K · 200 OpenAssistant chat (ChatML-wrapped) · 500 FinePDFs (5 langs).

Summary of v2-256k vs the field:

  • #1 overall (1.90) — beats every SOTA tokenizer on the multi-domain average.
  • #1 on multilingual prose by a wide margin (FLORES 1.79 vs Gemma 2.13, GPT-OSS 2.07).
  • #1 on chat (1.44, ChatML tokens working as intended).
  • Competitive on math/PDF (within 0.3 of the leader).
  • Loses on Python code (3.24 vs Llama 2.60). Llama 3's tiktoken-based BPE is more code-aggressive; even the v2 whitespace tokens don't fully close that gap.

v2 vs v1: per-language deltas on FLORES-200

FLORES-200 has parallel sentences across all languages (semantically equivalent translations), so fertility differences here are pure tokenizer effect (no content drift). Same 256k and 128k models, bold = v2 better, English first then alphabetical.

Languagev1 256kv2 256kΔ256kv1 128kv2 128kΔ128k
English (en)1.241.21−0.031.291.23−0.06
Albanian (sq)2.261.59−0.672.441.76−0.68
Basque (eu)2.051.90−0.152.282.12−0.17
Bosnian (bs)1.661.60−0.071.841.78−0.06
Bulgarian (bg)1.781.87+0.091.952.13+0.18
Catalan (ca)1.641.57−0.071.771.70−0.07
Croatian (hr)1.721.63−0.101.911.82−0.09
Czech (cs)1.551.79+0.241.722.04+0.31
Danish (da)1.621.54−0.071.761.69−0.07
Dutch (nl)1.621.53−0.091.771.68−0.09
Estonian (et)2.152.04−0.112.412.30−0.11
Finnish (fi)2.422.30−0.112.712.59−0.13
French (fr)1.601.53−0.061.741.67−0.07
Galician (gl)1.501.44−0.061.641.58−0.06
Georgian (ka)22.932.83−20.1022.933.30−19.63
German (de)1.481.68+0.191.611.86+0.25
Greek (el)2.242.12−0.112.632.44−0.20
Hungarian (hu)2.152.06−0.092.442.33−0.12
Icelandic (is)2.011.84−0.172.212.05−0.16
Irish (ga)1.711.60−0.111.911.79−0.12
Italian (it)1.351.51+0.171.451.66+0.21
Latvian (lv)3.011.94−1.073.182.20−0.98
Lithuanian (lt)2.041.99−0.052.302.27−0.03
Macedonian (mk)1.811.89+0.081.992.13+0.13
Maltese (mt)2.342.22−0.122.592.47−0.12
Norwegian (no)1.551.52−0.031.691.65−0.04
Polish (pl)1.741.90+0.171.952.16+0.21
Portuguese (pt)1.521.45−0.071.661.60−0.06
Romanian (ro)1.761.58−0.171.921.75−0.17
Serbian (sr)2.031.97−0.062.202.23+0.02
Slovak (sk)1.861.91+0.052.042.12+0.08
Slovene (sl)1.781.74−0.041.971.93−0.04
Spanish (es)1.471.41−0.061.601.54−0.05
Swedish (sv)1.701.66−0.041.851.81−0.04
Turkish (tr)2.121.92−0.202.402.16−0.24
Ukrainian (uk)2.162.13−0.032.422.46+0.04
Average (36 catalogue langs)2.431.79−0.642.622.00−0.62

v2-256k improves on 30/36 languages. Biggest wins: Georgian −20.10 (v1 was full byte-fallback; v2 has real script subwords), Latvian −1.07, Albanian −0.67. The few regressions (bg/cs/de/it/mk/pl/sk) are small (+0.05 to +0.24) and reflect that v2 spread vocabulary across more code/whitespace coverage. Note: lb/ru/cy aren't tested here — they're not in the catalogue and were dropped from v2's training scope.

Language coverage

36 catalogue languages: bg, bs, ca, cs, da, de, el, en, es, et, eu, fi, fr, ga, gl, hr, hu, is, it, ka (new in v2), lt, lv, mk, mt, nl, no, pl, pt, ro, sk, sl, sq, sr, sv, tr, uk.

Languages removed from v2 vs v1: lb (Luxembourgish), ru (Russian), cy (Welsh) — not in the OpenEuroLLM catalogue.

Training details

  • Algorithm: SentencePiece BPE
  • Vocab size: 262,144 (2^18)
  • Normalization: identity (lossless)
  • Byte fallback: enabled
  • Corpus: 500 GB streamed/sampled from the OpenEuroLLM "baby" cycle release shards on LUMI (10 May 2026 packer), spanning dclm, nemotron-cc, finepdfs, finepdfs-edu, olmo-mix (wiki/arxiv/pes2o), starcoder, finemath-4plus, megamath (text-code-block, web-pro), hplt-3.0, nemotron-cc-opus-1.1, nemotron-cc-tower+-0.1.
  • Mix: ~70% English / ~7% code / ~5% math / ~18% other-langs (35 catalogue non-English languages, equal allocation).
  • Character coverage: 0.9995
  • Max piece length: 16

Special tokens

Core (locked at fixed IDs, in-vocab):

TokenID
<unk>0
<bos>1
<eos>2
<pad>3

This fixes a v1 bug where <pad> was tacked on at vocabsize+0 (262144), out-of-vocab for downstream code that asserts `padtokenid < vocabsize`.

User-defined symbols (204 total) — new for v2

Whitespace family — better code efficiency (45 tokens)

Multi-character whitespace runs are reserved as single tokens so code with deep indentation doesn't burn a token per space. None of these were in v1; pretrained tokenizers without them tokenize a 32-space indent as 16+ tokens, v2 does it in 1.

BucketTokensReserved IDs
Multi-space indents" " (2 spaces), " " (3), … " "*32 (32)31 tokens
Tabs"\t", "\t\t", … "\t"*88 tokens
Multi-newline"\n\n", "\n\n\n", "\n\n\n\n"3 tokens
Common code combos"\r\n", "\t\n", " \n" (4-space-then-newline)3 tokens

Example:

python
>>> from transformers import AutoTokenizer
>>> tok = AutoTokenizer.from_pretrained("openeurollm/tokenizer-256k-v2")
>>> python_code = "def f():\n        return 1"  # 8-space indent
>>> len(tok.encode("def f():\n        return 1", add_special_tokens=False))
# v2: tokenizes the 8-space indent as ONE token
# Llama 3.1: tokenizes it as ~8 separate tokens (one per space-pair)
StarCoder-style code corpus markers (16 tokens)

Reserved for code-corpus-formatted inputs (<filename>foo.py\n... <file_sep>\n<reponame>OpenEuroLLM/x\n...) — biggest single contributor to StarCoder2's code quality:

<filename>, <reponame>, <file_sep>, <gh_stars>, <empty_output>, <issue_start>, <issue_comment>, <issue_closed>, <jupyter_start>, <jupyter_text>, <jupyter_code>, <jupyter_output>, <jupyter_script>, <commit_before>, <commit_msg>, <commit_after>.

v1 had none of these.

Fill-in-the-middle (FIM) — code completion (4 tokens)

Full 4-token FIM set for code-completion training. v1 had 3 (missing <fim_pad>).

<fim_prefix>, <fim_middle>, <fim_suffix>, <fim_pad>

Chat formats (4 tokens)

ChatML as the modern default; Gemma-style retained from v1 for compatibility.

FormatTokensNote
ChatML (primary)`<im_start>, <im_end>`New in v2 — Qwen/GPT-OSS standard
Gemma-style<start_of_turn>, <end_of_turn>Carried over from v1
Tool use (2 tokens)

<tool_call>, </tool_call> (carried over from v1).

Reasoning / chain-of-thought (2 tokens) — new for v2

<think>, </think> — DeepSeek-R1 / Qwen3 convention. Reserved up-front so future thinking-style post-training doesn't need to retokenize.

Multimodal (3 tokens)

<start_of_image>, <end_of_image>, <image_soft_token> (carried over from v1).

Reserved for future (128 tokens)

<unused_0><unused_127> — forward-compat slots. v1 had 100; v2 expands to 128 (Llama 3 reserves 256, this is a middle ground at ~0.05% of vocab).

v1 vs v2 special-tokens summary

Categoryv1v2Δ
Core specials in-vocab (unk/bos/eos/pad)❌ pad at vocab+0✅ pad=3fixed
Whitespace family❌ none✅ 45 tokensnew
StarCoder code markers❌ none✅ 16 tokensnew
FIM3 tokens4 tokens+ <fim_pad>
ChatMLnew
Gemma chatkept
Reasoning<think>/</think>new
Tool / multimodalkept
Reserved slots100128+28

Usage

python
from transformers import AutoTokenizer
tok = AutoTokenizer.from_pretrained("openeurollm/tokenizer-256k-v2")   # -> LlamaTokenizerFast

ids = tok("Hello world! Bonjour le monde.")["input_ids"]    # <bos> prepended
text = tok.decode(ids, skip_special_tokens=True)

The tokenizer is configured with add_bos_token=True and add_eos_token=False, so encoding prepends <bos> (id 1) and does not append <eos> (id 2).

Megatron-LM

--tokenizer-type HuggingFaceTokenizer \
--tokenizer-model openeurollm/tokenizer-256k-v2 \
--append-eod

--append-eod appends <eos> as the document separator. Use --tokenizer-type HuggingFaceTokenizer, not Llama2Tokenizer.

Or with SentencePiece directly:

python
import sentencepiece as spm
sp = spm.SentencePieceProcessor()
sp.Load("tokenizer.model")
ids = sp.EncodeAsIds("გამარჯობა მსოფლიო")  # "Hello world" in Georgian

Both paths produce identical ids, verified over 18,575 samples spanning 36 languages, code, math, chat and PDF text. The one intended difference is that transformers extracts the core specials from raw text, so a document containing the literal string <eos> encodes to id 2; SentencePiece encodes it as text. Pass split_special_tokens=True to opt out.

Files

  • tokenizer.json — HF fast tokenizer; this is the file transformers loads
  • tokenizer.model — SentencePiece BPE model
  • tokenizer.vocab — vocabulary listing
  • special_tokens_map.json — HF special tokens map
  • tokenizer_config.json — HF tokenizer config

Citation

Built for the OpenEuroLLM project (Horizon Europe). Source repo: <https://github.com/OpenEuroLLM/tokenizer>.