| """ |
| ZeroShot-1B — single-file LLaMA-style 1.19B trainer + inference. |
| |
| Architecture trimmed from the original spec to fit microbatch=4 on a 32 GB |
| 5090. Original was FFN=8192, ctx=4096 → ~29 GB activations alone, OOM. |
| This: FFN=5632, ctx=3072 → ~21 GB activations, ~29 GB peak. |
| |
| Subcommands: |
| python train.py base --no-compile |
| python train.py mid --checkpoint ckpt_base_final.pt --no-compile |
| python train.py finetune --checkpoint ckpt_mid_final.pt --no-compile |
| python train.py finetune --checkpoint ckpt_mid_final.pt --no-compile --skip_mid |
| python train.py generate --checkpoint ckpt_sft_final.pt --prompt "..." --chat |
| |
| Auto-resume: re-launching a stage picks up the newest matching ckpt in |
| --ckpt_dir (model + optimizer + step + RNG + dataset sample counter). Pass |
| --fresh to ignore. --checkpoint is for cross-stage *weight* init only |
| (fresh optimizer). |
| |
| Exit codes: |
| 0 — clean finish |
| 2 — recoverable failure (data, OOM, SIGTERM) — emergency ckpt saved, run.sh restarts |
| 130 — Ctrl-C |
| """ |
| import argparse |
| import math |
| import os |
| import queue |
| import random |
| import signal |
| import sys |
| import threading |
| import time |
| from dataclasses import asdict, dataclass |
| from pathlib import Path |
| from typing import Iterator |
|
|
| import numpy as np |
| import tiktoken |
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
| from datasets import load_dataset |
|
|
|
|
| |
| |
| |
|
|
| @dataclass |
| class ModelConfig: |
| vocab_size: int = 50304 |
| n_layers: int = 24 |
| n_heads: int = 16 |
| n_kv_heads: int = 4 |
| d_model: int = 2048 |
| d_ff: int = 5632 |
| max_seq_len: int = 3072 |
| rope_theta: float = 10000.0 |
| rms_eps: float = 1e-5 |
|
|
|
|
| @dataclass |
| class TrainConfig: |
| stage: str = "base" |
| micro_batch_size: int = 4 |
| grad_accum: int = 20 |
| seq_len: int = 3072 |
| max_steps: int = 25_000 |
| warmup_steps: int = 2_000 |
| peak_lr: float = 3e-4 |
| min_lr: float = 3e-5 |
| weight_decay: float = 0.1 |
| beta1: float = 0.9 |
| beta2: float = 0.95 |
| grad_clip: float = 1.0 |
| log_every: int = 10 |
| ckpt_interval_sec: int = 30 * 60 |
| ckpt_keep: int = 1 |
| ckpt_dir: str = "./checkpoints" |
| seed: int = 42 |
| cost_per_hour: float = 0.365 |
| wandb_project: str = "zeroshot-1b" |
| data_max_retries: int = 5 |
| data_retry_base_delay: float = 2.0 |
|
|
|
|
| STAGE_PRESETS = { |
| "base": dict( |
| max_steps=25_000, warmup_steps=2_000, peak_lr=3e-4, min_lr=3e-5, |
| ckpt_prefix="ckpt_base", micro_batch_size=4, grad_accum=20, |
| ), |
| "mid": dict( |
| max_steps=2_500, warmup_steps=200, peak_lr=1e-4, min_lr=1e-5, |
| ckpt_prefix="ckpt_mid", micro_batch_size=4, grad_accum=20, |
| ), |
| "finetune": dict( |
| max_steps=2_000, warmup_steps=100, peak_lr=5e-5, min_lr=5e-6, |
| ckpt_prefix="ckpt_sft", micro_batch_size=4, grad_accum=8, |
| ), |
| } |
|
|
|
|
| |
| |
| |
|
|
| class RMSNorm(nn.Module): |
| def __init__(self, dim, eps=1e-5): |
| super().__init__() |
| self.eps = eps |
| self.weight = nn.Parameter(torch.ones(dim)) |
|
|
| def forward(self, x): |
| x32 = x.float() |
| norm = x32 * torch.rsqrt(x32.pow(2).mean(-1, keepdim=True) + self.eps) |
| return (norm * self.weight.float()).type_as(x) |
|
|
|
|
| def precompute_rope(head_dim, max_seq_len, theta, device="cpu"): |
| inv_freq = 1.0 / (theta ** (torch.arange(0, head_dim, 2, device=device).float() / head_dim)) |
| t = torch.arange(max_seq_len, device=device).float() |
| freqs = torch.outer(t, inv_freq) |
| return freqs.cos(), freqs.sin() |
|
|
|
|
| def apply_rope(x, cos, sin): |
| |
| x1, x2 = x.chunk(2, dim=-1) |
| cos = cos[None, None, :, :] |
| sin = sin[None, None, :, :] |
| return torch.cat([x1 * cos - x2 * sin, x1 * sin + x2 * cos], dim=-1) |
|
|
|
|
| class Attention(nn.Module): |
| def __init__(self, cfg: ModelConfig): |
| super().__init__() |
| assert cfg.d_model % cfg.n_heads == 0 |
| assert cfg.n_heads % cfg.n_kv_heads == 0 |
| self.n_heads = cfg.n_heads |
| self.n_kv_heads = cfg.n_kv_heads |
| self.head_dim = cfg.d_model // cfg.n_heads |
| self.n_rep = cfg.n_heads // cfg.n_kv_heads |
| self.q_proj = nn.Linear(cfg.d_model, cfg.n_heads * self.head_dim, bias=False) |
| self.k_proj = nn.Linear(cfg.d_model, cfg.n_kv_heads * self.head_dim, bias=False) |
| self.v_proj = nn.Linear(cfg.d_model, cfg.n_kv_heads * self.head_dim, bias=False) |
| self.o_proj = nn.Linear(cfg.n_heads * self.head_dim, cfg.d_model, bias=False) |
|
|
| def forward(self, x, cos, sin, kv_cache=None, pos=0): |
| B, T, _ = x.shape |
| q = self.q_proj(x).view(B, T, self.n_heads, self.head_dim).transpose(1, 2) |
| k = self.k_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) |
| v = self.v_proj(x).view(B, T, self.n_kv_heads, self.head_dim).transpose(1, 2) |
|
|
| cos_t = cos[pos:pos + T] |
| sin_t = sin[pos:pos + T] |
| q = apply_rope(q, cos_t, sin_t) |
| k = apply_rope(k, cos_t, sin_t) |
|
|
| if kv_cache is not None: |
| k_cache, v_cache = kv_cache |
| k_cache[:, :, pos:pos + T] = k |
| v_cache[:, :, pos:pos + T] = v |
| k = k_cache[:, :, :pos + T] |
| v = v_cache[:, :, :pos + T] |
|
|
| |
| |
| k = k.repeat_interleave(self.n_rep, dim=1) |
| v = v.repeat_interleave(self.n_rep, dim=1) |
|
|
| |
| |
| |
| is_causal = (kv_cache is None) or (T > 1) |
| out = F.scaled_dot_product_attention(q, k, v, is_causal=is_causal) |
| out = out.transpose(1, 2).contiguous().view(B, T, -1) |
| return self.o_proj(out) |
|
|
|
|
| class SwiGLU(nn.Module): |
| def __init__(self, cfg: ModelConfig): |
| super().__init__() |
| self.gate_proj = nn.Linear(cfg.d_model, cfg.d_ff, bias=False) |
| self.up_proj = nn.Linear(cfg.d_model, cfg.d_ff, bias=False) |
| self.down_proj = nn.Linear(cfg.d_ff, cfg.d_model, bias=False) |
|
|
| def forward(self, x): |
| return self.down_proj(F.silu(self.gate_proj(x)) * self.up_proj(x)) |
|
|
|
|
| class Block(nn.Module): |
| def __init__(self, cfg: ModelConfig): |
| super().__init__() |
| self.attn_norm = RMSNorm(cfg.d_model, cfg.rms_eps) |
| self.attn = Attention(cfg) |
| self.mlp_norm = RMSNorm(cfg.d_model, cfg.rms_eps) |
| self.mlp = SwiGLU(cfg) |
|
|
| def forward(self, x, cos, sin, kv_cache=None, pos=0): |
| x = x + self.attn(self.attn_norm(x), cos, sin, kv_cache, pos) |
| x = x + self.mlp(self.mlp_norm(x)) |
| return x |
|
|
|
|
| class LLaMA(nn.Module): |
| def __init__(self, cfg: ModelConfig): |
| super().__init__() |
| self.cfg = cfg |
| self.tok_emb = nn.Embedding(cfg.vocab_size, cfg.d_model) |
| self.layers = nn.ModuleList([Block(cfg) for _ in range(cfg.n_layers)]) |
| self.norm = RMSNorm(cfg.d_model, cfg.rms_eps) |
|
|
| cos, sin = precompute_rope(cfg.d_model // cfg.n_heads, cfg.max_seq_len, cfg.rope_theta) |
| self.register_buffer("rope_cos", cos, persistent=False) |
| self.register_buffer("rope_sin", sin, persistent=False) |
|
|
| self.apply(self._init_weights) |
| for name, p in self.named_parameters(): |
| if name.endswith("o_proj.weight") or name.endswith("down_proj.weight"): |
| nn.init.normal_(p, mean=0.0, std=0.02 / math.sqrt(2 * cfg.n_layers)) |
|
|
| @staticmethod |
| def _init_weights(m): |
| if isinstance(m, nn.Linear): |
| nn.init.normal_(m.weight, mean=0.0, std=0.02) |
| if m.bias is not None: |
| nn.init.zeros_(m.bias) |
| elif isinstance(m, nn.Embedding): |
| nn.init.normal_(m.weight, mean=0.0, std=0.02) |
|
|
| def forward(self, idx, targets=None, kv_caches=None, pos=0, ignore_index=-100): |
| x = self.tok_emb(idx) |
| for i, layer in enumerate(self.layers): |
| kv = kv_caches[i] if kv_caches is not None else None |
| x = layer(x, self.rope_cos, self.rope_sin, kv, pos) |
| x = self.norm(x) |
| logits = F.linear(x, self.tok_emb.weight) |
| loss = None |
| if targets is not None: |
| loss = F.cross_entropy( |
| logits.view(-1, logits.size(-1)).float(), |
| targets.view(-1), |
| ignore_index=ignore_index, |
| ) |
| return logits, loss |
|
|
| def num_params(self): |
| return sum(p.numel() for p in self.parameters()) |
|
|
| def configure_optimizer(self, lr, weight_decay, betas): |
| |
| |
| import bitsandbytes as bnb |
| decay, no_decay = [], [] |
| for _, p in self.named_parameters(): |
| if not p.requires_grad: |
| continue |
| (decay if p.dim() >= 2 else no_decay).append(p) |
| groups = [ |
| {"params": decay, "weight_decay": weight_decay}, |
| {"params": no_decay, "weight_decay": 0.0}, |
| ] |
| opt = bnb.optim.AdamW8bit(groups, lr=lr, betas=betas) |
| bnb.optim.GlobalOptimManager.get_instance().register_module_override( |
| self.tok_emb, "weight", {"optim_bits": 32} |
| ) |
| return opt |
|
|
|
|
| |
| |
| |
|
|
| ENC = tiktoken.get_encoding("gpt2") |
| EOT = ENC.eot_token |
| IGNORE = -100 |
|
|
| USER_TAG = "\n<|user|>\n" |
| ASSISTANT_TAG = "\n<|assistant|>\n" |
|
|
|
|
| class _StreamError(RuntimeError): |
| pass |
|
|
|
|
| def _retry_iter(open_fn, max_retries, base_delay): |
| """Run open_fn() to get a fresh iterator, yield from it; on exception |
| recreate via open_fn() with exp backoff. open_fn closes over the right |
| skip count (set on stage start; on hard restart, run.sh re-invokes us |
| and we recreate from the latest ckpt's samples_consumed).""" |
| attempt = 0 |
| while True: |
| try: |
| it = open_fn() |
| for x in it: |
| yield x |
| attempt = 0 |
| return |
| except StopIteration: |
| return |
| except Exception as e: |
| attempt += 1 |
| if attempt > max_retries: |
| raise _StreamError(f"max retries ({max_retries}) exceeded: {e}") from e |
| delay = base_delay * (2 ** (attempt - 1)) |
| print(f"[data] {type(e).__name__}: {e}; retry {attempt}/{max_retries} in {delay:.1f}s", |
| flush=True) |
| time.sleep(delay) |
|
|
|
|
| def _open_fineweb(seed, skip): |
| ds = load_dataset("HuggingFaceFW/fineweb-edu", name="sample-10BT", |
| split="train", streaming=True) |
| ds = ds.shuffle(seed=seed, buffer_size=10_000) |
| return ds.skip(skip) if skip else ds |
|
|
|
|
| def _open_cosmopedia(seed, skip): |
| ds = load_dataset("HuggingFaceTB/cosmopedia", name="web_samples_v2", |
| split="train", streaming=True) |
| ds = ds.shuffle(seed=seed, buffer_size=10_000) |
| return ds.skip(skip) if skip else ds |
|
|
|
|
| def _open_ultrachat(seed, skip): |
| ds = load_dataset("HuggingFaceH4/ultrachat_200k", split="train_sft", streaming=True) |
| ds = ds.shuffle(seed=seed, buffer_size=2_000) |
| return ds.skip(skip) if skip else ds |
|
|
|
|
| def _doc_tokens(ds_iter, text_key): |
| for sample in ds_iter: |
| text = sample.get(text_key) or "" |
| if not text: |
| continue |
| ids = ENC.encode_ordinary(text) |
| ids.append(EOT) |
| for t in ids: |
| yield t |
|
|
|
|
| def _pack(token_iter: Iterator[int], seq_len): |
| buf = [] |
| for tok in token_iter: |
| buf.append(tok) |
| if len(buf) >= seq_len + 1: |
| chunk = buf[: seq_len + 1] |
| del buf[: seq_len] |
| x = torch.tensor(chunk[:-1], dtype=torch.long) |
| y = torch.tensor(chunk[1:], dtype=torch.long) |
| yield x, y |
|
|
|
|
| def base_loader(seq_len, seed, skip_samples, max_retries, base_delay): |
| yield from _pack( |
| _doc_tokens(_retry_iter(lambda: _open_fineweb(seed, skip_samples), |
| max_retries, base_delay), "text"), |
| seq_len, |
| ) |
|
|
|
|
| def mid_loader(seq_len, seed, skip_samples, max_retries, base_delay, mix_ratio=0.1): |
| """90% FineWeb-Edu / 10% Cosmopedia, switched at document boundaries.""" |
| fw_iter = _doc_tokens( |
| _retry_iter(lambda: _open_fineweb(seed, skip_samples), max_retries, base_delay), "text") |
| co_iter = _doc_tokens( |
| _retry_iter(lambda: _open_cosmopedia(seed + 1, skip_samples // 10), max_retries, base_delay), "text") |
| rng = random.Random(seed) |
|
|
| def mixed(): |
| while True: |
| src = co_iter if rng.random() < mix_ratio else fw_iter |
| while True: |
| t = next(src) |
| yield t |
| if t == EOT: |
| break |
|
|
| yield from _pack(mixed(), seq_len) |
|
|
|
|
| def _format_chat(messages): |
| ids, mask = [], [] |
| for msg in messages: |
| role = msg.get("role") |
| content = (msg.get("content") or "").strip() |
| if not content: |
| continue |
| if role == "user": |
| tag = ENC.encode_ordinary(USER_TAG + content) |
| ids.extend(tag); mask.extend([0] * len(tag)) |
| elif role == "assistant": |
| head = ENC.encode_ordinary(ASSISTANT_TAG) |
| body = ENC.encode_ordinary(content) |
| ids.extend(head); mask.extend([0] * len(head)) |
| ids.extend(body); mask.extend([1] * len(body)) |
| ids.append(EOT); mask.append(1) |
| return ids, mask |
|
|
|
|
| def sft_loader(seq_len, seed, skip_samples, max_retries, base_delay): |
| for sample in _retry_iter(lambda: _open_ultrachat(seed, skip_samples), max_retries, base_delay): |
| ids, mask = _format_chat(sample.get("messages") or []) |
| if len(ids) < 8: |
| continue |
| ids = ids[: seq_len + 1] |
| mask = mask[: seq_len + 1] |
| pad_n = (seq_len + 1) - len(ids) |
| if pad_n > 0: |
| ids += [EOT] * pad_n |
| mask += [0] * pad_n |
| x = torch.tensor(ids[:-1], dtype=torch.long) |
| y = torch.tensor( |
| [t if m else IGNORE for t, m in zip(ids[1:], mask[1:])], |
| dtype=torch.long, |
| ) |
| yield x, y |
|
|
|
|
| class _Prefetcher: |
| """Background thread fills a bounded queue. Errors propagate on next().""" |
| def __init__(self, gen, max_prefetch=8): |
| self.q = queue.Queue(maxsize=max_prefetch) |
| self._sentinel = object() |
| self._error = None |
| self.thread = threading.Thread(target=self._run, args=(gen,), daemon=True) |
| self.thread.start() |
|
|
| def _run(self, gen): |
| try: |
| for x in gen: |
| self.q.put(x) |
| except BaseException as e: |
| self._error = e |
| finally: |
| self.q.put(self._sentinel) |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| x = self.q.get() |
| if x is self._sentinel: |
| if self._error is not None: |
| raise self._error |
| raise StopIteration |
| return x |
|
|
|
|
| class StagedLoader: |
| """Wraps a sample generator into a batch iterator and counts samples consumed.""" |
| def __init__(self, gen_fn, train_cfg, stage_seed, skip_samples=0, prefetch=8): |
| self.samples_consumed = skip_samples |
| self.micro_batch = train_cfg.micro_batch_size |
| gen = gen_fn( |
| seq_len=train_cfg.seq_len, |
| seed=stage_seed, |
| skip_samples=skip_samples, |
| max_retries=train_cfg.data_max_retries, |
| base_delay=train_cfg.data_retry_base_delay, |
| ) |
| self._gen = _Prefetcher(gen, max_prefetch=prefetch) |
|
|
| def __iter__(self): |
| return self |
|
|
| def __next__(self): |
| xs, ys = [], [] |
| for _ in range(self.micro_batch): |
| x, y = next(self._gen) |
| xs.append(x); ys.append(y) |
| self.samples_consumed += 1 |
| return torch.stack(xs), torch.stack(ys) |
|
|
|
|
| def build_loader(stage, train_cfg, stage_seed, skip_samples=0): |
| gen = {"base": base_loader, "mid": mid_loader, "finetune": sft_loader}[stage] |
| return StagedLoader(gen, train_cfg, stage_seed, skip_samples) |
|
|
|
|
| |
| |
| |
|
|
| def cosine_lr(step, warmup, max_steps, peak, min_lr): |
| if step < warmup: |
| return peak * (step + 1) / max(1, warmup) |
| if step >= max_steps: |
| return min_lr |
| progress = (step - warmup) / max(1, max_steps - warmup) |
| return min_lr + 0.5 * (peak - min_lr) * (1.0 + math.cos(math.pi * progress)) |
|
|
|
|
| def find_latest_ckpt(ckpt_dir: Path, prefix: str): |
| files = list(ckpt_dir.glob(f"{prefix}_step*.pt")) |
| return max(files, key=lambda p: p.stat().st_mtime) if files else None |
|
|
|
|
| def prune_ckpts(ckpt_dir: Path, prefix: str, keep: int): |
| files = sorted(ckpt_dir.glob(f"{prefix}_step*.pt"), key=lambda p: p.stat().st_mtime) |
| for f in files[:-keep]: |
| try: |
| f.unlink() |
| except OSError: |
| pass |
|
|
|
|
| def get_rng_state(): |
| return { |
| "torch": torch.get_rng_state(), |
| "torch_cuda": torch.cuda.get_rng_state_all() if torch.cuda.is_available() else None, |
| "numpy": np.random.get_state(), |
| "python": random.getstate(), |
| } |
|
|
|
|
| def set_rng_state(state): |
| torch.set_rng_state(state["torch"].cpu().byte()) |
| if state.get("torch_cuda") is not None and torch.cuda.is_available(): |
| torch.cuda.set_rng_state_all([s.cpu().byte() for s in state["torch_cuda"]]) |
| np.random.set_state(state["numpy"]) |
| random.setstate(state["python"]) |
|
|
|
|
| def save_checkpoint(path, model, optimizer, step, train_cfg, model_cfg, samples_consumed): |
| payload = { |
| "model": model.state_dict(), |
| "optimizer": optimizer.state_dict(), |
| "step": step, |
| "train_cfg": asdict(train_cfg), |
| "model_cfg": asdict(model_cfg), |
| "samples_consumed": samples_consumed, |
| "rng": get_rng_state(), |
| } |
| tmp = str(path) + ".tmp" |
| torch.save(payload, tmp) |
| os.replace(tmp, path) |
|
|
|
|
| def load_checkpoint(path, model, optimizer=None, map_location="cuda"): |
| ckpt = torch.load(path, map_location=map_location, weights_only=False) |
| sd = ckpt["model"] |
| if any(k.startswith("_orig_mod.") for k in sd): sd = {k.removeprefix("_orig_mod."): v for k, v in sd.items()} |
| model.load_state_dict(sd) |
| if optimizer is not None and "optimizer" in ckpt: |
| optimizer.load_state_dict(ckpt["optimizer"]) |
| return ckpt |
|
|
|
|
| def estimate_vram_gb(model_cfg: ModelConfig, train_cfg: TrainConfig, n_params: int): |
| weight_gb = n_params * 2 / 1e9 |
| grad_gb = n_params * 2 / 1e9 |
| optim_gb = n_params * 2 / 1e9 + 0.82 |
| fixed = weight_gb + grad_gb + optim_gb |
| B, T = train_cfg.micro_batch_size, train_cfg.seq_len |
| per_layer = B * T * (13 * model_cfg.d_model + 8 * model_cfg.d_ff) * 2 / 1e9 |
| return fixed, per_layer * model_cfg.n_layers |
|
|
|
|
| def print_startup(model, train_cfg, model_cfg, ckpt_prefix, start_step): |
| n = model.num_params() |
| fixed, acts = estimate_vram_gb(model_cfg, train_cfg, n) |
| tps = train_cfg.micro_batch_size * train_cfg.grad_accum * train_cfg.seq_len |
| bar = "=" * 64 |
| print(bar) |
| print(f" ZeroShot-1B — stage: {train_cfg.stage} (prefix: {ckpt_prefix})") |
| print(bar) |
| print(f" params: {n/1e9:.3f}B ({n:,})") |
| print(f" dtype: bfloat16 optim: AdamW8bit (bnb) ctx: {train_cfg.seq_len}") |
| print(f" arch: L={model_cfg.n_layers} H={model_cfg.n_heads} KV={model_cfg.n_kv_heads} " |
| f"d={model_cfg.d_model} ffn={model_cfg.d_ff}") |
| print(f" micro_batch: {train_cfg.micro_batch_size} grad_accum: {train_cfg.grad_accum} " |
| f"tokens/step: {tps:,}") |
| print(f" schedule: cosine warmup={train_cfg.warmup_steps} peak={train_cfg.peak_lr:.1e} " |
| f"min={train_cfg.min_lr:.1e} steps={train_cfg.max_steps}") |
| print(f" total tokens: {tps*train_cfg.max_steps/1e9:.2f}B (start step {start_step})") |
| print(f" vram (est): fixed {fixed:.1f} + acts {acts:.1f} = {fixed+acts:.1f} GB / 32 GB") |
| print(f" ckpt every: {train_cfg.ckpt_interval_sec//60} min wall → " |
| f"{train_cfg.ckpt_dir}/ (keep last {train_cfg.ckpt_keep})") |
| print(bar, flush=True) |
|
|
|
|
| def fmt_eta(seconds): |
| if seconds <= 0 or math.isinf(seconds): |
| return "?" |
| h = int(seconds // 3600); m = int((seconds % 3600) // 60) |
| return f"{h:d}h{m:02d}m" |
|
|
|
|
| |
| |
| |
|
|
| def train(stage: str, args): |
| preset = STAGE_PRESETS[stage] |
| train_cfg = TrainConfig(stage=stage, **{k: v for k, v in preset.items() if k != "ckpt_prefix"}) |
| ckpt_prefix = preset["ckpt_prefix"] |
|
|
| if args.lr is not None: train_cfg.peak_lr = args.lr |
| if args.min_lr is not None: train_cfg.min_lr = args.min_lr |
| if args.batch_size is not None: train_cfg.micro_batch_size = args.batch_size |
| if args.grad_accum is not None: train_cfg.grad_accum = args.grad_accum |
| if args.steps is not None: train_cfg.max_steps = args.steps |
| if args.warmup is not None: train_cfg.warmup_steps = args.warmup |
| if args.ckpt_dir is not None: train_cfg.ckpt_dir = args.ckpt_dir |
| if args.seed is not None: train_cfg.seed = args.seed |
|
|
| ckpt_dir = Path(train_cfg.ckpt_dir) |
| ckpt_dir.mkdir(parents=True, exist_ok=True) |
|
|
| device = "cuda" |
| torch.manual_seed(train_cfg.seed) |
| np.random.seed(train_cfg.seed) |
| random.seed(train_cfg.seed) |
| torch.set_float32_matmul_precision("high") |
|
|
| model_cfg = ModelConfig() |
| model = LLaMA(model_cfg).to(device=device, dtype=torch.bfloat16) |
| optimizer = model.configure_optimizer( |
| lr=train_cfg.peak_lr, |
| weight_decay=train_cfg.weight_decay, |
| betas=(train_cfg.beta1, train_cfg.beta2), |
| ) |
|
|
| start_step = 0 |
| samples_consumed = 0 |
| resume_path = None if args.fresh else find_latest_ckpt(ckpt_dir, ckpt_prefix) |
|
|
| if resume_path is not None: |
| print(f"[resume] {resume_path}") |
| ckpt = load_checkpoint(resume_path, model, optimizer, map_location=device) |
| start_step = ckpt["step"] |
| samples_consumed = ckpt.get("samples_consumed", 0) |
| if "rng" in ckpt: |
| set_rng_state(ckpt["rng"]) |
| elif args.checkpoint is not None: |
| init_path = Path(args.checkpoint) |
| if not init_path.is_absolute() and not init_path.exists(): |
| init_path = ckpt_dir / args.checkpoint |
| print(f"[init-from] {init_path} (weights only, fresh optimizer)") |
| load_checkpoint(init_path, model, optimizer=None, map_location=device) |
| elif stage != "base": |
| raise SystemExit(f"{stage} requires --checkpoint pointing at the prior stage's ckpt " |
| f"(no auto-resume ckpt found in {ckpt_dir})") |
|
|
| stage_seed = train_cfg.seed + {"base": 0, "mid": 1000, "finetune": 2000}[stage] |
| model = torch.compile(model, mode="default") |
| print_startup(model, train_cfg, model_cfg, ckpt_prefix, start_step) |
|
|
| use_wandb = bool(os.environ.get("WANDB_API_KEY")) and not args.no_wandb |
| if use_wandb: |
| import wandb |
| wandb.init( |
| project=train_cfg.wandb_project, |
| name=f"{stage}-{int(time.time())}", |
| config={**asdict(train_cfg), **asdict(model_cfg)}, |
| resume="allow", |
| ) |
|
|
| loader = build_loader(stage, train_cfg, stage_seed, samples_consumed) |
| data_iter = iter(loader) |
| tokens_per_step = train_cfg.micro_batch_size * train_cfg.grad_accum * train_cfg.seq_len |
|
|
| |
| _state = {"step": start_step} |
|
|
| def emergency_save(reason): |
| path = ckpt_dir / f"{ckpt_prefix}_emergency.pt" |
| save_checkpoint(path, model, optimizer, _state["step"], train_cfg, model_cfg, |
| loader.samples_consumed) |
| print(f"[emergency] saved {path} reason: {reason}", flush=True) |
|
|
| def on_sigterm(signum, frame): |
| print("[signal] SIGTERM received", flush=True) |
| emergency_save("SIGTERM") |
| sys.exit(2) |
|
|
| signal.signal(signal.SIGTERM, on_sigterm) |
|
|
| |
| model.train() |
| run_start = time.time() |
| last_ckpt_time = time.time() |
| t_window = time.time() |
| tokens_window = 0 |
| loss_window = 0.0 |
| loss_count = 0 |
|
|
| try: |
| for step in range(start_step, train_cfg.max_steps): |
| _state["step"] = step |
| lr = cosine_lr(step, train_cfg.warmup_steps, train_cfg.max_steps, |
| train_cfg.peak_lr, train_cfg.min_lr) |
| for g in optimizer.param_groups: |
| g["lr"] = lr |
|
|
| optimizer.zero_grad(set_to_none=True) |
| accum_loss = 0.0 |
| for _ in range(train_cfg.grad_accum): |
| x, y = next(data_iter) |
| x = x.to(device, non_blocking=True) |
| y = y.to(device, non_blocking=True) |
| _, loss = model(x, targets=y) |
| (loss / train_cfg.grad_accum).backward() |
| accum_loss += loss.item() |
| accum_loss /= train_cfg.grad_accum |
|
|
| grad_norm = torch.nn.utils.clip_grad_norm_(model.parameters(), train_cfg.grad_clip) |
| optimizer.step() |
|
|
| loss_window += accum_loss |
| loss_count += 1 |
| tokens_window += tokens_per_step |
|
|
| if (step + 1) % train_cfg.log_every == 0: |
| dt = time.time() - t_window |
| tps = tokens_window / max(dt, 1e-6) |
| avg_loss = loss_window / loss_count |
| elapsed = time.time() - run_start |
| eta = (train_cfg.max_steps - (step + 1)) * (dt / train_cfg.log_every) |
| cost = (elapsed / 3600.0) * train_cfg.cost_per_hour |
| print( |
| f"step {step+1:>6d}/{train_cfg.max_steps} | " |
| f"loss {avg_loss:.4f} | lr {lr:.2e} | grad {grad_norm.item():.2f} | " |
| f"{tps/1e3:.1f}k tok/s | " |
| f"elapsed {fmt_eta(elapsed)} | eta {fmt_eta(eta)} | ${cost:.2f}" |
| ) |
| if use_wandb: |
| import wandb |
| wandb.log({ |
| "loss": avg_loss, "lr": lr, "grad_norm": grad_norm.item(), |
| "tokens_per_sec": tps, "step": step + 1, |
| "elapsed_sec": elapsed, "cost_usd": cost, |
| }) |
| t_window = time.time() |
| tokens_window = 0 |
| loss_window = 0.0 |
| loss_count = 0 |
|
|
| if time.time() - last_ckpt_time >= train_cfg.ckpt_interval_sec: |
| path = ckpt_dir / f"{ckpt_prefix}_step{step+1:07d}.pt" |
| save_checkpoint(path, model, optimizer, step + 1, train_cfg, model_cfg, |
| loader.samples_consumed) |
| prune_ckpts(ckpt_dir, ckpt_prefix, train_cfg.ckpt_keep) |
| last_ckpt_time = time.time() |
| print(f"[ckpt] {path.name} saved at step {step+1}, " |
| f"can resume from step {step+1}", flush=True) |
|
|
| except _StreamError as e: |
| print(f"[fatal] dataloader: {e}", flush=True) |
| emergency_save(f"_StreamError: {e}") |
| sys.exit(2) |
| except torch.cuda.OutOfMemoryError as e: |
| print(f"[fatal] OOM: {e}", flush=True) |
| emergency_save("OOM") |
| sys.exit(2) |
| except KeyboardInterrupt: |
| print("[interrupt] user", flush=True) |
| emergency_save("KeyboardInterrupt") |
| sys.exit(130) |
|
|
| final = ckpt_dir / f"{ckpt_prefix}_final.pt" |
| save_checkpoint(final, model, optimizer, train_cfg.max_steps, train_cfg, model_cfg, |
| loader.samples_consumed) |
| elapsed = time.time() - run_start |
| cost = (elapsed / 3600.0) * train_cfg.cost_per_hour |
| print(f"[done] {final} total elapsed {fmt_eta(elapsed)} approx cost ${cost:.2f}") |
|
|
|
|
| |
| |
| |
|
|
| def alloc_kv_cache(cfg: ModelConfig, batch_size, max_len, device, dtype): |
| head_dim = cfg.d_model // cfg.n_heads |
| return [ |
| ( |
| torch.zeros(batch_size, cfg.n_kv_heads, max_len, head_dim, device=device, dtype=dtype), |
| torch.zeros(batch_size, cfg.n_kv_heads, max_len, head_dim, device=device, dtype=dtype), |
| ) |
| for _ in range(cfg.n_layers) |
| ] |
|
|
|
|
| @torch.no_grad() |
| def sample(model, prompt_ids, max_new=200, temperature=0.8, top_k=50, device="cuda"): |
| model.eval() |
| cfg = model.cfg |
| T0 = len(prompt_ids) |
| max_len = min(T0 + max_new, cfg.max_seq_len) |
| kv = alloc_kv_cache(cfg, 1, max_len, device, torch.bfloat16) |
|
|
| idx = torch.tensor([prompt_ids], dtype=torch.long, device=device) |
| logits, _ = model(idx, kv_caches=kv, pos=0) |
| next_logits = logits[:, -1, :] |
|
|
| out = list(prompt_ids) |
| for i in range(max_new): |
| if T0 + i >= cfg.max_seq_len: |
| break |
| if temperature <= 0: |
| tok = next_logits.argmax(-1, keepdim=True) |
| else: |
| scaled = next_logits / temperature |
| if top_k: |
| v, _ = torch.topk(scaled, min(top_k, scaled.size(-1))) |
| scaled = scaled.masked_fill(scaled < v[:, [-1]], -float("inf")) |
| probs = torch.softmax(scaled.float(), dim=-1) |
| tok = torch.multinomial(probs, 1) |
| tid = tok.item() |
| out.append(tid) |
| if tid == EOT: |
| break |
| logits, _ = model(tok, kv_caches=kv, pos=T0 + i) |
| next_logits = logits[:, -1, :] |
| return out |
|
|
|
|
| def generate_cmd(args): |
| device = "cuda" |
| ckpt = torch.load(args.checkpoint, map_location=device, weights_only=False) |
| cfg = ModelConfig(**ckpt["model_cfg"]) if "model_cfg" in ckpt else ModelConfig() |
| model = LLaMA(cfg).to(device=device, dtype=torch.bfloat16) |
| sd = ckpt["model"] |
| if any(k.startswith("_orig_mod.") for k in sd): sd = {k.removeprefix("_orig_mod."): v for k, v in sd.items()} |
| model.load_state_dict(sd) |
|
|
| prompt = f"\n<|user|>\n{args.prompt}\n<|assistant|>\n" if args.chat else args.prompt |
| ids = ENC.encode_ordinary(prompt) |
|
|
| t0 = time.time() |
| out = sample(model, ids, args.max_new, args.temperature, args.top_k, device) |
| dt = time.time() - t0 |
| new_tokens = len(out) - len(ids) |
| print(ENC.decode(out)) |
| print(f"\n[gen] {new_tokens} new tokens in {dt:.2f}s = {new_tokens/dt:.1f} tok/s") |
|
|
|
|
| |
| |
| |
|
|
| def build_parser(): |
| p = argparse.ArgumentParser() |
| sub = p.add_subparsers(dest="cmd", required=True) |
|
|
| def add_train_args(sp): |
| sp.add_argument("--no-compile", action="store_true", |
| help="Required on Blackwell; compile is never invoked here regardless.") |
| sp.add_argument("--checkpoint", type=str, default=None, |
| help="Initialize weights from this file (for cross-stage seeding).") |
| sp.add_argument("--fresh", action="store_true", |
| help="Ignore any existing ckpt in --ckpt_dir for this stage; start over.") |
| sp.add_argument("--ckpt_dir", type=str, default=None) |
| sp.add_argument("--lr", type=float, default=None) |
| sp.add_argument("--min_lr", type=float, default=None) |
| sp.add_argument("--batch_size", type=int, default=None) |
| sp.add_argument("--grad_accum", type=int, default=None) |
| sp.add_argument("--steps", type=int, default=None) |
| sp.add_argument("--warmup", type=int, default=None) |
| sp.add_argument("--seed", type=int, default=None) |
| sp.add_argument("--no-wandb", action="store_true") |
|
|
| add_train_args(sub.add_parser("base")) |
| add_train_args(sub.add_parser("mid")) |
| ft = sub.add_parser("finetune"); add_train_args(ft) |
| ft.add_argument("--skip_mid", action="store_true", |
| help="UX flag: confirms starting SFT directly from a mid checkpoint.") |
|
|
| g = sub.add_parser("generate") |
| g.add_argument("--checkpoint", required=True) |
| g.add_argument("--prompt", default="Once upon a time") |
| g.add_argument("--max_new", type=int, default=200) |
| g.add_argument("--temperature", type=float, default=0.8) |
| g.add_argument("--top_k", type=int, default=50) |
| g.add_argument("--chat", action="store_true", |
| help="Wrap prompt in <|user|>/<|assistant|> tags (use with SFT checkpoints).") |
| return p |
|
|
|
|
| def main(): |
| args = build_parser().parse_args() |
| if args.cmd == "generate": |
| generate_cmd(args) |
| else: |
| train(args.cmd, args) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|