mLattice / experiments /eval_ruler.py
Ex0bit's picture
Docker Space: actual demo server, 3 checkpoints, readable dark text
2f06744 verified
Raw History Blame Contribute Delete
4.53 kB
#!/usr/bin/env python3
"""RULER-style single-needle retrieval evaluation.
RULER S-NIAH (Single Needle In A Haystack) is the standard long-context
benchmark: embed one fact ("needle") in a long context of distractor text
("haystack"), then ask the model to retrieve it. This tests whether the mixer
can find one specific association among many tokens of noise.
Protocol:
* Generate a haystack of N key-value pairs (distractors) from the vocabulary
* Embed ONE target pair (the needle) at a specified depth
* After the full sequence, query the needle's key
* The model must return the needle's value
This differs from capacity (which queries many pairs) — here we test whether
a SINGLE fact survives among noise, which is the harder retrieval problem.
"""
from __future__ import annotations
import json
import os
import sys
import time
import torch
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.join(HERE, "..")
sys.path.insert(0, os.path.join(ROOT, "src"))
from mulattice.protocol import SynthCfg, build_model, force_fast_scan, logits_at, synth_batch
DEVICE = os.environ.get("DEVICE", "cuda" if torch.cuda.is_available() else "cpu")
CKPT = os.path.join(ROOT, "checkpoints", "capacity8k.pt")
def run_needle(n_haystack: int, filler: int, depth_frac: float, seed: int, cfg, model):
"""Place one needle at `depth_frac` through a haystack of n_haystack pairs."""
g = torch.Generator().manual_seed(seed)
# Build a standard batch, then extract the needle at the right position
b = synth_batch(1, n_haystack + 1, 1, cfg, filler=filler, generator=g)
tokens = b["tokens"]
L = tokens.shape[1]
# The needle is the pair at index `needle_idx` in the store pass
block = filler + 2
needle_idx = int(n_haystack * depth_frac)
needle_key = int(tokens[0, needle_idx * block + filler])
needle_val = int(tokens[0, needle_idx * block + filler + 1])
# Query the needle
S2 = (n_haystack + 1) * block
tokens[0, S2] = needle_key
tokens[0, S2 + 1] = cfg.SEP
ans_pos = torch.tensor([[S2 + 1]])
gold = torch.tensor([[needle_val]])
with torch.no_grad():
pred = logits_at(model, tokens.to(DEVICE), ans_pos.to(DEVICE)).argmax(-1).cpu()
correct = int(pred[0, 0]) == int(gold[0, 0])
return correct, needle_key, needle_val, int(pred[0, 0]), L
def main():
cfg = SynthCfg()
ck = torch.load(CKPT, map_location="cpu", weights_only=False)
cfg.addr_scale = float(ck["proto_cfg"]["addr_scale"])
model = build_model(cfg)
model.load_state_dict(ck["model"])
model = model.to(DEVICE).eval()
force_fast_scan(model)
print(f"RULER S-NIAH evaluation on {DEVICE}", flush=True)
print(f"model: capacity8k.pt, scale={cfg.addr_scale:.0f}", flush=True)
print()
# Test at increasing haystack sizes and depths
configs = []
for n_hay in [256, 1024, 4096, 8192]:
for depth in [0.05, 0.25, 0.50, 0.75, 0.95]:
configs.append((n_hay, 14, depth))
results = []
n_correct = 0
n_total = 0
for n_hay, filler, depth in configs:
# 5 seeds per config
for seed in range(5):
ok, k, v, p, L = run_needle(n_hay, filler, depth, seed + 100, cfg, model)
results.append({
"n_haystack": n_hay, "filler": filler, "depth": depth,
"seed": seed, "correct": bool(ok), "T": int(L),
})
n_correct += ok
n_total += 1
acc = sum(r["correct"] for r in results if r["n_haystack"] == n_hay) / \
sum(1 for r in results if r["n_haystack"] == n_hay)
T = n_hay * 16 + 2
print(f" haystack={n_hay:5d} pairs, T≈{T:6d}: "
f"{sum(r['correct'] for r in results if r['n_haystack']==n_hay)}/"
f"{sum(1 for r in results if r['n_haystack']==n_hay)} "
f"({acc:.4f})", flush=True)
overall = n_correct / n_total
print(f"\nOverall: {n_correct}/{n_total} = {overall:.4f}", flush=True)
# Breakdown by depth
print("\nBy depth:")
for d in [0.05, 0.25, 0.50, 0.75, 0.95]:
sub = [r for r in results if r["depth"] == d]
acc = sum(r["correct"] for r in sub) / len(sub)
print(f" depth {d:4.0%}: {acc:.4f} ({sum(r['correct'] for r in sub)}/{len(sub)})")
out = os.path.join(ROOT, "results", "ruler_sniah.json")
with open(out, "w") as f:
json.dump({"overall": overall, "results": results}, f, indent=2)
print(f"\nsaved {out}")
if __name__ == "__main__":
main()