One byte-level BPE tokenizer, frozen once and reused for ~10 years across every model from 100M → 1T params. South-Indian-first (Kannada / Tamil / Telugu / Malayalam), plus code, math, and broad multilingual coverage. Built only from open, license-audited data.
Everything lives in a single file: asterizer.py (config + all stages + CLI).
python3 -m pip install tokenizers datasets "huggingface_hub[hf_transfer]" tiktoken transformers sentencepiece
# interactive (asks vocab size + GB):
python3 asterizer.py
export HF_TOKEN=... # gated permissive code (starcoderdata) - accept the dataset terms on HF first
# -- STEP 1 — instant config/plan check, NO download (seconds) --
python3 asterizer.py --vocab 8K --gb 1 --dry-run --yes --allow-license-review
# -- STEP 2 — tiny END-TO-END test (~1 GB, a few min): proves download -> clean -> corpus -> train ->
# eval -> smoke all work and write a real tokenizer dir, with NO errors --
python3 asterizer.py --vocab 8K --gb 1 --yes --allow-license-review
# → if a bucket under-fills on the tiny sample, just bump --gb (e.g. --gb 3) and re-run.
# -- STEP 3 — pin source versions, then the PRODUCTION FREEZE (rented high-RAM CPU box): 32K+64K+128K
# from ONE shared corpus, all gates on (~14–24 h, ≥256–512 GB RAM, no GPU) --
python3 asterizer.py --lock
python3 asterizer.py --vocab 32K,64K,128K --gb 240 --yes --require-lock --allow-license-review
# validate / inspect an existing tokenizer:
python3 asterizer.py --smoke runs/128K_240gb/tokenizer/asterizer_128K_v1.json
# FULL held-out fertility + compression vs cl100k / o200k / Llama-3 / Qwen / Gemma / Sarvam:
python3 asterizer.py --eval runs/128K_240gb/tokenizer/asterizer_128K_v1.json
# post-freeze edits:
python3 asterizer.py --promote tok.json --names "<|new_ctrl|>,<|tool2|>" --out tok2.json # size FIXED, sidecars auto-synced
python3 asterizer.py --extend tok.json --new-data new.jsonl --num-new 2000 --out tok2.json # GROWS vocab
Each build writes a complete HuggingFace directory:
tokenizer.json + asterizer_<v>_v1.json + tokenizer_config.json + special_tokens_map.json
• spec_hash.txt + fertility_report.json , with the shared corpus provenance in manifest.json .
| Aspect | Decision | Why |
|---|---|---|
| Algorithm | byte-level BPE, unk_token=None , all 256 bytes seeded |
never an UNK in 10 years — any script/emoji/code round-trips |
| Normalizer | NFC (never NFKC) | canonical Indic forms; NFKC would destroy x² , full-width, code |
| Pre-tokenizer | GPT-4o (o200k) regex via Split(Regex) → ByteLevel(use_regex=False) |
its letter classes include \p{M} → Indic vowel-signs/viramas stay glued to the consonant (GPT-2/cl100k/Llama-3 split them) |
| Digits | individual ( \p{N} ) |
stable arithmetic; CamelCase also splits (good for code) |
| Post-processor | ByteLevel(trim_offsets=True) |
correct offsets for training pipelines |
| Specials | 32 named + reserved (power-of-two budget) | learned-merge count stays exact; reserved slots are promotable |
| max_token_length | 32 bytes (~10 Indic chars) | real Dravidian words form, no giant overfit tokens |
| min_frequency | 2 | don't prune rare South-Indian syllable merges |
vocab_size = 256 bytes + 32 named + reserved + learned ):
| profile | vocab_size | reserved | learned | use |
|---|---|---|---|---|
| 64K | 65,536 | 992 | 64,256 | 1B–4B models (embedding tax acceptable) |
| 128K | 131,072 | 2,016 | 128,768 | 7B+ models (flagship; multilingual/code headroom) |
Freeze policy (per the review): use 64K for 1B–4B, 128K for 7B+. At d_model=2048 a 131,072 vocab is ~268M tied-embedding params vs ~134M for 64K — a heavy tax on a 1B model.
Corpus (40% South-Indian, scales to --gb ): South-Indian 40% · code 17% (StarCoderData, 15 languages, license review-tagged) · english 10% · other-Indic 8% (hi/bn/gu/mr/pa/or) · european 5% · more-european 3.5% (el/tr/pl/nl/sv/ka/hy) · SEA 3.5% (th/vi/id/km/my/lo) · cjk 4.5% · semitic 3.8% (ar/fa/ur/he) · cyrillic 2.5% · african 1.5% (sw/am/yo/ha) · coverage 0.5% (bo/si) · math 1.5%. 26 scripts, ~40 languages. Every bucket is coverage-gated. --gb N scales to exactly N GB.
| Blocker | Status |
|---|---|
| Stale 64K artifact, not from current spec | ✅ quarantined ( STALE_DO_NOT_FREEZE.txt ); rebuild required |
| Not HF/vLLM/TGI directory-ready; pad/eos None | ✅ full sidecars + bos/eos/pad now emitted |
Model configs vocab_size 64000/128000 ≠ 65536/131072 |
✅ fixed in all 8 FORGE configs |
Packer uses uint32 at vocab 65536 (fits uint16) |
✅ condition < 65536 → <= 65536 (3 sites in forge.py) |
| Corpus not universal | ✅ expanded 12 → 26 scripts, ~25 languages |
| Cleaning strips ZWJ/ZWNJ | ✅ now preserved (Indic conjuncts, Persian/Urdu, emoji) |
| Dataset revisions not pinned | ✅ revision plumbing + manifest.json corpus sha256 anchor |
| License safety asserted, not enforced | ✅ per-source license tags + manifest audit |
stage_extend breaks reserved-slot promise |
✅ new stage_promote (size FIXED); extend clearly labeled vocab-growth |
Smoke gate masked by ... or True |
✅ _smoke_gate aborts the build on failure |
| No per-language/domain fertility report | ✅ fertility_report.json (23 language + domain probes) |
| 64K vs 128K guidance | ✅ documented freeze policy (64K ≤ 4B, 128K ≥ 7B) |
| Rebuild both on one pinned corpus; emit reports | ⌛ run the lock-gated freeze on the rented box ( --vocab 64K,128K --gb 240 --require-lock ) |
| Blocker | Status |
|---|---|
No built artifact to validate ( runs/ absent) |
⌛ run the lock-gated freeze on the rented box |
| 45 source revisions unpinned (resolved only post-build) | ✅ --lock -> sources.lock.json (60/60 pinned) before download; --require-lock gates the build |
| Coverage gate only on South-Indian @ 50% | ✅ EVERY bucket gated (default 60%, required 85%, per-bucket min_frac overrides) |
Code license mixed-github(review) |
✅ swapped to permissive bigcode/starcoderdata (gated, HF_TOKEN); codeparrot kept as a commented dev fallback |
stage_promote leaves sidecars to manual edit |
✅ auto-syncs tokenizer_config.json + special_tokens_map.json + canonical tokenizer.json |
| Eval too small (23 one-sentence probes) | ✅ --eval : held-out streamed corpora, per-language bytes/token + tokens/word vs cl100k/o200k/Llama-3/Qwen/Gemma/Sarvam |
| added: Urdu absent from corpus | ✅ urd_Arab in semitic bucket + eval |
| added: ZWJ-emoji only tested non-ZWJ | ✅ ZWJ probe in smoke + eval domains |
| Finding | Status |
|---|---|
Reference corpus 60 GB but sources sum to 60.5 GB ( --gb 240 → 242) |
✅ REFERENCE_TOTAL_GB auto-computed from BUCKETS → --gb N = exactly N GB |
.done marker written before the coverage gate → resume can skip a bad bucket forever |
✅ floor checked before the marker; final gate clears markers of under-filled buckets so resume re-downloads (validated with a stubbed streamer) |
Code tagged permissive without a stronger filter |
✅ relabeled permissive(bigcode-filtered; review) ; comment documents per-file-license filtering for a hard freeze |
| No built artifacts to smoke-test | ⌛ unchanged — run the build on the rented box |
A two-brain refinement loop hardens asterizer.py round by round: worker = the build author, reviewer = gpt-5.5 via test1/auto_refine_loop.py (ARM proxy). Each round sends the summary + full source, gets a JSON verdict, and applies fixes. The reviewer's #1 blocker every round is "no built artifacts exist", so APPROVED requires actually running the 240 GB build — the code/gate hardening below is what the loop produced. test1/professor_review.py appends each round to the table below automatically (logged here in the CHANGELOG, no separate log files).
| Round | UTC | Verdict | Issues | Pros |
|---|---|---|---|---|
| 5 | 2026-06-26T18:18:2Z | NEEDS_WORK | 7 | 6 |
| 4 | 2026-06-26T22:42Z | NEEDS_WORK | 8 | 8 |
| 3 | 2026-06-26T22:34Z | NEEDS_WORK | 11 | 8 |
| 2 | 2026-06-26T22:20Z | NEEDS_WORK | 8 | 8 |
| 1 | 2026-06-26T21:59Z | NEEDS_WORK | 9 | 6 |
Fixes applied across rounds (all in asterizer.py , validated offline):
license_gate (--allow-license-review); config-fingerprint checkpoints; exact _gb_tag run-dir keys; shuffled sampling + _sample_digest.json ; hash-based held-out split written to raw/heldout/*.jsonl ; smoke decodes with skip_special_tokens=False + special-literal cases; --extend gated behind ASTERIZER_EXPERIMENTAL ..done markers store the fingerprint (stale corpus can't be reused); the post-clean gate runs even on resume (was bypassable). Plus per-source coverage floor, eval-threshold freeze gate, whole-dir SHA256SUMS.txt , manifest env + NOTICE.txt .eval-gate-bypass-on-resume fixed (freeze folded into the tokenizer fingerprint); run_stage validates expected outputs exist; vocab-conformance gate; lock inventory check; provenance copied into the tokenizer dir; promote writes promotions.json + spec_hash before checksums..done marker was written before the coverage gate ran, so an under-filled (e.g. gated/failed) bucket got checkpointed and was skipped on every resume while the gate kept failing — a permanent fail loop.REFERENCE_TOTAL_GB = 60 but the source quotas summed to 60.5 (Urdu added +0.5), so --gb 240 actually scaled to ~242 GB.REFERENCE_TOTAL_GB is recomputed as the exact sum of all source quotas, so --gb N scales to exactly N GB and never drifts when sources change.permissive — an upstream claim, not a filter we ran.permissive(bigcode-filtered; review) ; the bucket comment documents that a hard commercial freeze should accept the upstream filter explicitly or filter on a per-file license column (e.g. the-stack-dedup). Surfaced in manifest.json + sources.lock.json .--lock → sources.lock.json )
manifest.json after the build.--lock resolves every source's HF commit SHA + license decision into sources.lock.json before download; the build pins load_dataset(revision=...) from it and --require-lock aborts unless it is fully pinned. The 240 GB pull is now reproducible by construction.min_frac ) or the build aborts — a silent source/auth failure can't gut any script.codeparrot-clean (Python, mixed GitHub licenses, tagged mixed-github(review) ).bigcode/starcoderdata (already license-filtered to permissive; 15 languages; gated → HF_TOKEN ). codeparrot retained as a clearly-commented dev fallback. the-stack-v2-dedup stores content as S3 pointers → not plain-streamable, so StarCoderData is the practical pick.stage_promote keeps the whole HF dir in lockstep
tokenizer_config.json + special_tokens_map.json and re-saves the canonical tokenizer.json — IDs and vocab size still unchanged. Validated 311→311, sidecars synced.--eval )
fertility_report.json = 23 one-sentence probes.--evaldir files) + domain probes (legal/biomed/finance/yaml/url/diff/ZWJ-emoji/noisy-web), measured as bytes/token + tokens/word head-to-head against cl100k, o200k, Llama-3, Qwen, Gemma, Sarvam. Runs after each freeze build (non-fatal) and standalone via --eval .urd_Arab to the corpus (semitic bucket) and an Urdu probe; added a real ZWJ-emoji family ( 👨👩👧 ) to smoke + eval so the preserved U+200D is actually exercised.stage_smoke fails if any HF sidecar is missing, a STALE_* marker exists in the dir/parent, named specials ≠ 32, or reserved count ≠ the profile's. Verified: old 64K now FAILS with all 6 reasons; fresh builds still PASS.stage_download records actual per-source bytes ( raw/_download_stats.json ) and aborts if a required bucket is under MIN_REQUIRED_FRAC (50%) of target.revision plumbing existed but all sources were None → no real pinning; no actual bytes.write_manifest records actual_bytes per source and resolves each dataset's commit SHA via HfApi into resolved_revision . Copy resolved_revision → a source revision to hard-pin a rebuild. Corpus sha256 remains the primary anchor.codeparrot-clean is Python + mixed GitHub licenses. For a tokenizer (a statistical merge table, not redistributed code/weights) mixed-license training data is widely treated as non-infringing (GPT-4/Llama did the same). For maximum white-label cleanliness, set HF_TOKEN and swap the code bucket to a permissive subset ( bigcode/the-stack-v2-dedup ). The license is tagged mixed-github(review) and surfaced in manifest.json so it's a conscious choice.ZERO_WIDTH stripped U+200C (ZWNJ) and U+200D (ZWJ).lambda: (stage_smoke(...) or True) → stage marked done even on FAIL._smoke_gate() calls sys.exit(...) on failure; stage is not checkpointed.stage_extend appended tokens (grew vocab_size ), violating the promise.stage_promote renames free <|reserved_i|> slots in place (IDs + size UNCHANGED); stage_extend kept but loudly labeled as vocab-growth (needs embedding resize + continue-pretrain). New --promote / --extend CLI.load_dataset streamed by name (no revision); no license metadata.license + optional revision ; manifest.json records every source, license, revision, seed, and the corpus sha256 (the true reproducibility anchor).corpus.jsonl is deterministic, so its hash pins the build.fertility_report.json with tokens, bytes/token, tokens/word for 23 probes (all scripts + legal/biomed/finance/social-code-mixed/code-diff).vocab_size = 64000/128000; packer uint16 if vocab < 65536 (so the real 65536 vocab wasted uint32 ).uint32 at 64K doubles token-bin storage for nothing.runs/32K_6gb and runs/64K_12GB looked freezable but predate every change.STALE_DO_NOT_FREEZE.txt in each; real freeze rebuilds on the rented box.ByteLevel(use_regex=True) = GPT-2 regex; \p{L}+ excludes \p{M}+ , so Indic marks split off the consonant at the pre-token boundary → BPE cannot merge syllables.\p{M} ) via Split(Regex) + ByteLevel .<|im_start|>/<|im_end|> (ChatML), <|endofprompt|> , <|repo_name|>/<|file_sep|> (repo-level code), <|image_start|>/<|image_end|>/<|image_pad|> (vision patches).tokenizer.json (canonical name AutoTokenizer needs), tokenizer_config.json , special_tokens_map.json with a ChatML chat_template ; unk_token=None , bos/eos/pad declared.AutoTokenizer.from_pretrained and renders the chat template; gates PASS/FAIL.runs/_data_<gb>gb/ ; 64K and 128K train from the same corpus. --vocab 64K,128K comma list. Download progress bar with %/rate/ETA.The full 240 GB build needs ~150–200 GB RAM peak (BPE holds word counts in RAM). Rent a CPU box with ≥ 256–386 GB RAM, ~1 TB disk. Then:
export HF_TOKEN=... # gated permissive code (starcoderdata)
python3 asterizer.py --lock # pin source SHAs -> sources.lock.json (commit it)
python3 asterizer.py --vocab 64K,128K --gb 240 --yes --require-lock
Freeze checklist before shipping: sources.lock.json fully pinned · every bucket passed its coverage floor · smoke PASS (gate) · fertility_report.json reviewed per language and vs baselines · manifest.json licenses audited (all permissive/open) · spec_hash.txt recorded · FORGE vocab_size matches tokenizer.get_vocab_size().