Languages¶
pyroid's filters and dedup reproduce datatrove 0.10.1
beyond English. datatrove picks a word tokenizer for each of its 4,343 language codes (its
tokenizer_assignment.csv), and every word count, ratio, n-gram and shingle depends on that
tokenizer's words. pyroid ports the rule-based tokenizers, jieba for Chinese, SudachiPy for
Japanese, pyvi for Vietnamese and PyThaiNLP's tokenizers for Thai to Rust. That makes 4,049 of the
4,343 codes native, 98.5% of FineWeb-2's bytes, with datatrove's decisions (parity), in
Rust on every thread. Japanese needs a SudachiDict dictionary installed (Japanese)
and Thai needs PyThaiNLP (Thai); without either, 4,025 codes (89.9%) are native. The
other 294 codes (318 without either) use statistical segmenters that pyroid does not port. For
them, python_tokenizer=True calls datatrove's own tokenizer and runs the rest in Rust.
The largest languages are native: Russian, German, Spanish, French and Arabic together are 48% of FineWeb-2's bytes, Chinese another 11.6%, Japanese 7.2%, Vietnamese 1.5% and Thai 1.3%.
The language table¶
datatrove 0.10.1's tokenizer for each code, and the share of FineWeb-2's training data (1,913 subsets, 22.9 TB) in those codes:
| datatrove tokenizer | codes | FineWeb-2 bytes | pyroid |
|---|---|---|---|
SpaCyTokenizer(code), a rule-based spacy.blank(code): the 57 spaCy languages af … yo, including xx, spaCy's multi-language tokenizer (1,995 codes such as uzn_Latn) |
3,742 | 74.8% | native |
IndicNLPTokenizer: indic-nlp-library's 16 languages (hi bn ur ta te mr ne ml kn gu si pa or as sa kK) |
129 | 1.9% | native (words and sentences) |
WhitespaceTokenizer: spaCy xx words and datatrove's regex sentences (such as kat_Geor) |
128 | 0.1% | native |
SpaCyTokenizer("zh"): spaCy's Chinese tokenizer with jieba (such as cmn_Hani, yue_Hani) |
10 | 11.6% | native (pyroid.jieba) |
SpaCyTokenizer("ja"): spaCy's Japanese pipeline with datatrove's SudachiPy tokenizer (ja, ja_Jpan, jpn, jpn_Jpan) |
4 | 7.2% | native with an installed SudachiDict (Japanese), else python_tokenizer=True |
SpaCyTokenizer("vi"): spaCy's Vietnamese tokenizer with pyvi (such as vie_Latn) |
16 | 1.5% | native (Vietnamese) |
ThaiTokenizer: PyThaiNLP's newmm words and crfcut sentences (such as tha_Thai) |
20 | 1.3% | native with PyThaiNLP installed (Thai), else python_tokenizer=True |
StanzaTokenizer (255 codes), BurmeseTokenizer (14), TibetanTokenizer (10), LaoTokenizer (6), KhmerTokenizer (5), KiwiTokenizer (4) |
294 | 1.4% | python_tokenizer=True |
none: FineWeb-2's und_* subsets (except und and und_Cyrl, which datatrove gives xx) |
— | 0.15% | ValueError, as datatrove |
- Native codes are tokenized in Rust. For the spaCy languages pyroid ports spaCy 3.8.18's
prefix, suffix, infix and URL rules, each language's tokenizer exceptions, the special-case
matcher and the sentencizer, extracted from spaCy's own objects (48,032 rules). For the Indic
languages it ports indic-nlp-library's
trivial_tokenizeandsentence_split, and for the whitespace codes datatrove'sWhitespaceTokenizer. Chinese is spaCy'sChineseTokenizeroverpyroid.jieba, the port of jieba 0.42.1, with jieba's default dictionary. Japanese is datatrove'sJapaneseTokenizer(SudachiPy in split mode A) and the sentencizer, over a port of SudachiPy 0.7.0's tokenizer that reads an installed SudachiDict dictionary. Vietnamese is spaCy'sVietnameseTokenizerwith pyvi 0.1.1: its syllable pattern, its CRF (pyvi's model and n-gram list are bundled) and its rules for joining syllables into words. Thai is PyThaiNLP 5.4.0's newmm (maximal matching over its word list, within Thai character clusters) and crfcut (a CRF over newmm's words), reading the installed PyThaiNLP's word list and sentence model. Both CRFs run on a Rust reader of CRFsuite's model files. Words and sentences match datatrove's. - The
python_tokenizer=Truecodes need a segmenter pyroid does not port: Stanza's neural models, Kiwi, khmer-nltk, LaoNLP, botok and pyidaungsu. Withoutpython_tokenizer=Truethese codes raiseNotImplementedError, and so do the Japanese codes when no dictionary is installed and the Thai codes without PyThaiNLP. - Codes datatrove does not know raise datatrove's
ValueErrorwhen the call starts.
How to choose¶
Look the code up in pyroid.filters.languages() (the same function is pyroid.dedup.languages).
Its keys are what language= accepts, in datatrove's spellings ("deu_Latn", "deu", "de",
...). Each value says which tokenizer datatrove uses and whether pyroid's is native (here with a
Japanese dictionary and PyThaiNLP installed; without the dictionary the 4 Japanese codes are
native=False, and without PyThaiNLP the 20 Thai codes):
import pyroid.filters as F
langs = F.languages() # {code: Language(tokenizer, arg, native)}, sorted by code
langs["deu_Latn"] # Language(tokenizer='SpaCyTokenizer', arg='de', native=True)
langs["hin_Deva"] # Language(tokenizer='IndicNLPTokenizer', arg='hi', native=True)
langs["kat_Geor"] # Language(tokenizer='WhitespaceTokenizer', arg='', native=True)
langs["cmn_Hani"] # Language(tokenizer='SpaCyTokenizer', arg='zh', native=True)
langs["jpn_Jpan"] # Language(tokenizer='SpaCyTokenizer', arg='ja', native=True)
langs["vie_Latn"] # Language(tokenizer='SpaCyTokenizer', arg='vi', native=True)
langs["tha_Thai"] # Language(tokenizer='ThaiTokenizer', arg='', native=True)
langs["kaz_Cyrl"] # Language(tokenizer='StanzaTokenizer', arg='kk', native=False)
sum(lang.native for lang in langs.values()) # 4049 (of 4343)
F.english_languages()[:4] # ['en', 'en_Latn', 'eng', 'eng_Latn']: SpaCyTokenizer("en")
Then:
- If
native=True, passlanguage=. Nothing else is needed (no datatrove, no spaCy), and the work runs in Rust on every thread. - If
native=False, passlanguage=andpython_tokenizer=True, with datatrove 0.10.1 and the segmenter installed. TheNotImplementedErroryou get without them gives thepip installline. (For the Japanese codes,pip install sudachidict-coremakes them native instead, and for the Thai codespip install pythainlp.) Each document is tokenized by datatrove's own tokenizer, in Python, on the calling thread, so the call runs at that tokenizer's speed and does not scale withthreads=. On 8 cores datatrove's 8 processes can beat it (Speed). For a large corpus in such a language, run several processes, or keep datatrove for those steps. - If the code is not in the table, the call raises datatrove's
ValueError. Pass a code datatrove knows, or a tokenizer object (below).
language= may also be a word tokenizer object, as datatrove allows: a datatrove WordTokenizer
instance, or any object with word_tokenize(text) (and span_tokenize(text) for C4). It is
called in Python, as on the python_tokenizer=True path and with the same FallbackWarning, but
needs no python_tokenizer=True.
What takes language=¶
| API | language= |
python_tokenizer= |
|---|---|---|
pyroid.filters: gopher_quality, gopher_repetition, c4_quality, c4_lines, fineweb_quality, their *_reasons, and the configs (GopherQuality(language=...), ...) |
any datatrove code, or a tokenizer object | yes |
pyroid.filters.apply |
each step's own language |
yes, for every step |
pyroid.dedup: minhash_signatures, minhash_dedup, MinHashDedup |
any datatrove code, or a tokenizer object | yes |
pyroid.dedup.sentence_dedup, SentenceDedup |
native codes only (a str) |
no: other codes raise NotImplementedError |
pii_replace, symbol_lines, URLFilter, url_filter |
no tokenizer | — |
c4_badwords, BadWordsFilter |
languages=: each document's word-list language (datatrove's doc.metadata["language"]) |
— |
What python_tokenizer=True does, from the modules' documentation (pyroid/filters.pyi,
pyroid/dedup.pyi):
- Each document is tokenized by datatrove's own tokenizer for
language, in Python, on the calling thread. It needs datatrove 0.10.1 and the segmenter installed, andpyroid.PyroidErrorsays what is missing. - The rest runs in Rust on those words, so the decisions are datatrove's. That is the filter
math, or
simplify_text, shingling, MinHash, bucketing and clustering.threads=applies to the Rust part only. - The filters only tokenize the documents that reach a word-based check.
- A
C4Qualitystep (which splits sentences with the tokenizer) runs as datatrove's ownC4QualityFilter. Inapply, later steps then see (and tokenize again) its rewritten text, as in a datatrove pipeline, and consecutive steps with the same tokenizer share one tokenization per document. - Each function emits one
pyroid.FallbackWarningper language (C4 steps their own;MinHashDeduponce, when the index is made). - It also works for native codes, where it uses datatrove's tokenizer. That is slower and no more exact, so it is only needed for the others.
Sentence dedup has no Python-tokenizer path. A code whose datatrove tokenizer pyroid does not
port raises NotImplementedError, unless no tokenizer is needed (split_sentences=False and
min_doc_words, min_num_sentences and min_words_to_remove_span all <= 0). The datatrove
sentence-dedup steps in pyroid.integrations.datatrove run datatrove's own code
for such codes, with a FallbackWarning.
The datatrove drop-in steps
In pyroid.integrations.datatrove, the Gopher, C4, FineWeb, MinhashDedupSignature and
sentence-dedup steps run in Rust for every native code. For the other codes they run
datatrove's own code, with a FallbackWarning. The drop-ins have no python_tokenizer=True
path. See the datatrove guide.
Native languages: German and French¶
FineWeb-2 sets thresholds and stop words per language; pass them as you would to datatrove:
import pyroid.dedup as D
import pyroid.filters as F
de = [
"Die Stadt liegt am Ufer des Flusses und war seit dem Mittelalter wichtig für Kaufleute aus "
"vielen Ländern. Heute leben hier etwa achtzigtausend Menschen. Die Altstadt mit ihren "
"engen Gassen und alten Häusern zieht jedes Jahr viele Besucher an. Im Sommer finden auf "
"dem Marktplatz Konzerte statt, und im Winter gibt es einen kleinen Weihnachtsmarkt. Der "
"Bahnhof liegt nicht weit vom Zentrum entfernt, und die Züge fahren regelmäßig in die "
"größeren Städte der Region. Wer die Umgebung erkunden möchte, kann Fahrräder mieten und "
"den Wegen am Fluss folgen.",
"Jetzt kaufen! Jetzt kaufen! Jetzt kaufen!",
]
stop_words = ["der", "die", "das", "und", "mit", "nicht", "es", "in", "zu", "den"]
F.gopher_quality_reasons(de, language="deu_Latn", stop_words=stop_words)
# [None, 'gopher_short_doc']
fr = [
"Le village se trouve au pied de la montagne, à quelques kilomètres de la frontière. "
"On y vient pour les randonnées en été et pour le ski en hiver. L'église date du "
"douzième siècle et le marché a lieu chaque samedi matin devant la mairie. "
"Les habitants parlent volontiers de l'histoire de leur région, qui a longtemps vécu "
"de l'agriculture et de l'élevage. Aujourd'hui, le tourisme est devenu la principale "
"source de revenus, mais les fermes n'ont pas disparu.",
]
result = F.apply(
fr,
[
F.GopherRepetition(language="fra_Latn"),
F.FineWebQuality(language="fra_Latn"),
F.GopherQuality(language="fra_Latn", stop_words=["le", "la", "de", "et", "les"]),
],
)
result.keep # [True]
near = [de[0], de[0].replace("achtzigtausend", "neunzigtausend"), fr[0]]
D.minhash_dedup(near, language="deu_Latn") # [True, False, True]
datatrove's GopherQualityFilter(language="deu_Latn", stop_words=stop_words) gives the same two
reasons, and the same French chain keeps the document. The MinHash signatures are datatrove's.
Chinese¶
cmn_Hani and the other SpaCyTokenizer("zh") codes are native. datatrove segments them with
jieba through spaCy, and pyroid runs the same segmentation in Rust, so neither datatrove nor
jieba is needed:
import pyroid.dedup as D
import pyroid.filters as F
zh = [
"这座城市位于河边,自古以来就是重要的商业中心。今天,这里大约有八万人口。"
"老城区的街道很窄,房子也很古老,每年都吸引很多游客。夏天,广场上有音乐会;"
"冬天,有一个小小的圣诞市场。火车站离市中心不远,火车定期开往附近的大城市。"
"想要了解周边地区的人,可以租一辆自行车,沿着河边的小路骑行。",
]
zh.append(zh[0].replace("八万", "九万")) # a near-duplicate
F.fineweb_quality(zh, language="cmn_Hani") # [True, True]
D.minhash_dedup(zh, language="cmn_Hani") # [True, False]
The words are those of jieba 0.42.1 with its default dictionary, as datatrove gets them from an
unmodified jieba. Words added with pyroid.jieba.add_word or load_userdict do not change them.
To tokenize with a customised jieba, pass python_tokenizer=True and change jieba itself.
Japanese¶
jpn_Jpan and the other SpaCyTokenizer("ja") codes (ja, ja_Jpan, jpn) are native when a
SudachiDict dictionary is installed. datatrove segments them with SudachiPy in split mode A,
through spaCy. pyroid ports SudachiPy 0.7.0's tokenizer and datatrove's handling around it
(40,000-byte chunks, the alignment of morphemes with the text, the sentencizer) to Rust, so
neither datatrove, spaCy nor SudachiPy is needed, only the dictionary:
$ pip install sudachidict-core
import pyroid.dedup as D
import pyroid.filters as F
F.languages()["jpn_Jpan"] # Language(tokenizer='SpaCyTokenizer', arg='ja', native=True)
ja = [
"この町は川のほとりにあり、昔から商業の中心として栄えてきました。今では約八万人が暮らしています。"
"旧市街の通りは狭く、古い家が並び、毎年多くの観光客が訪れます。夏には広場で音楽会が開かれ、"
"冬には小さなクリスマスマーケットが立ちます。駅は中心部から近く、近くの大きな町へ電車が定期的に走っています。"
"周辺を見て回りたい人は、自転車を借りて川沿いの小道を走ることができます。",
]
ja.append(ja[0].replace("八万", "九万")) # a near-duplicate
F.fineweb_quality(ja, language="jpn_Jpan") # [True, True]
D.minhash_dedup(ja, language="jpn_Jpan") # [True, False]
pyroid does not ship the dictionary: SudachiDict core's system.dic is 202 MB (the
sudachidict-core wheel is 77 MB). pyroid reads the system.dic of the installed
sudachidict-core, version 20261015 or newer (the SudachiPy 0.7 format), and finds the package
without importing it or SudachiPy. If PYROID_SUDACHI_DICT is set, the file it names is read
instead. The dictionary is read once per process, into memory (about 200 MB), the first time a
filter pipeline, MinHash or sentence dedup is built for a Japanese code. A dictionary in an older
format raises ValueError, and a path that cannot be read OSError. languages() does not open
the file, so with PYROID_SUDACHI_DICT naming a file that cannot be read, the Japanese codes still
show native=True and the datatrove steps do not fall back to datatrove's code. Parity was
checked with sudachidict-core 20261015 (Parity).
One error differs in type: where SudachiPy's input limit stops datatrove (a 40,000-byte chunk
whose normalization passes 65,535 bytes), datatrove raises sudachipy.errors.SudachiError and
pyroid raises ValueError with the same message.
Without a dictionary these codes are not native: they raise NotImplementedError, whose message
says to install sudachidict-core, unless you pass python_tokenizer=True as in
the Python tokenizer section.
Vietnamese¶
vie_Latn and the other SpaCyTokenizer("vi") codes are native, with nothing to install.
datatrove segments them with spaCy's VietnameseTokenizer, which labels syllables with pyvi
0.1.1's CRF and joins them into words ("Việt Nam", "Hồ Chí Minh"). pyroid ports the syllable
pattern, pyvi's features and spaCy's joining rules to Rust and bundles pyvi's model and n-gram
list (MIT, 1 MB), so neither datatrove, spaCy, pyvi nor scikit-learn is needed:
import pyroid.dedup as D
import pyroid.filters as F
vi = [
"Thị trấn nằm bên bờ sông và từ thời Trung cổ đã là nơi buôn bán quan trọng của các thương "
"nhân đến từ nhiều nước. Ngày nay ở đây có khoảng tám mươi nghìn người sinh sống. Khu phố cổ "
"với những con đường hẹp và những ngôi nhà cũ thu hút rất nhiều du khách mỗi năm. Vào mùa hè, "
"trên quảng trường có các buổi hòa nhạc, còn vào mùa đông có một chợ Giáng sinh nhỏ. Nhà ga "
"không xa trung tâm, và tàu hỏa chạy thường xuyên đến các thành phố lớn trong vùng. Ai muốn "
"khám phá vùng xung quanh có thể thuê xe đạp và đi theo những con đường nhỏ dọc bờ sông.",
]
vi.append(vi[0].replace("dọc bờ sông", "dọc bờ hồ")) # a near-duplicate
F.fineweb_quality(vi, language="vie_Latn") # [True, True]
D.minhash_dedup(vi, language="vie_Latn") # [True, False]
Case tests and lowercasing in pyvi's features follow the running interpreter's Unicode version, as they do in Python.
Thai¶
tha_Thai and the other ThaiTokenizer codes are native when PyThaiNLP is installed. datatrove
splits words with PyThaiNLP's newmm-safe (maximal matching over its word list, within Thai
character clusters, in chunks for long texts) and sentences with crfcut (a CRF over newmm's
words). pyroid ports both, and the CRFsuite tagger crfcut uses, to Rust. It does not ship
PyThaiNLP's word list (words_th.txt, CC0) or sentence model (sentenceseg_crfcut.model,
CC-BY-4.0): it reads those of the installed pythainlp, which datatrove's Thai tokenizer needs
anyway, so the words follow the word list of the PyThaiNLP you have:
$ pip install pythainlp
import pyroid.dedup as D
import pyroid.filters as F
th = [
"เมืองนี้ตั้งอยู่ริมแม่น้ำ และเป็นศูนย์กลางการค้าที่สำคัญมาตั้งแต่สมัยโบราณ "
"ปัจจุบันมีผู้คนอาศัยอยู่ราวแปดหมื่นคน ย่านเมืองเก่ามีถนนแคบและบ้านเรือนเก่าแก่ "
"ซึ่งดึงดูดนักท่องเที่ยวจำนวนมากทุกปี ในฤดูร้อนมีการแสดงดนตรีที่ลานกลางเมือง "
"ส่วนในฤดูหนาวมีตลาดคริสต์มาสเล็ก ๆ สถานีรถไฟอยู่ไม่ไกลจากใจกลางเมือง "
"และมีรถไฟวิ่งไปยังเมืองใหญ่ในภูมิภาคเป็นประจำ "
"ผู้ที่อยากสำรวจพื้นที่โดยรอบสามารถเช่าจักรยานและปั่นไปตามทางเลียบแม่น้ำได้",
]
th.append(th[0].replace("แปดหมื่น", "เก้าหมื่น")) # a near-duplicate
# Thai lines rarely end in punctuation; FineWeb-2 turns that check off for Thai.
F.fineweb_quality(th, language="tha_Thai", line_punct_thr=0) # [True, True]
D.minhash_dedup(th, language="tha_Thai") # [True, False]
pyroid finds the package without importing it and reads corpus/words_th.txt and
corpus/sentenceseg_crfcut.model once per process (5.6 MB), the first time a filter
pipeline, MinHash or sentence dedup is built for a Thai code. If PYROID_PYTHAINLP_CORPUS is
set, the files of the directory it names are read instead. A file that cannot be read raises
OSError, and a model that is not a CRFsuite model ValueError. The algorithms are PyThaiNLP
5.4.0's, and parity was checked with PyThaiNLP 5.4.0 (Parity); newer releases change
the word list, which pyroid then reads.
Without PyThaiNLP these codes are not native: they raise NotImplementedError, whose message says
to install pythainlp. (datatrove's own Thai tokenizer also needs python-crfsuite for its
sentences; pyroid does not.)
The Python tokenizer: Khmer with khmer-nltk¶
khm_Khmr is KhmerTokenizer(), which segments with khmer-nltk's CRF. pyroid does not port it.
Without python_tokenizer=True, the call says what to do:
NotImplementedError: pyroid has no native word tokenizer for language 'khm_Khmr': datatrove
tokenizes it with KhmerTokenizer() (khmer-nltk). Pass python_tokenizer=True to tokenize it with
datatrove's own tokenizer in Python, on the calling thread (needs `pip install datatrove==0.10.1
regex khmer-nltk`); the filter math still runs in Rust.
With datatrove 0.10.1 and khmer-nltk 1.6 installed:
km = [
"ភ្នំពេញជារាជធានីនៃប្រទេសកម្ពុជា។ ទីក្រុងនេះស្ថិតនៅលើច្រាំងទន្លេមេគង្គ និងទន្លេសាប។ "
"ប្រជាជនជាច្រើនរស់នៅទីនេះ ហើយភ្ញៀវទេសចរមកលេងរៀងរាល់ឆ្នាំ។",
]
km.append(km[0].replace("រៀងរាល់ឆ្នាំ", "ជារៀងរាល់ឆ្នាំ")) # a near-duplicate
F.fineweb_quality(km, language="khm_Khmr", python_tokenizer=True) # [True, True]
D.minhash_dedup(km, language="khm_Khmr", python_tokenizer=True) # [True, False]
Each call warns once:
FallbackWarning: pyroid.filters.fineweb_quality: tokenizing language 'khm_Khmr' with datatrove's
KhmerTokenizer() (khmer-nltk) in Python, one document at a time on the calling thread; the filter
math runs in Rust. Expect the tokenizer's speed, not pyroid's. This warning is shown once.
An unknown code raises datatrove's error before any document is read:
F.gopher_quality(km, language="und_Zzzz")
# ValueError: Language 'und_Zzzz' doesn't have a tokenizer assigned. Pass in a WordTokenizer
# directly or update tokenizer_assignment.csv
Parity¶
Every check below found 0 differences against datatrove 0.10.1 (spaCy 3.8.18; SudachiPy 0.7.0 with sudachidict-core 20261015 for Japanese; pyvi 0.1.1 for Vietnamese; PyThaiNLP 5.4.0 with python-crfsuite 0.9.12 for Thai; CPython 3.13 for the live checks):
| Check | Rows |
|---|---|
| Tokenizer replay, committed goldens: English (3,559) and 61 per-language files (57 spaCy languages, whitespace, IndicNLP hi/ur/bn) | 43,146 |
| … and, not committed: whole FineWeb-2 slices (ar, de, fr, ru, es, hi), 10k fuzz rows per tokenizer, a 57-language FineWeb-2 smoke set | 662,530 |
| IndicNLP replay, with 8k fuzz rows per CPython 3.11–3.15 (each version matches its own) | 118,690 |
| Whitespace tokenizer replay | 118,990 |
| Filter suites, 8 FineWeb-2 slices × 8 cases: FineWeb-2's 3 filters with that language's thresholds and stop words, and datatrove's defaults including C4 | 16,000 decisions |
| MinHash with FineWeb-2's configuration (xxhash/64) and the language: signatures and removed set, 8 slices × 300 documents with near-duplicates injected | 2,400 + 2,400 |
| Hypothesis against live datatrove: script-specific text, 22 native languages and jieba, SudachiPy, pyvi and PyThaiNLP | 92 tests × 300 examples |
Chinese, native: the cmn_Hani slice's 8 filter cases, and its MinHash signatures and removed set |
2,000 decisions + 300 + 300 |
Chinese word segmentation, pyroid.jieba against jieba 0.42.1 |
580,793 comparisons |
Japanese, native: the jpn_Jpan slice's 8 filter cases, and its MinHash signatures and removed set |
2,000 decisions + 300 + 300 |
Japanese tokenizer replay, committed recordings: SudachiPy's morphemes for 4,567 texts (special cases, FineWeb-2 lines, fuzz, a code-point sample), datatrove's words, spans and sentences for 4,205 texts (54 documents over 40,000 bytes), 1,911 chunk_text_on_bytes cases, the spaCy tag of all 1,558 POS |
12,241 |
… and, not committed: every code point, the FineWeb-2 jpn_Jpan test slice (7,809 lines, 250 documents) and the first 20,000 training documents (32.5 M characters: 610,624 lines with 18,879,619 morphemes, and the documents' words and spans) |
777,691 |
Vietnamese and Thai, native: the vie_Latn and tha_Thai slices' 8 filter cases each, and their MinHash signatures and removed sets |
4,000 decisions + 600 + 600 |
| Vietnamese tokenizer replay, committed recordings: syllables, CRF labels, pyvi's words, and datatrove's words, spans and sentences for 4,697 texts (special cases, FineWeb-2 documents, fuzz, a code-point sample) | 4,697 |
… and, not committed: every code point, the vie_Latn test slice and the first 20,000 training documents (90.9 M characters), 151.5 M compared items |
94,392 |
| Thai tokenizer replay, committed recordings: character clusters, newmm with and without safe mode, crfcut's words and labels, and datatrove's words, spans and sentences for 3,165 texts (special cases, FineWeb-2 documents, fuzz, long texts, a code-point sample) | 3,165 |
… and, not committed: every code point, the tha_Thai test slice, the first 20,000 training documents (60.1 M characters), documents of 100k to 200k characters, synthetic dictionaries that reach newmm's 50-edge cutoff, and CPython 3.13 to 3.15 |
122,667 |
Japanese also has 12 Hypothesis properties against live datatrove
(python/tests/test_filters_japanese.py): 628 examples per run, and 11 of them also at 300
examples each. Vietnamese and Thai have 80 tests against live datatrove
(python/tests/test_filters_vithai.py): every filter, FineWeb-2's chains, words, sentence dedup
and MinHash, with 1,046 Hypothesis examples per run (5,230 in CI).
Sentence dedup has FineWeb-2 parity cases on six slices (deu_Latn, fra_Latn, rus_Cyrl,
spa_Latn, arb_Arab, hin_Deva), for sentences and lines, and Vietnamese and Thai are checked
against datatrove's three stages in test_filters_vithai.py. The Stanza, Kiwi, Lao, Tibetan and
Burmese segmenters were not installed in these runs, so their python_tokenizer=True path is
untested; Khmer's ran for the example above. See Compatibility.
Vietnamese words follow spaCy's quirk with a token that is exactly IS_ALPHA as the other spaCy
tokenizers do: pyroid gives what datatrove gives the first time in a process (see
the filters guide).
Quirks reproduced as datatrove has them:
- In C4 for Hindi and Bengali, indic-nlp re-joins sentences with
" ", so datatrove'stext.indexraisesValueErroron some documents (9 of 250 realhin_Devadocuments). pyroid raises the same. - Urdu sentences (C4 only) follow
datatrove[multilingual], where indic-nlp imports urduhack. With plain datatrove and indic-nlp-library, datatrove raisesModuleNotFoundErrorfor every Urdu sentence call.
spaCy is quadratic on long glued runs of characters (9.6 s for one 4.7 kB text); pyroid is linear there.
Speed¶
These timings come from a Docker/OrbStack VM on Apple Silicon, indicative. They cover FineWeb-2's
filter chain (Gopher repetition, FineWeb quality and Gopher quality, with each language's
settings) and its MinHash signatures, on 1,000 documents per language (each FineWeb-2 slice four
times). "8" is 8 threads for pyroid and a warmed 8-process Pool for datatrove. Medians of 5
runs (datatrove: 2), with every timed document checked first for the same decisions and
signatures. Release wheel, CPython 3.13, load average 6 to 16:
| Language | Tokenizer | Filters, 1 thread (µs per document, datatrove → pyroid) | Speedup: filters / MinHash, 1 thread | 8 threads vs 8 processes |
|---|---|---|---|---|
deu_Latn |
native | 8,342 → 122 | 68× / 53× | 54× / 55× |
fra_Latn |
native | 16,853 → 195 | 86× / 64× | 15×* / 42× |
spa_Latn |
native | 14,553 → 218 | 67× / 51× | 28× / 31× |
rus_Cyrl |
native | 16,478 → 457 | 36× / 36× | 55× / 42× |
arb_Arab |
native | 8,225 → 145 | 57× / 49× | 42× / 38× |
hin_Deva |
native (IndicNLP) | 2,514 → 126 | 20× / 10× | 15× / 9× |
cmn_Hani |
native (jieba)† | 10,628 → 242 | 44× / 44× | 43× / 42× |
jpn_Jpan |
native (SudachiPy port)‡ | 11,210 → 1,444 | 7.8× / 6.4× | 9.2× / 6.9× |
vie_Latn |
native (pyvi port)§ | 18,203 → 839 | 22× / 25× | 27× / 34× |
tha_Thai |
native (PyThaiNLP port)§ | 9,045 → 704 | 13× / 15× | 14× / 10× |
* An outlier: pyroid with 8 threads was slower than with 1 thread in this run (246 µs per document) while the load rose. It needs a rerun before it is quoted.
† Measured on 2026-11-07, after the jieba port, at load average 1 to 2. Before it, cmn_Hani went
through python_tokenizer=True, at 1.2× / 0.9× on one thread and 0.4× with 8.
‡ Measured on 2026-11-09, after the SudachiPy port, at load average 0.6 to 1.9. Before it, jpn_Jpan
went through python_tokenizer=True, at 1.2× / 0.9× on one thread and 0.3× / 0.2× with 8.
Japanese gains less than the other native languages because SudachiPy's lattice dominates on both
sides: pyroid's whole MinHash (1,151 µs per document, one thread) takes less time than SudachiPy's
own tokenize() call on the same chunks (1,583 µs).
§ Measured on 2026-11-09, after the pyvi and PyThaiNLP ports, at load average 2.1 at the start
(7.4 after the 8-worker runs). Before them, both went through python_tokenizer=True, at 1.5× /
1.5× (Vietnamese) and 1.5× / 1.6× (Thai) on one thread and 0.4× / 0.2× and 0.6× / 0.7× with 8;
that run overlapped other builds on the VM (load average 3.5 to 23), so only its ratios are given.
The FineWeb-2 Vietnamese documents are long (4,278 characters on average); tokenizing them takes
about 320 µs per document of pyroid's 839.
The native languages run the filters 36–86× faster than datatrove on one thread (Vietnamese: 22×, Hindi: 20×, Thai: 13×, Japanese: 7.8×). The Python-tokenizer path runs at about the reference tokenizer's speed (1.2–1.5×, measured for Chinese, Japanese, Vietnamese and Thai before their ports) and does not scale with threads, so datatrove's 8 processes beat it (0.2–0.7×). Adding the languages did not slow English down: old and new builds measured within noise on 5,000 FineWeb documents. See Benchmarks.
Binary size¶
The language tables add 1.11 MB to the release extension module (fat LTO, stripped, aarch64). It
grew from 3.29 to 4.40 MB, or from 1.20 to 1.46 MB gzipped. Chinese adds
pyroid.jieba's dictionary and HMM tables: 3.0 MB more (5.58 to 8.60 MB), and 1.5 MB
more in the wheel. Japanese adds the SudachiPy port and SudachiPy's resource files (char.def,
unk.def, rewrite.def, Apache-2.0), but not the dictionary: the release extension module
(aarch64, abi3) grew from 9.19 to 9.32 MB, and the wheel by 65 KB.
Vietnamese and Thai add the CRFsuite reader, the two ports and pyvi's bundled model and word list
(0.99 MB of it), but not PyThaiNLP's data: the release extension module grew from 9.32 to 10.44 MB
(+1.12 MB), and the wheel from 4.02 to 4.45 MB.