"""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)