Boundary-marker pretokenization: experiments, paper draft, and downstream LM setup - #7
Closed
sanderland wants to merge 83 commits into
Closed
Boundary-marker pretokenization: experiments, paper draft, and downstream LM setup#7sanderland wants to merge 83 commits into
sanderland wants to merge 83 commits into
Conversation
Explores replacing the leading-space convention with an explicit boundary marker token <|>, so a word has one canonical form regardless of whether a space preceded it. The single space between two adjacent markable spans is elided at encode time and reconstructed at decode time from the resulting pair of touching markers. The baseline scriptenc3_cb spends 20-43% of its vocabulary on ' X'/'X' duplicate pairs (64% of emitted tokens at 16k on FineWeb). Marking words alone costs 7.5% compression; extending marking to punctuation brings that to 1.0-1.6%; extending it to digits overtakes the baseline, reaching +1.17% chars/token at 64k on a mixed prose+code corpus with duplicate pairs down from 13,480 to 120 and faster training. Contents: - scriptenc_marker_v4.py: words marked unconditionally, punctuation marked only on a side whose single space was elided (the asymmetry is what keeps decoding unambiguous). Includes the word-adjacency fix for two different word scripts meeting with no space, which produced phantom spaces on Greek-letter identifiers in code. - scriptenc_marker_v5.py: v4 plus digits, which account for ~98% of the single spaces v4 fails to elide. - multilang_grid.py: FineWiki grid over the hybrid/ 6-language set, resumable. - paper.md, prior_results.json, multilang_result.json: write-up and measurements. Prototypes only; no existing code paths are touched. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
The mixed prose+code grid only showed v5's crossover for a tokenizer trained on mixed data, leaving open whether it survives without code in the training set. FineWiki English (prose only, normalize_whitespace as the registry applies it) answers that: v5 is ahead at every vocabulary size, +1.46% / +2.63% / +3.16% chars/token at 16k / 32k / 64k BPE and +2.89% at 64k MinGram, with duplicate pairs down from plain's 1,381-6,767 (15.6-20.6% of vocabulary) to 4-6. Also corrects a limitation in the write-up: the crossover point is corpus-dependent, not a general property. On mixed prose+code v5 needs 32k to overtake the baseline; on FineWiki English it is already ahead at 16k. Remaining five languages (de, fi, ru, ar, ko) still running. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Full grid over the hybrid/ language set (en, de, fi, ru, ar, ko - three Latin plus Cyrillic, Arabic and Hangul), three vocabulary sizes, BPE throughout and MinGram at 64k on one language per script family. 63 cells, zero roundtrip failures. v5 (words + punctuation + digits) beats the baseline in all six languages at 32k and 64k, and five of six at 16k, averaging +0.67% / +1.88% / +2.74% chars/token at 16k / 32k / 64k. v4 (no digit marking) is uniformly negative, -2.52% to -3.37% on average. The margin grows monotonically with vocabulary in every language, and MinGram reproduces the BPE ordering. Korean is the hardest case and worth noting: it carries the largest duplicate tax to remove (31.0% of a 64k vocabulary) yet gains the least (+1.01% at 64k, -1.28% at 16k). Hangul syllable blocks give it the lowest absolute compression of the six (2.28 vs 4.05-4.40 chars/token), so word spans are short and the two marker tokens are proportionally larger overhead. Added to limitations, along with the sub-1GB per-language budget. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Replaces the v4/v5 prototypes with one BoundaryScriptPretokenizer whose
boundary_targets config selects which unit kinds are delimited: bnd_w (words),
bnd_wp (words+punctuation), bnd_wpd (words+punctuation+digits). All are
ScriptEncodingV3 with enforce_char_boundaries, differing only in the boundary
setting, so comparisons isolate the scheme. Distinct configs also give distinct
hash() values, fixing the corpus-cache collision the earlier prototypes had.
Key design change: a word span is now a maximal run of characters from ANY
space-using script, merged across script changes, and delimited only at its
outer edges. So 'latin' followed directly by Cyrillic is one span, with no
marker between them:
latin<Cyrillic> -> <|>latin | <Cyrillic><|>
This removes the previous adjacency special case. Two unconditionally-delimited
word spans can no longer be adjacent, so a touching-marker pair is unambiguously
an elided space with no exception needed, and words keep one canonical form. The
internal script split is preserved (the marker rides the first and last chunk),
so no BPE merge crosses a script change the baseline would forbid.
Adds 412 tests covering roundtrip over mixed-script/CJK/combining-mark text and
tests/data/taylorswift.txt, the no-triple-marker and touching-pair invariants,
span merging, per-target marking behaviour, multi-space runs left untouched, the
merge constraint, and hash distinctness.
Also adds the FineWiki 1GB x 6-language x 4-pretokenizer x {bpe,mingram} runner
at the paper's 32,768 vocabulary. It commits and pushes every finished cell with
its trained tokenizer, runs BPE across all languages before MinGram, and skips
completed cells, so the ~15-20h run survives container wipes and resumes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
The previous run wiped the working tree partway through English. Cause was
peak disk, not the training itself: staging 1 GB of normalized text per language
under text_blocks/ (978 MB for English alone) plus corpora for all six languages
exceeds the session's disk allowance.
Two changes:
* no text staging at all. from_text_batches now consumes a generator reading
parquet row groups directly, so only the pretokenized corpora touch disk.
Costs one ~186s re-read per pretokenizer, which is cheap next to training.
* one language at a time, both trainers, then its corpora are deleted. Peak
disk is now four corpora for a single language (~200 MB) rather than
twenty-four plus 6 GB of text.
Reordered from trainer-major to language-major as a consequence, so an
interrupted run yields complete results for whole languages instead of one
trainer across all six.
The per-cell commit-and-push held up under the wipe: en_plain_bpe survived with
its trained tokenizer. At 1 GB and 32,768 vocabulary it gives 3.8310 chars/token,
2,350,831 unique chunks, 152s to train, 3,196 duplicate pairs occupying 18.5% of
the vocabulary, 0 roundtrip failures.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
The container clears the working tree roughly every 30-60 minutes. A cell that takes longer than that interval can never finish, so ordering matters: BPE cells run ~8 minutes each and always complete, MinGram cells run ~25-50 minutes at 1 GB and are marginal. Switched from language-major to trainer-major so all 24 BPE cells finish across all six languages before any MinGram cell is attempted. Corpora are still freed per language and rebuilt for the MinGram pass, which costs ~300s each - cheap next to the training it feeds. First 1 GB comparison, en at 32,768 vocabulary, BPE: plain 3.8310 ch/tok, 3,196 duplicate pairs (18.5% of vocab), 27,729 words bnd_w 3.2190 ch/tok (-15.97%), 4 duplicate pairs, 15,493 words Marking words alone is much weaker here than the earlier per-script prototype was at 80M/16k (-7.5%). Consistent with the mechanism: with only words marked, a space following punctuation cannot be elided and becomes its own token, where the baseline absorbs it into the next word. bnd_wp and bnd_wpd target exactly that. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
A run died partway through English with aiohttp ClientResponseError 503 from the
HF xet CDN during a parquet row-group read. Neither a disk wipe nor OOM - just a
transient service error with no retry path.
* row-group reads now retry with exponential backoff, reopening the parquet
handle between attempts, since the underlying HTTP file object does not
recover on its own.
* opening the parquet file retries the same way.
* each grid cell is wrapped so a failure logs and moves on instead of aborting
the remaining cells. A skipped cell never enters results, so the next run
retries it.
en_bnd_wp_bpe completed before the failure: 3.7199 ch/tok at 1 GB / 32,768 vocab,
-2.90% against the baseline, versus bnd_w's -15.97%. Marking punctuation recovers
most of what words-only gives up, which is what the space-after-punctuation
mechanism predicts.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Arabic streamed 483,519,844 characters instead of the intended 1 GB because the reader only ever opened shard 000_00000. Arabic has 4 shards (3.67 GB total) and Korean has 2 (2.19 GB); their first shards hold roughly 483M and 734M characters respectively. English, German, Finnish and Russian were unaffected because their first shard alone already exceeds 1 GB, so the four completed languages are unaffected and their results stand. stream_batches now lists a language's shards via the dataset tree API and reads them in order until CHARS_PER_LANG is reached. Shard listing and opening both retry with backoff, as row-group reads already did. No Arabic cell had been committed when this was caught, so nothing needed invalidating; its cached eval slice was dropped so it is rebuilt from the full 1 GB stream. BPE results so far, 1 GB per language at 32,768 additional vocabulary: lang plain bnd_w bnd_wp bnd_wpd duplicate pairs en 3.8310 -15.97% -2.90% +3.77% 3196 -> 4 de 4.1110 -12.56% -3.59% +1.60% 3226 -> 4 fi 3.9836 -12.64% -2.94% +2.02% 3835 -> 4 ru 3.7955 -13.72% -2.35% +2.67% 3297 -> 5 The three-step progression is identical in every language: words alone lose heavily, punctuation recovers most of it, digits push past the baseline. Zero roundtrip failures throughout. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
24 cells at 250M characters: en, ru, ko x four pretokenizers x {BPE, MinGram},
evaluation withheld. The 1 GB MinGram half could not complete - ~26 min per cell
against a ~30-60 min wipe interval left one cell of 24 standing - so the
comparison was rerun at a scale where cells finish.
lang trainer plain bnd_w bnd_wp bnd_wpd
en bpe 3.7580 -15.24% -3.42% +3.38%
en mingram 3.8019 -15.64% -3.78% +3.02%
ru bpe 3.8094 -11.66% -1.35% +2.39%
ru mingram 3.8636 -12.02% -1.63% +2.27%
ko bpe 2.1793 -17.04% -4.23% -0.67%
ko mingram 2.2055 -17.79% -5.05% -1.51%
MinGram gains +1.17/+1.42/+1.20% over BPE on the baseline and preserves the
bnd_w < bnd_wp < bnd_wpd ordering everywhere, so the effect is not a BPE
artifact. It helps the baseline slightly more than bnd_wpd, costing 0.1-0.8pp of
margin.
Korean changes sign with scale: +0.88% at 1 GB, -0.67% at 250M. Hangul gives it
the shortest spans of the six languages, so two marker tokens per span are
proportionally heavy and need more data to amortise. The abstract's "all six
languages" claim is now explicitly scoped to 1 GB, and the limitation on short
spans records that the gain can be negative.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
paper.md had no citations and was not in ACL format. Adds acl_latex.tex and custom.bib under marker_experiments/paper/, with paper.md kept as the working draft. Content follows the Markdown draft with three changes: an Introduction that frames the contribution as the choice of WHICH units to delimit rather than the marker mechanism itself; citations throughout; and the mixed prose+code results compressed to a paragraph since they come from the earlier per-script variant. Tables use booktabs and are numerically unchanged. Two caveats are recorded in the directory README rather than left implicit. The source has never been compiled. The container has no TeX toolchain, and the ACL style files could not be fetched (GitHub raw 404, API unreachable), so acl.sty and acl_natbib.bst must be downloaded separately. Structure is validated only by script: brace balance, matched environments, and every \cite key resolving against the bib. The bibliography is unverified. There was no access to the ACL Anthology or any bibliographic database, so entries are marked [repo] when taken from this repository's own source comments, [standard] when widely cited and believed correct, and [check] when reconstructed from memory. The [check] entries - GPT-2, Noiseless Channel, FineWeb, FineWiki, CodeParrot, Rosetta Code, Llama 2 - need author lists, venues and years confirmed. A bibliography of plausible-looking wrong entries is worse than an incomplete one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Reading paper_utils/ and README.md surfaced the three papers this line of work
already has, and two citation errors in the first draft of the bibliography.
BPE Stays on SCRIPT Land & Arnett arXiv 2505.24689
Which Pieces Does Unigram Tokenization Really Need?
Land & Pinter arXiv 2512.12641
MinGram Land arXiv 2606.27019
Titles, author lists and dates were fetched from arXiv and are verified.
Corrections:
* MinGram was attributed to Schmidt et al. It is Land (2026). Schmidt et al.
(2024) is PathPiece, whose minimum-increase pruning rule MinGram's optional
prune criterion follows; both are now cited for the right things.
* SCRIPT was not cited at all, despite the baseline throughout being its
scriptenc3_cb pretokenizer. Now cited in the introduction and in the section
that describes what the scheme modifies.
* The unigram-ablation paper is cited where the trainer's pruning and
overshoot machinery is discussed.
Style now follows this repository's own table generators: booktabs with
\cmidrule group separators, a \relchange macro for relative-change columns,
languages spelled out rather than ISO codes, and method names matching the
MinGram tables. The six-language set is attributed to this line of work rather
than introduced as if new.
Structure validated by script: 17 bib entries, 17 cited, none undefined, none
unused, braces balanced, environments matched. Still never compiled - no TeX
toolchain here.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
…less BPE
Replaces the hand-written bibliography with the supplied curated one, verbatim,
using its ACL Anthology keys throughout. 59 entries, 34 cited, no undefined
keys.
Reading the supplied references surfaced prior art the draft was missing
entirely, so a Related Work section was added:
* SuperBPE and Boundless BPE both extend tokens ACROSS the pre-tokenization
boundary. This work goes the other way - pre-tokens stay word-sized and the
boundary is made explicit - which is worth stating directly rather than
leaving a reader to wonder. Noted that the two are complementary rather than
competing, and that the combination is not evaluated here.
* PickyBPE, Scaffold-BPE and Magikarp all remove vocabulary that exists only
as merge scaffolding. The duplication measured here is a different
phenomenon - both members of a ' the'/'the' pair are heavily used - but the
motivation is shared, and the digit-handling result shows reclaiming unused
capacity does not by itself buy compression.
* The compression-as-proxy debate is now cited on both sides.
Also: MinGram's dynamic-program segmentation versus BPE's greedy replay is
attributed to Uzan et al.; the Korean scale result points to jamo-level work and
the byte-premium fairness literature; limitations cite the morphological
alignment axis that is likewise unmeasured.
Three claims had no entry in the supplied bibliography and were rewritten rather
than left resting on invented references: Gage's original BPE now rests on
Sennrich et al. alone, GPT-2 byte-level BPE is replaced by a pre-tokenization
reference that fits the claim better, and the digit-splitting rationale is
stated without attributing it to a specific model. Four dataset/tool entries not
in the supplied file are appended and marked unverified.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
The paper used one marker glyph for both sides of a span without saying why that is sound, which made the design look more general than it is. Added a scope subsection: a single glyph works here only because pretokenization fixes which chunk each marker belongs to before any search runs, so neither BPE merging nor the MinGram dynamic program is ever offered a choice about which piece covers a marker. Where a segmenter tiles a flat glyph stream instead - a minimum-piece method with no pre-tokenization barrier, as reported for at least one production tokenizer - a single glyph is ambiguous, because a word-initial piece form can absorb a marker that closed the preceding word and the segmenter takes whichever tiling is cheaper. Direction then has to come from a distinct pair of glyphs or a directional matching constraint. Also recorded what a second glyph would and would not buy: it costs one atomic token and no extra piece-form variants, since order already distinguishes the two forms, but it removes neither the seam restriction (a token can still end in a dangling opening marker) nor the need for span merging (an opening marker meeting a closing one is still indistinguishable from an elided space). One glyph versus two is a consequence of pretokenizing into chunks, not an independent simplification. Limitations now note that the scheme is untested under a flat-stream segmenter. Adds tokencontributions-claude to the bibliography, marked unverified for author and year. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
…izer
A title-case span is emitted as a shift code plus its lowercased form and an
all-caps span as a caps-lock code plus its lowercased form, so 'The'/'the' and
'NASA'/'nasa' share vocabulary entries. Whole spans only; mixed case such as
'GaN' or 'WiFi' is left literal. Off by default, and it changes the config hash
so cached corpora cannot collide.
The cat -> <|><^>the<|> | <|>cat<|>
NASA rocket -> <|><^^>nasa<|> | <|>rocket<|>
GaN WiFi -> <|>GaN<|> | <|>WiFi<|>
This is the same duplication argument as the leading space, applied to case, so
the code sits inside the span markers and 'The' is exactly 'the' plus one code.
Invertibility is verified rather than assumed, because Unicode case mapping is
not a bijection: U+0130 lowercases to two characters, U+1E9E uppercases to 'SS',
and titlecase digraphs such as U+01C5 do not survive a lower/upper round trip.
Every candidate is re-transformed and compared, and anything that does not
reproduce the source exactly stays literal.
Round-trip checked three ways, all clean:
* every cased letter in Unicode - 2,842 characters, three word shapes each,
0 failures;
* adversarial cases including Greek final sigma, titlecase digraphs and
combining marks, 17/17;
* real FineWiki text in English, German, Russian, Greek and Turkish - 2,000
documents, 4.7M characters, 0 failures. Turkish and Greek were included
deliberately, being where dotted/dotless i and final sigma live.
599 tests pass, including caps combined with digit splitting.
Compression is not yet measured; this commit is the design and its correctness.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
en, 250M characters, 32,768 vocabulary, BPE, evaluation withheld - the same
setup as the digit axis, so the plain cell there is directly comparable.
Compares bnd_wpd with and without caps codes.
Alongside chars/token it measures the case-duplication tax: vocabulary entries
whose decoded text has a distinct-cased counterpart also present ('The'/'the'),
the case analogue of the ' X'/'X' pair count that motivates the whole scheme.
That number decides whether the waste being reclaimed is large enough to matter,
independently of whether reclaiming it pays.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Auto-committed per cell so partial progress survives container wipes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
en, 250M characters, 32,768 vocabulary, BPE, evaluation withheld. variant chars/tok vs plain case pairs alpha entries plain 3.7580 - - - bnd_wpd 3.8849 +3.38% 5017 26661 bnd_wpd + caps 3.8832 +3.33% 91 21819 Case duplication falls from 5,017 pairs to 91 - 29.1% of the vocabulary to 0.5% - for -0.04% compression, which is noise. Zero roundtrip failures, identical training time, and unique chunk count essentially unchanged. This is the third instance of the same pattern in this work: reclaiming vocabulary does not convert into compression. Here it also does not cost anything, so a canonical case form is effectively free. It is the reverse of the digit result, where bounding the markable set was necessary to avoid a penalty - case has only two productive forms per word, so the duplication is bounded to begin with, whereas a digit run has one form per distinct number. Adds the experiment script, its results, the trained tokenizers, and a paper section. Also commits the non-ASCII numeric guard and caps tests that were sitting uncommitted after a working-tree wipe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
The compression results say nothing about language modelling, which is the paper's principal limitation. This wires the boundary tokenizers into the existing pynanochat harness so someone with GPUs can close that gap. Four things the harness needed: boundary_tokenizer.py -- Pretokenizer.REGISTRY is filled by __init_subclass__, so a fresh subprocess doing BPETokenizer.load on a boundary model raises KeyError: 'BoundaryScriptPretokenizer'. This module imports for the registration side effect and re-exports the tokenizer classes unchanged (BoundaryBPETokenizer is BPETokenizer), so the fix is one --tokenizer-class flag rather than a subclass. train_matched.py -- the grid fixed additional_vocab_size=32768, so the arms have different TOTAL vocabularies (34478/34479/34481): right for a compression control, wrong downstream where vocabulary sets the parameter count and hence the token horizon. This matches the total (default 34,685, the MinGram downstream table's) and exits non-zero if the arms disagree. smoke_test.py -- every check that does not need a GPU: fresh-process load for both serialisations, the pynanochat.Tokenizer contract, dense-id space and synthetic BOS, the uint16 bound, adapter round-trip on marker/caps/ digit/mixed-script text, batch-vs-single encode. Clean on all 56 existing tokenizers and on a freshly trained matched set. run_arms.sh + collect_results.py -- resumable train/check/run/collect, and a TSV whose columns match what the paper's table generators already read. The GPU leg is untested: this container has neither torch nor a GPU, so write_token_bytes and everything after it are unexercised. SMOKE=1 covers exactly that, and the README says so plainly. Also fixes a naming collision the smoke test surfaced: caps_grid.py (250M chars) wrote the same filenames as the 1 GB grid and had silently replaced that grid's en_bnd_wpd artifact. Its outputs are now prefixed, and the two affected files are renamed to what they actually are. The 1 GB results are unaffected -- that cell recorded unique_chunks=2,072,665 against 906,491 for the 250M corpus -- and no downstream arm reuses grid artifacts. get_boundary_pretokenizer now also builds the _caps variants and takes config overrides; the three original variants keep their hashes, so cached corpora stay valid. 599/599 boundary tests pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
Someone cloning this needs the branch, not main. Names the repo, the branch and the PR, and says why the editable install is load-bearing: the eval's child processes run with cwd set to the nanochat clone and resolve marker_experiments.* through the editable .pth, so a non-editable install fails at tokenizer load. Adds a no-GPU verification step to run before requesting a node, the disk and CPU budget, the per-arm summary collect_results.py prints, and what to send back. manifest.json is no longer gitignored -- it is the record that the arms were vocabulary-matched, which is the one thing a reader cannot reconstruct from the TSV alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A single atomic token
<|>delimits spans, and the space between two adjacent delimited spans is elided at encode time and reconstructed from the resulting pair of touching markers. This removes the' the'/'the'duplication the leading-space convention creates.Design
Word spans are delimited unconditionally on both sides, so a word has one canonical form regardless of what precedes it. A word span is a maximal run from any space-using script, merged across script changes — without merging, two unconditional markers meet at a script boundary, become indistinguishable from an elided space, and decode fabricates one. Punctuation and digits are delimited only on a side whose single space was elided, which is required for the same reason.
bpe_merge_allowedbans merges across touching markers, so'<|>the<|><|>'can never become a token.Results
1 GB FineWiki, 6 languages × 4 pretokenizers × {BPE, MinGram}, 0 round-trip failures. Boundary markers on words + punctuation + digits (
bnd_wpd) beat the SCRIPT-v3 baseline in all six languages, mean +2.14 % chars/token (en +3.77, de +1.60, fi +2.02, ru +2.67, ar +1.91, ko +0.88). Delimiting words alone is much worse (−13.75 %) — punctuation and digits are what make the scheme pay. Duplicate' X'/'X'pairs drop from 3,159–5,244 to 4–10; the baseline spends 18.3–30.4 % of its vocabulary on them.MinGram (250 M) preserves the ordering, and Korean changes sign with scale (+0.88 % at 1 GB, −0.67 % at 250 M).
Digit handling bounds the markable set. Unbounded, every distinct number acquires up to four marked forms;
RTL3caps it at 1,110 strings andSPLITat 10. Measured on English: None +3.38 %, SPLIT +3.71 %, RTL3 +3.37 % — the tax is real but worth only +0.33 pp.Caps codes (Claude-3 style
<^>/<^^>on whole word spans) cut case-duplicate pairs from 5,017 to 91, i.e. 29.1 % → 0.5 % of the vocabulary, for −0.04 % compression. Which is the recurring finding, now three times over: reclaiming vocabulary does not convert into compression.Downstream LM setup
The compression numbers say nothing about modelling quality, which was the draft's principal limitation.
marker_experiments/downstream/wires the tokenizers into the existingpynanochatharness — the same path the MinGram downstream table used — and adds the four things that path needed:boundary_tokenizer.py—Pretokenizer.REGISTRYis filled by__init_subclass__, so a fresh subprocess callingBPETokenizer.loadon a boundary model raisesKeyError: 'BoundaryScriptPretokenizer'. This module imports for the registration side effect and re-exports the tokenizer classes unchanged (BoundaryBPETokenizer is BPETokenizer), making the fix one--tokenizer-classflag rather than a subclass.train_matched.py— the grid fixedadditional_vocab_size=32768, so the arms have different total vocabularies (34478/34479/34481, since the marker and caps codes are extra atomic tokens). Right for a compression control; wrong downstream, where vocabulary size sets the parameter count and hence nanochat's token horizon. This matches the total instead (default 34,685, the MinGram table's) and exits non-zero if the arms disagree.smoke_test.py— fresh-process load for both serialisations, thepynanochat.Tokenizercontract, dense-id space and synthetic BOS, thevocab <= 65535bound, adapter round-trip on marker/caps/digit/mixed-script text, batch-vs-single encode.run_arms.sh+collect_results.py— resumable train → check → run → collect, producing a TSV whose columns match what the paper's table generators already read.README.mdthere has the full cluster instructions: branch, editable-install requirement, a no-GPU verification step,SMOKE=1pipeline check, the knob table, a Slurm array template, and what to send back.Testing
marker_experiments/test_boundary.py: 599 passed — round-trip (includingtests/data/taylorswift.txt), touching-pair and no-triple-marker invariants, span merging across scripts, per-target behaviour, multi-space runs left untouched, the merge constraint, config-hash distinctness,digit_handling, non-ASCII numerics, caps codes.smoke_test.py: 0 failures over all 56 checked-in tokenizers and over a freshly trained matched set.The GPU leg is untested. This container has neither
torchnor a GPU, sowrite_token_bytesand everything downstream of it are unexercised.SMOKE=1covers exactly that leg, and the README states this plainly.Three caveats recorded in the work
train_batches()now withholds it, but the committed 1 GB numbers predate that fix: the leak is identical across pretokenizers, so the reported gaps are unaffected while absolute chars/token is optimistic. Disclosed in §4.1 of the draft.caps_grid.py(250 M chars) wrote the same paths as the 1 GB grid and had silently replaced that grid'sen_bnd_wpdartifact — three "different" tokenizers produced byte-identical merge lists, which is what surfaced it. Its outputs are now prefixed and the two affected files renamed to what they actually are. The 1 GB results are unaffected — that cell recordedunique_chunks=2,072,665against 906,491 for the 250 M corpus — and no downstream arm reuses grid artifacts. The 1 GBen_bnd_wpdartifact itself is gone and would need retraining.en_plain_{None,SPLIT,RTL3}also failed fresh-process load, withKeyError: 'DigitAwareScriptPretokenizer'— same root cause, different class, defined insidedigit_split_grid.py. Moved tomarker_experiments/digit_pretokenizer.py; the registry is keyed by class name, so the config hash is unchanged (PT-ec369df5, still matching the cacheddigitsplit250_en_plain_SPLITcorpus) and no result changes.Also included:
marker_experiments/paper.mdand the ACL LaTeX port undermarker_experiments/paper/.🤖 Generated with Claude Code
https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k