File size: 7,617 Bytes
77f690c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
#!/usr/bin/env python3
"""bank_eval.py (day-52) — does the artifact bank COMPOUND solve-rate, weights FROZEN?

Sealed meter = data/af/band_meter_v3.jsonl (320, DISJOINT from fuel_big). For each
meter problem we RETRIEVE the top-K most similar banked (statement, solution) pairs
by pure-python BM25 over tokenized statements, build a few-shot prompt (analogous
solved problems from the disjoint pool — never the answer), greedy-generate with the
frozen base, and grade on ALL the target's sampled tests.

COMPOUNDING TEST: sweep bank size S over BANK_SIZES (first S bank records = the
retrievable set; S=0 = no retrieval = frozen-base control). Compounding = solved/320
rises monotonically with S while the model never changes.

Env: BASE (/workspace/RSI/newbase), METER, BANK (/workspace/RSI/outputs/bank.jsonl),
     BANK_SIZES ("0,100,300,600,1000"), K (3), EVAL_TC (40), MAXTOK (900),
     MAXLEN (16384), GPU_UTIL (0.85), N (meter subset cap, for smoke tests),
     OUT (/workspace/RSI/outputs/bank_eval.json).
Prints: BANK_COMPOUNDING sizes=[...] solved=[...]
"""
import os, sys, json, math, re, tempfile, subprocess
from collections import Counter
from concurrent.futures import ThreadPoolExecutor
sys.path.insert(0, "/workspace/RSI")
from src.utils.vllm_backend import VLLMModelLoader          # noqa: E402
from src.utils.external_benchmarks import _extract_code     # noqa: E402

BASE = os.environ.get("BASE", "/workspace/RSI/newbase")
METER = os.environ.get("METER", "/workspace/RSI/data/af/band_meter_v3.jsonl")
BANK = os.environ.get("BANK", "/workspace/RSI/outputs/bank.jsonl")
SIZES = [int(x) for x in os.environ.get("BANK_SIZES", "0,100,300,600,1000").split(",")]
K = int(os.environ.get("K", "3"))
EVAL_TC = int(os.environ.get("EVAL_TC", "40"))
MAXTOK = int(os.environ.get("MAXTOK", "900"))
MAXLEN = int(os.environ.get("MAXLEN", "16384"))
N = int(os.environ.get("N", "100000"))
OUT = os.environ.get("OUT", "/workspace/RSI/outputs/bank_eval.json")
BNB = {"load_in_4bit": True, "bnb_4bit_compute_dtype": "bfloat16"}

# ---- grading (identical semantics to band_ruler.py) ----
def _norm(s): return "\n".join(l.rstrip() for l in str(s).strip().splitlines())
def _run1(code, inp):
    d = tempfile.mkdtemp(); p = os.path.join(d, "s.py"); open(p, "w").write(code)
    try: return subprocess.run([sys.executable, p], input=str(inp), capture_output=True, text=True, timeout=6).stdout
    except Exception: return ""
    finally:
        try: os.remove(p); os.rmdir(d)
        except Exception: pass
