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)
|