fela-acml2026 / scripts /run_eval.py
itstheraj's picture
FELA: training code, checkpoints, and evaluation results
a24d5a6
Raw
History Blame
20 kB
from __future__ import annotations
import argparse
import io
import json
import math
import sys
import time
from pathlib import Path
from typing import Any
import torch
sys.path.insert(0, str(Path(__file__).parent.parent))
from model_cpu_gpt2 import CPUGPT, gpt2_small_config, smoke_config
BYTES_PER_TOKEN = 4.0
SEQ_LEN = 1024
BATCH_SIZE = 8
def _load_ckpt(path: str) -> dict:
if path.startswith("s3://"):
import boto3
bucket, key = path[5:].split("/", 1)
buf = io.BytesIO()
boto3.client("s3").download_fileobj(bucket, key, buf)
buf.seek(0)
return torch.load(buf, map_location="cpu", weights_only=False)
return torch.load(path, map_location="cpu", weights_only=False)
def _build_our_model(config_name: str, ckpt_path: str | None) -> CPUGPT:
cfg_map = {
"gpt2-small": gpt2_small_config,
"smoke": smoke_config,
}
if config_name not in cfg_map:
raise ValueError(
f"Unknown config '{config_name}'. Choose from: {list(cfg_map)}"
)
cfg = cfg_map[config_name]()
model = CPUGPT(cfg)
if ckpt_path:
ckpt = _load_ckpt(ckpt_path)
state = ckpt.get("model", ckpt)
state = {k.replace("_orig_mod.", ""): v for k, v in state.items()}
model.load_state_dict(state, strict=True)
print(f"Loaded checkpoint: {ckpt_path}", flush=True)
else:
print("No checkpoint supplied — using random weights.", flush=True)
model.eval()
return model
def _require_lm_eval():
try:
import lm_eval
except ImportError:
print(
"\nERROR: lm-evaluation-harness is not installed.\n"
"Install it with:\n"
" pip install lm-eval>=0.4.2\n",
file=sys.stderr,
)
sys.exit(1)
try:
from lm_eval.api.model import LM as _LMBase
except ImportError:
_LMBase = object
class LMEvalWrapper(_LMBase):
def __init__(self, model: CPUGPT):
super().__init__()
self.model = model
self.model.eval()
try:
import tiktoken
self._enc = tiktoken.get_encoding("gpt2")
except ImportError:
raise RuntimeError("tiktoken is required: pip install tiktoken")
try:
import tiktoken
self._enc = tiktoken.get_encoding("gpt2")
except ImportError:
raise RuntimeError("tiktoken is required: pip install tiktoken")
def _tokenize(self, text: str) -> list[int]:
return self._enc.encode_ordinary(text)
@torch.no_grad()
def _logprobs_for_tokens(self, tokens: list[int]) -> list[float]:
max_T = self.model.cfg.seq_len
gla_chunk = self.model.cfg.gla_chunk
log_probs: list[float] = []
start = 0
while start < len(tokens) - 1:
end = min(start + max_T + 1, len(tokens))
chunk = tokens[start:end]
x_ids = chunk[:-1]
targets = chunk[1:]
T_raw = len(x_ids)
T_padded = max(
gla_chunk, ((T_raw + gla_chunk - 1) // gla_chunk) * gla_chunk
)
T_padded = min(T_padded, max_T)
if T_padded > T_raw:
pad_len = T_padded - T_raw
x_ids = list(x_ids) + [0] * pad_len
x = torch.tensor(x_ids, dtype=torch.long).unsqueeze(0)
tgt_t = torch.tensor(targets, dtype=torch.long)
logits = self.model(x)
logits = logits.squeeze(0).float()
logits = logits[:T_raw]
lp = torch.log_softmax(logits, dim=-1)
tgt_lp = lp[torch.arange(len(tgt_t)), tgt_t]
log_probs.extend(tgt_lp.tolist())
if end == len(tokens):
break
start = end - 1 - max_T // 2
return log_probs
def loglikelihood(self, requests) -> list[tuple[float, bool]]:
results = []
for req in requests:
ctx_str, cont_str = req.args if hasattr(req, "args") else req
ctx_ids = self._tokenize(ctx_str)
cont_ids = self._tokenize(cont_str)
tokens = ctx_ids + cont_ids
if not tokens:
results.append((-float("inf"), False))
continue
all_lp = self._logprobs_for_tokens(tokens)
cont_lp = all_lp[-len(cont_ids) :]
total = sum(cont_lp)
greedy = self._is_greedy(ctx_ids, cont_ids)
results.append((total, greedy))
return results
@torch.no_grad()
def _is_greedy(self, ctx_ids: list[int], cont_ids: list[int]) -> bool:
if not cont_ids:
return True
max_T = self.model.cfg.seq_len
gla_chunk = self.model.cfg.gla_chunk
tokens = (ctx_ids + cont_ids)[-max_T:]
ctx_T = min(len(ctx_ids), len(tokens) - len(cont_ids))
x_ids = tokens[:-1] if len(tokens) > 1 else tokens
T_raw = len(x_ids)
T_padded = max(gla_chunk, ((T_raw + gla_chunk - 1) // gla_chunk) * gla_chunk)
T_padded = min(T_padded, max_T)
if T_padded > T_raw:
x_ids = list(x_ids) + [0] * (T_padded - T_raw)
x = torch.tensor(x_ids, dtype=torch.long).unsqueeze(0)
logits = self.model(x).squeeze(0).float()[:T_raw]
for i, tok in enumerate(cont_ids):
pos = ctx_T - 1 + i
if pos >= logits.shape[0]:
return False
if logits[pos].argmax().item() != tok:
return False
return True
@torch.no_grad()
def generate_until(self, requests) -> list[str]:
results = []
for req in requests:
ctx_str, gen_kwargs = req.args if hasattr(req, "args") else req
until = gen_kwargs.get("until", ["\n"])
max_new_toks = gen_kwargs.get("max_gen_toks", 50)
ctx_ids = self._tokenize(ctx_str)
max_T = self.model.cfg.seq_len
if len(ctx_ids) > max_T:
ctx_ids = ctx_ids[-max_T:]
prompt = torch.tensor(ctx_ids, dtype=torch.long).unsqueeze(0)
out_ids = self.model.generate(
prompt,
max_new_tokens=max_new_toks,
temperature=0.0,
top_k=1,
)
generated_ids = out_ids[0].tolist()
generated_str = self._enc.decode(generated_ids)
for stop in until:
idx = generated_str.find(stop)
if idx != -1:
generated_str = generated_str[:idx]
results.append(generated_str)
return results
def loglikelihood_rolling(self, requests) -> list[tuple[float]]:
results = []
for req in requests:
(text_str,) = req.args if hasattr(req, "args") else req
tokens = self._tokenize(text_str)
if len(tokens) < 2:
results.append((-float("inf"),))
continue
all_lp = self._logprobs_for_tokens(tokens)
results.append((sum(all_lp),))
return results
@torch.no_grad()
def _bpb_on_tokens(model: CPUGPT, tokens: torch.Tensor) -> dict:
model.eval()
total_loss = 0.0
total_toks = 0
n_batches = (len(tokens) - 1) // (SEQ_LEN * BATCH_SIZE)
t0 = time.perf_counter()
for i in range(n_batches):
s = i * SEQ_LEN * BATCH_SIZE
chunk = tokens[s : s + SEQ_LEN * BATCH_SIZE + 1]
if len(chunk) < SEQ_LEN * BATCH_SIZE + 1:
break
x = chunk[:-1].view(BATCH_SIZE, SEQ_LEN)
y = chunk[1:].view(BATCH_SIZE, SEQ_LEN)
loss = model(x, y)
total_loss += loss.item() * y.numel()
total_toks += y.numel()
elapsed = time.perf_counter() - t0
nll = total_loss / max(total_toks, 1)
bpb = nll / (math.log(2) * BYTES_PER_TOKEN)
return {"bpb": bpb, "nll": nll, "tokens": total_toks, "elapsed_s": elapsed}
def _stream_owt_tokens(n_tokens: int, seed: int = 999) -> torch.Tensor:
try:
from datasets import load_dataset
except ImportError:
raise RuntimeError(
"datasets library is required for wikitext BPB: pip install datasets"
)
import tiktoken
enc = tiktoken.get_encoding("gpt2")
print(
f"Streaming OWT (seed={seed}) for {n_tokens / 1e6:.1f}M tokens...", flush=True
)
ds = load_dataset("openwebtext", split="train", streaming=True)
ds = ds.shuffle(seed=seed, buffer_size=10_000)
buf: list[int] = []
for item in ds:
text = item.get("text", "")
if text:
buf.extend(enc.encode_ordinary(text))
if len(buf) >= n_tokens:
break
return torch.tensor(buf[:n_tokens], dtype=torch.long)
def _eval_wikitext_bpb(model: CPUGPT, val_tokens: int = 1_000_000) -> dict:
tokens = _stream_owt_tokens(val_tokens)
results = _bpb_on_tokens(model, tokens)
return results
def _build_hf_wrapper():
try:
from transformers import GPT2LMHeadModel, GPT2Tokenizer
except ImportError:
print(
"\nERROR: transformers is not installed (needed for --baseline).\n"
"Install with: pip install transformers\n",
file=sys.stderr,
)
sys.exit(1)
print("Loading HuggingFace gpt2 baseline...", flush=True)
hf_model = GPT2LMHeadModel.from_pretrained("gpt2")
hf_model.eval()
class _HFWrapper:
def __init__(self, m):
self.model = m
self._vocab_size = m.config.vocab_size
self._seq_len = m.config.n_positions
import tiktoken
self._enc = tiktoken.get_encoding("gpt2")
def _tokenize(self, text: str) -> list[int]:
return self._enc.encode_ordinary(text)
@torch.no_grad()
def _logprobs_for_tokens(self, tokens: list[int]) -> list[float]:
max_T = self._seq_len
log_probs: list[float] = []
start = 0
while start < len(tokens) - 1:
end = min(start + max_T + 1, len(tokens))
chunk = tokens[start:end]
x = torch.tensor(chunk[:-1], dtype=torch.long).unsqueeze(0)
tgt = torch.tensor(chunk[1:], dtype=torch.long)
out = self.model(x)
logits = out.logits.squeeze(0).float()
lp = torch.log_softmax(logits, dim=-1)
tgt_lp = lp[torch.arange(len(tgt)), tgt]
log_probs.extend(tgt_lp.tolist())
if end == len(tokens):
break
start = end - 1 - max_T // 2
return log_probs
@torch.no_grad()
def bpb_on_tokens(self, tokens: torch.Tensor) -> dict:
total_loss = 0.0
total_toks = 0
n_batches = (len(tokens) - 1) // (1024 * BATCH_SIZE)
t0 = time.perf_counter()
for i in range(n_batches):
s = i * 1024 * BATCH_SIZE
chunk = tokens[s : s + 1024 * BATCH_SIZE + 1]
if len(chunk) < 1024 * BATCH_SIZE + 1:
break
x = chunk[:-1].view(BATCH_SIZE, 1024)
y = chunk[1:].view(BATCH_SIZE, 1024)
out = self.model(x)
logits = out.logits.float()
loss = torch.nn.functional.cross_entropy(
logits.view(-1, logits.size(-1)), y.reshape(-1), reduction="sum"
)
total_loss += loss.item()
total_toks += y.numel()
elapsed = time.perf_counter() - t0
nll = total_loss / max(total_toks, 1)
bpb = nll / (math.log(2) * BYTES_PER_TOKEN)
return {"bpb": bpb, "nll": nll, "tokens": total_toks, "elapsed_s": elapsed}
return _HFWrapper(hf_model)
def _run_lm_eval_tasks(wrapper, tasks: list[str], limit: int | None = None) -> dict:
_require_lm_eval()
try:
import lm_eval
from lm_eval import evaluator
except ImportError:
print("lm_eval import failed.", file=sys.stderr)
sys.exit(1)
results = evaluator.evaluate(
lm=wrapper,
task_dict=lm_eval.tasks.get_task_dict(tasks),
limit=limit,
)
return results.get("results", {})
def _print_markdown_table(our_results: dict, baseline_results: dict | None):
headers = ["Task", "Metric", "Our Model"]
if baseline_results:
headers.append("GPT-2 Baseline")
rows = []
for task, metrics in our_results.items():
for metric, val in metrics.items():
if isinstance(val, float):
our_val = f"{val:.4f}"
base_val = ""
if baseline_results and task in baseline_results:
bv = baseline_results[task].get(metric)
base_val = f"{bv:.4f}" if isinstance(bv, float) else str(bv)
row = [task, metric, our_val]
if baseline_results:
row.append(base_val)
rows.append(row)
col_widths = [
max(len(h), max((len(r[i]) for r in rows), default=0))
for i, h in enumerate(headers)
]
def _fmt_row(r):
return (
"| "
+ " | ".join(cell.ljust(col_widths[i]) for i, cell in enumerate(r))
+ " |"
)
sep = "| " + " | ".join("-" * w for w in col_widths) + " |"
print(_fmt_row(headers))
print(sep)
for row in rows:
print(_fmt_row(row))
def main():
p = argparse.ArgumentParser(description="Formal eval harness for FNO+GLA model")
p.add_argument("--ckpt", default=None, help="Checkpoint path (or s3://...)")
p.add_argument(
"--config", default="gpt2-small", help="Model config: gpt2-small | smoke"
)
p.add_argument(
"--tasks",
default="wikitext",
help="Comma-separated: hellaswag,lambada,piqa,boolq,wikitext",
)
p.add_argument("--output", required=True, help="Output JSON path")
p.add_argument("--baseline", action="store_true", help="Also run HF gpt2 baseline")
p.add_argument(
"--limit", type=int, default=None, help="Max examples per task (None=all)"
)
p.add_argument(
"--val-tokens",
type=int,
default=1_000_000,
help="Tokens to use for wikitext BPB (default 1M)",
)
args = p.parse_args()
task_list = [t.strip() for t in args.tasks.split(",") if t.strip()]
print(f"\n=== run_eval.py ===", flush=True)
print(f"Config: {args.config}", flush=True)
print(f"Ckpt: {args.ckpt or '(random weights)'}", flush=True)
print(f"Tasks: {task_list}", flush=True)
print(f"Baseline: {args.baseline}", flush=True)
print(flush=True)
our_model = _build_our_model(args.config, args.ckpt)
print(f"Model params: {our_model.param_count() / 1e6:.1f}M", flush=True)
our_results: dict[str, Any] = {}
base_results: dict[str, Any] = {}
if "wikitext" in task_list:
print("\nRunning wikitext BPB eval (streaming OWT)...", flush=True)
wt_res = _eval_wikitext_bpb(our_model, val_tokens=args.val_tokens)
our_results["wikitext"] = {
"bpb": wt_res["bpb"],
"nll_nats": wt_res["nll"],
"tokens_evald": wt_res["tokens"],
}
print(f" wikitext BPB: {wt_res['bpb']:.4f}", flush=True)
if args.baseline:
hf = _build_hf_wrapper()
tokens = _stream_owt_tokens(args.val_tokens)
bwt = hf.bpb_on_tokens(tokens)
base_results["wikitext"] = {
"bpb": bwt["bpb"],
"nll_nats": bwt["nll"],
"tokens_evald": bwt["tokens"],
}
print(f" wikitext BPB (baseline): {bwt['bpb']:.4f}", flush=True)
lm_tasks = [t for t in task_list if t != "wikitext"]
if lm_tasks:
_require_lm_eval()
wrapper = LMEvalWrapper(our_model)
print(f"\nRunning lm-eval tasks: {lm_tasks}...", flush=True)
lm_res = _run_lm_eval_tasks(wrapper, lm_tasks, limit=args.limit)
our_results.update(lm_res)
if args.baseline:
hf_wrapper = _build_hf_wrapper()
_lm_base_cls = type(wrapper).__mro__[1]
class _HFLMWrapper(_lm_base_cls):
def __init__(self, hf):
super().__init__()
import tiktoken
self._enc = tiktoken.get_encoding("gpt2")
self._hf = hf
def _tokenize(self, text: str) -> list[int]:
return self._enc.encode_ordinary(text)
@torch.no_grad()
def _logprobs_for_tokens(self, tokens):
return hf_wrapper._logprobs_for_tokens(tokens)
def loglikelihood(self, requests):
results = []
for req in requests:
ctx_str, cont_str = req.args if hasattr(req, "args") else req
ctx_ids = self._tokenize(ctx_str)
cont_ids = self._tokenize(cont_str)
tokens = ctx_ids + cont_ids
if not tokens:
results.append((-float("inf"), False))
continue
all_lp = self._logprobs_for_tokens(tokens)
cont_lp = all_lp[-len(cont_ids) :]
results.append((sum(cont_lp), False))
return results
def loglikelihood_rolling(self, requests):
results = []
for req in requests:
(text_str,) = req.args if hasattr(req, "args") else req
tokens = self._tokenize(text_str)
if len(tokens) < 2:
results.append((-float("inf"),))
continue
all_lp = self._logprobs_for_tokens(tokens)
results.append((sum(all_lp),))
return results
@torch.no_grad()
def generate_until(self, requests):
results = []
for req in requests:
ctx_str, gen_kwargs = req.args if hasattr(req, "args") else req
until = gen_kwargs.get("until", ["\n"])
max_new_toks = gen_kwargs.get("max_gen_toks", 50)
ctx_ids = self._tokenize(ctx_str)
x = torch.tensor(ctx_ids, dtype=torch.long).unsqueeze(0)
out = hf_wrapper.model.generate(
x, max_new_tokens=max_new_toks, do_sample=False
)
gen_ids = out[0, len(ctx_ids) :].tolist()
gen_str = self._enc.decode(gen_ids)
for stop in until:
idx = gen_str.find(stop)
if idx != -1:
gen_str = gen_str[:idx]
results.append(gen_str)
return results
hf_lm_wrapper = _HFLMWrapper(hf_wrapper)
base_lm_res = _run_lm_eval_tasks(hf_lm_wrapper, lm_tasks, limit=args.limit)
base_results.update(base_lm_res)
print("\n" + "=" * 60)
print("Results")
print("=" * 60)
_print_markdown_table(our_results, base_results if args.baseline else None)
output = {
"model_path": args.ckpt or "random_weights",
"config": args.config,
"tasks": task_list,
"results": our_results,
}
if args.baseline:
output["baseline_results"] = base_results
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
with open(out_path, "w") as f:
json.dump(output, f, indent=2)
print(f"\nResults saved to: {out_path}", flush=True)
if __name__ == "__main__":
main()