import argparse import gc import logging import math import os import time from contextlib import nullcontext from pathlib import Path os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") import sys _script_dir = os.path.dirname(os.path.abspath(__file__)) _project_root = os.path.dirname(_script_dir) if _project_root not in sys.path: sys.path.insert(0, _project_root) import torch import torch.distributed as dist import torch.nn as nn import torch.nn.functional as F from torch.utils.checkpoint import checkpoint as grad_ckpt from model_cpu_gpt2 import ( CPUGPT, CPUGPTConfig, get_config, gpt2_small_config, smoke_config, ) logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s") log = logging.getLogger(__name__) BYTE_PAD = 0 BYTE_BOS = 1 BYTE_EOS = 2 BYTE_MASK = 3 BYTE_SEP = 4 BYTE_OFFSET = 5 BYTE_VOCAB = 261 torch.backends.cuda.matmul.allow_tf32 = True torch.backends.cudnn.allow_tf32 = True torch.set_float32_matmul_precision("high") torch.backends.cuda.enable_flash_sdp(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={}) @torch.no_grad() 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: CPUGPT, cfg: CPUGPTConfig, lr_matrix=0.02, lr_emb=0.2, lr_lm=0.004, wd=0.0, betas=(0.8, 0.95), ) -> MuonAdamW: 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=betas, eps=1e-8, wd=0, ), 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=betas, eps=1e-8, wd=0, ), dict( kind="adamw", params=scalar_params, lr=lr_matrix * scale, betas=betas, eps=1e-8, wd=0, ), ] 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: float, warmup: float = 0.01, min_ratio: float = 0.1 ) -> float: 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 _load_shard(path: str, seq_len: int): import pyarrow.parquet as pq tbl = pq.read_table(path, columns=["tokens"]) tokens = torch.tensor(tbl["tokens"].to_pylist()[0], dtype=torch.long) n = (len(tokens) // seq_len) * seq_len return tokens[:n].view(-1, seq_len) def _load_shard_bin(path: str, seq_len: int): import numpy as np data = np.memmap(path, dtype=np.int32, mode="r") n = (len(data) // seq_len) * seq_len return torch.from_numpy(data[:n].copy()).long().view(-1, seq_len) def _load_shard_text(path: str, seq_len: int): import tiktoken enc = tiktoken.get_encoding("r50k_base") with open(path, "r", encoding="utf-8", errors="replace") as f: text = f.read() tokens = torch.tensor(enc.encode(text), dtype=torch.long) n = (len(tokens) // seq_len) * seq_len if n == 0: return torch.zeros(0, seq_len, dtype=torch.long) return tokens[:n].view(-1, seq_len) def _load_wikitext_bytes(max_bytes: int = 2_000_000) -> torch.Tensor: from datasets import load_dataset ds = load_dataset( "Salesforce/wikitext", "wikitext-103-raw-v1", split="test", trust_remote_code=True, ) buf: list[int] = [] for row in ds: text = row["text"] if not text.strip(): continue raw = text.encode("utf-8", errors="replace") buf.append(BYTE_BOS) buf.extend(b + BYTE_OFFSET for b in raw) buf.append(BYTE_EOS) if len(buf) >= max_bytes: break return torch.tensor(buf[:max_bytes], dtype=torch.long) def _owt_bytes_producer( seq_len: int, q: "queue.Queue[tuple[torch.Tensor, torch.Tensor, int]]", batch_size: int, seed: int, ) -> None: from datasets import load_dataset ep = 1 buf: list[int] = [] batch_x: list[torch.Tensor] = [] batch_y: list[torch.Tensor] = [] while True: ds = load_dataset( "Skylion007/openwebtext", split="train", streaming=True, trust_remote_code=True, ) ds = ds.shuffle(seed=seed + ep, buffer_size=10_000) for doc in ds: raw = doc["text"].encode("utf-8", errors="replace") buf.append(BYTE_BOS) buf.extend(b + BYTE_OFFSET for b in raw) buf.append(BYTE_EOS) while len(buf) >= seq_len + 1: chunk = buf[: seq_len + 1] buf = buf[seq_len + 1 :] batch_x.append(torch.tensor(chunk[:-1], dtype=torch.long)) batch_y.append(torch.tensor(chunk[1:], dtype=torch.long)) if len(batch_x) == batch_size: q.put((torch.stack(batch_x), torch.stack(batch_y), ep)) batch_x, batch_y = [], [] ep += 1 def make_loader( data_dir: str, seq_len: int, device: str, batch_size: int = 1, data_format: str = "parquet", ): import glob import queue import threading buf: queue.Queue = queue.Queue(maxsize=4) if data_format == "bytes": t = threading.Thread( target=_owt_bytes_producer, args=(seq_len, buf, batch_size, 42), daemon=True, ) t.start() while True: x, y, ep = buf.get() yield x.to(device), y.to(device), ep return if data_format == "bin": shards = sorted(glob.glob(os.path.join(data_dir, "*.bin"))) load_fn = _load_shard_bin elif data_format == "text": shards = sorted(glob.glob(os.path.join(data_dir, "*.txt"))) load_fn = _load_shard_text else: shards = sorted(glob.glob(os.path.join(data_dir, "*.parquet"))) load_fn = _load_shard if not shards: raise FileNotFoundError(f"No *.{data_format} files found in {data_dir}") def _producer(): ep = 1 while True: for shard in shards: seqs = load_fn(shard, seq_len + 1) if len(seqs) == 0: continue idx = torch.randperm(len(seqs)) batch_x, batch_y = [], [] for i in range(len(idx)): row = seqs[idx[i]] batch_x.append(row[:-1]) batch_y.append(row[1:]) if len(batch_x) == batch_size: buf.put((torch.stack(batch_x), torch.stack(batch_y), ep)) batch_x, batch_y = [], [] ep += 1 t = threading.Thread(target=_producer, daemon=True) t.start() while True: x, y, epoch = buf.get() yield x.to(device), y.to(device), epoch def _make_ckpt_forward(original_forward): def _ckpt_fwd(x): return grad_ckpt(original_forward, x, use_reentrant=False) return _ckpt_fwd def save_ckpt(model, opt, step, path, keep=2): raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) torch.save({"step": step, "model": raw.state_dict(), "opt": opt.state_dict()}, path) log.info(f"checkpoint saved → {path}") old = sorted(Path(path).parent.glob("step_[0-9]*.pt"))[:-keep] for p in old: p.unlink(missing_ok=True) def load_ckpt(model, opt, path): ck = torch.load(path, map_location="cpu") raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) raw.load_state_dict(ck["model"]) opt.load_state_dict(ck["opt"]) return ck["step"] def _eval_val(model, val_tokens: torch.Tensor, seq_len: int, device: str) -> float: raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) raw.eval() T = seq_len total_loss = 0.0 total_cnt = 0 num_win = (len(val_tokens) - 1) // T with torch.no_grad(): for w in range(num_win): x = val_tokens[w * T : (w + 1) * T].unsqueeze(0).to(device) y = val_tokens[w * T + 1 : (w + 1) * T + 1].unsqueeze(0).to(device) if y.shape[1] < T: break loss = raw(x, y) total_loss += loss.item() * T total_cnt += T raw.train() return total_loss / max(total_cnt, 1) def gpu_mem_mb(device) -> float: try: return torch.cuda.max_memory_allocated(device) / (1024 * 1024) except Exception: return -1.0 def train(args): use_ddp = args.num_gpus > 1 local_rank = 0 global_rank = 0 if use_ddp: dist.init_process_group(backend="nccl") local_rank = dist.get_rank() % args.num_gpus global_rank = dist.get_rank() torch.cuda.set_device(local_rank) device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu" is_master = global_rank == 0 torch.manual_seed(args.seed) try: cfg = get_config(args.config) except ValueError: cfg = gpt2_small_config() if args.seq_len: cfg.seq_len = args.seq_len if args.n_embd: cfg.n_embd = args.n_embd if args.n_layer: cfg.n_layer = args.n_layer if args.gla_chunk: cfg.gla_chunk = args.gla_chunk model = CPUGPT(cfg).to(device) nparams = model.param_count() if is_master: log.info(f"model: {nparams / 1e6:.1f}M params config={cfg}") if args.grad_checkpoint: for block in model.blocks: block.forward = _make_ckpt_forward(block.forward) if is_master: log.info("gradient checkpointing enabled") if args.compile: import torch._dynamo as _dynamo _dynamo.config.suppress_errors = True model = torch.compile(model, backend="inductor", fullgraph=False) if is_master: log.info("torch.compile active (inductor)") if use_ddp: model = nn.parallel.DistributedDataParallel( model, device_ids=[local_rank], output_device=local_rank ) opt = build_optimizer( model, cfg, lr_matrix=args.matrix_lr, lr_emb=args.emb_lr, wd=args.weight_decay ) total_tokens = int(float(args.tokens)) tokens_per_step = args.total_batch seq_len = cfg.seq_len dev_batch = args.device_batch world_size = dist.get_world_size() if use_ddp else 1 tokens_per_micro = dev_batch * seq_len * world_size grad_accum = max(1, tokens_per_step // max(tokens_per_micro, 1)) total_steps = max(1, total_tokens // tokens_per_step) if is_master: log.info( f"total_tokens={total_tokens / 1e9:.2f}B steps={total_steps} " f"grad_accum={grad_accum} device_batch={dev_batch} world_size={world_size}" ) if args.precision == "bf16" and torch.cuda.is_available(): amp_ctx = torch.autocast("cuda", dtype=torch.bfloat16) else: amp_ctx = nullcontext() loader = make_loader( args.data_dir, seq_len, device, batch_size=dev_batch, data_format=args.data_format, ) ckpt_dir = Path(args.ckpt_dir) if is_master: ckpt_dir.mkdir(parents=True, exist_ok=True) if use_ddp: dist.barrier() step = 0 resume = sorted(ckpt_dir.glob("step_*.pt")) if resume: saved_initial_lrs = [g["initial_lr"] for g in opt.param_groups] step = load_ckpt(model, opt, resume[-1]) for g, ilr in zip(opt.param_groups, saved_initial_lrs): g["initial_lr"] = ilr if is_master: log.info(f"resumed from step {step}") if step == 0 and is_master: init_pt = ckpt_dir / "init.pt" raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) torch.save(raw.state_dict(), init_pt) log.info(f"init.pt saved → {init_pt}") if args.btm and args.s3_bucket: import io as _io import boto3 as _boto3 _s3i = _boto3.client("s3") _buf = _io.BytesIO() torch.save(raw.state_dict(), _buf) _buf.seek(0) _init_key = f"{args.run_name}/init.pt" _s3i.upload_fileobj(_buf, args.s3_bucket, _init_key) log.info(f"init.pt uploaded → s3://{args.s3_bucket}/{_init_key}") val_tokens = None if args.val_shard and os.path.exists(args.val_shard): import pyarrow.parquet as _pq _tbl = _pq.read_table(args.val_shard, columns=["tokens"]) val_tokens = torch.tensor(_tbl["tokens"].to_pylist()[0], dtype=torch.long) if is_master: log.info(f"val shard: {len(val_tokens):,} tokens from {args.val_shard}") elif args.data_format == "bytes": if is_master: log.info("loading WikiText-103 test split as byte val ...") val_tokens = _load_wikitext_bytes(max_bytes=2_000_000) if is_master: log.info( f"byte val: {len(val_tokens):,} byte tokens from WikiText-103 test" ) diloco = args.num_nodes > 1 and args.master_ip and not args.btm s3 = None velocity = None ref_state = None inner_since_sync = 0 outer_step = 0 if diloco: from diloco_sync import ( diloco_outer_step, init_gloo, load_latest_checkpoint, save_outer_checkpoint, ) init_gloo(args.master_ip, args.gloo_port, args.node_rank, args.num_nodes) if args.s3_bucket: import boto3 s3 = boto3.client("s3") ckpt = load_latest_checkpoint( s3, args.s3_bucket, args.run_name, args.node_rank ) if ckpt is not None: raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) raw.load_state_dict(ckpt["model"]) opt.load_state_dict(ckpt["optimizer"]) velocity = ckpt.get("velocity") outer_step = ckpt["outer_step"] if is_master: log.info(f"resumed outer_step={outer_step}") raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) ref_state = {k: v.clone().cpu() for k, v in raw.state_dict().items()} if is_master: log.info( f"DiLoCo/GLOO: {args.num_nodes} nodes, inner_steps={args.inner_steps}, " f"master={args.master_ip}:{args.gloo_port}" ) if is_master: log.info(f"training for {total_steps} steps on {device}") if step == 0 and is_master: x, y, _ = next(loader) with torch.no_grad(), amp_ctx: loss = model(x, y) log.info( f"step=0 initial loss={loss.item():.4f} gpu_mem={gpu_mem_mb(device):.0f}MB" ) t0 = time.time() tokens_trained = step * tokens_per_step while step < total_steps: progress = step / total_steps lrm = lr_multiplier(progress, min_ratio=args.lr_min_ratio) for g in opt.param_groups: g["lr"] = g["initial_lr"] * lrm opt.zero_grad(set_to_none=True) last_loss = 0.0 for _ in range(grad_accum): x, y, epoch = next(loader) with amp_ctx: loss = model(x, y) / grad_accum loss.backward() last_loss += loss.item() torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0) opt.step() dt = time.time() - t0 t0 = time.time() tokens_trained += tokens_per_step step += 1 if diloco: inner_since_sync += 1 if step % 10 == 0 and is_master: tok_per_sec = int(tokens_per_step / dt) cost_1m = ( (args.instance_price / 3600.0) / (tok_per_sec / 1e6) if tok_per_sec > 0 else 0.0 ) log.info( f"step={step}/{total_steps} ({100 * progress:.1f}%) " f"loss={last_loss:.4f} tok/s={tok_per_sec:,} " f"gpu_mem={gpu_mem_mb(device):.0f}MB epoch={epoch} cost_1m={cost_1m:.3f}$" ) if step % args.checkpoint_every == 0 and is_master: save_ckpt(model, opt, step, ckpt_dir / f"step_{step:06d}.pt") if ( val_tokens is not None and args.val_every > 0 and step % args.val_every == 0 and is_master ): val_nats = _eval_val(model, val_tokens, seq_len, device) if args.data_format == "bytes": log.info( f"val step={step} val_nats={val_nats:.4f} " f"val_bpb={val_nats / 0.6931:.4f}" ) else: log.info( f"val step={step} val_nats={val_nats:.4f} " f"val_bpb_approx={val_nats / 0.6931 / 4.0:.4f}" ) if step == 1: gc.collect() gc.freeze() gc.disable() if ( diloco and inner_since_sync >= args.inner_steps and (total_steps - step) >= args.inner_steps ): raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) velocity = diloco_outer_step( raw, ref_state, velocity, args.outer_lr, args.outer_momentum, ) outer_step += 1 inner_since_sync = 0 ref_state = {k: v.clone().cpu() for k, v in raw.state_dict().items()} if is_master: log.info(f"outer_step={outer_step} complete") save_outer_checkpoint( s3, args.s3_bucket, args.run_name, outer_step, step, args.node_rank, raw, opt.state_dict(), velocity, ) if is_master: log.info(f"training complete final_loss={last_loss:.4f}") save_ckpt(model, opt, step, ckpt_dir / f"step_{step:06d}_final.pt") if args.btm and args.s3_bucket: import io import boto3 s3_btm = boto3.client("s3") buf = io.BytesIO() raw = ( model.module if isinstance(model, nn.parallel.DistributedDataParallel) else model ) torch.save(raw.state_dict(), buf) buf.seek(0) btm_key = f"{args.run_name}/node_{args.node_rank:04d}/latest.pt" s3_btm.upload_fileobj(buf, args.s3_bucket, btm_key) log.info(f"BTM upload complete → s3://{args.s3_bucket}/{btm_key}") if use_ddp: dist.destroy_process_group() def parse_args(): p = argparse.ArgumentParser( description="GPU training script for FNO+GLA language model." ) p.add_argument( "--config", default="gpt2-small", choices=["smoke", "gpt2-small", "gpt2-1b", "gpt2-8b", "byte-125m"], help="Model config name", ) p.add_argument("--tokens", type=float, default=2e9, help="Total training tokens") p.add_argument( "--total-batch", type=int, default=131072, help="Global batch size in tokens per optimizer step", ) p.add_argument( "--device-batch", type=int, default=4, help="Sequences per GPU per micro-step" ) p.add_argument("--matrix-lr", type=float, default=0.01) p.add_argument("--emb-lr", type=float, default=0.02) p.add_argument("--weight-decay", type=float, default=0.1) p.add_argument("--seed", type=int, default=42) p.add_argument( "--seq-len", type=int, default=None, help="Override config seq_len (e.g. 32768 for long-context)", ) p.add_argument("--n-embd", type=int, default=None) p.add_argument("--n-layer", type=int, default=None) p.add_argument( "--gla-chunk", type=int, default=None, help="GLA intra-chunk size (default: from config)", ) p.add_argument( "--lr-min-ratio", type=float, default=0.1, help="Cosine decay floor as fraction of peak LR (default 0.1 = 10%%)", ) p.add_argument("--checkpoint-every", type=int, default=50) p.add_argument("--data-dir", default=os.path.expanduser("~/data")) p.add_argument("--ckpt-dir", default="checkpoints/gpu_gpt2") p.add_argument( "--num-gpus", type=int, default=1, help="Number of GPUs on this node (enables DDP when > 1)", ) p.add_argument( "--grad-checkpoint", action="store_true", help="Enable gradient checkpointing per block (needed for seq_len=32K)", ) p.add_argument( "--precision", default="bf16", choices=["bf16", "fp32"], help="Training precision (default bf16, A100 supports BF16 natively)", ) p.add_argument( "--compile", action="store_true", help="Wrap model with torch.compile(inductor)" ) p.add_argument( "--no-compile", action="store_true", help="Explicitly disable torch.compile (useful for smoke tests)", ) p.add_argument( "--data-format", default="parquet", choices=["parquet", "bin", "text", "bytes"], help="Shard format: parquet (default), bin (numpy memmap int32), " "text (raw .txt tokenized with tiktoken), or bytes " "(streams OWT raw UTF-8 bytes via HuggingFace — no tokenizer)", ) p.add_argument("--node-rank", type=int, default=0) p.add_argument("--num-nodes", type=int, default=1) p.add_argument("--inner-steps", type=int, default=500) p.add_argument("--outer-lr", type=float, default=0.7) p.add_argument("--outer-momentum", type=float, default=0.9) p.add_argument("--s3-bucket", default=None) p.add_argument("--run-name", default="gpu_gpt2") p.add_argument( "--master-ip", default=None, help="IP of rank-0 node for GLOO rendezvous (enables DiLoCo when set)", ) p.add_argument("--gloo-port", type=int, default=23456) p.add_argument( "--btm", action="store_true", help="Branch-Train-Merge: upload final model to S3 for merge", ) p.add_argument("--val-shard", default=None) p.add_argument("--val-every", type=int, default=500) p.add_argument( "--instance-price", type=float, default=12.0, help="EC2 spot price per hour (default 12.0 for p4d.24xlarge)", ) return p.parse_args() if __name__ == "__main__": args = parse_args() if args.no_compile: args.compile = False train(args)