Skip to content

Boundary-marker pretokenization: experiments, paper draft, and downstream LM setup - #7

Closed
sanderland wants to merge 83 commits into
mainfrom
claude/fineweb-space-neighbors-k10ufw
Closed

Boundary-marker pretokenization: experiments, paper draft, and downstream LM setup#7
sanderland wants to merge 83 commits into
mainfrom
claude/fineweb-space-neighbors-k10ufw

Conversation

@sanderland

@sanderland sanderland commented Jul 30, 2026

Copy link
Copy Markdown
Owner

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_allowed bans 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; RTL3 caps it at 1,110 strings and SPLIT at 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 existing pynanochat harness — the same path the MinGram downstream table used — and adds the four things that path needed:

  • boundary_tokenizer.pyPretokenizer.REGISTRY is filled by __init_subclass__, so a fresh subprocess calling 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), making the fix 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, 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, the pynanochat.Tokenizer contract, dense-id space and synthetic BOS, the vocab <= 65535 bound, 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.md there has the full cluster instructions: branch, editable-install requirement, a no-GPU verification step, SMOKE=1 pipeline check, the knob table, a Slurm array template, and what to send back.

Testing

  • marker_experiments/test_boundary.py: 599 passed — round-trip (including tests/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 three original boundary variants keep their config hashes, so cached corpora stay valid.

The GPU leg is untested. This container has neither torch nor a GPU, so write_token_bytes and everything downstream of it are unexercised. SMOKE=1 covers exactly that leg, and the README states this plainly.

Three caveats recorded in the work

  • Evaluation leak. The held-out slice was initially also in the training corpus. 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.
  • A filename collision found while smoke-testing. caps_grid.py (250 M chars) wrote the same paths as the 1 GB grid and had silently replaced that grid's en_bnd_wpd artifact — 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 recorded unique_chunks=2,072,665 against 906,491 for the 250 M corpus — and no downstream arm reuses grid artifacts. The 1 GB en_bnd_wpd artifact itself is gone and would need retraining.
  • A second unregistered pretokenizer. en_plain_{None,SPLIT,RTL3} also failed fresh-process load, with KeyError: 'DigitAwareScriptPretokenizer' — same root cause, different class, defined inside digit_split_grid.py. Moved to marker_experiments/digit_pretokenizer.py; the registry is keyed by class name, so the config hash is unchanged (PT-ec369df5, still matching the cached digitsplit250_en_plain_SPLIT corpus) and no result changes.

Also included: marker_experiments/paper.md and the ACL LaTeX port under marker_experiments/paper/.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PE4L4w3oY91vHCMK9uw32k

claude added 30 commits July 26, 2026 11:16
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
claude added 29 commits July 27, 2026 20:44
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
@sanderland sanderland closed this Jul 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants