echo-1 / ml /evaluate.py
lupodevelop's picture
echo-1 Stage 0 explainer: diffusion vs autoregressive, execution-verified
3afc977 verified
Raw
History Blame Contribute Delete
9.68 kB
"""Evaluate by execution and produce the accuracy-vs-latency curve (EVALUATION.md).
For each eval record the model predicts the hole; we reconstruct prefix + hole +
suffix and verify by execution against the held-out tests (pass@1). Latency is
wall-clock at batch 1. The diffusion model is swept over denoising steps N (the
test-time-compute knob); the AR baseline is a single sequential decode. Results
are grouped by difficulty so we can see where, at iso-latency, the diffusion
substrate wins.
Usage:
python -m ml.evaluate --diff runs/diff --ar runs/ar --eval data/eval.jsonl \
--steps 1,2,4,8,16,32 --out runs/curve.csv
"""
from __future__ import annotations
import argparse
import csv
import time
import torch
from . import ar, block_diffusion
from .ar import build_prompt
import numpy as np
from .config import ModelConfig, TaskConfig
from .data import (_ids_canvas, block_for_eval, encode_ar, encode_diffusion,
load_records, make_block, make_block_lua)
from .model import Transformer, amp_ctx
from .tokenizer import Tokenizer
from .verify import verify_batch
def load_model(path: str, device: str):
ckpt = torch.load(f"{path}/model.pt", map_location=device, weights_only=False)
mcfg = ModelConfig(**ckpt["model_cfg"])
model = Transformer(mcfg, causal=(ckpt["mode"] == "ar")).to(device)
model.load_state_dict(ckpt["model"])
model.eval()
tok = Tokenizer.load(f"{path}/tokenizer.json")
task = TaskConfig(**ckpt["task_cfg"])
return model, tok, task, ckpt["mode"]
def pick_device() -> str:
if torch.backends.mps.is_available():
return "mps"
if torch.cuda.is_available():
return "cuda"
return "cpu"
def rec_seed(rec) -> int:
"""Stable per-record seed for the eval block (independent of list position)."""
return rec.get("seed", 0) % 2147483647
def get_block(tok, source, frac, seed):
"""(pre, blk, suf) for the record — id lists in lua mode, strings in char."""
rng = np.random.RandomState(seed + 1)
return make_block_lua(source, frac, rng, tok) if tok.mode == "lua" else make_block(source, frac, rng)
def reconstruct(tok, blk, pred_ids):
"""Full program from prefix + predicted block + suffix."""
if tok.mode == "lua":
return tok.decode(list(blk[0]) + list(pred_ids) + list(blk[2]))
return blk[0] + tok.decode(pred_ids) + blk[2]
def eval_diffusion(model, tok, task, records, n_inner, device, frac):
"""Block-diffusion eval at one n_inner setting (latency knob)."""
cands, lats, kept = [], [], []
for i, rec in enumerate(records):
blk = get_block(tok, rec["source"], frac, rec_seed(rec))
if blk is None:
continue
enc = _ids_canvas(tok, blk[0], blk[1], blk[2], task, ar=False) if tok.mode == "lua" \
else encode_diffusion(tok, blk[0], blk[1], blk[2], task)
if enc is None:
continue
ids, region, _block_id, attn = enc
ids_row = torch.from_numpy(ids).to(device)
region_row = torch.from_numpy(region).to(device)
attn_row = torch.from_numpy(attn).to(device)
t0 = time.perf_counter()
with amp_ctx(device):
toks = block_diffusion.sample(model, ids_row, region_row, attn_row, tok, task, n_inner)
if device == "mps":
torch.mps.synchronize()
lat = (time.perf_counter() - t0) * 1000.0
cands.append({"source": reconstruct(tok, blk, toks), "tests": rec["tests"]})
lats.append(lat)
kept.append(i)
return cands, lats, kept
def eval_ar(model, tok, task, records, device, frac):
cands, lats, kept = [], [], []
for i, rec in enumerate(records):
blk = get_block(tok, rec["source"], frac, rec_seed(rec))
if blk is None:
continue
head = build_prompt(tok, blk[0], blk[2], task)
if head is None:
continue
head_ids = torch.tensor(head, device=device)
t0 = time.perf_counter()
with amp_ctx(device):
ids_out = model.generate(head_ids, max_new=task.max_decode, eos_id=tok.eos_id)
if device == "mps":
torch.mps.synchronize()
lat = (time.perf_counter() - t0) * 1000.0
cands.append({"source": reconstruct(tok, blk, ids_out), "tests": rec["tests"]})
lats.append(lat)
kept.append(i)
return cands, lats, kept
def summarize(rows, records, kept, passes, lats, model_name, steps, frac):
"""Group pass@1 and latency by difficulty, for one masking fraction."""
by_diff = {}
for k, p, lat in zip(kept, passes, lats):
d = records[k]["difficulty"]
by_diff.setdefault(d, []).append((p, lat))
for d in sorted(by_diff):
ps = [p for p, _ in by_diff[d]]
ls = [lat for _, lat in by_diff[d]]
rows.append({
"model": model_name, "frac": frac, "steps": steps, "difficulty": d,
"n": len(ps), "pass@1": round(sum(ps) / len(ps), 4),
"mean_latency_ms": round(sum(ls) / len(ls), 2),
})
rows.append({
"model": model_name, "frac": frac, "steps": steps,
"difficulty": "all", "n": len(passes),
"pass@1": round(sum(passes) / max(1, len(passes)), 4),
"mean_latency_ms": round(sum(lats) / max(1, len(lats)), 2),
})
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--diff", help="diffusion run dir")
ap.add_argument("--ar", help="AR run dir")
ap.add_argument("--eval", default="data/eval.jsonl")
ap.add_argument("--steps", default="1,2,4,8", help="block-diffusion n_inner values to sweep")
ap.add_argument("--fracs", default="0.15,0.3,0.45", help="block fractions to sweep")
ap.add_argument("--tile_size", type=int, default=0, help="override remask tile size (0=keep checkpoint)")
ap.add_argument("--limit", type=int, default=0, help="cap eval records (0=all)")
ap.add_argument("--out", default="runs/curve.csv")
args = ap.parse_args()
device = pick_device()
all_records = load_records(args.eval)
if args.limit:
all_records = all_records[: args.limit]
print(f"eval records={len(all_records)} device={device}")
def warmup(model, task):
ids = torch.zeros(1, task.seq_len, dtype=torch.long, device=device)
keep = torch.ones(1, task.seq_len, dtype=torch.bool, device=device)
with amp_ctx(device):
model(ids, keep)
if device == "mps":
torch.mps.synchronize()
rows = []
step_list = [int(s) for s in args.steps.split(",") if s]
frac_list = [float(x) for x in args.fracs.split(",") if x]
# Load both models once; reuse across fractions.
def warmup_block(model, tok, task):
# Warm the cached block-decode kernels so the first timed record is clean.
ids = torch.zeros(task.seq_len, dtype=torch.long, device=device)
region = torch.zeros(task.seq_len, dtype=torch.bool, device=device)
region[4 : 4 + 2 * task.block_len] = True
attn = torch.ones(task.seq_len, dtype=torch.bool, device=device)
with amp_ctx(device):
block_diffusion.sample(model, ids, region, attn, tok, task, 2)
if device == "mps":
torch.mps.synchronize()
dmodel = dtok = dtask = None
if args.diff:
dmodel, dtok, dtask, m = load_model(args.diff, device)
assert m == "diffusion"
if args.tile_size:
dtask.tile_size = args.tile_size
warmup(dmodel, dtask)
warmup_block(dmodel, dtok, dtask)
amodel = atok = atask = None
if args.ar:
amodel, atok, atask, m = load_model(args.ar, device)
assert m == "ar"
warmup(amodel, atask)
tok0 = dtok if dtok is not None else atok
task0 = dtask if dtask is not None else atask
for frac in frac_list:
# Common-fit at THIS fraction: both encodings must fit the same records.
def fits_both(rec):
blk = get_block(tok0, rec["source"], frac, rec_seed(rec))
if blk is None:
return False
if tok0.mode == "lua":
d = _ids_canvas(tok0, blk[0], blk[1], blk[2], task0, ar=False)
a = _ids_canvas(tok0, blk[0], blk[1], blk[2], task0, ar=True)
else:
d = encode_diffusion(tok0, blk[0], blk[1], blk[2], task0)
a = encode_ar(tok0, blk[0], blk[1], blk[2], task0)
return d is not None and a is not None
records = [r for r in all_records if fits_both(r)] if (args.diff and args.ar) else all_records
print(f"\n--- frac={frac} common-fit={len(records)} ---")
if args.diff:
for N in step_list:
cands, lats, kept = eval_diffusion(dmodel, dtok, dtask, records, N, device, frac)
passes = verify_batch(cands)
summarize(rows, records, kept, passes, lats, "diffusion", N, frac)
o = rows[-1]
print(f" diffusion n_inner={N:>2} pass@1={o['pass@1']:.3f} lat={o['mean_latency_ms']:.1f}ms")
if args.ar:
cands, lats, kept = eval_ar(amodel, atok, atask, records, device, frac)
passes = verify_batch(cands)
summarize(rows, records, kept, passes, lats, "ar", "NA", frac)
o = rows[-1]
print(f" ar pass@1={o['pass@1']:.3f} lat={o['mean_latency_ms']:.1f}ms")
with open(args.out, "w", newline="") as f:
w = csv.DictWriter(f, fieldnames=["model", "frac", "steps", "difficulty", "n", "pass@1", "mean_latency_ms"])
w.writeheader()
w.writerows(rows)
print(f"\nwrote {args.out}")
if __name__ == "__main__":
main()