Download train.py from changcheng967/flashlm-v6-supernova: direct link, hf CLI and curl.
- Browser
- Download file 26.6 kB
-
https://huggingface.co/changcheng967/flashlm-v6-supernova/resolve/main/train.py
- Command line
-
hf download hf://changcheng967/flashlm-v6-supernova/train.py
-
curl -L -o train.py https://huggingface.co/changcheng967/flashlm-v6-supernova/resolve/main/train.py
26.6 kB
| #!/usr/bin/env python3 | |
| """ | |
| FlashLM v6 "SUPERNOVA" — P-RCSM Architecture (Linear-Only) | |
| ============================================================= | |
| Novel architecture: Parallel Recursive Compositional State Machine | |
| """ | |
| import os | |
| import sys | |
| import time | |
| import math | |
| import json | |
| import gc | |
| import argparse | |
| import numpy as np | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from pathlib import Path | |
| # ============================================================================ | |
| # CONFIGURATION | |
| # ============================================================================ | |
| class Config: | |
| # Model | |
| vocab_size = 4096 | |
| d_model = 192 # Reduced from 256 - match v5.2 | |
| n_layers = 6 # Reduced from 8 - match v5.2 | |
| d_ffn_mult = 2 # Reduced from 3 - FFN hidden = 384 | |
| n_scales = 2 # Reduced from 3 - fewer scale computations | |
| d_reason = 64 # Reduced from 96 - smaller reasoning dim | |
| n_mem_slots = 8 # Reduced from 16 - fewer memory slots | |
| d_planner = 32 # Reduced from 64 - smaller planner | |
| dropout = 0.0 | |
| # Training | |
| seq_len = 128 | |
| batch_size = 4 | |
| grad_accum = 2 | |
| max_lr = 4e-3 | |
| min_lr = 1e-4 | |
| warmup_steps = 100 | |
| weight_decay = 0.05 | |
| grad_clip = 1.0 | |
| total_hours = 12.0 | |
| # Data | |
| data_dir = 'data' | |
| out_dir = 'out_v6' | |
| max_train_tokens = 5_000_000 | |
| # Logging | |
| log_every = 50 | |
| eval_every = 300 | |
| save_every = 1000 | |
| gen_every = 500 | |
| # ============================================================================ | |
| # BITLINEAR 1.58-bit — Ternary Weights {-1, 0, +1} | |
| # ============================================================================ | |
| class BitLinear(nn.Module): | |
| """Ternary weight linear layer with STE.""" | |
| def __init__(self, in_features, out_features, bias=False): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.empty(out_features, in_features)) | |
| self.bias = nn.Parameter(torch.zeros(out_features)) if bias else None | |
| nn.init.kaiming_normal_(self.weight, mode='fan_out') | |
| def forward(self, x): | |
| scale = self.weight.abs().mean(dim=1, keepdim=True).clamp(min=1e-5) | |
| w_q = torch.round(self.weight / scale).clamp(-1, 1) | |
| w = self.weight + (w_q * scale - self.weight).detach() | |
| return F.linear(x, w, self.bias) | |
| # ============================================================================ | |
| # RMS NORM | |
| # ============================================================================ | |
| class RMSNorm(nn.Module): | |
| def __init__(self, dim, eps=1e-6): | |
| super().__init__() | |
| self.weight = nn.Parameter(torch.ones(dim)) | |
| self.eps = eps | |
| def forward(self, x): | |
| rms = torch.sqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) | |
| return x / rms * self.weight | |
| # ============================================================================ | |
| # TOKEN MIXER: Gated Linear Mixer (NO Conv1d — pure F.linear) | |
| # ============================================================================ | |
| class GatedLinearMixer(nn.Module): | |
| """ | |
| Token mixing using only F.linear — fast on CPU. | |
| Replaces GatedConvMixer (Conv1d was catastrophically slow). | |
| Uses causal shift: concat current + previous token, project down. | |
| """ | |
| def __init__(self, d_model): | |
| super().__init__() | |
| self.norm = RMSNorm(d_model) | |
| self.mix = BitLinear(d_model * 2, d_model) | |
| self.gate_proj = BitLinear(d_model, d_model) | |
| self.value_proj = BitLinear(d_model, d_model) | |
| def forward(self, x): | |
| h = self.norm(x) | |
| # Causal shift: previous token (zero-padded at position 0) | |
| h_prev = F.pad(h[:, :-1, :], (0, 0, 1, 0)) | |
| mixed = self.mix(torch.cat([h, h_prev], dim=-1)) | |
| gate = torch.sigmoid(self.gate_proj(h)) | |
| value = self.value_proj(mixed) | |
| return gate * value | |
| # ============================================================================ | |
| # CHANNEL MIXER: Ternary GLU | |
| # ============================================================================ | |
| class TernaryGLU(nn.Module): | |
| """SiLU-gated FFN with ternary weights.""" | |
| def __init__(self, d_model, d_ffn): | |
| super().__init__() | |
| self.norm = RMSNorm(d_model) | |
| self.gate_up = BitLinear(d_model, d_ffn * 2) | |
| self.down = BitLinear(d_ffn, d_model) | |
| def forward(self, x): | |
| h = self.norm(x) | |
| gu = self.gate_up(h) | |
| gate, up = gu.chunk(2, dim=-1) | |
| return self.down(F.silu(gate) * up) | |
| # ============================================================================ | |
| # P-RCSM: Parallel Recursive Compositional State Machine (Linear-Only) | |
| # ============================================================================ | |
| class MultiScaleLinearBank(nn.Module): | |
| """ | |
| Replaces OperationLibrary + depth iteration. | |
| Multi-scale reasoning using ONLY F.linear — NO Conv1d. | |
| Each "scale" processes tokens at a different temporal offset (1, 2, 4, 8). | |
| A learned router softly combines outputs (like MoE over scales). | |
| This captures different reasoning depths in a single parallel pass. | |
| """ | |
| def __init__(self, d_reason, n_scales=4): | |
| super().__init__() | |
| self.n_scales = n_scales | |
| self.norm = RMSNorm(d_reason) | |
| # Each scale: linear transform on [current, shifted_by_offset] | |
| self.scales = nn.ModuleList([ | |
| BitLinear(d_reason * 2, d_reason) for _ in range(n_scales) | |
| ]) | |
| # Learned scale router | |
| self.router = nn.Linear(d_reason, n_scales, bias=False) | |
| def forward(self, x): | |
| # x: (B, S, d_reason) | |
| h = self.norm(x) | |
| # Each scale looks at a different temporal offset | |
| scale_outs = [] | |
| for i, scale_proj in enumerate(self.scales): | |
| offset = 2 ** i # offsets: 1, 2, 4, 8 | |
| # Shift: grab token from `offset` positions ago | |
| h_shifted = F.pad(h[:, :-offset, :], (0, 0, offset, 0)) | |
| # Concat current + shifted, project | |
| scale_outs.append(scale_proj(torch.cat([h, h_shifted], dim=-1))) | |
| # Stack and route | |
| stacked = torch.stack(scale_outs, dim=-1) # (B, S, d_reason, n_scales) | |
| weights = F.softmax(self.router(h), dim=-1).unsqueeze(2) # (B, S, 1, n_scales) | |
| combined = (stacked * weights).sum(dim=-1) # (B, S, d_reason) | |
| return combined | |
| class HierarchicalStateGate(nn.Module): | |
| """ | |
| Replaces AdaptiveDepthController. | |
| A compressed "planner" modulates the reasoning flow. | |
| Planner updates slowly (once per block via mean-pool). | |
| Executor (d_reason) is modulated at every position. | |
| """ | |
| def __init__(self, d_reason, d_planner): | |
| super().__init__() | |
| self.planner_to_gate = BitLinear(d_planner, d_reason) | |
| self.executor_to_planner = BitLinear(d_reason, d_planner) | |
| self.planner_update = BitLinear(d_planner, d_planner) | |
| self.norm = RMSNorm(d_planner) | |
| def forward(self, executor_state, planner_state): | |
| """ | |
| executor_state: (B, S, d_reason) | |
| planner_state: (B, 1, d_planner) — broadcasts over S | |
| Returns: modulated executor, updated planner | |
| """ | |
| # Planner gates the executor | |
| gate = torch.sigmoid(self.planner_to_gate(planner_state)) | |
| modulated = executor_state * gate | |
| # Update planner from executor summary | |
| executor_summary = executor_state.mean(dim=1, keepdim=True) | |
| planner_input = self.executor_to_planner(executor_summary) | |
| new_planner = planner_state + self.planner_update( | |
| self.norm(planner_state + planner_input) | |
| ) | |
| return modulated, new_planner | |
| class SlotMemoryAttention(nn.Module): | |
| """ | |
| Replaces AssociativeMemory sequential read/write. | |
| Learned slots + single cross-attention read. | |
| ONE matmul, NO Python loops. | |
| """ | |
| def __init__(self, d_reason, n_slots=16): | |
| super().__init__() | |
| self.n_slots = n_slots | |
| self.slots = nn.Parameter(torch.randn(n_slots, d_reason) * 0.01) | |
| self.query_proj = BitLinear(d_reason, d_reason) | |
| self.mix_gate = BitLinear(d_reason * 2, d_reason) | |
| def forward(self, x): | |
| # x: (B, S, d_reason) | |
| query = self.query_proj(x) | |
| # Cross-attention over slots | |
| attn_scores = torch.matmul(query, self.slots.t()) / math.sqrt(x.size(-1)) | |
| attn_weights = F.softmax(attn_scores, dim=-1) | |
| mem_read = torch.matmul(attn_weights, self.slots) | |
| # Gate and combine | |
| combined = torch.cat([x, mem_read], dim=-1) | |
| return self.mix_gate(combined) | |
| class PRCSM(nn.Module): | |
| """ | |
| Parallel Recursive Compositional State Machine. | |
| All F.linear, zero Conv1d, zero Python loops. | |
| """ | |
| def __init__(self, d_model, d_reason=96, d_planner=64, n_scales=4, n_mem_slots=16): | |
| super().__init__() | |
| self.down_proj = BitLinear(d_model, d_reason) | |
| self.conv_bank = MultiScaleLinearBank(d_reason, n_scales) | |
| self.state_gate = HierarchicalStateGate(d_reason, d_planner) | |
| self.memory = SlotMemoryAttention(d_reason, n_mem_slots) | |
| self.up_proj = BitLinear(d_reason, d_model) | |
| self.res_gate = nn.Parameter(torch.zeros(1)) | |
| def forward(self, x, planner_state): | |
| r = self.down_proj(x) | |
| r = r + self.conv_bank(r) | |
| r, new_planner = self.state_gate(r, planner_state) | |
| r = r + self.memory(r) | |
| out = x + torch.sigmoid(self.res_gate) * self.up_proj(r) | |
| return out, new_planner | |
| # ============================================================================ | |
| # SUPERNOVA BLOCK | |
| # ============================================================================ | |
| class SupernovaBlock(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| d_ffn = config.d_model * config.d_ffn_mult | |
| self.token_mix = GatedLinearMixer(config.d_model) | |
| self.reasoning = PRCSM( | |
| config.d_model, config.d_reason, config.d_planner, | |
| config.n_scales, config.n_mem_slots | |
| ) | |
| self.channel_mix = TernaryGLU(config.d_model, d_ffn) | |
| def forward(self, x, planner_state): | |
| x = x + self.token_mix(x) | |
| x, planner_state = self.reasoning(x, planner_state) | |
| x = x + self.channel_mix(x) | |
| return x, planner_state | |
| # ============================================================================ | |
| # FlashLM v6 "SUPERNOVA" | |
| # ============================================================================ | |
| class FlashLMv6(nn.Module): | |
| def __init__(self, config): | |
| super().__init__() | |
| self.config = config | |
| self.embed = nn.Embedding(config.vocab_size, config.d_model) | |
| self.embed_scale = config.d_model ** -0.5 | |
| self.blocks = nn.ModuleList([ | |
| SupernovaBlock(config) for _ in range(config.n_layers) | |
| ]) | |
| self.ln_out = RMSNorm(config.d_model) | |
| self.head = BitLinear(config.d_model, config.vocab_size) | |
| # Planner initial state (learned) | |
| self.planner_init = nn.Parameter(torch.randn(1, 1, config.d_planner) * 0.01) | |
| nn.init.normal_(self.embed.weight, std=0.02) | |
| # Stats | |
| total = sum(p.numel() for p in self.parameters()) | |
| ternary = sum( | |
| p.numel() for m in self.modules() | |
| if isinstance(m, BitLinear) for p in m.parameters() | |
| if p is m.weight | |
| ) | |
| print(f"\n{'='*60}") | |
| print(f" FlashLM v6 'SUPERNOVA' — P-RCSM Architecture (Linear-Only)") | |
| print(f"{'='*60}") | |
| print(f" Total parameters: {total:,} ({total/1e6:.1f}M)") | |
| print(f" Ternary parameters: {ternary:,} ({100*ternary/total:.1f}%)") | |
| print(f" Model RAM: {total*4/1024/1024:.1f} MB (float32)") | |
| # Removed 10M parameter limit - model is designed to scale | |
| print(f"{'='*60}\n") | |
| def forward(self, x, targets=None): | |
| B = x.size(0) | |
| h = self.embed(x) * self.embed_scale | |
| planner = self.planner_init.expand(B, -1, -1) | |
| for block in self.blocks: | |
| h, planner = block(h, planner) | |
| logits = self.head(self.ln_out(h)) | |
| if targets is not None: | |
| loss = F.cross_entropy(logits.view(-1, logits.size(-1)), targets.view(-1)) | |
| return loss | |
| return logits | |
| def generate(self, idx, max_new_tokens, temperature=0.8, top_k=40): | |
| self.eval() | |
| for _ in range(max_new_tokens): | |
| ctx = idx[:, -self.config.seq_len:] | |
| logits = self(ctx) | |
| logits = logits[:, -1, :] / max(temperature, 1e-5) | |
| if top_k > 0: | |
| v, _ = torch.topk(logits, min(top_k, logits.size(-1))) | |
| logits[logits < v[:, [-1]]] = float('-inf') | |
| probs = F.softmax(logits, dim=-1) | |
| idx = torch.cat([idx, torch.multinomial(probs, 1)], dim=1) | |
| self.train() | |
| return idx | |
| # ============================================================================ | |
| # DATA PREPARATION (v5-style: in-memory, single write) | |
| # ============================================================================ | |
| def prepare_data(config): | |
| data_dir = Path(config.data_dir) | |
| out_dir = Path(config.out_dir) | |
| data_dir.mkdir(exist_ok=True) | |
| out_dir.mkdir(exist_ok=True) | |
| tok_path = data_dir / 'tokenizer_v6.json' | |
| token_bin = out_dir / 'train_tokens_v6.bin' | |
| # Download if needed | |
| valid_path = data_dir / 'TinyStories-valid.txt' | |
| if not valid_path.exists(): | |
| print("Downloading TinyStories validation set...") | |
| import urllib.request | |
| url = "https://huggingface.co/datasets/roneneldan/TinyStories/resolve/main/TinyStoriesV2-GPT4-valid.txt" | |
| urllib.request.urlretrieve(url, str(valid_path)) | |
| print(f" Downloaded: {valid_path.stat().st_size / 1e6:.1f} MB") | |
| # Train tokenizer if needed | |
| if not tok_path.exists(): | |
| print("Training tokenizer...") | |
| from tokenizers import Tokenizer | |
| from tokenizers.models import BPE | |
| from tokenizers.trainers import BpeTrainer | |
| from tokenizers.pre_tokenizers import ByteLevel | |
| tokenizer = Tokenizer(BPE()) | |
| tokenizer.pre_tokenizer = ByteLevel() | |
| tokenizer.train( | |
| files=[str(valid_path)], | |
| trainer=BpeTrainer( | |
| vocab_size=config.vocab_size, | |
| min_frequency=2, | |
| special_tokens=["<pad>", "<unk>", "<bos>", "<|eos|>"] | |
| ) | |
| ) | |
| tokenizer.save(str(tok_path)) | |
| print(f" Tokenizer saved: {tok_path}") | |
| from tokenizers import Tokenizer | |
| tokenizer = Tokenizer.from_file(str(tok_path)) | |
| actual_vocab = tokenizer.get_vocab_size() | |
| config.vocab_size = actual_vocab | |
| print(f" Vocab size: {actual_vocab}") | |
| # Tokenize if needed | |
| if token_bin.exists() and token_bin.stat().st_size > 1000: | |
| print("Loading cached tokens...") | |
| tokens = np.fromfile(str(token_bin), dtype=np.uint16) | |
| else: | |
| print("Tokenizing...") | |
| with open(str(valid_path), 'r', encoding='utf-8', errors='ignore') as f: | |
| text = f.read() | |
| stories = [s.strip() for s in text.split('\n\n') if len(s.strip()) > 50] | |
| eos_id = tokenizer.token_to_id('<|eos|>') or 0 | |
| all_tokens = [] | |
| for i, story in enumerate(stories): | |
| ids = tokenizer.encode(story).ids | |
| all_tokens.extend(ids) | |
| all_tokens.append(eos_id) | |
| if len(all_tokens) >= config.max_train_tokens: | |
| break | |
| if (i + 1) % 5000 == 0: | |
| print(f" {len(all_tokens):,} tokens from {i+1:,} stories...") | |
| tokens = np.array(all_tokens[:config.max_train_tokens], dtype=np.uint16) | |
| tokens.tofile(str(token_bin)) | |
| print(f" Saved {len(tokens):,} tokens to {token_bin}") | |
| gc.collect() | |
| # Train/val split | |
| split = int(len(tokens) * 0.9) | |
| train_tokens = torch.from_numpy(tokens[:split].astype(np.int64)) | |
| val_tokens = torch.from_numpy(tokens[split:].astype(np.int64)) | |
| print(f" Train: {len(train_tokens):,} tokens | Valid: {len(val_tokens):,} tokens") | |
| return tokenizer, train_tokens, val_tokens | |
| # ============================================================================ | |
| # BATCH SAMPLING | |
| # ============================================================================ | |
| def get_batch(data, batch_size, seq_len): | |
| ix = torch.randint(len(data) - seq_len - 1, (batch_size,)) | |
| x = torch.stack([data[i:i+seq_len] for i in ix]) | |
| y = torch.stack([data[i+1:i+seq_len+1] for i in ix]) | |
| return x, y | |
| # ============================================================================ | |
| # LR SCHEDULE | |
| # ============================================================================ | |
| def get_lr(step, config): | |
| if step < config.warmup_steps: | |
| return config.max_lr * (step + 1) / config.warmup_steps | |
| decay_steps = step - config.warmup_steps | |
| cycle = 500 | |
| progress = (decay_steps % cycle) / cycle | |
| current_max = config.max_lr * (0.85 ** (decay_steps // cycle)) | |
| return config.min_lr + 0.5 * (current_max - config.min_lr) * (1 + math.cos(math.pi * progress)) | |
| # ============================================================================ | |
| # EVALUATION | |
| # ============================================================================ | |
| def evaluate(model, val_tokens, config, max_batches=25): | |
| model.eval() | |
| total_loss, total_tokens = 0.0, 0 | |
| for _ in range(max_batches): | |
| x, y = get_batch(val_tokens, config.batch_size, config.seq_len) | |
| loss = model(x, targets=y) | |
| total_loss += loss.item() * x.numel() | |
| total_tokens += x.numel() | |
| model.train() | |
| avg = total_loss / max(total_tokens, 1) | |
| return {'loss': avg, 'ppl': math.exp(min(avg, 20))} | |
| # ============================================================================ | |
| # SPEED CALIBRATION | |
| # ============================================================================ | |
| def calibrate_speed(model, config): | |
| print(" Running speed calibration...") | |
| model.train() | |
| x = torch.randint(0, config.vocab_size, (config.batch_size, config.seq_len)) | |
| y = torch.randint(0, config.vocab_size, (config.batch_size, config.seq_len)) | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=1e-4) | |
| # Warmup (2 passes) | |
| for _ in range(2): | |
| loss = model(x, targets=y) | |
| loss.backward() | |
| optimizer.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| # Timed runs | |
| t0 = time.time() | |
| n_runs = 5 | |
| for _ in range(n_runs): | |
| for _ in range(config.grad_accum): | |
| loss = model(x, targets=y) / config.grad_accum | |
| loss.backward() | |
| optimizer.step() | |
| optimizer.zero_grad(set_to_none=True) | |
| elapsed = time.time() - t0 | |
| tokens_per_step = config.batch_size * config.seq_len * config.grad_accum | |
| tok_per_sec = (n_runs * tokens_per_step) / elapsed | |
| step_time_ms = (elapsed / n_runs) * 1000 | |
| print(f" Speed: {tok_per_sec:.0f} tok/s") | |
| print(f" Time per step: {step_time_ms:.0f}ms") | |
| total_steps = int(config.total_hours * 3600 / (elapsed / n_runs)) | |
| total_tokens = total_steps * tokens_per_step | |
| print(f" Estimated total steps: {total_steps:,}") | |
| print(f" Estimated total tokens: {total_tokens/1e6:.1f}M") | |
| return tok_per_sec | |
| # ============================================================================ | |
| # TRAINING | |
| # ============================================================================ | |
| def train(config): | |
| # CRITICAL: Set to 2 threads for optimal CPU performance on small models | |
| # Too many threads cause synchronization overhead that slows down small matrix ops | |
| torch.set_num_threads(2) | |
| os.environ['OMP_NUM_THREADS'] = '2' | |
| print(f"{'='*64}") | |
| print(f" FlashLM v6 'SUPERNOVA' — Training") | |
| print(f"{'='*64}") | |
| print(f" PyTorch: {torch.__version__}") | |
| print(f" Threads: 2 (optimized for small models)") | |
| try: | |
| import psutil | |
| print(f" RAM: {psutil.Process().memory_info().rss // 1024 // 1024} MB") | |
| except ImportError: | |
| pass | |
| print(f" Training time: {config.total_hours}h") | |
| out_dir = Path(config.out_dir) | |
| out_dir.mkdir(exist_ok=True) | |
| # Data | |
| print(f"\n{'-'*64}") | |
| print(f" STEP 1: Data Preparation") | |
| print(f"{'-'*64}") | |
| tokenizer, train_tokens, val_tokens = prepare_data(config) | |
| try: | |
| import psutil | |
| print(f" RAM after data: {psutil.Process().memory_info().rss // 1024 // 1024} MB") | |
| except ImportError: | |
| pass | |
| # Model | |
| print(f"\n{'-'*64}") | |
| print(f" STEP 2: Model Construction") | |
| print(f"{'-'*64}") | |
| model = FlashLMv6(config) | |
| try: | |
| import psutil | |
| print(f" RAM after model: {psutil.Process().memory_info().rss // 1024 // 1024} MB") | |
| except ImportError: | |
| pass | |
| optimizer = torch.optim.AdamW( | |
| model.parameters(), lr=config.max_lr, | |
| betas=(0.9, 0.95), weight_decay=config.weight_decay | |
| ) | |
| # Resume | |
| step, tokens_seen, best_val = 0, 0, float('inf') | |
| ckpt_path = out_dir / 'latest_ckpt.pt' | |
| if ckpt_path.exists(): | |
| try: | |
| ckpt = torch.load(ckpt_path, map_location='cpu', weights_only=False) | |
| model.load_state_dict(ckpt['model']) | |
| optimizer.load_state_dict(ckpt['optimizer']) | |
| step = ckpt.get('step', 0) | |
| tokens_seen = ckpt.get('tokens_seen', 0) | |
| best_val = ckpt.get('best_val', float('inf')) | |
| print(f" Resumed from step {step}, {tokens_seen/1e6:.1f}M tokens") | |
| except Exception as e: | |
| print(f" Could not load checkpoint: {e}") | |
| print(f" Starting fresh training") | |
| # Speed calibration | |
| tok_per_sec = calibrate_speed(model, config) | |
| # Training | |
| tokens_per_step = config.batch_size * config.seq_len * config.grad_accum | |
| prompts = ["Once upon a time", "The little dog", "A girl named"] | |
| print(f"\n{'='*64}") | |
| print(f" TRAINING START") | |
| print(f"{'='*64}") | |
| t_start = time.time() | |
| log_loss = 0.0 | |
| log_count = 0 | |
| model.train() | |
| while True: | |
| elapsed = time.time() - t_start | |
| if elapsed / 3600 >= config.total_hours: | |
| print(f"\n Time limit reached ({elapsed/3600:.2f}h)") | |
| break | |
| optimizer.zero_grad(set_to_none=True) | |
| for _ in range(config.grad_accum): | |
| x, y = get_batch(train_tokens, config.batch_size, config.seq_len) | |
| loss = model(x, targets=y) / config.grad_accum | |
| loss.backward() | |
| log_loss += loss.item() | |
| tokens_seen += x.numel() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), config.grad_clip) | |
| lr = get_lr(step, config) | |
| for pg in optimizer.param_groups: | |
| pg['lr'] = lr | |
| optimizer.step() | |
| step += 1 | |
| log_count += 1 | |
| # Logging | |
| if step % config.log_every == 0: | |
| avg_loss = log_loss / max(log_count, 1) | |
| tps = tokens_seen / max(elapsed, 1) | |
| ppl = math.exp(min(avg_loss / config.grad_accum, 20)) | |
| print(f" Step {step:5d} | Loss {avg_loss/config.grad_accum:.4f} | PPL {ppl:8.1f} | " | |
| f"LR {lr:.1e} | {tps:,.0f} tok/s | {tokens_seen/1e6:.2f}M tok | " | |
| f"{elapsed/3600:.2f}h") | |
| log_loss = 0.0 | |
| log_count = 0 | |
| # Evaluation | |
| if step % config.eval_every == 0: | |
| metrics = evaluate(model, val_tokens, config) | |
| tag = '' | |
| if metrics['loss'] < best_val: | |
| best_val = metrics['loss'] | |
| torch.save(model.state_dict(), out_dir / 'best_model.pt') | |
| tag = ' * BEST' | |
| print(f" >>> EVAL | Val Loss {metrics['loss']:.4f} | Val PPL {metrics['ppl']:.1f}{tag}") | |
| # Generation | |
| if step % config.gen_every == 0 and step > 0: | |
| model.eval() | |
| print(f"\n {'~'*50}") | |
| for p in prompts[:2]: | |
| ids = tokenizer.encode(p).ids | |
| inp = torch.tensor([ids], dtype=torch.long) | |
| out = model.generate(inp, max_new_tokens=50, temperature=0.8, top_k=40) | |
| text = tokenizer.decode(out[0].tolist()) | |
| print(f" > {text[:150]}") | |
| print(f" {'~'*50}\n") | |
| model.train() | |
| # Save | |
| if step % config.save_every == 0: | |
| torch.save({ | |
| 'step': step, 'tokens_seen': tokens_seen, | |
| 'best_val': best_val, | |
| 'model': model.state_dict(), | |
| 'optimizer': optimizer.state_dict(), | |
| 'config': {k: v for k, v in vars(config).items() if not k.startswith('_')}, | |
| }, ckpt_path) | |
| print(f" Checkpoint saved at step {step}") | |
| # GC | |
| if step % 100 == 0: | |
| gc.collect() | |
| # Final save | |
| torch.save(model.state_dict(), out_dir / 'final_model.pt') | |
| # Final eval | |
| metrics = evaluate(model, val_tokens, config) | |
| print(f"\n{'='*64}") | |
| print(f" TRAINING COMPLETE") | |
| print(f"{'='*64}") | |
| print(f" Steps: {step:,}") | |
| print(f" Tokens: {tokens_seen/1e6:.2f}M") | |
| print(f" Time: {(time.time()-t_start)/3600:.2f}h") | |
| print(f" Final Val Loss: {metrics['loss']:.4f}") | |
| print(f" Final Val PPL: {metrics['ppl']:.1f}") | |
| print(f" Best Val Loss: {best_val:.4f}") | |
| print(f"{'='*64}") | |
| # Final generations | |
| model.eval() | |
| print("\n Sample Generations:") | |
| for p in prompts: | |
| ids = tokenizer.encode(p).ids | |
| inp = torch.tensor([ids], dtype=torch.long) | |
| out = model.generate(inp, max_new_tokens=80, temperature=0.8, top_k=40) | |
| print(f"\n > {tokenizer.decode(out[0].tolist())[:200]}") | |
| # ============================================================================ | |
| # ENTRY POINT | |
| # ============================================================================ | |
| if __name__ == '__main__': | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument('--hours', type=float, default=12.0) | |
| parser.add_argument('--d_model', type=int, default=192) | |
| parser.add_argument('--n_layers', type=int, default=6) | |
| parser.add_argument('--d_reason', type=int, default=64) | |
| parser.add_argument('--d_planner', type=int, default=32) | |
| parser.add_argument('--seq_len', type=int, default=128) | |
| args = parser.parse_args() | |
| config = Config() | |
| config.total_hours = args.hours | |
| config.d_model = args.d_model | |
| config.n_layers = args.n_layers | |
| config.d_reason = args.d_reason | |
| config.d_planner = args.d_planner | |
| config.seq_len = args.seq_len | |
| train(config) | |