lfm-opencode-lora-rl-code / eval_adapter.py
SaylorTwift's picture
SaylorTwift HF Staff
eval: pass purpose=eval (server requires it with eval_sampling)
e7a4c0e verified
Raw History Blame
5.24 kB
"""Held-out evaluation: base model vs LoRA adapter on SmolDataEnvs test tasks.
Runs N test tasks through the env server under OpenCode, once per model, and
reports pass@1 (first graded attempt correct) for each, plus the per-task
agreement breakdown the article's comparisons used.
The vLLM server must serve the base model AND the adapter as a named LoRA:
vllm serve LiquidAI/LFM2.5-2.6B --enable-lora --max-lora-rank 32 \
--lora-modules trained=<adapter-repo> ...
Config via env:
ENV_URL env server (default http://127.0.0.1:8000)
VLLM_URL policy server (default http://127.0.0.1:8001)
TEST_SPLIT served test split (default /app/prepared/datasets/test)
BASE_MODEL served base model id (default LiquidAI/LFM2.5-2.6B)
ADAPTER_NAME LoRA name on the server (default "trained"); "" evaluates base only
N_TASKS how many test tasks (default 50)
CONCURRENCY parallel rollouts (default 8; keep <= MAX_CONCURRENT_ENVS)
OUT results JSON path (default /app/eval_results.json)
"""
import json
import os
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from harbor_env import HarborEnv # re-export of openenv.harbor.client.HarborEnv
ENV_URL = os.environ.get("ENV_URL", "http://127.0.0.1:8000")
VLLM_URL = os.environ.get("VLLM_URL", "http://127.0.0.1:8001")
TEST_SPLIT = os.environ.get("TEST_SPLIT", "/app/prepared/datasets/test")
BASE_MODEL = os.environ.get("BASE_MODEL", "LiquidAI/LFM2.5-2.6B")
ADAPTER_NAME = os.environ.get("ADAPTER_NAME", "trained")
N_TASKS = int(os.environ.get("N_TASKS", "50"))
CONCURRENCY = int(os.environ.get("CONCURRENCY", "8"))
OUT = os.environ.get("OUT", "/app/eval_results.json")
_local = threading.local()
def run_one(task_index, model):
"""One graded rollout. Returns (correct, failed, error_or_none)."""
if not hasattr(_local, "env"):
_local.env = HarborEnv(base_url=ENV_URL)
kwargs = dict(
split=TEST_SPLIT,
task_index=task_index,
harness="opencode",
sandbox=os.environ.get("SANDBOX", "hf-sandbox"),
reward_key="correctness,reward",
llm_url=VLLM_URL,
model=model,
purpose="eval", # required by the server when eval_sampling is passed
eval_sampling={"temperature": 1.0, "top_p": 1.0},
agent_step_limit=17, # production eval cap from the FineEnvs Space
agent_timeout_sec=600,
)
try:
result = _local.env.run_rollout(**kwargs)
except TypeError:
# older server without eval_sampling: retry without it
kwargs.pop("eval_sampling")
result = _local.env.run_rollout(**kwargs)
except Exception as exc:
return task_index, model, None, True, f"{type(exc).__name__}: {exc}"
if not result.ok or result.reward is None:
return task_index, model, None, True, getattr(result, "error", None) or "not ok"
return task_index, model, float(result.reward) == 1.0, False, None
def main():
models = [BASE_MODEL] + ([ADAPTER_NAME] if ADAPTER_NAME else [])
work = [(i, m) for i in range(N_TASKS) for m in models]
cells = {} # (task_index, model) -> {"correct": bool|None, "failed": bool, "error": str|None}
with ThreadPoolExecutor(max_workers=CONCURRENCY) as pool:
futures = [pool.submit(run_one, i, m) for i, m in work]
done = 0
for fut in as_completed(futures):
i, m, correct, failed, err = fut.result()
cells[(i, m)] = {"correct": correct, "failed": failed, "error": err}
done += 1
if done % 20 == 0 or done == len(work):
print(f"[eval] {done}/{len(work)} cells done", flush=True)
summary = {"n_tasks": N_TASKS, "models": {}}
for m in models:
scorable = [c["correct"] for (i, mm), c in cells.items() if mm == m and not c["failed"]]
failed = sum(1 for (i, mm), c in cells.items() if mm == m and c["failed"])
summary["models"][m] = {
"pass_at_1": sum(scorable) / len(scorable) if scorable else None,
"solved": sum(scorable),
"scorable": len(scorable),
"failed_rollouts": failed,
}
if len(models) == 2:
b, a = models
both = sum(1 for i in range(N_TASKS)
if cells.get((i, b), {}).get("correct") and cells.get((i, a), {}).get("correct"))
only_a = sum(1 for i in range(N_TASKS)
if not cells.get((i, b), {}).get("correct") and cells.get((i, a), {}).get("correct"))
only_b = sum(1 for i in range(N_TASKS)
if cells.get((i, b), {}).get("correct") and not cells.get((i, a), {}).get("correct"))
summary["agreement"] = {"both_solved": both, "only_adapter": only_a, "only_base": only_b}
pa = summary["models"][a]["pass_at_1"]
pb = summary["models"][b]["pass_at_1"]
if pa is not None and pb is not None:
summary["delta"] = pa - pb
print(json.dumps(summary, indent=2))
with open(OUT, "w") as f:
json.dump({"summary": summary,
"cells": {f"{i}|{m}": c for (i, m), c in cells.items()}}, f, indent=2)
print(f"[eval] wrote {OUT}")
if __name__ == "__main__":
main()