Eval cells now record turns/tool_calls/completion+prompt tokens; summary gets efficiency-vs-base ratios
Browse files- eval_adapter.py +15 -5
- eval_watch.py +39 -13
eval_adapter.py
CHANGED
|
@@ -65,15 +65,25 @@ def run_one(task_index, model):
|
|
| 65 |
kwargs.pop("eval_sampling")
|
| 66 |
result = env.run_rollout(**kwargs)
|
| 67 |
except Exception as exc:
|
| 68 |
-
return task_index, model, None, True, f"{type(exc).__name__}: {exc}"
|
| 69 |
finally:
|
| 70 |
try:
|
| 71 |
env.close()
|
| 72 |
except Exception:
|
| 73 |
pass
|
| 74 |
if not result.ok or result.reward is None:
|
| 75 |
-
return task_index, model, None, True, getattr(result, "error", None) or "not ok"
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
|
| 79 |
def main():
|
|
@@ -87,8 +97,8 @@ def main():
|
|
| 87 |
futures = [pool.submit(run_one, i, m) for i, m in work]
|
| 88 |
done = 0
|
| 89 |
for fut in as_completed(futures):
|
| 90 |
-
i, m, correct, failed, err = fut.result()
|
| 91 |
-
cells[(i, m)] = {"correct": correct, "failed": failed, "error": err}
|
| 92 |
done += 1
|
| 93 |
if done % 20 == 0 or done == len(work):
|
| 94 |
print(f"[eval] {done}/{len(work)} cells done", flush=True)
|
|
|
|
| 65 |
kwargs.pop("eval_sampling")
|
| 66 |
result = env.run_rollout(**kwargs)
|
| 67 |
except Exception as exc:
|
| 68 |
+
return task_index, model, None, True, f"{type(exc).__name__}: {exc}", None
|
| 69 |
finally:
|
| 70 |
try:
|
| 71 |
env.close()
|
| 72 |
except Exception:
|
| 73 |
pass
|
| 74 |
if not result.ok or result.reward is None:
|
| 75 |
+
return task_index, model, None, True, getattr(result, "error", None) or "not ok", None
|
| 76 |
+
turns = getattr(result, "turns", []) or []
|
| 77 |
+
metrics = {
|
| 78 |
+
"turns": getattr(result, "n_turns", 0) or len(turns),
|
| 79 |
+
"tool_calls": sum(getattr(t, "n_tools", 0) for t in turns),
|
| 80 |
+
"completion_tokens": sum(len(getattr(t, "completion_token_ids", []) or []) for t in turns),
|
| 81 |
+
# Each turn resends the whole conversation, so this grows ~quadratically
|
| 82 |
+
# with turn count — the quantity the article's input-token savings cut.
|
| 83 |
+
"prompt_tokens_total": sum(len(getattr(t, "prompt_token_ids", []) or []) for t in turns),
|
| 84 |
+
"wall_s": getattr(result, "wall_s", 0.0),
|
| 85 |
+
}
|
| 86 |
+
return task_index, model, float(result.reward) == 1.0, False, None, metrics
|
| 87 |
|
| 88 |
|
| 89 |
def main():
|
|
|
|
| 97 |
futures = [pool.submit(run_one, i, m) for i, m in work]
|
| 98 |
done = 0
|
| 99 |
for fut in as_completed(futures):
|
| 100 |
+
i, m, correct, failed, err, metrics = fut.result()
|
| 101 |
+
cells[(i, m)] = {"correct": correct, "failed": failed, "error": err, "metrics": metrics}
|
| 102 |
done += 1
|
| 103 |
if done % 20 == 0 or done == len(work):
|
| 104 |
print(f"[eval] {done}/{len(work)} cells done", flush=True)
|
eval_watch.py
CHANGED
|
@@ -62,23 +62,42 @@ def lora_rpc(route, name, path=None):
|
|
| 62 |
urllib.request.urlopen(req, timeout=900)
|
| 63 |
|
| 64 |
|
| 65 |
-
def
|
|
|
|
| 66 |
cells = json.loads(Path(cells_path).read_text())["cells"]
|
| 67 |
scored = [c["correct"] for c in cells.values() if not c["failed"]]
|
| 68 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 69 |
|
| 70 |
|
| 71 |
def main():
|
| 72 |
fs = repo_files()
|
| 73 |
# 1. Base-model cells, once. Every checkpoint compares against these.
|
| 74 |
-
if
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 75 |
print("[watch] evaluating base model first", flush=True)
|
| 76 |
run_eval(BASE_MODEL, "/tmp/base_cells.json")
|
| 77 |
upload("/tmp/base_cells.json", "eval/base_cells.json")
|
| 78 |
-
elif not Path("/tmp/base_cells.json").exists():
|
| 79 |
-
# A previous worker already computed base cells: reuse them locally.
|
| 80 |
-
local = api.hf_hub_download(REPO, "eval/base_cells.json")
|
| 81 |
-
Path("/tmp/base_cells.json").write_text(Path(local).read_text())
|
| 82 |
|
| 83 |
summary = {}
|
| 84 |
if "eval/summary.json" in fs:
|
|
@@ -105,14 +124,21 @@ def main():
|
|
| 105 |
print(f"[watch] {ckpt} eval failed ({exc}); will retry next poll", flush=True)
|
| 106 |
continue
|
| 107 |
upload(f"/tmp/{ckpt}_cells.json", f"eval/{ckpt}_results.json")
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 113 |
Path("/tmp/summary.json").write_text(json.dumps(summary, indent=2))
|
| 114 |
upload("/tmp/summary.json", "eval/summary.json")
|
| 115 |
-
print(f"[watch] {ckpt}: pass@1={
|
|
|
|
|
|
|
| 116 |
time.sleep(POLL_S)
|
| 117 |
|
| 118 |
|
|
|
|
| 62 |
urllib.request.urlopen(req, timeout=900)
|
| 63 |
|
| 64 |
|
| 65 |
+
def agg(cells_path):
|
| 66 |
+
"""pass@1 plus efficiency means over solved tasks (the article's second axis)."""
|
| 67 |
cells = json.loads(Path(cells_path).read_text())["cells"]
|
| 68 |
scored = [c["correct"] for c in cells.values() if not c["failed"]]
|
| 69 |
+
solved = [c["metrics"] for c in cells.values() if c["correct"] and c.get("metrics")]
|
| 70 |
+
|
| 71 |
+
def mean(key):
|
| 72 |
+
vals = [m[key] for m in solved if key in m]
|
| 73 |
+
return round(sum(vals) / len(vals), 1) if vals else None
|
| 74 |
+
|
| 75 |
+
return {
|
| 76 |
+
"pass_at_1": (sum(scored) / len(scored)) if scored else None,
|
| 77 |
+
"scorable": len(scored),
|
| 78 |
+
"solved": len(solved),
|
| 79 |
+
"tool_calls_solved_mean": mean("tool_calls"),
|
| 80 |
+
"turns_solved_mean": mean("turns"),
|
| 81 |
+
"completion_tokens_solved_mean": mean("completion_tokens"),
|
| 82 |
+
"prompt_tokens_solved_mean": mean("prompt_tokens_total"),
|
| 83 |
+
}
|
| 84 |
|
| 85 |
|
| 86 |
def main():
|
| 87 |
fs = repo_files()
|
| 88 |
# 1. Base-model cells, once. Every checkpoint compares against these.
|
| 89 |
+
# Recomputed if the cached file predates per-cell metrics.
|
| 90 |
+
if "eval/base_cells.json" in fs:
|
| 91 |
+
local = api.hf_hub_download(REPO, "eval/base_cells.json")
|
| 92 |
+
Path("/tmp/base_cells.json").write_text(Path(local).read_text())
|
| 93 |
+
if "eval/base_cells.json" not in fs or any(
|
| 94 |
+
c.get("metrics") is None
|
| 95 |
+
for c in json.loads(Path("/tmp/base_cells.json").read_text())["cells"].values()
|
| 96 |
+
if c["correct"]
|
| 97 |
+
):
|
| 98 |
print("[watch] evaluating base model first", flush=True)
|
| 99 |
run_eval(BASE_MODEL, "/tmp/base_cells.json")
|
| 100 |
upload("/tmp/base_cells.json", "eval/base_cells.json")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
summary = {}
|
| 103 |
if "eval/summary.json" in fs:
|
|
|
|
| 124 |
print(f"[watch] {ckpt} eval failed ({exc}); will retry next poll", flush=True)
|
| 125 |
continue
|
| 126 |
upload(f"/tmp/{ckpt}_cells.json", f"eval/{ckpt}_results.json")
|
| 127 |
+
a, b = agg(f"/tmp/{ckpt}_cells.json"), agg("/tmp/base_cells.json")
|
| 128 |
+
entry = dict(a)
|
| 129 |
+
entry["base_pass_at_1"] = b["pass_at_1"]
|
| 130 |
+
if a["pass_at_1"] is not None and b["pass_at_1"] is not None:
|
| 131 |
+
entry["delta"] = a["pass_at_1"] - b["pass_at_1"]
|
| 132 |
+
for key in ("tool_calls_solved_mean", "turns_solved_mean",
|
| 133 |
+
"completion_tokens_solved_mean", "prompt_tokens_solved_mean"):
|
| 134 |
+
if a.get(key) is not None and b.get(key):
|
| 135 |
+
entry[key.replace("_mean", "_vs_base")] = round(a[key] / b[key], 3)
|
| 136 |
+
summary[ckpt] = entry
|
| 137 |
Path("/tmp/summary.json").write_text(json.dumps(summary, indent=2))
|
| 138 |
upload("/tmp/summary.json", "eval/summary.json")
|
| 139 |
+
print(f"[watch] {ckpt}: pass@1={a['pass_at_1']} (base {b['pass_at_1']}), "
|
| 140 |
+
f"tool_calls={a['tool_calls_solved_mean']} (base {b['tool_calls_solved_mean']})",
|
| 141 |
+
flush=True)
|
| 142 |
time.sleep(POLL_S)
|
| 143 |
|
| 144 |
|