fbfhbfbfbv / day53 /scripts /bank_eval.py
td-builder's picture
Duplicate from td-builder/RSI
77f690c
Raw History Blame Contribute Delete
7.62 kB
#!/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)