| |
| """Train Fractus-1B on quality data — NO gradient checkpointing (5x faster). |
| |
| 24.8 tok/s. 200k tokens × 5 epochs = ~11h on CPU. |
| """ |
| import argparse, gc, math, os, sys, time |
| sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) |
| import torch, torch.nn as nn, torch.nn.functional as F |
| from fractus.model_1b import Fractus1B |
| from fractus.tokenizer import FractusTokenizer |
|
|
| HF_TOKEN = os.environ.get("HF_TOKEN", "") |
| HF_REPO = "thefinalboss/Fractus" |
|
|
| def save_ckpt(model, epoch, loss, acc, d): |
| p = os.path.join(d, f"fractus_1b_epoch{epoch}.pt") |
| torch.save({"model_state": model.state_dict(), |
| "config": model.config, "epoch": epoch, |
| "loss": loss, "accuracy": acc}, p) |
| sz = os.path.getsize(p)/1e6 |
| print(f" [ckpt] {p} ({sz:.0f}MB)", flush=True) |
| if HF_TOKEN: |
| try: |
| from huggingface_hub import HfApi |
| api = HfApi(token=HF_TOKEN) |
| api.upload_file(path_or_fileobj=p, path_in_repo=f"checkpoints/fractus_1b_epoch{epoch}.pt", |
| repo_id=HF_REPO, repo_type="model") |
| print(f" [HF] Uploaded", flush=True) |
| except: pass |
|
|
| def main(): |
| p = argparse.ArgumentParser() |
| p.add_argument("--epochs", type=int, default=5) |
| p.add_argument("--seq-len", type=int, default=32) |
| p.add_argument("--lr", type=float, default=1e-4) |
| p.add_argument("--max-tokens", type=int, default=200000) |
| args = p.parse_args() |
| torch.set_num_threads(os.cpu_count() or 6) |
| torch.manual_seed(42) |
|
|
| corpus = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data", "mega_500k.pt") |
| tokens = torch.load(corpus, weights_only=False).long()[:args.max_tokens] |
| print(f"Corpus: {len(tokens):,} tokens", flush=True) |
|
|
| print("Building Fractus-1B...", flush=True) |
| model = Fractus1B( |
| vocab_size=50257, d_model=768, n_layers=8, n_heads=12, d_head=64, |
| n_levels=2, n_experts=64, top_k=2, expert_d_ff=1024, siren_rank=16, |
| max_seq_len=args.seq_len, |
| ) |
| n = model.n_params() |
| cap = model.n_effective_capacity() |
| print(f" {n/1e6:.0f}M params, {cap/1e9:.2f}B capacity, {n*4/1e9:.1f}GB RAM", flush=True) |
|
|
| opt = torch.optim.AdamW(model.parameters(), lr=args.lr, weight_decay=0.01) |
| sched = torch.optim.lr_scheduler.CosineAnnealingLR(opt, T_max=args.epochs, eta_min=1e-5) |
| tok = FractusTokenizer.gpt2_compatible() |
| seq = args.seq_len |
| ckpt_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "checkpoints") |
| os.makedirs(ckpt_dir, exist_ok=True) |
|
|
| n_steps = len(tokens) // seq |
| print(f"\nTraining {args.epochs} epochs, {n_steps:,} steps/epoch", flush=True) |
| print("=" * 70, flush=True) |
|
|
| initial_loss = None |
| for epoch in range(args.epochs): |
| model.train() |
| t0 = time.perf_counter() |
| ep_loss = 0.0 |
| ep_n = 0 |
|
|
| for i in range(0, len(tokens) - seq - 1, seq): |
| inp = tokens[i:i+seq].unsqueeze(0) |
| tgt = tokens[i+1:i+seq+1].unsqueeze(0) |
| opt.zero_grad() |
| logits, aux = model(inp) |
| ce = nn.functional.cross_entropy(logits.reshape(-1, 50257), tgt.reshape(-1)) |
| loss = ce + 0.001 * aux |
| loss.backward() |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) |
| opt.step() |
| sched.step() |
| ep_loss += ce.item() |
| ep_n += 1 |
| if initial_loss is None: |
| initial_loss = ce.item() |
| if ep_n % 1000 == 0: |
| el = time.perf_counter() - t0 |
| tps = (i + seq) / el |
| print(f" e{epoch+1} step {ep_n:5d}/{n_steps} ce={ce.item():.3f} " |
| f"{tps:.0f} tok/s {el/60:.0f}min", flush=True) |
|
|
| el = time.perf_counter() - t0 |
| avg = ep_loss / max(ep_n, 1) |
| ppl = math.exp(min(avg, 20)) |
| print(f"\nEpoch {epoch+1}/{args.epochs}: loss={avg:.3f} ppl={ppl:.1f} " |
| f"{len(tokens)/el:.0f} tok/s {el/60:.0f}min", flush=True) |
| save_ckpt(model, epoch+1, avg, 0, ckpt_dir) |
|
|
| |
| model.eval() |
| with torch.no_grad(): |
| p_ids = tok.encode("def fibonacci")[:seq] |
| inp = torch.tensor([p_ids], dtype=torch.long) |
| logits, _ = model(inp) |
| gen = p_ids + logits[0].argmax(dim=-1).tolist()[:20] |
| print(f" Sample: {tok.decode(gen)[:120]}", flush=True) |
| gc.collect() |
|
|
| |
| print("\n=== COHERENCE TEST ===", flush=True) |
| model.eval() |
| for prompt in ["What is Python?", "def sort", "The sun is", "Explain AI"]: |
| with torch.no_grad(): |
| p_ids = tok.encode(prompt)[:seq] |
| inp = torch.tensor([p_ids], dtype=torch.long) |
| logits, _ = model(inp) |
| gen = p_ids + logits[0].argmax(dim=-1).tolist()[:30] |
| print(f" [{prompt}] -> {tok.decode(gen)[:120]}", flush=True) |
|
|
| print(f"\nDone. Loss: {initial_loss:.3f} -> {avg:.3f}", flush=True) |
|
|
| if __name__ == "__main__": |
| main() |
|
|