fela-acml2026 / scripts /paper_eval.py
itstheraj's picture
FELA: training code, checkpoints, and evaluation results
a24d5a6
Raw
History Blame
36.3 kB
from __future__ import annotations
import argparse
import csv
import io
import json
import math
import os
import sys
import time
import traceback
from pathlib import Path
from typing import Any
import torch
import torch.nn as nn
import torch.nn.functional as F
sys.path.insert(0, str(Path(__file__).parent.parent))
from model_cpu_gpt2 import CPUGPT, CPUGPTConfig, get_config
def _load_ckpt(path: str, device: str = "cpu") -> dict:
if path.startswith("s3://"):
import boto3
bucket, key = path[5:].split("/", 1)
buf = io.BytesIO()
print(f"[eval] Downloading {path} from S3 ...", flush=True)
boto3.client("s3").download_fileobj(bucket, key, buf)
buf.seek(0)
return torch.load(buf, map_location=device, weights_only=False)
return torch.load(path, map_location=device, weights_only=False)
def _build_model(config_name: str, ckpt_path: str, device: str) -> CPUGPT:
cfg = get_config(config_name)
cfg.seq_len = 65536
model = CPUGPT(cfg)
if ckpt_path:
raw = _load_ckpt(ckpt_path, device="cpu")
state = raw.get("model", raw)
state = {
k.replace("module.", "").replace("_orig_mod.", ""): v
for k, v in state.items()
}
missing, unexpected = model.load_state_dict(state, strict=False)
if missing:
print(f" [warn] missing keys: {missing[:5]}", flush=True)
if unexpected:
print(f" [warn] unexpected keys: {unexpected[:5]}", flush=True)
print(f"[eval] Loaded {ckpt_path}", flush=True)
model = model.to(device)
model.eval()
total_params = sum(p.numel() for p in model.parameters())
print(f"[eval] Model params: {total_params / 1e6:.1f}M", flush=True)
return model
def _tokenizer():
import tiktoken
return tiktoken.get_encoding("r50k_base")
def _gpt2_baseline(device: str, size: str = "gpt2") -> tuple[Any, Any]:
from transformers import GPT2LMHeadModel, GPT2TokenizerFast
print(f"[eval] Loading GPT-2 baseline ({size}) ...", flush=True)
tok = GPT2TokenizerFast.from_pretrained(size)
mdl = GPT2LMHeadModel.from_pretrained(size).to(device).eval()
return mdl, tok
def eval_wikitext_bpb(
model: CPUGPT, device: str, seq_len: int = 1024, n_tokens: int = 5_000_000
) -> dict:
print("[eval] WikiText-103 BPB ...", flush=True)
import tiktoken
from datasets import load_dataset
enc = tiktoken.get_encoding("r50k_base")
ds = load_dataset("wikitext", "wikitext-103-raw-v1", split="test", streaming=True)
buf: list[int] = []
for item in ds:
t = item.get("text", "")
if t.strip():
buf.extend(enc.encode_ordinary(t))
if len(buf) >= n_tokens:
break
buf = buf[:n_tokens]
print(f" WikiText-103 tokens: {len(buf):,}", flush=True)
tokens = torch.tensor(buf, dtype=torch.long)
total_loss = 0.0
total_toks = 0
with torch.no_grad():
for i in range(0, len(tokens) - seq_len, seq_len):
chunk = tokens[i : i + seq_len + 1].to(device)
x, y = chunk[:-1].unsqueeze(0), chunk[1:].unsqueeze(0)
loss = model(x, y)
total_loss += loss.item() * seq_len
total_toks += seq_len
nats_per_token = total_loss / total_toks
bpb = nats_per_token / math.log(2)
perplexity = math.exp(nats_per_token)
print(
f" BPB={bpb:.4f} PPL={perplexity:.2f} loss={nats_per_token:.4f}", flush=True
)
return {
"bpb": bpb,
"perplexity": perplexity,
"loss_nats": nats_per_token,
"tokens": total_toks,
"seq_len": seq_len,
}
def eval_wikitext_bpb_gpt2(
model: Any,
tokenizer: Any,
device: str,
seq_len: int = 1024,
n_tokens: int = 5_000_000,
) -> dict:
from datasets import load_dataset
ds = load_dataset("wikitext", "wikitext-103-raw-v1", split="test", streaming=True)
buf: list[int] = []
for item in ds:
t = item.get("text", "")
if t.strip():
buf.extend(tokenizer.encode(t))
if len(buf) >= n_tokens:
break
buf = buf[:n_tokens]
tokens = torch.tensor(buf, dtype=torch.long)
total_loss = 0.0
total_toks = 0
with torch.no_grad():
for i in range(0, len(tokens) - seq_len, seq_len):
chunk = tokens[i : i + seq_len + 1].to(device)
x, y = chunk[:-1].unsqueeze(0), chunk[1:].unsqueeze(0)
out = model(x, labels=y)
total_loss += out.loss.item() * seq_len
total_toks += seq_len
nats = total_loss / total_toks
return {
"bpb": nats / math.log(2),
"perplexity": math.exp(nats),
"loss_nats": nats,
"tokens": total_toks,
}
class OurModelLM:
def __init__(self, model: CPUGPT, device: str):
import tiktoken
self.model = model
self.device = device
self._enc = tiktoken.get_encoding("r50k_base")
self._vocab = 50257
self._eot = self._enc.eot_token
@property
def eot_token_id(self) -> int:
return self._eot
@property
def max_length(self) -> int:
return 1024
@property
def max_gen_toks(self) -> int:
return 256
@property
def batch_size(self) -> int:
return 1
@property
def device(self):
return self._device
@device.setter
def device(self, v):
self._device = v
def tok_encode(self, text: str) -> list[int]:
return self._enc.encode_ordinary(text)
def tok_decode(self, tokens: list[int]) -> str:
return self._enc.decode(tokens)
def _model_call(self, inps: torch.Tensor) -> torch.Tensor:
with torch.no_grad():
logits = self.model(inps.to(self._device))
return logits.float()
def _model_generate(
self, context: torch.Tensor, max_length: int, eos_token_id: int
) -> torch.Tensor:
return _greedy_generate(
self.model, context.to(self._device), max_length, eos_token_id, self._device
)
def loglikelihood(self, requests):
results = []
for ctx, cont in requests:
ctx_ids = self.tok_encode(ctx)
cont_ids = self.tok_encode(cont)
all_ids = ctx_ids + cont_ids
if len(all_ids) > self.max_length:
all_ids = all_ids[-self.max_length :]
inp = torch.tensor([all_ids], dtype=torch.long).to(self._device)
logits = self._model_call(inp)
log_probs = F.log_softmax(logits, dim=-1)
cont_start = len(ctx_ids) - max(0, len(all_ids) - self.max_length)
ll = 0.0
for j, tok in enumerate(cont_ids):
pos = cont_start + j - 1
if 0 <= pos < log_probs.size(1):
ll += log_probs[0, pos, tok].item()
is_greedy = all(
logits[0, cont_start + j - 1].argmax().item() == cont_ids[j]
for j in range(len(cont_ids))
if 0 <= cont_start + j - 1 < logits.size(1)
)
results.append((ll, is_greedy))
return results
def loglikelihood_rolling(self, requests):
return [self.loglikelihood([("", t)])[0] for t in requests]
def generate_until(self, requests):
out = []
for ctx, until in requests:
ids = self.tok_encode(ctx)
inp = torch.tensor([ids], dtype=torch.long).to(self._device)
gen = _greedy_generate(
self.model, inp, self.max_length, self._eot, self._device
)
new_ids = gen[0, len(ids) :].tolist()
text = self.tok_decode(new_ids)
for stop in until if isinstance(until, list) else [until]:
if stop in text:
text = text[: text.index(stop)]
break
out.append(text)
return out
def _greedy_generate(
model: CPUGPT, inp: torch.Tensor, max_length: int, eos_id: int, device: str
) -> torch.Tensor:
cur = inp
with torch.no_grad():
for _ in range(max_length - cur.size(1)):
logits = model(cur)
next_tok = logits[:, -1, :].argmax(dim=-1, keepdim=True)
cur = torch.cat([cur, next_tok], dim=1)
if next_tok.item() == eos_id:
break
return cur
def run_lm_eval(
model: CPUGPT, device: str, tasks: list[str], limit: int | None = 500
) -> dict:
print(f"[eval] lm-eval tasks: {tasks} (limit={limit}) ...", flush=True)
try:
import lm_eval
from lm_eval import evaluator
from lm_eval import tasks as lm_tasks
except ImportError:
print(" lm-eval not installed — skipping", flush=True)
return {}
lm = OurModelLM(model, device)
results = {}
for task in tasks:
try:
res = evaluator.simple_evaluate(
model=lm,
tasks=[task],
num_fewshot=0,
limit=limit,
bootstrap_iters=100,
)
results[task] = res["results"].get(task, {})
acc = results[task].get("acc,none", results[task].get("acc", "?"))
print(f" {task}: acc={acc}", flush=True)
except Exception as e:
print(f" {task}: ERROR — {e}", flush=True)
results[task] = {"error": str(e)}
return results
def profile_gmacs(model: CPUGPT, device: str, seq_lens: list[int]) -> list[dict]:
print("[eval] GMACs profiling ...", flush=True)
rows = []
cfg = model.cfg if hasattr(model, "cfg") else None
total_params = sum(p.numel() for p in model.parameters())
for T in seq_lens:
if cfg and hasattr(cfg, "gla_chunk") and T % cfg.gla_chunk != 0:
continue
x = torch.randint(0, 50257, (1, T), device=device)
try:
with torch.no_grad():
with torch.profiler.profile(
activities=[torch.profiler.ProfilerActivity.CPU]
+ (
[torch.profiler.ProfilerActivity.CUDA]
if device != "cpu"
else []
),
with_flops=True,
record_shapes=True,
) as prof:
_ = model(x)
total_flops = sum(e.flops for e in prof.key_averages() if e.flops > 0)
gmacs = total_flops / 2 / 1e9
rows.append(
{
"seq_len": T,
"gmacs": gmacs,
"params_m": total_params / 1e6,
"model": "FNO+GLA",
}
)
print(f" seq_len={T}: {gmacs:.2f} GMACs", flush=True)
except Exception as e:
print(f" seq_len={T}: profiling error — {e}", flush=True)
rows.append(
{"seq_len": T, "gmacs": None, "error": str(e), "model": "FNO+GLA"}
)
d_model = cfg.n_embd if cfg else 2048
n_head = cfg.n_head if cfg else 16
n_layer = cfg.n_layer if cfg else 24
d_head = d_model // n_head
for T in seq_lens:
qkv_macs = 3 * T * d_model * d_model
attn_macs = T * T * d_head * n_head
out_macs = T * d_model * d_model
ffn_macs = 2 * T * d_model * (d_model * 4)
per_layer = qkv_macs + attn_macs + out_macs + ffn_macs
total_macs = per_layer * n_layer / 1e9
rows.append({"seq_len": T, "gmacs": total_macs, "model": "SDPA (theoretical)"})
return rows
def vram_benchmark(model: CPUGPT, device: str, seq_lens: list[int]) -> list[dict]:
if device == "cpu":
print("[eval] VRAM benchmark skipped (CPU)", flush=True)
return []
print("[eval] VRAM benchmark ...", flush=True)
rows = []
cfg = model.cfg if hasattr(model, "cfg") else None
chunk = cfg.gla_chunk if cfg else 256
for T in seq_lens:
if T % chunk != 0:
continue
try:
x = torch.randint(0, 50257, (1, T), device=device)
torch.cuda.reset_peak_memory_stats(device)
torch.cuda.synchronize()
with torch.no_grad():
_ = model(x)
torch.cuda.synchronize()
vram_mb = torch.cuda.max_memory_allocated(device) / 1024**2
rows.append(
{"seq_len": T, "vram_mb": vram_mb, "oom": 0, "model": "FNO+GLA"}
)
print(f" FNO+GLA seq_len={T}: {vram_mb:.0f} MB", flush=True)
except torch.cuda.OutOfMemoryError:
rows.append({"seq_len": T, "vram_mb": None, "oom": 1, "model": "FNO+GLA"})
print(f" FNO+GLA seq_len={T}: OOM", flush=True)
finally:
torch.cuda.empty_cache()
try:
d = cfg.n_embd if cfg else 2048
h = cfg.n_head if cfg else 16
attn = nn.MultiheadAttention(d, h, batch_first=True).to(device).eval()
x = torch.randn(1, T, d, device=device)
torch.cuda.reset_peak_memory_stats(device)
torch.cuda.synchronize()
with torch.no_grad():
_ = attn(x, x, x, need_weights=False)
torch.cuda.synchronize()
vram_mb_sdpa = torch.cuda.max_memory_allocated(device) / 1024**2
rows.append(
{
"seq_len": T,
"vram_mb": vram_mb_sdpa,
"oom": 0,
"model": "Standard Attention (SDPA)",
}
)
print(f" SDPA seq_len={T}: {vram_mb_sdpa:.0f} MB", flush=True)
del attn, x
except (torch.cuda.OutOfMemoryError, RuntimeError):
rows.append(
{
"seq_len": T,
"vram_mb": None,
"oom": 1,
"model": "Standard Attention (SDPA)",
}
)
print(f" SDPA seq_len={T}: OOM", flush=True)
finally:
torch.cuda.empty_cache()
return rows
def throughput_benchmark(
model: CPUGPT,
device: str,
seq_lens: list[int],
n_warmup: int = 3,
n_steps: int = 10,
) -> list[dict]:
print("[eval] Throughput benchmark ...", flush=True)
rows = []
cfg = model.cfg if hasattr(model, "cfg") else None
chunk = cfg.gla_chunk if cfg else 256
for T in seq_lens:
if T % chunk != 0:
continue
try:
x = torch.randint(0, 50257, (1, T), device=device)
for _ in range(n_warmup):
with torch.no_grad():
_ = model(x)
if device != "cpu":
torch.cuda.synchronize()
t0 = time.perf_counter()
for _ in range(n_steps):
with torch.no_grad():
_ = model(x)
if device != "cpu":
torch.cuda.synchronize()
elapsed = time.perf_counter() - t0
tps = T * n_steps / elapsed
rows.append({"seq_len": T, "tok_per_sec": tps, "model": "FNO+GLA"})
print(f" seq_len={T}: {tps:,.0f} tok/s", flush=True)
except Exception as e:
print(f" seq_len={T}: error — {e}", flush=True)
return rows
PROMPTS = [
"The universe is approximately 13.8 billion years old. Scientists believe",
"Once upon a time in a small village near the mountains, there lived",
"The key difference between machine learning and traditional programming is",
"To make a perfect omelette, you will need the following ingredients:",
"The French Revolution began in 1789 when",
"In quantum mechanics, the uncertainty principle states that",
"The stock market crashed in 1929 because",
]
def generate_samples(
model: CPUGPT,
device: str,
gpt2_model: Any,
gpt2_tokenizer: Any,
max_new_tokens: int = 150,
temperature: float = 0.8,
top_p: float = 0.9,
) -> list[dict]:
import tiktoken
enc = tiktoken.get_encoding("r50k_base")
def _sample_our(prompt: str) -> str:
ids = enc.encode_ordinary(prompt)
inp = torch.tensor([ids], dtype=torch.long, device=device)
generated = list(ids)
with torch.no_grad():
for _ in range(max_new_tokens):
logits = model(inp)[:, -1, :]
logits = logits / temperature
probs = F.softmax(logits, dim=-1)
sorted_probs, sorted_idx = torch.sort(probs, descending=True)
cumsum = sorted_probs.cumsum(dim=-1)
mask = (cumsum - sorted_probs) > top_p
sorted_probs[mask] = 0
sorted_probs /= sorted_probs.sum()
next_tok = sorted_idx[torch.multinomial(sorted_probs, 1)].item()
if next_tok == enc.eot_token:
break
generated.append(next_tok)
inp = torch.cat([inp, torch.tensor([[next_tok]], device=device)], dim=1)
return enc.decode(generated[len(ids) :])
def _sample_gpt2(prompt: str) -> str:
ids = gpt2_tokenizer.encode(prompt, return_tensors="pt").to(device)
with torch.no_grad():
out = gpt2_model.generate(
ids,
max_new_tokens=max_new_tokens,
do_sample=True,
temperature=temperature,
top_p=top_p,
pad_token_id=gpt2_tokenizer.eos_token_id,
)
return gpt2_tokenizer.decode(out[0, ids.size(1) :], skip_special_tokens=True)
results = []
for prompt in PROMPTS:
print(f" Generating: '{prompt[:40]}...'", flush=True)
our_text = _sample_our(prompt)
gpt2_text = _sample_gpt2(prompt)
results.append({"prompt": prompt, "ours": our_text, "gpt2": gpt2_text})
return results
def _write_samples_md(samples: list[dict], path: Path) -> None:
lines = ["# Qualitative Sample Comparison: FNO+GLA vs GPT-2\n"]
for i, s in enumerate(samples, 1):
lines.append(f"## Prompt {i}\n")
lines.append(f"**Prompt:** {s['prompt']}\n")
lines.append(f"**FNO+GLA (ours):**\n{s['ours']}\n")
lines.append(f"**GPT-2 1.5B:**\n{s['gpt2']}\n")
lines.append("---\n")
path.write_text("\n".join(lines))
def parse_training_log(log_path: str) -> list[dict]:
rows = []
try:
with open(log_path) as f:
for line in f:
if "step=" not in line:
continue
try:
parts = dict(p.split("=") for p in line.split() if "=" in p)
step_str = parts.get("step", "")
if "/" in step_str:
step = int(step_str.split("/")[0])
else:
step = int(step_str)
loss = float(parts.get("loss", 0))
tps = float(parts.get("tok/s", "0").replace(",", ""))
rows.append(
{
"step": step,
"loss": loss,
"tok_per_sec": tps,
"bpb": loss / math.log(2),
}
)
except Exception:
pass
except FileNotFoundError:
print(f" [warn] training log not found: {log_path}", flush=True)
return rows
def make_figures(results: dict, output_dir: Path) -> None:
figs_dir = output_dir / "figures"
figs_dir.mkdir(parents=True, exist_ok=True)
try:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np
except ImportError:
print("[eval] matplotlib not available — skipping figures", flush=True)
return
plt.rcParams.update({"font.size": 12, "figure.dpi": 150})
vram = results.get("vram_benchmark", [])
if vram:
fig, ax = plt.subplots(figsize=(7, 4.5))
ours = [
(r["seq_len"], r["vram_mb"])
for r in vram
if "FNO" in r["model"] and r.get("vram_mb")
]
sdpa = [
(r["seq_len"], r["vram_mb"])
for r in vram
if "SDPA" in r["model"] and r.get("vram_mb")
]
oom_s = [r["seq_len"] for r in vram if "SDPA" in r["model"] and r.get("oom")]
if ours:
xs, ys = zip(*sorted(ours))
ax.plot(xs, ys, "b-o", label="FNO+GLA (ours, O(N))", linewidth=2)
if sdpa:
xs, ys = zip(*sorted(sdpa))
ax.plot(xs, ys, "r--s", label="Standard Attn (O(N²))", linewidth=2)
for x in oom_s:
ax.axvline(x, color="red", alpha=0.3, linestyle=":")
if oom_s:
ax.annotate(
"SDPA OOM →",
xy=(oom_s[0], ax.get_ylim()[1] * 0.9),
color="red",
fontsize=10,
)
ax.set_xlabel("Sequence Length")
ax.set_ylabel("Peak VRAM (MB)")
ax.set_title("Fig 1 — Memory Scaling: FNO+GLA vs Standard Attention")
ax.legend()
ax.set_xscale("log", base=2)
fig.tight_layout()
fig.savefig(figs_dir / "fig1_vram_scaling.png")
plt.close(fig)
print(" fig1_vram_scaling.png saved", flush=True)
gmacs = results.get("gmacs_profile", [])
if gmacs:
fig, ax = plt.subplots(figsize=(7, 4.5))
ours = [
(r["seq_len"], r["gmacs"])
for r in gmacs
if r["model"] == "FNO+GLA" and r.get("gmacs")
]
sdpa = [
(r["seq_len"], r["gmacs"])
for r in gmacs
if "SDPA" in r["model"] and r.get("gmacs")
]
if ours:
xs, ys = zip(*sorted(ours))
ax.plot(xs, ys, "b-o", label="FNO+GLA (ours)", linewidth=2)
if sdpa:
xs, ys = zip(*sorted(sdpa))
ax.plot(xs, ys, "r--s", label="SDPA (theoretical)", linewidth=2)
ax.set_xlabel("Sequence Length")
ax.set_ylabel("GMACs")
ax.set_title("Fig 2 — Compute: FNO+GLA vs SDPA")
ax.legend()
ax.set_xscale("log", base=2)
fig.tight_layout()
fig.savefig(figs_dir / "fig2_gmacs_scaling.png")
plt.close(fig)
print(" fig2_gmacs_scaling.png saved", flush=True)
lm_res = results.get("lm_eval", {})
wt = results.get("wikitext", {})
if lm_res or wt:
tasks_show = [
"arc_easy",
"arc_challenge",
"hellaswag",
"piqa",
"winogrande",
"lambada_openai",
"boolq",
]
ours_acc = []
task_names = []
for t in tasks_show:
if t in lm_res:
a = lm_res[t].get("acc,none", lm_res[t].get("acc"))
if a is not None:
ours_acc.append(float(a) * 100)
task_names.append(t.replace("_openai", "").replace("_", "\n"))
if task_names:
fig, ax = plt.subplots(figsize=(9, 5))
x = np.arange(len(task_names))
bars = ax.bar(
x, ours_acc, 0.5, label="FNO+GLA 1.13B (ours)", color="#2196F3"
)
gpt2_124m = {
"arc_easy": 44.5,
"arc_challenge": 22.3,
"hellaswag": 31.6,
"piqa": 64.6,
"winogrande": 51.7,
"lambada\nopenai": 35.6,
"boolq": 58.5,
}
ref_acc = [gpt2_124m.get(n.replace("\n", "\n"), 0) for n in task_names]
ax.bar(
x + 0.25,
ref_acc,
0.25,
label="GPT-2 124M (published)",
color="#FF9800",
alpha=0.7,
)
ax.set_xticks(x + 0.125)
ax.set_xticklabels(task_names, fontsize=10)
ax.set_ylabel("Accuracy (%)")
ax.set_title("Fig 3 — Zero-Shot Benchmarks: FNO+GLA vs GPT-2")
ax.legend()
ax.set_ylim(0, 100)
for bar in bars:
ax.text(
bar.get_x() + bar.get_width() / 2,
bar.get_height() + 0.5,
f"{bar.get_height():.1f}",
ha="center",
fontsize=9,
)
fig.tight_layout()
fig.savefig(figs_dir / "fig3_benchmark_bars.png")
plt.close(fig)
print(" fig3_benchmark_bars.png saved", flush=True)
curve = results.get("training_curve", [])
if curve:
fig, ax1 = plt.subplots(figsize=(8, 4.5))
steps = [r["step"] for r in curve]
losses = [r["loss"] for r in curve]
bpbs = [r["bpb"] for r in curve]
ax1.plot(steps, losses, "b-", linewidth=1.5, alpha=0.8, label="Training loss")
ax1.set_xlabel("Training Step")
ax1.set_ylabel("Loss (nats)", color="blue")
ax1.tick_params(axis="y", labelcolor="blue")
ax2 = ax1.twinx()
ax2.plot(steps, bpbs, "r-", linewidth=1, alpha=0.5, label="BPB")
ax2.set_ylabel("BPB", color="red")
ax2.tick_params(axis="y", labelcolor="red")
if bpbs:
ax2.annotate(
f"Final BPB: {bpbs[-1]:.3f}",
xy=(steps[-1], bpbs[-1]),
xytext=(steps[-1] * 0.7, bpbs[-1] * 1.05),
arrowprops=dict(arrowstyle="->"),
)
ax1.set_title("Fig 4 — Training Curve (FNO+GLA 1.13B, 5B tokens, 8×H200)")
lines1, labels1 = ax1.get_legend_handles_labels()
lines2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lines1 + lines2, labels1 + labels2, loc="upper right")
fig.tight_layout()
fig.savefig(figs_dir / "fig4_training_curve.png")
plt.close(fig)
print(" fig4_training_curve.png saved", flush=True)
wt_our = results.get("wikitext_ours", {})
wt_gpt2s = results.get("wikitext_gpt2_small", {})
wt_gpt2l = results.get("wikitext_gpt2_large", {})
if wt_our:
fig, ax = plt.subplots(figsize=(7, 4))
models = ["GPT-2 124M", "GPT-2 1.5B", "FNO+GLA 1.13B\n(ours, 5B tokens)"]
bpbs_list = [
wt_gpt2s.get("bpb", 4.87),
wt_gpt2l.get("bpb", 3.92),
wt_our.get("bpb", 0),
]
colors = ["#FF9800", "#FF5722", "#2196F3"]
bars = ax.barh(models, bpbs_list, color=colors, edgecolor="white")
ax.set_xlabel("WikiText-103 BPB (lower = better)")
ax.set_title("Fig 5 — WikiText-103 Bits-per-Byte Comparison")
for bar, bpb in zip(bars, bpbs_list):
ax.text(
bpb + 0.02,
bar.get_y() + bar.get_height() / 2,
f"{bpb:.3f}",
va="center",
fontsize=11,
)
fig.tight_layout()
fig.savefig(figs_dir / "fig5_wikitext_bpb.png")
plt.close(fig)
print(" fig5_wikitext_bpb.png saved", flush=True)
tput = results.get("throughput", [])
if tput:
fig, ax = plt.subplots(figsize=(7, 4.5))
xs = [r["seq_len"] for r in tput]
ys = [r["tok_per_sec"] for r in tput]
ax.plot(xs, ys, "b-o", linewidth=2)
ax.set_xlabel("Sequence Length")
ax.set_ylabel("Throughput (tokens/sec)")
ax.set_title("Fig 6 — Inference Throughput vs Sequence Length (A10G)")
ax.set_xscale("log", base=2)
fig.tight_layout()
fig.savefig(figs_dir / "fig6_throughput.png")
plt.close(fig)
print(" fig6_throughput.png saved", flush=True)
print("[eval] All figures saved to", figs_dir, flush=True)
MODEL_CARD = """\
---
license: apache-2.0
language:
- en
tags:
- causal-lm
- fno
- gated-linear-attention
- efficient-transformers
- long-context
datasets:
- Skylion007/openwebtext
---
# FNO+GLA: Fourier Neural Operator + Gated Linear Attention LLM
**Architecture**: FNO sequence mixer (O(N log N)) + GLA recurrent mixer (O(N)) —
no quadratic attention, runs 32K context on a single A10G GPU.
**Model size**: 1.13B parameters
**Training**: 5B tokens (OpenWebText), 8×H200 SXM, seq_len=32,768
**Pattern**: SSSL (3 FNO + 1 GLA per 4-layer group)
## Results
| Benchmark | FNO+GLA 1.13B | GPT-2 1.5B |
|----------------|---------------|------------|
| WikiText-103 BPB | see eval | ~3.92 |
| ARC-Easy | see eval | ~50.4 |
| HellaSwag | see eval | ~41.4 |
| PIQA | see eval | ~70.8 |
## Memory Efficiency
FNO+GLA uses O(N log N) memory for the FNO path and O(N) for GLA,
vs O(N²) for standard attention. Fits 32K context in 24GB VRAM where
standard attention OOMs at ~16K.
## Usage
```python
# Load weights and run inference — see scripts/paper_eval.py
```
## Citation
```
@misc{fela-acml2026,
title={FNO+GLA: Efficient Long-Context Language Modeling},
year={2026}
}
```
"""
def push_to_hf(
ckpt_path: str,
results: dict,
output_dir: Path,
hf_repo: str,
hf_token: str | None = None,
) -> None:
print(f"[eval] Pushing to HuggingFace: {hf_repo} ...", flush=True)
try:
from huggingface_hub import HfApi
api = HfApi(token=hf_token)
readme_path = output_dir / "README.md"
readme_path.write_text(MODEL_CARD)
results_path = output_dir / "paper_eval.json"
results_path.write_text(json.dumps(results, indent=2, default=str))
for f in output_dir.rglob("*"):
if not f.is_file():
continue
if f.suffix in (".pt",):
continue
repo_path = str(f.relative_to(output_dir))
try:
api.upload_file(
path_or_fileobj=str(f),
path_in_repo=f"results/{repo_path}",
repo_id=hf_repo,
repo_type="model",
)
print(f" uploaded: results/{repo_path}", flush=True)
except Exception as e:
print(f" WARN: {repo_path} failed: {e}", flush=True)
api.upload_file(
path_or_fileobj=str(readme_path),
path_in_repo="README.md",
repo_id=hf_repo,
repo_type="model",
)
if ckpt_path and not ckpt_path.startswith("s3://"):
ckpt_file = Path(ckpt_path)
if ckpt_file.exists():
print(
f" uploading checkpoint ({ckpt_file.stat().st_size / 1e9:.1f}GB)...",
flush=True,
)
api.upload_file(
path_or_fileobj=str(ckpt_file),
path_in_repo=f"checkpoints/{ckpt_file.name}",
repo_id=hf_repo,
repo_type="model",
)
print(
f"[eval] HuggingFace push complete: https://huggingface.co/{hf_repo}",
flush=True,
)
except Exception as e:
print(f"[eval] HuggingFace push failed: {e}", flush=True)
traceback.print_exc()
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--ckpt", required=True)
ap.add_argument("--config", default="gpt2-1b")
ap.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
ap.add_argument("--output-dir", default="results/paper_eval_v2")
ap.add_argument("--train-log", default="/workspace/train_out.log")
ap.add_argument("--hf-repo", default="itstheraj/fela-acml2026")
ap.add_argument("--hf-token", default=None)
ap.add_argument("--skip-lm-eval", action="store_true")
ap.add_argument("--skip-hf", action="store_true")
ap.add_argument("--lm-eval-limit", type=int, default=500)
args = ap.parse_args()
out = Path(args.output_dir)
out.mkdir(parents=True, exist_ok=True)
(out / "figures").mkdir(exist_ok=True)
print(f"\n{'=' * 60}", flush=True)
print(f"FNO+GLA Paper Evaluation — {args.config} on {args.device}", flush=True)
print(f"Checkpoint: {args.ckpt}", flush=True)
print(f"Output: {out}", flush=True)
print(f"{'=' * 60}\n", flush=True)
results: dict = {}
model = _build_model(args.config, args.ckpt, args.device)
gpt2_sm_mdl, gpt2_sm_tok = _gpt2_baseline(args.device, "gpt2")
gpt2_lg_mdl, gpt2_lg_tok = _gpt2_baseline(args.device, "gpt2-large")
print("\n[1/9] Training curve ...", flush=True)
results["training_curve"] = parse_training_log(args.train_log)
print(f" {len(results['training_curve'])} steps parsed", flush=True)
print("\n[2/9] WikiText-103 BPB ...", flush=True)
results["wikitext_ours"] = eval_wikitext_bpb(model, args.device)
results["wikitext_gpt2_small"] = eval_wikitext_bpb_gpt2(
gpt2_sm_mdl, gpt2_sm_tok, args.device
)
results["wikitext_gpt2_large"] = eval_wikitext_bpb_gpt2(
gpt2_lg_mdl, gpt2_lg_tok, args.device
)
results["wikitext"] = results["wikitext_ours"]
print("\n[3/9] Open LLM Leaderboard benchmarks ...", flush=True)
if not args.skip_lm_eval:
tasks = [
"arc_easy",
"arc_challenge",
"hellaswag",
"piqa",
"winogrande",
"lambada_openai",
"boolq",
]
results["lm_eval"] = run_lm_eval(
model, args.device, tasks, limit=args.lm_eval_limit
)
else:
print(" [skipped]", flush=True)
results["lm_eval"] = {}
print("\n[4/9] GMACs profiling ...", flush=True)
seq_lens_bench = [512, 1024, 2048, 4096, 8192, 16384, 32768, 65536]
results["gmacs_profile"] = profile_gmacs(model, args.device, seq_lens_bench)
print("\n[5/9] VRAM benchmark ...", flush=True)
results["vram_benchmark"] = vram_benchmark(model, args.device, seq_lens_bench)
print("\n[6/9] Throughput benchmark ...", flush=True)
results["throughput"] = throughput_benchmark(model, args.device, seq_lens_bench)
print("\n[7/9] Qualitative samples ...", flush=True)
results["samples"] = generate_samples(model, args.device, gpt2_lg_mdl, gpt2_lg_tok)
_write_samples_md(results["samples"], out / "samples_v2.md")
def _save_csv(rows: list[dict], path: Path) -> None:
if not rows:
return
with open(path, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=rows[0].keys())
w.writeheader()
w.writerows(rows)
_save_csv(results["gmacs_profile"], out / "gmacs_profile.csv")
_save_csv(results["vram_benchmark"], out / "vram_benchmark_v2.csv")
_save_csv(results["throughput"], out / "throughput_v2.csv")
(out / "paper_eval.json").write_text(json.dumps(results, indent=2, default=str))
print(f"\n[eval] Results saved to {out / 'paper_eval.json'}", flush=True)
print("\n" + "=" * 60, flush=True)
print("RESULTS SUMMARY", flush=True)
print("=" * 60, flush=True)
wt = results.get("wikitext_ours", {})
if wt:
print(f"WikiText-103 BPB (ours): {wt.get('bpb', '?'):.4f}", flush=True)
print(
f"WikiText-103 PPL (ours): {wt.get('perplexity', '?'):.2f}",
flush=True,
)
wt_s = results.get("wikitext_gpt2_small", {})
wt_l = results.get("wikitext_gpt2_large", {})
if wt_s:
print(f"WikiText-103 BPB (GPT-2 124M): {wt_s.get('bpb', '?'):.4f}", flush=True)
if wt_l:
print(f"WikiText-103 BPB (GPT-2 1.5B): {wt_l.get('bpb', '?'):.4f}", flush=True)
for task, res in results.get("lm_eval", {}).items():
acc = res.get("acc,none", res.get("acc", "?"))
print(
f" {task:<25} {float(acc) * 100:.1f}%"
if isinstance(acc, (int, float))
else f" {task:<25} ?",
flush=True,
)
print("=" * 60, flush=True)
print("\n[8/9] Generating paper figures ...", flush=True)
make_figures(results, out)
print("\n[9/9] HuggingFace push ...", flush=True)
if not args.skip_hf:
hf_token = args.hf_token or os.environ.get("HF_TOKEN")
push_to_hf(args.ckpt, results, out, args.hf_repo, hf_token)
else:
print(" [skipped]", flush=True)
print(f"\n[eval] COMPLETE. Results at: {out}", flush=True)
if __name__ == "__main__":
main()