Download scripts/scaling_midpoint_v2.py from itstheraj/fela-acml2026: direct link, hf CLI and curl.
- Browser
- Download file 15.7 kB
-
https://huggingface.co/itstheraj/fela-acml2026/resolve/main/scripts/scaling_midpoint_v2.py
- Command line
-
hf download hf://itstheraj/fela-acml2026/scripts/scaling_midpoint_v2.py
-
curl -L -o scaling_midpoint_v2.py https://huggingface.co/itstheraj/fela-acml2026/resolve/main/scripts/scaling_midpoint_v2.py
15.7 kB
| import argparse | |
| import collections | |
| import json | |
| import logging | |
| import math | |
| import os | |
| import sys | |
| import time | |
| from pathlib import Path | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| import torch | |
| import torch.distributed as dist | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| _root = Path(__file__).parent.parent | |
| if str(_root) not in sys.path: | |
| sys.path.insert(0, str(_root)) | |
| import model_cpu_gpt2 as _m | |
| _m._fla_available = False | |
| from model_cpu_gpt2 import CPUGPT, CPUGPTConfig | |
| logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") | |
| log = logging.getLogger(__name__) | |
| def fela_250m_config() -> CPUGPTConfig: | |
| return CPUGPTConfig( | |
| n_layer=20, | |
| n_embd=1024, | |
| n_head=16, | |
| ffn_hidden=2752, | |
| fno_modes=256, | |
| gla_chunk=256, | |
| seq_len=8192, | |
| layer_pattern="SSSL", | |
| vocab_size=50257, | |
| bias=False, | |
| ) | |
| polar_express_coeffs = [ | |
| (8.156554524902461, -22.48329292557795, 15.878769915207462), | |
| (4.042929935166739, -2.808917465908714, 0.5000178451051316), | |
| (3.8916678022926607, -2.772484153217685, 0.5060648178503393), | |
| (3.285753657755655, -2.3681294933425376, 0.46449024233003106), | |
| (2.3465413258596377, -1.7097828382687081, 0.42323551169305323), | |
| ] | |
| def adamw_step(p, grad, m, v, step, lr, b1, b2, eps, wd): | |
| p.mul_(1 - lr * wd) | |
| m.lerp_(grad, 1 - b1) | |
| v.lerp_(grad.square(), 1 - b2) | |
| bc1 = 1 - b1**step | |
| bc2 = 1 - b2**step | |
| p.addcdiv_(m / bc1, (v / bc2).sqrt_().add_(eps), value=-lr) | |
| def muon_step(grads_stack, params, mom_buf, lr, momentum=0.95, ns_steps=3): | |
| mom_buf.lerp_(grads_stack, 1 - momentum) | |
| X = mom_buf.float() | |
| X = X / (X.norm(dim=(-2, -1), keepdim=True) * 1.02 + 1e-6) | |
| for a, b, c in polar_express_coeffs[:ns_steps]: | |
| if X.size(-2) >= X.size(-1): | |
| A = X.mT @ X | |
| X = a * X + X @ (b * A + c * (A @ A)) | |
| else: | |
| A = X @ X.mT | |
| X = a * X + (b * A + c * (A @ A)) @ X | |
| torch._foreach_sub_(params, list((X * lr).to(params[0].dtype).unbind(0))) | |
| class MuonAdamW(torch.optim.Optimizer): | |
| def __init__(self, param_groups): | |
| super().__init__(param_groups, defaults={}) | |
| def step(self): | |
| for g in self.param_groups: | |
| if g["kind"] == "adamw": | |
| for p in g["params"]: | |
| if p.grad is None: | |
| continue | |
| st = self.state[p] | |
| if not st: | |
| st["step"] = 0 | |
| st["m"] = torch.zeros_like(p) | |
| st["v"] = torch.zeros_like(p) | |
| st["step"] += 1 | |
| adamw_step( | |
| p, | |
| p.grad, | |
| st["m"], | |
| st["v"], | |
| st["step"], | |
| g["lr"], | |
| *g["betas"], | |
| g["eps"], | |
| g.get("wd", 0), | |
| ) | |
| elif g["kind"] == "muon": | |
| params = g["params"] | |
| if not params: | |
| continue | |
| p0 = params[0] | |
| st = self.state[p0] | |
| stacked = torch.stack( | |
| [p.grad for p in params if p.grad is not None] | |
| ).float() | |
| if not st: | |
| st["mom"] = torch.zeros_like(stacked) | |
| lr = g["lr"] * max(1.0, p0.shape[-2] / max(p0.shape[-1], 1)) ** 0.5 | |
| muon_step(stacked, params, st["mom"], lr, g.get("momentum", 0.95)) | |
| def build_optimizer(model, cfg, lr_matrix=0.01, lr_emb=0.02, lr_lm=0.002): | |
| scale = (cfg.n_embd / 768) ** -0.5 | |
| raw = ( | |
| model.module | |
| if isinstance(model, nn.parallel.DistributedDataParallel) | |
| else model | |
| ) | |
| matrix_params, scalar_params = [], [] | |
| for block in raw.blocks: | |
| for p in block.parameters(): | |
| (matrix_params if p.ndim == 2 else scalar_params).append(p) | |
| groups = [ | |
| dict( | |
| kind="adamw", | |
| params=list(raw.wte.parameters()), | |
| lr=lr_emb * scale, | |
| betas=(0.8, 0.95), | |
| eps=1e-8, | |
| ), | |
| dict( | |
| kind="adamw", | |
| params=list(raw.lm_head.parameters()) | |
| if raw.lm_head.weight is not raw.wte.weight | |
| else [], | |
| lr=lr_lm * scale, | |
| betas=(0.8, 0.95), | |
| eps=1e-8, | |
| ), | |
| dict( | |
| kind="adamw", | |
| params=scalar_params, | |
| lr=lr_matrix * scale, | |
| betas=(0.8, 0.95), | |
| eps=1e-8, | |
| ), | |
| ] | |
| for shape in sorted({p.shape for p in matrix_params}): | |
| ps = [p for p in matrix_params if p.shape == shape] | |
| groups.append(dict(kind="muon", params=ps, lr=lr_matrix, momentum=0.95)) | |
| opt = MuonAdamW(groups) | |
| for g in opt.param_groups: | |
| g["initial_lr"] = g["lr"] | |
| return opt | |
| def lr_multiplier(progress, warmup=0.01, min_ratio=0.1): | |
| if progress < warmup: | |
| return progress / warmup | |
| t = (progress - warmup) / (1.0 - warmup) | |
| return min_ratio + (1.0 - min_ratio) * 0.5 * (1.0 + math.cos(math.pi * t)) | |
| def make_loader(data_dir, seq_len, device, batch_size=1, rank=0, world_size=1): | |
| import glob | |
| import queue | |
| import threading | |
| all_parquet = sorted(glob.glob(os.path.join(data_dir, "*.parquet"))) | |
| all_bins = sorted(glob.glob(os.path.join(data_dir, "*.bin"))) | |
| if all_parquet: | |
| import pyarrow.parquet as pq | |
| shards = all_parquet[rank::world_size] or all_parquet | |
| def load_fn(path): | |
| tbl = pq.read_table(path, columns=["tokens"]) | |
| t = torch.tensor(tbl["tokens"].to_pylist()[0], dtype=torch.long) | |
| n = (len(t) // (seq_len + 1)) * (seq_len + 1) | |
| return t[:n].view(-1, seq_len + 1) | |
| elif all_bins: | |
| import numpy as np | |
| shards = all_bins[rank::world_size] or all_bins | |
| def load_fn(path): | |
| data = np.memmap(path, dtype=np.int32, mode="r") | |
| n = (len(data) // (seq_len + 1)) * (seq_len + 1) | |
| return torch.from_numpy(data[:n].copy()).long().view(-1, seq_len + 1) | |
| else: | |
| raise FileNotFoundError(f"No parquet or bin shards in {data_dir}") | |
| buf: queue.Queue = queue.Queue(maxsize=4) | |
| def _producer(): | |
| ep = 1 | |
| while True: | |
| for shard in shards: | |
| seqs = load_fn(shard) | |
| if len(seqs) == 0: | |
| continue | |
| idx = torch.randperm(len(seqs)) | |
| batch = [] | |
| for i in idx: | |
| batch.append(seqs[i]) | |
| if len(batch) == batch_size: | |
| chunk = torch.stack(batch) | |
| buf.put((chunk[:, :-1], chunk[:, 1:], ep)) | |
| batch = [] | |
| ep += 1 | |
| threading.Thread(target=_producer, daemon=True).start() | |
| while True: | |
| x, y, ep = buf.get() | |
| yield x.to(device), y.to(device), ep | |
| def eval_wikitext_bpb(model, cfg, device): | |
| import tiktoken | |
| try: | |
| from datasets import load_dataset | |
| try: | |
| ds = load_dataset( | |
| "Salesforce/wikitext", | |
| "wikitext-103-raw-v1", | |
| split="test", | |
| trust_remote_code=True, | |
| ) | |
| except Exception: | |
| ds = load_dataset( | |
| "wikitext", "wikitext-103-raw-v1", split="test", trust_remote_code=True | |
| ) | |
| text = "\n\n".join(ds["text"]) | |
| except Exception as e: | |
| log.warning(f"WikiText load failed: {e}") | |
| return {"bpb": float("nan"), "nats": float("nan")} | |
| enc = tiktoken.get_encoding("gpt2") | |
| tokens = torch.tensor(enc.encode(text[:5_000_000]), dtype=torch.long)[:1_000_000] | |
| raw = ( | |
| model.module | |
| if isinstance(model, nn.parallel.DistributedDataParallel) | |
| else model | |
| ) | |
| raw.eval() | |
| seq_len = cfg.seq_len | |
| n_chunks = min(100, (len(tokens) - 1) // seq_len) | |
| total_nll, total_tok = 0.0, 0 | |
| with torch.no_grad(): | |
| for i in range(n_chunks): | |
| x = tokens[i * seq_len : (i + 1) * seq_len].unsqueeze(0).to(device) | |
| y = tokens[i * seq_len + 1 : (i + 1) * seq_len + 1].unsqueeze(0).to(device) | |
| if x.shape[1] < seq_len: | |
| break | |
| pad = cfg.gla_chunk - (x.shape[1] % cfg.gla_chunk or cfg.gla_chunk) | |
| if pad < cfg.gla_chunk: | |
| x = F.pad(x, (0, pad)) | |
| logits = raw(x) | |
| lgt = logits[0, :seq_len].float() | |
| loss = F.cross_entropy(lgt, y[0], reduction="sum") | |
| total_nll += loss.item() | |
| total_tok += seq_len | |
| nats = total_nll / max(total_tok, 1) | |
| bpb = nats / math.log(2) | |
| ppl = math.exp(nats) | |
| log.info(f"WikiText eval: BPB={bpb:.4f} nats={nats:.4f} PPL={ppl:.2f}") | |
| return {"bpb": bpb, "nats": nats, "ppl": ppl, "tokens_evaluated": total_tok} | |
| def main(): | |
| parser = argparse.ArgumentParser() | |
| parser.add_argument("--data-dir", default="/data/openwebtext") | |
| parser.add_argument("--out-dir", default="results/scaling_midpoint_v2") | |
| parser.add_argument("--device-batch", type=int, default=4) | |
| parser.add_argument("--smoke", action="store_true") | |
| parser.add_argument("--resume", type=str, default=None) | |
| args = parser.parse_args() | |
| use_ddp = "RANK" in os.environ | |
| local_rank, global_rank = 0, 0 | |
| world_size = 1 | |
| if use_ddp: | |
| dist.init_process_group(backend="nccl") | |
| local_rank = dist.get_rank() % torch.cuda.device_count() | |
| global_rank = dist.get_rank() | |
| world_size = dist.get_world_size() | |
| torch.cuda.set_device(local_rank) | |
| device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu" | |
| is_master = global_rank == 0 | |
| num_gpus = world_size | |
| cfg = fela_250m_config() | |
| model = CPUGPT(cfg).to(device) | |
| if use_ddp: | |
| model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank]) | |
| raw = model.module if use_ddp else model | |
| actual_params = raw.param_count() | |
| if is_master: | |
| log.info(f"FELA ~250M v2: {actual_params / 1e6:.1f}M params") | |
| log.info(f" n_layer={cfg.n_layer} n_embd={cfg.n_embd} seq_len={cfg.seq_len}") | |
| log.info(f" lr_emb=0.02 lr_matrix=0.01 warmup=1% [FIXED from v1]") | |
| opt = build_optimizer(model, cfg) | |
| chinchilla_tokens = actual_params * 20 | |
| if args.smoke: | |
| chinchilla_tokens = 10_000_000 | |
| log.info("Smoke: 10M tokens") | |
| else: | |
| if is_master: | |
| log.info(f"Chinchilla target: {chinchilla_tokens / 1e9:.2f}B tokens") | |
| total_batch_tokens = args.device_batch * cfg.seq_len * num_gpus | |
| total_steps = max(1, int(chinchilla_tokens / total_batch_tokens)) | |
| if is_master: | |
| log.info(f" total_steps={total_steps} batch_tokens={total_batch_tokens:,}") | |
| out_dir = Path(args.out_dir) | |
| ckpt_dir = out_dir / "ckpt" | |
| if is_master: | |
| ckpt_dir.mkdir(parents=True, exist_ok=True) | |
| start_step = 0 | |
| if args.resume: | |
| ck = torch.load(args.resume, map_location="cpu") | |
| sd = { | |
| k.replace("module.", "").replace("_orig_mod.", ""): v | |
| for k, v in ck["model"].items() | |
| } | |
| raw.load_state_dict(sd) | |
| try: | |
| opt.load_state_dict(ck["opt"]) | |
| except Exception: | |
| pass | |
| start_step = ck.get("step", 0) | |
| if is_master: | |
| log.info(f"Resumed from {args.resume} at step {start_step}") | |
| loader = make_loader( | |
| args.data_dir, cfg.seq_len, device, args.device_batch, global_rank, world_size | |
| ) | |
| loss_window: collections.deque = collections.deque(maxlen=50) | |
| t0 = time.perf_counter() | |
| step = start_step | |
| for x, y, _ in loader: | |
| if step >= total_steps: | |
| break | |
| model.train() | |
| progress = step / total_steps | |
| mult = lr_multiplier(progress) | |
| for g in opt.param_groups: | |
| g["lr"] = g["initial_lr"] * mult | |
| with torch.autocast(device_type=device.split(":")[0], dtype=torch.bfloat16): | |
| loss = model(x, y) | |
| loss.backward() | |
| nn.utils.clip_grad_norm_(model.parameters(), 1.0) | |
| opt.step() | |
| opt.zero_grad(set_to_none=True) | |
| step += 1 | |
| raw_loss = loss.item() | |
| loss_window.append(raw_loss) | |
| if is_master and step % 100 == 0: | |
| elapsed = time.perf_counter() - t0 | |
| tps = (step - start_step) * total_batch_tokens / elapsed | |
| eta_h = (total_steps - step) * total_batch_tokens / max(tps, 1) / 3600 | |
| avg_loss = sum(loss_window) / len(loss_window) | |
| log.info( | |
| f"step={step}/{total_steps} ({100 * step / total_steps:.0f}%) " | |
| f"loss={raw_loss:.4f} avg50={avg_loss:.4f} " | |
| f"tok/s={tps:,.0f} ETA={eta_h:.1f}h" | |
| ) | |
| if step >= 10000 and avg_loss > 6.0: | |
| log.error( | |
| f"DIVERGENCE DETECTED: avg50={avg_loss:.4f} > 6.0 at step {step}" | |
| ) | |
| log.error("Aborting. Check LR config or data.") | |
| if use_ddp: | |
| dist.destroy_process_group() | |
| sys.exit(1) | |
| if is_master and step % 5000 == 0: | |
| ck_path = ckpt_dir / f"step_{step:06d}.pt" | |
| torch.save( | |
| {"step": step, "model": raw.state_dict(), "opt": opt.state_dict()}, | |
| ck_path, | |
| ) | |
| avg_loss = sum(loss_window) / len(loss_window) | |
| log.info(f"Checkpoint saved: {ck_path} avg50_loss={avg_loss:.4f}") | |
| if is_master: | |
| torch.save( | |
| {"step": step, "model": raw.state_dict(), "opt": opt.state_dict()}, | |
| ckpt_dir / "final.pt", | |
| ) | |
| log.info(f"Training complete at step {step}. Saved final.pt") | |
| if use_ddp: | |
| dist.barrier() | |
| if is_master: | |
| log.info("=== WikiText BPB eval ===") | |
| raw.eval() | |
| wt = eval_wikitext_bpb(raw, cfg, device) | |
| tokens_trained = step * total_batch_tokens | |
| summary = { | |
| "model": "FELA_250M_v2", | |
| "params_M": actual_params / 1e6, | |
| "n_layer": cfg.n_layer, | |
| "n_embd": cfg.n_embd, | |
| "seq_len": cfg.seq_len, | |
| "tokens_trained": tokens_trained, | |
| "chinchilla_optimal_tokens": chinchilla_tokens, | |
| "pct_chinchilla": round(tokens_trained / chinchilla_tokens * 100, 1), | |
| "steps": step, | |
| "wikitext_bpb": wt["bpb"], | |
| "wikitext_nats": wt.get("nats"), | |
| "wikitext_ppl": wt.get("ppl"), | |
| "lr_emb": 0.02, | |
| "lr_matrix": 0.01, | |
| "fix_note": "v1 had lr_emb=0.1 (5x too high), caused divergence at step 25K", | |
| } | |
| (out_dir / "wikitext.json").write_text(json.dumps(wt, indent=2)) | |
| (out_dir / "summary.json").write_text(json.dumps(summary, indent=2)) | |
| print("\n" + "=" * 60) | |
| print("SCALING LAW DATA POINTS") | |
| print("=" * 60) | |
| print(f" {'Model':<20} {'Params':>8} {'Tokens':>8} {'BPB':>8}") | |
| print(f" {'-' * 48}") | |
| print(f" {'FELA 124M (v9)':<20} {'124M':>8} {'2B':>8} {'1.297':>8}") | |
| print( | |
| f" {'FELA ~250M (v2)':<20} {actual_params / 1e6:>7.0f}M " | |
| f"{tokens_trained / 1e9:>7.1f}B {wt['bpb']:>8.4f}" | |
| ) | |
| print(f" {'FELA 1.13B (22B)':<20} {'1130M':>8} {'22B':>8} {'TBD':>8}") | |
| print("=" * 60) | |
| log.info(f"Outputs: {out_dir}/") | |
| if use_ddp: | |
| dist.destroy_process_group() | |
| if __name__ == "__main__": | |
| main() | |