File size: 4,526 Bytes
2f06744
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
#!/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()