brianschwabauer's picture
Full open release: source code + training script + detailed config + transparent model card. No black boxes.
059f836 verified
Raw
History Blame Contribute Delete
11.2 kB
"""
Train MDLM-BPE v3 (scaled model) on 2M Ultra-FineWeb documents.
Model: 207M params, d_model=1024, 10 layers, 16 heads, seq_len=128
Data: 2M docs → ~4M sequences of 128 tokens → ~500M tokens
Training: 3 epochs, bf16, gradient accumulation, streaming data
"""
import json
import sys
import time
import math
import argparse
import numpy as np
from pathlib import Path
import torch
import torch.nn.functional as F
from torch.utils.data import DataLoader, TensorDataset
REPO = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(REPO / "src"))
DEVICE = "cuda" if torch.cuda.is_available() else "cpu"
CHECKPOINT_DIR = REPO / "checkpoints"
RESULTS_DIR = REPO / "results"
DATA_DIR = REPO / "data"
from mdlm_bpe_v3 import (
MDLMConfig, MDLMBPEV3, BPETokenizer,
forward_mask_bpe, mdlm_loss, sample_semi_ar,
)
def prepare_data(seq_len=128):
"""Tokenize 2M docs and pack into 128-token sequences.
DISK-STREAMING: writes directly to numpy memmap, never holds
all sequences in Python lists. Peak RAM = chunk_size only.
"""
input_file = DATA_DIR / "ultra_fineweb_en_1m.jsonl"
output_file = DATA_DIR / f"train_tokens_v3_{seq_len}.npy"
if output_file.exists():
arr = np.load(output_file, mmap_mode='r')
print(f" Cached: {output_file} ({len(arr):,} seqs, mmap)")
return np.array(arr)
from tokenizers import Tokenizer
tok_path = REPO / "tokenizer" / "bpe_tokenizer.json"
tokenizer = Tokenizer.from_file(str(tok_path))
bos_id = tokenizer.token_to_id("<bos>")
eos_id = tokenizer.token_to_id("<eos>")
pad_id = tokenizer.token_to_id("<pad>")
# Count lines
print(f" Counting docs...")
total_docs = sum(1 for _ in open(input_file))
print(f" {total_docs:,} documents")
# Estimate max sequences: each doc produces ~3 seqs of 128 tokens
# Use a generous pre-allocated memmap, truncate after
max_seqs = total_docs * 4 # generous upper bound
tmp_file = DATA_DIR / "train_tokens_v3_tmp.npy"
arr = np.memmap(str(tmp_file), dtype=np.int16, mode='w+',
shape=(max_seqs, seq_len))
CHUNK = 1000 # smaller chunks = less peak RAM
n_seqs = 0
current = []
processed = 0
print(f" Streaming tokenize → memmap (chunk={CHUNK})...")
with open(input_file) as f:
chunk_texts = []
for line in f:
chunk_texts.append(json.loads(line)["content"])
if len(chunk_texts) >= CHUNK:
encoded = tokenizer.encode_batch(chunk_texts)
chunk_texts = [] # free immediately
for enc in encoded:
ids = enc.ids
if len(ids) > seq_len * 4:
ids = ids[:seq_len * 2]
doc = [bos_id] + ids + [eos_id]
for tok in doc:
current.append(tok)
if len(current) >= seq_len:
arr[n_seqs] = current[:seq_len]
n_seqs += 1
current = []
processed += CHUNK
if processed % 50000 == 0:
print(f" [{processed:,}/{total_docs:,}] "
f"seqs={n_seqs:,} RAM≈free")
if chunk_texts:
encoded = tokenizer.encode_batch(chunk_texts)
for enc in encoded:
ids = enc.ids
if len(ids) > seq_len * 4:
ids = ids[:seq_len * 2]
doc = [bos_id] + ids + [eos_id]
for tok in doc:
current.append(tok)
if len(current) >= seq_len:
arr[n_seqs] = current[:seq_len]
n_seqs += 1
current = []
# Handle remainder
if current:
while len(current) < seq_len:
current.append(pad_id)
arr[n_seqs] = current[:seq_len]
n_seqs += 1
# Truncate to actual size and save properly
del arr
arr_final = np.memmap(str(tmp_file), dtype=np.int16, mode='r',
shape=(n_seqs, seq_len))
final = np.array(arr_final[:n_seqs])
np.save(output_file, final)
del arr_final, final
tmp_file.unlink() # cleanup temp
print(f" Saved: {n_seqs:,} sequences ({n_seqs*seq_len*2/1e6:.1f} MB)")
# Return mmap view to avoid loading entire array into RAM
return np.load(output_file, mmap_mode='r'), n_seqs
def train_v3(epochs=3, batch_size=32, lr=3e-4, seq_len=128,
warmup_ratio=0.05, eval_every=500, gradient_accumulation=4):
"""Train MDLM-BPE v3."""
print("=" * 70)
print("TRAINING MDLM-BPE v3 (207M PARAMS)")
print("=" * 70)
# Data
print("Loading data...")
result = prepare_data(seq_len=seq_len)
if isinstance(result, tuple):
tokens, n_seqs = result
else:
tokens = result
n_seqs = len(tokens)
print(f" Total sequences: {n_seqs:,}")
# Use int16 (not int32) to halve memory, convert to long on GPU per-batch
tokens_int16 = np.array(tokens, dtype=np.int16)
del tokens
tokens_tensor = torch.from_numpy(tokens_int16).long()
dataset = TensorDataset(tokens_tensor)
loader = DataLoader(dataset, batch_size=batch_size, shuffle=True,
drop_last=True, num_workers=2, pin_memory=True)
# Model
tokenizer = BPETokenizer()
config = MDLMConfig(
vocab_size=tokenizer.vocab_size,
d_model=1024,
n_heads=16,
n_layers=10,
max_seq_len=256, # support both training (128) and SFT (256)
)
model = MDLMBPEV3(config, pad_id=tokenizer.pad_id,
mask_id=tokenizer.mask_id).to(DEVICE)
n_params = sum(p.numel() for p in model.parameters())
print(f" Model: {n_params:,} ({n_params/1e6:.1f}M)")
print(f" Data: {len(tokens_tensor):,} seqs × {seq_len} tokens = {len(tokens_tensor)*seq_len:,} tokens")
print(f" Epochs: {epochs}, Batch: {batch_size}, Accum: {gradient_accumulation}")
print(f" Effective batch: {batch_size * gradient_accumulation}")
print(f" Opt steps/epoch: {len(loader)//gradient_accumulation:,}")
print()
# Optimizer
optimizer = torch.optim.AdamW(
model.parameters(), lr=lr, weight_decay=0.01, betas=(0.9, 0.95),
)
optimizer_steps_per_epoch = len(loader) // gradient_accumulation
total_optimizer_steps = optimizer_steps_per_epoch * epochs
scheduler = torch.optim.lr_scheduler.OneCycleLR(
optimizer, max_lr=lr, total_steps=total_optimizer_steps,
pct_start=warmup_ratio,
)
# Training
model.train()
micro_step = 0
opt_step = 0
best_eval = float('inf')
losses = []
start = time.time()
accum_loss = 0.0
for epoch in range(epochs):
ep_loss = 0
ep_count = 0
for batch in loader:
micro_step += 1
batch_tokens = batch[0].to(DEVICE, non_blocking=True)
with torch.amp.autocast('cuda', dtype=torch.bfloat16):
loss = mdlm_loss(model, batch_tokens, tokenizer.mask_id)
loss = loss / gradient_accumulation
loss.backward()
accum_loss += loss.item()
if micro_step % gradient_accumulation == 0:
opt_step += 1
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
if opt_step <= total_optimizer_steps:
scheduler.step()
optimizer.zero_grad(set_to_none=True)
ep_loss += accum_loss
ep_count += 1
losses.append(accum_loss)
accum_loss = 0.0
if opt_step % 100 == 0:
elapsed = time.time() - start
tps = micro_step * batch_size * seq_len / elapsed
lr_cur = optimizer.param_groups[0]['lr']
print(f" [E{epoch+1} O{opt_step:,}] loss={losses[-1]:.4f} "
f"avg={ep_loss/ep_count:.4f} lr={lr_cur:.2e} "
f"{opt_step/elapsed:.1f} opt/s {tps:,.0f} tok/s")
if opt_step % eval_every == 0:
model.eval()
eval_loss = quick_eval(model, loader, tokenizer.mask_id)
model.train()
ppl = math.exp(min(eval_loss, 15))
print(f" → eval_loss={eval_loss:.4f} PPL={ppl:.1f}")
if eval_loss < best_eval:
best_eval = eval_loss
torch.save({
"model_state": model.state_dict(),
"config": config.to_dict(),
"step": opt_step,
"eval_loss": eval_loss,
"ppl": ppl,
}, CHECKPOINT_DIR / "mdlm_bpe_v3_best.pt")
samples = sample_semi_ar(
model, tokenizer, seq_len=seq_len,
n_samples=3, block_size=4, temperature=0.7,
)
print(f" → Best samples (semi-AR):")
for i, s in enumerate(samples):
print(f" [{i}] {s.strip()[:120]}")
elapsed = time.time() - start
print(f"\n{'='*70}")
print(f"TRAINING COMPLETE — {elapsed:.1f}s ({elapsed/60:.1f} min)")
print(f" Optimizer steps: {opt_step}")
print(f" Best eval loss: {best_eval:.4f} (PPL={math.exp(min(best_eval,15)):.1f})")
torch.save({
"model_state": model.state_dict(),
"config": config.to_dict(),
"step": opt_step,
"losses": losses[-1000:],
}, CHECKPOINT_DIR / "mdlm_bpe_v3_final.pt")
results = {
"optimizer_steps": opt_step,
"time": elapsed,
"best_eval_loss": best_eval,
"best_ppl": math.exp(min(best_eval, 15)),
"final_loss": losses[-1],
"tokens_trained": micro_step * batch_size * seq_len,
}
with open(RESULTS_DIR / "mdlm_bpe_v3_training.json", "w") as f:
json.dump(results, f, indent=2)
return model
def quick_eval(model, loader, mask_id, n_batches=20):
model.eval()
losses = []
with torch.no_grad():
for i, batch in enumerate(loader):
if i >= n_batches:
break
tokens = batch[0].to(DEVICE)
with torch.amp.autocast('cuda', dtype=torch.bfloat16):
loss = mdlm_loss(model, tokens, mask_id)
losses.append(loss.item())
return np.mean(losses)
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("--epochs", type=int, default=3)
parser.add_argument("--batch-size", type=int, default=32)
parser.add_argument("--lr", type=float, default=3e-4)
parser.add_argument("--seq-len", type=int, default=128)
parser.add_argument("--accum", type=int, default=4)
args = parser.parse_args()
train_v3(epochs=args.epochs, batch_size=args.batch_size,
lr=args.lr, seq_len=args.seq_len, gradient_accumulation=args.accum)