_pool = ThreadPoolExecutor(max_workers=96)
def grade(code, ins, outs):
    if not code or not ins: return 0.0
    tc = list(zip(ins, outs)); step = max(1, len(tc)//EVAL_TC); tc = tc[::step][:EVAL_TC]
    return sum(_pool.map(lambda io: _norm(_run1(code, io[0])) == _norm(io[1]), tc)) / max(1, len(tc))

# ---- pure-python BM25 (no deps) ----
_tok_re = re.compile(r"[a-z0-9]+")
def toks(s): return _tok_re.findall(s.lower())

class BM25:
    def __init__(self, docs, k1=1.5, b=0.75):
        self.k1, self.b = k1, b
        self.docs = [Counter(toks(d)) for d in docs]
        self.dl = [sum(c.values()) for c in self.docs]
        self.avgdl = (sum(self.dl) / len(self.dl)) if self.dl else 1.0
        df = Counter()
        for c in self.docs: df.update(c.keys())
        n = len(self.docs)
        self.idf = {t: math.log(1 + (n - d + 0.5) / (d + 0.5)) for t, d in df.items()}
    def top(self, query, k):
        q = toks(query)
        scores = []
        for i, c in enumerate(self.docs):
            s = 0.0
            for t in q:
                f = c.get(t, 0)
                if not f: continue
                s += self.idf.get(t, 0.0) * f * (self.k1 + 1) / (f + self.k1 * (1 - self.b + self.b * self.dl[i] / self.avgdl))
            scores.append((s, i))
        scores.sort(reverse=True)
        return [i for s, i in scores[:k] if s > 0]

def split_chat(p):
    """(prefix incl user tag, user body, suffix from user <|im_end|>)."""
    a = p.find("<|im_start|>user\n")
    if a == -1: return None
    a += len("<|im_start|>user\n")
    b = p.find("<|im_end|>", a)
    if b == -1: return None
    return p[:a], p[a:b], p[b:]

def fewshot_prompt(target_prompt, exemplars):
    """exemplars = [(statement, solution)]; insert into the user turn, budget to MAXLEN."""
    sp = split_chat(target_prompt)
    if sp is None:
        prefix, body, suffix = "", target_prompt, ""
    else:
        prefix, body, suffix = sp
    budget = (MAXLEN - MAXTOK - 128) * 3  # ~3 chars/token, conservative
    exs = list(exemplars)
    while True:
        blocks = []
        for i, (st, sol) in enumerate(exs, 1):
            blocks.append(f"### Solved example {i}\nProblem:\n{st[:1500]}\n\nCorrect solution:\n```python\n{sol[:2500]}\n```\n")
        head = ("Here are previously solved problems similar to the one you must solve. "
                "Study their approaches, then solve the NEW problem.\n\n" + "\n".join(blocks) +
                "\n### NEW problem (solve THIS one)\n") if exs else ""
        full = prefix + head + body + suffix
        if len(full) <= budget or not exs: return full
        exs = exs[:-1]  # drop least-similar exemplar until it fits

meter = [json.loads(l) for l in open(METER) if l.strip()][:N]
for r in meter:
    assert r.get("prompt") and r.get("inputs") and r.get("outputs"), f"bad meter row {r.get('pid')}"
bank = [json.loads(l) for l in open(BANK) if l.strip()] if os.path.exists(BANK) else []
SIZES = sorted(set(min(s, len(bank)) for s in SIZES))
print(f"[bank_eval] meter={len(meter)} bank={len(bank)} sizes={SIZES} K={K} BASE={BASE}", flush=True)

loader = VLLMModelLoader(model_path=BASE, dtype="bfloat16", max_model_len=MAXLEN,
    gpu_memory_utilization=float(os.environ.get("GPU_UTIL", "0.85")),
    allow_remote_code=True, quantization_config=BNB, enforce_eager=True)
loader.load()
assert getattr(loader, "_llm", None) is not None, "FATAL: vLLM did not load (no cross-backend verdicts)"
print(f"[bank_eval] loaded {BASE} backend=vllm (FROZEN, no adapter)", flush=True)

results = {}   # size -> {"solved": int, "bits": [...], "mean_partial": float}
for S in SIZES:
    sub = bank[:S]
    if S > 0:
        bm = BM25([b["statement"] for b in sub])
    prompts = []
    n_with_ex = 0
    for r in meter:
        if S == 0:
            prompts.append(r["prompt"]); continue
        stmt = split_chat(r["prompt"])
        query = stmt[1] if stmt else r["prompt"]
        idxs = bm.top(query, K)
        exs = [(sub[i]["statement"], sub[i]["solution"]) for i in idxs]
        if exs: n_with_ex += 1
        prompts.append(fewshot_prompt(r["prompt"], exs))
    print(f"[bank_eval] S={S}: {n_with_ex}/{len(meter)} meter problems got exemplars; generating (greedy)...", flush=True)
    outs = loader.generate_batch(prompts, max_new_tokens=MAXTOK, temperature=0.0, top_p=1.0)
    bits, part = [], []
    for r, o in zip(meter, outs):
        g = grade(_extract_code(o) or o, r["inputs"], r["outputs"])
        part.append(g); bits.append(1 if g >= 0.999 else 0)
    results[S] = {"solved": sum(bits), "bits": bits, "mean_partial": sum(part) / len(part)}
    print(f"[bank_eval] S={S}: solved {sum(bits)}/{len(meter)} mean_partial={sum(part)/len(part):.4f}", flush=True)

os.makedirs(os.path.dirname(OUT), exist_ok=True)
json.dump({"base": BASE, "meter": METER, "bank": BANK, "k": K,
           "pids": [r["pid"] for r in meter],
           "sizes": SIZES, "results": {str(s): results[s] for s in SIZES}}, open(OUT, "w"))
print(f"[bank_eval] results -> {OUT}", flush=True)
print(f"BANK_COMPOUNDING sizes={SIZES} solved={[results[s]['solved'] for s in SIZES]}", flush=True)