Download scripts/chunk_ablation.py from itstheraj/fela-acml2026: direct link, hf CLI and curl.
- Browser
- Download file 26.2 kB
-
https://huggingface.co/itstheraj/fela-acml2026/resolve/main/scripts/chunk_ablation.py
- Command line
-
hf download hf://itstheraj/fela-acml2026/scripts/chunk_ablation.py
-
curl -L -o chunk_ablation.py https://huggingface.co/itstheraj/fela-acml2026/resolve/main/scripts/chunk_ablation.py
26.2 kB
| 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={}) | |
| 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() | |