"""Train a byte-level BPE and build audited, streaming 1024-token LM data."""
from __future__ import annotations
import argparse
from collections import Counter
import json
import os
os.environ.setdefault('RAYON_NUM_THREADS', '4')
import random
from pathlib import Path
import time
import numpy as np
import pyarrow.parquet as pq
from tokenizers import Tokenizer, models, trainers, pre_tokenizers, decoders
from common import *
CHAT_TEMPLATE = "{% for message in messages %}{{ '<|im_start|>' + message['role'] + '\n' + message['content'] + '<|im_end|>\n' }}{% endfor %}{% if add_generation_prompt %}{{ '<|im_start|>assistant\n' }}{% endif %}"
def rows(path, seed=SEED):
pf = pq.ParquetFile(path)
groups = list(range(pf.num_row_groups))
random.Random(seed).shuffle(groups)
for group in groups:
row_offset = sum(pf.metadata.row_group(i).num_rows for i in range(group))
for batch in pf.iter_batches(batch_size=128, row_groups=[group],
columns=['source','kind','prompt','think','answer']):
for i, row in enumerate(batch.to_pylist()):
yield row_offset + i, row
row_offset += batch.num_rows
def train_tokenizer(source, manifest, out, sample_bytes):
from transformers import PreTrainedTokenizerFast
counts = Counter()
byte_counts = Counter()
def texts():
# Each source file gets the same byte allowance, avoiding a tokenizer
# dominated by the two largest distillation files.
per_file = max(65536, sample_bytes // len(manifest['files']))
for item in manifest['files']:
for _, row in rows(source / item['name']):
if split_for(row['prompt']) != 'train': continue
text = render_row(row)
if text is None: continue
yield text
counts[item['name']] += 1
byte_counts[item['name']] += len(text.encode('utf-8'))
if byte_counts[item['name']] >= per_file: break
print('TOKENIZER_SAMPLE', item['name'], counts[item['name']], flush=True)
tokenizer = Tokenizer(models.BPE())
tokenizer.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False, use_regex=True)
tokenizer.decoder = decoders.ByteLevel()
trainer = trainers.BpeTrainer(vocab_size=VOCAB, min_frequency=2,
special_tokens=SPECIALS,
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(),
show_progress=False)
tokenizer.train_from_iterator(texts(), trainer=trainer)
if tokenizer.get_vocab_size() < VOCAB:
tokenizer.add_tokens([f'<|reserved_{i}|>' for i in range(VOCAB-tokenizer.get_vocab_size())])
assert tokenizer.get_vocab_size() == VOCAB
fast = PreTrainedTokenizerFast(tokenizer_object=tokenizer,
pad_token=SPECIALS[0], bos_token=SPECIALS[1], eos_token=SPECIALS[2],
additional_special_tokens=SPECIALS[3:], model_max_length=CONTEXT,
clean_up_tokenization_spaces=False)
fast.chat_template = CHAT_TEMPLATE
out.mkdir(parents=True, exist_ok=True)
fast.save_pretrained(out)
report = tokenizer_checks(fast)
report.update({'training_split_only': True, 'sample_rows_by_file': dict(counts),
'sample_utf8_bytes_by_file': dict(byte_counts),
'normalizer': None, 'algorithm': 'byte-level BPE',
'tokenizer_sha256': sha(out/'tokenizer.json')})
dump(out/'tokenizer_audit.json', report)
print('TOKENIZER_VERIFIED', report['vocab_size'], flush=True)
return fast
def tokenizer_checks(fast):
samples = ['Hello, world!\n Two spaces stay.',
'x² + y² = π; √2 ≈ 1.414; café — naïve 🚀',
'def add(a, b):\n return a + b\n',
r'\frac{13}{6} = 2.1667; $x \\ y$',
'<|im_start|>assistant\n\nCheck.\n\n42<|im_end|>']
for text in samples:
ids = fast.encode(text, add_special_tokens=False)
assert fast.decode(ids, skip_special_tokens=False, clean_up_tokenization_spaces=False) == text
assert all(0 <= t < VOCAB for t in ids)
special_ids = {s: fast.encode(s, add_special_tokens=False) for s in SPECIALS}
assert special_ids == {s:[i] for i,s in enumerate(SPECIALS)}, special_ids
assert fast.model_max_length == CONTEXT
return {'vocab_size': len(fast), 'special_token_ids': special_ids,
'round_trip_cases_passed': len(samples), 'context': CONTEXT,
'no_unknown_token': fast.unk_token_id is None}
class TokenWriter:
# 512 MB per shard: below the requested 7 GB ceiling.
def __init__(self, directory, split, shard_tokens=256_000_000):
self.directory = Path(directory); self.directory.mkdir(parents=True, exist_ok=True)
self.split = split; self.shard_tokens = shard_tokens
self.real_tokens = 0; self.total_tokens = 0; self.current_tokens = 0
self.files = []; self.handle = None; self.path = None
def add(self, ids, padding=False):
ids = np.asarray(ids, dtype='= 64:
flush()
if file_train >= quota: break
flush()
print('TOKENIZED_FILE', item['name'], 'train_tokens', file_train,
'total_train', writers['train'].real_tokens, 'seconds', round(time.time()-started), flush=True)
splits = {s:w.finish() for s,w in writers.items()}
assert all(splits[s]['blocks'] > 0 for s in splits), 'Each split needs data'
assert sum(x['bytes'] for s in splits.values() for x in s['files']) < MAX_SOURCE_BYTES
assert all(x['bytes'] < MAX_FILE_BYTES for s in splits.values() for x in s['files'])
result = {'created_utc':now(), 'seed':SEED, 'dtype':'little-endian uint16',
'context':CONTEXT, 'vocab_size':VOCAB,
'source_manifest_sha256':sha(manifest_path),
'tokenizer_sha256':sha(output/'tokenizer'/'tokenizer.json'),
'requested_train_tokens':train_tokens,
'sampling':'Deterministically shuffled row groups; per-file train-token quota proportional to compressed file bytes; retain whole records including final answers. Quotas may overshoot by one encoding batch.',
'split_rule':'SHA256(NFKC/casefold/whitespace-normalized prompt) modulo 1000: validation 0..4; test 5..9; train 10..999. Split before tokenizer fitting and packing.',
'packing':'Separate continuous token streams per split; EOS separates records; long documents continue into later windows. Attention resets every 1024 tokens; cross-document attention within a window is allowed.',
'loss':'All next-token targets except pad=0; no assistant-only loss at scratch-pretraining stage.',
'rows_scanned':scanned, 'selected_rows':dict(selected), 'exact_duplicate_rows_removed':duplicates,
'source_tokens':dict(source_tokens), 'kinds':dict(kinds), 'splits':splits}
dump(output/'split_record_audit.json', sample_audit)
dump(output/'corpus_manifest.json', result)
model_config().save_pretrained(output/'model_config')
print('PREPARE_COMPLETE', json.dumps({k:v['real_tokens'] for k,v in splits.items()}), flush=True)
if __name__ == '__main__':
ap=argparse.ArgumentParser()
ap.add_argument('--source',required=True); ap.add_argument('--manifest',required=True)
ap.add_argument('--out',required=True)
ap.add_argument('--train-tokens',type=int,default=1_520_000_000)
ap.add_argument('--tokenizer-sample-bytes',type=int,default=128_000_000)
ap.add_argument('--tokenizer')
args=ap.parse_args()
prepare(args.source,args.manifest,args.out,args.train_tokens,args.tokenizer_sample_bytes,tokenizer_path=args.tokenizer)