| import argparse |
| import gc |
| import json |
| import logging |
| import math |
| import os |
| import random |
| 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 |
|
|
| _script_dir = os.path.dirname(os.path.abspath(__file__)) |
| _root = os.path.dirname(_script_dir) |
| if _root not in sys.path: |
| sys.path.insert(0, _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__) |
|
|
| CHUNK_VARIANTS = [64, 256, 512, 1024] |
|
|
|
|
| def make_124m_config(gla_chunk: int) -> CPUGPTConfig: |
| return CPUGPTConfig( |
| n_layer=12, |
| n_embd=768, |
| n_head=12, |
| ffn_hidden=2048, |
| fno_modes=256, |
| gla_chunk=gla_chunk, |
| seq_len=8192, |
| layer_pattern="SSSL", |
| vocab_size=50257, |
| ) |
|
|
|
|
| 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, 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.02, 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): |
| import glob |
| import queue |
| import threading |
|
|
| shards = sorted(glob.glob(os.path.join(data_dir, "*.parquet"))) |
| if not shards: |
| shards = sorted(glob.glob(os.path.join(data_dir, "*.bin"))) |
| if not shards: |
| raise FileNotFoundError(f"No parquet or bin shards in {data_dir}") |
| import numpy as np |
|
|
| 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: |
| import pyarrow.parquet as pq |
|
|
| def load_fn(path): |
| tbl = pq.read_table(path, columns=["tokens"]) |
| tokens = torch.tensor(tbl["tokens"].to_pylist()[0], dtype=torch.long) |
| n = (len(tokens) // (seq_len + 1)) * (seq_len + 1) |
| return tokens[:n].view(-1, seq_len + 1) |
|
|
| 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 train_variant( |
| chunk: int, |
| args, |
| out_dir: Path, |
| device: str, |
| is_master: bool, |
| use_ddp: bool, |
| local_rank: int, |
| ): |
| log.info(f"=== Training gla_chunk={chunk} ===") |
| cfg = make_124m_config(chunk) |
| model = CPUGPT(cfg).to(device) |
| if use_ddp: |
| model = nn.parallel.DistributedDataParallel(model, device_ids=[local_rank]) |
| opt = build_optimizer(model, cfg) |
| if is_master: |
| raw = model.module if use_ddp else model |
| log.info( |
| f" params: {raw.param_count() / 1e6:.1f}M seq_len={cfg.seq_len} chunk={chunk}" |
| ) |
|
|
| total_batch = args.device_batch * max(args.num_gpus, 1) |
| total_steps = max(1, int(args.tokens / (total_batch * cfg.seq_len))) |
| log.info( |
| f" steps={total_steps} total_batch={total_batch} tokens≈{total_steps * total_batch * cfg.seq_len / 1e6:.0f}M" |
| ) |
|
|
| loader = make_loader(args.data_dir, cfg.seq_len, device, args.device_batch) |
| ctx = ( |
| torch.autocast(device_type="cuda", dtype=torch.bfloat16) |
| if "cuda" in device |
| else torch.no_grad().__class__() |
| ) |
|
|
| step = 0 |
| t0 = time.perf_counter() |
| 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 |
|
|
| if is_master and step % 50 == 0: |
| elapsed = time.perf_counter() - t0 |
| tps = step * total_batch * cfg.seq_len / elapsed |
| log.info( |
| f" chunk={chunk} step={step}/{total_steps} ({100 * step / total_steps:.0f}%) " |
| f"loss={loss.item():.4f} tok/s={tps:,.0f}" |
| ) |
|
|
| ckpt_dir = out_dir / f"variant_{chunk}" / "ckpt" |
| ckpt_dir.mkdir(parents=True, exist_ok=True) |
| if is_master: |
| raw = model.module if use_ddp else model |
| torch.save({"step": step, "model": raw.state_dict()}, ckpt_dir / "final.pt") |
| log.info(f" saved {ckpt_dir}/final.pt") |
|
|
| return model, cfg |
|
|
|
|
| _HAY = ( |
| "The forest was quiet except for the occasional rustle of leaves in the breeze. " |
| "Sunlight filtered through the canopy, casting dappled shadows on the mossy ground. " |
| "A small stream wound its way between ancient oak trees, its water clear and cold. " |
| "Birds called to one another across the branches, their songs filling the still air. " |
| "Somewhere in the distance, a woodpecker drummed steadily against hollow bark. " |
| "The smell of earth and pine needles rose from the path with each careful step. " |
| "Nothing moved except the shadows and the water and the swaying tops of the tallest trees. " |
| ) * 300 |
|
|
|
|
| def eval_needle(model, cfg, device, n_trials=20, seed=42): |
| import tiktoken |
|
|
| enc = tiktoken.get_encoding("gpt2") |
| hay_ids = enc.encode_ordinary(_HAY) |
| rng = random.Random(seed) |
|
|
| ctx_lens = [512, 1024, 2048, 4096, cfg.seq_len] |
| depths = [0.1, 0.3, 0.5, 0.7, 0.9] |
|
|
| raw = ( |
| model.module |
| if isinstance(model, nn.parallel.DistributedDataParallel) |
| else model |
| ) |
| raw.eval() |
| results = [] |
|
|
| for ctx_len in ctx_lens: |
| for depth in depths: |
| n_correct, n_valid = 0, 0 |
| for _ in range(n_trials): |
| code = str(rng.randint(10000, 99999)) |
| dist_ = str(rng.randint(10000, 99999)) |
| while dist_ == code: |
| dist_ = str(rng.randint(10000, 99999)) |
|
|
| needle_ids = enc.encode_ordinary( |
| f" The passkey is {code}. Remember this passkey." |
| ) |
| query_ids = enc.encode_ordinary(" The passkey is") |
| code_ids = enc.encode_ordinary(" " + code) |
| dist_ids = enc.encode_ordinary(" " + dist_) |
|
|
| overhead = ( |
| len(needle_ids) + len(query_ids) + max(len(code_ids), len(dist_ids)) |
| ) |
| hay_budget = ctx_len - overhead |
| if hay_budget < 64: |
| continue |
|
|
| prefix_len = int(hay_budget * depth) |
| suffix_len = hay_budget - prefix_len |
| start = rng.randint(0, max(0, len(hay_ids) - hay_budget - 1)) |
| hay_slice = hay_ids[start : start + hay_budget] |
| context = ( |
| hay_slice[:prefix_len] |
| + needle_ids |
| + hay_slice[prefix_len : prefix_len + suffix_len] |
| + query_ids |
| ) |
|
|
| def avg_nll(answer_ids): |
| combined = context + answer_ids |
| remainder = len(combined) % cfg.gla_chunk |
| extra = (cfg.gla_chunk - remainder) % cfg.gla_chunk |
| full = combined + [0] * extra |
| inp = torch.tensor([full], dtype=torch.long, device=device) |
| with torch.no_grad(): |
| logits = raw(inp) |
| s = len(context) - 1 |
| lgt = logits[0, s : s + len(answer_ids)].float() |
| tgt = torch.tensor(answer_ids, dtype=torch.long, device=device) |
| return F.cross_entropy(lgt, tgt, reduction="mean").item() |
|
|
| try: |
| nll_c = avg_nll(code_ids) |
| nll_d = avg_nll(dist_ids) |
| n_valid += 1 |
| if nll_c < nll_d: |
| n_correct += 1 |
| except Exception: |
| pass |
|
|
| acc = n_correct / n_valid if n_valid else float("nan") |
| results.append( |
| { |
| "ctx_len": ctx_len, |
| "depth": depth, |
| "accuracy": acc, |
| "n_correct": n_correct, |
| "n_valid": n_valid, |
| } |
| ) |
| log.info( |
| f" [needle] chunk={cfg.gla_chunk} ctx={ctx_len} depth={depth:.1f} " |
| f"acc={acc:.0%} ({n_correct}/{n_valid})" |
| ) |
| return results |
|
|
|
|
| def eval_throughput(model, cfg, device): |
| raw = ( |
| model.module |
| if isinstance(model, nn.parallel.DistributedDataParallel) |
| else model |
| ) |
| raw.eval() |
| results = [] |
| ctx_lens = [512, 1024, 2048, 4096, cfg.seq_len] |
| for ctx_len in ctx_lens: |
| padded = math.ceil(ctx_len / cfg.gla_chunk) * cfg.gla_chunk |
| x = torch.zeros(1, padded, dtype=torch.long, device=device) |
| for _ in range(2): |
| with torch.no_grad(): |
| raw(x) |
| torch.cuda.synchronize() if "cuda" in device else None |
| t0 = time.perf_counter() |
| reps = 5 |
| for _ in range(reps): |
| with torch.no_grad(): |
| raw(x) |
| torch.cuda.synchronize() if "cuda" in device else None |
| dt = (time.perf_counter() - t0) / reps |
| tps = padded / dt |
| results.append( |
| {"ctx_len": ctx_len, "tok_per_sec": tps, "ms_per_seq": dt * 1000} |
| ) |
| log.info( |
| f" [throughput] chunk={cfg.gla_chunk} ctx={ctx_len} → {tps:,.0f} tok/s" |
| ) |
| return results |
|
|
|
|
| def eval_wikitext(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: |
| log.warning(" Could not load WikiText-103; skipping BPB eval") |
| return {"bpb": float("nan")} |
|
|
| enc = tiktoken.get_encoding("gpt2") |
| tokens = torch.tensor(enc.encode(text[:4_000_000]), dtype=torch.long) |
| tokens = tokens[:1_000_000] |
|
|
| raw = ( |
| model.module |
| if isinstance(model, nn.parallel.DistributedDataParallel) |
| else model |
| ) |
| raw.eval() |
|
|
| seq_len = cfg.seq_len |
| n_chunks = min(50, (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 or y.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) |
| log.info(f" [wikitext] chunk={cfg.gla_chunk} BPB={bpb:.4f}") |
| return {"bpb": bpb, "nats": nats, "tokens_evaluated": total_tok} |
|
|
|
|
| def make_figures(all_results, out_dir: Path): |
| try: |
| import matplotlib |
|
|
| matplotlib.use("Agg") |
| import matplotlib.pyplot as plt |
| import numpy as np |
| except ImportError: |
| log.warning("matplotlib not available; skipping figures") |
| return |
|
|
| chunks = sorted(all_results.keys()) |
| colors = ["#E84855", "#F4A261", "#2E86AB", "#264653"] |
|
|
| fig, axes = plt.subplots(2, 2, figsize=(14, 9), sharex=True, sharey=True) |
| axes = axes.flatten() |
| for idx, chunk in enumerate(chunks): |
| rows = all_results[chunk]["needle"] |
| ctx_u = sorted(set(r["ctx_len"] for r in rows)) |
| dep_u = sorted(set(r["depth"] for r in rows)) |
| grid = np.full((len(dep_u), len(ctx_u)), float("nan")) |
| for r in rows: |
| ci = ctx_u.index(r["ctx_len"]) |
| di = dep_u.index(r["depth"]) |
| if not (isinstance(r["accuracy"], float) and math.isnan(r["accuracy"])): |
| grid[di, ci] = r["accuracy"] |
| ax = axes[idx] |
| im = ax.imshow(grid, vmin=0, vmax=1, cmap="RdYlGn", aspect="auto") |
| ax.set_title(f"gla_chunk={chunk}", fontsize=12, fontweight="bold") |
| ax.set_xticks(range(len(ctx_u))) |
| ax.set_xticklabels( |
| [f"{c // 1024}K" if c >= 1024 else str(c) for c in ctx_u], fontsize=9 |
| ) |
| ax.set_yticks(range(len(dep_u))) |
| ax.set_yticklabels([f"{int(d * 100)}%" for d in dep_u], fontsize=9) |
| ax.set_xlabel("Context Length", fontsize=10) |
| ax.set_ylabel("Needle Depth", fontsize=10) |
| for di in range(len(dep_u)): |
| for ci in range(len(ctx_u)): |
| v = grid[di, ci] |
| if not math.isnan(v): |
| col = "black" if 0.25 < v < 0.75 else "white" |
| ax.text( |
| ci, |
| di, |
| f"{v:.0%}", |
| ha="center", |
| va="center", |
| fontsize=9, |
| color=col, |
| fontweight="bold", |
| ) |
| plt.colorbar(im, ax=ax) |
| fig.suptitle( |
| "Passkey Retrieval Accuracy by GLA Chunk Size\n(124M FELA, 1B tokens)", |
| fontsize=14, |
| ) |
| fig.tight_layout() |
| fig.savefig(out_dir / "fig_chunk_retrieval.png", dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| log.info(" saved fig_chunk_retrieval.png") |
|
|
| fig, ax = plt.subplots(figsize=(8, 5)) |
| for chunk, color in zip(chunks, colors): |
| rows = all_results[chunk].get("throughput", []) |
| if not rows: |
| continue |
| xs = [r["ctx_len"] for r in rows] |
| ys = [r["tok_per_sec"] for r in rows] |
| ax.plot( |
| xs, |
| ys, |
| "o-", |
| color=color, |
| linewidth=2.5, |
| markersize=6, |
| label=f"chunk={chunk}", |
| ) |
| ax.set_xscale("log", base=2) |
| ax.set_xlabel("Context Length (tokens)", fontsize=12) |
| ax.set_ylabel("Throughput (tok/s)", fontsize=12) |
| ax.set_title( |
| "Throughput vs. Context Length\n(124M FELA — chunk size effect)", fontsize=13 |
| ) |
| ax.legend(fontsize=11) |
| ax.grid(True, alpha=0.3) |
| fig.tight_layout() |
| fig.savefig(out_dir / "fig_chunk_throughput.png", dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| log.info(" saved fig_chunk_throughput.png") |
|
|
| fig, axes = plt.subplots(1, 2, figsize=(11, 4)) |
| acc_hard, bpb_vals = [], [] |
| for chunk in chunks: |
| needle_rows = all_results[chunk]["needle"] |
| match = [ |
| r |
| for r in needle_rows |
| if r["ctx_len"] == 4096 and abs(r["depth"] - 0.1) < 0.01 |
| ] |
| acc_hard.append(match[0]["accuracy"] if match else float("nan")) |
| bpb_vals.append(all_results[chunk].get("wikitext", {}).get("bpb", float("nan"))) |
|
|
| c_labels = [f"chunk={c}" for c in chunks] |
| ax1 = axes[0] |
| bars = ax1.bar(c_labels, acc_hard, color=colors) |
| ax1.axhline(0.5, color="gray", linestyle="--", alpha=0.7, label="random") |
| ax1.set_ylim(0, 1) |
| ax1.set_ylabel("Retrieval Accuracy") |
| ax1.set_title("Hard case: ctx=4K, depth=10%\n(needle at very start)") |
| ax1.legend(fontsize=9) |
| for bar, v in zip(bars, acc_hard): |
| if not math.isnan(v): |
| ax1.text( |
| bar.get_x() + bar.get_width() / 2, |
| v + 0.02, |
| f"{v:.0%}", |
| ha="center", |
| fontsize=10, |
| fontweight="bold", |
| ) |
|
|
| ax2 = axes[1] |
| bars2 = ax2.bar(c_labels, bpb_vals, color=colors) |
| ax2.set_ylabel("WikiText BPB (lower = better)") |
| ax2.set_title("Language quality\n(WikiText-103 BPB)") |
| for bar, v in zip(bars2, bpb_vals): |
| if not math.isnan(v): |
| ax2.text( |
| bar.get_x() + bar.get_width() / 2, |
| v + 0.005, |
| f"{v:.3f}", |
| ha="center", |
| fontsize=10, |
| fontweight="bold", |
| ) |
|
|
| fig.suptitle("Chunk Size Tradeoff: Retrieval vs. Quality", fontsize=13) |
| fig.tight_layout() |
| fig.savefig(out_dir / "fig_chunk_summary.png", dpi=150, bbox_inches="tight") |
| plt.close(fig) |
| log.info(" saved fig_chunk_summary.png") |
|
|
|
|
| def main(): |
| parser = argparse.ArgumentParser(description="GLA chunk-size ablation") |
| parser.add_argument("--data-dir", default="/data/openwebtext") |
| parser.add_argument( |
| "--tokens", |
| type=float, |
| default=1e9, |
| help="training tokens per variant (default 1B)", |
| ) |
| parser.add_argument( |
| "--device-batch", type=int, default=4, help="per-device batch size (sequences)" |
| ) |
| parser.add_argument( |
| "--num-gpus", type=int, default=0, help="override GPU count (0 = auto-detect)" |
| ) |
| parser.add_argument("--out-dir", default="results/chunk_ablation") |
| parser.add_argument("--needle-trials", type=int, default=20) |
| parser.add_argument( |
| "--smoke", |
| action="store_true", |
| help="fast smoke test: 5M tokens, 3 needle trials", |
| ) |
| parser.add_argument( |
| "--eval-only", |
| type=str, |
| default=None, |
| help="path to existing results dir — skip training, just eval+figures", |
| ) |
| parser.add_argument( |
| "--chunks", |
| type=int, |
| nargs="+", |
| default=CHUNK_VARIANTS, |
| help="which chunk sizes to run (default: 64 256 512 1024)", |
| ) |
| args = parser.parse_args() |
|
|
| if args.smoke: |
| args.tokens = 5e6 |
| args.needle_trials = 3 |
|
|
| use_ddp = False |
| local_rank, global_rank = 0, 0 |
| if "RANK" in os.environ: |
| dist.init_process_group(backend="nccl") |
| local_rank = dist.get_rank() % torch.cuda.device_count() |
| global_rank = dist.get_rank() |
| use_ddp = True |
| torch.cuda.set_device(local_rank) |
|
|
| num_gpus = args.num_gpus or ( |
| torch.cuda.device_count() if torch.cuda.is_available() else 0 |
| ) |
| args.num_gpus = max(1, num_gpus) |
| device = f"cuda:{local_rank}" if torch.cuda.is_available() else "cpu" |
| is_master = global_rank == 0 |
|
|
| out_dir = Path(args.out_dir) |
| if is_master: |
| out_dir.mkdir(parents=True, exist_ok=True) |
|
|
| all_results = {} |
|
|
| for chunk in args.chunks: |
| variant_dir = out_dir / f"variant_{chunk}" |
| if is_master: |
| variant_dir.mkdir(parents=True, exist_ok=True) |
|
|
| model, cfg = train_variant( |
| chunk, args, out_dir, device, is_master, use_ddp, local_rank |
| ) |
|
|
| if use_ddp: |
| dist.barrier() |
|
|
| if is_master: |
| raw = model.module if use_ddp else model |
| raw.eval() |
|
|
| log.info(f"--- Evaluating chunk={chunk} ---") |
| needle_res = eval_needle(raw, cfg, device, n_trials=args.needle_trials) |
| (variant_dir / "needle.json").write_text(json.dumps(needle_res, indent=2)) |
|
|
| tput_res = eval_throughput(raw, cfg, device) |
| (variant_dir / "throughput.json").write_text(json.dumps(tput_res, indent=2)) |
|
|
| wiki_res = eval_wikitext(raw, cfg, device) |
| (variant_dir / "wikitext.json").write_text(json.dumps(wiki_res, indent=2)) |
|
|
| all_results[chunk] = { |
| "needle": needle_res, |
| "throughput": tput_res, |
| "wikitext": wiki_res, |
| "params_M": raw.param_count() / 1e6, |
| "gla_chunk": chunk, |
| } |
|
|
| del model |
| gc.collect() |
| if torch.cuda.is_available(): |
| torch.cuda.empty_cache() |
|
|
| if use_ddp: |
| dist.barrier() |
|
|
| if is_master: |
| (out_dir / "summary.json").write_text(json.dumps(all_results, indent=2)) |
| log.info(f"Summary written to {out_dir}/summary.json") |
|
|
| make_figures(all_results, out_dir) |
|
|
| print("\n" + "=" * 65) |
| print("CHUNK ABLATION SUMMARY") |
| print("=" * 65) |
| print( |
| f"{'chunk':>8} {'4K/10% acc':>10} {'4K/50% acc':>10} {'4K/90% acc':>10} {'BPB':>7}" |
| ) |
| print("-" * 65) |
| for chunk in sorted(all_results.keys()): |
| nr = all_results[chunk]["needle"] |
|
|
| def get_acc(ctx, dep): |
| m = [ |
| r |
| for r in nr |
| if r["ctx_len"] == ctx and abs(r["depth"] - dep) < 0.01 |
| ] |
| return m[0]["accuracy"] if m else float("nan") |
|
|
| bpb = all_results[chunk].get("wikitext", {}).get("bpb", float("nan")) |
| print( |
| f"{chunk:>8} {get_acc(4096, 0.1):>10.0%} {get_acc(4096, 0.5):>10.0%} " |
| f"{get_acc(4096, 0.9):>10.0%} {bpb:>7.4f}" |
| ) |
| print("=" * 65) |
|
|
| if use_ddp: |
| dist.destroy_process_group() |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|